summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--requirements/requirements-common.txt1
-rw-r--r--yaksh/admin.py4
-rw-r--r--yaksh/forms.py47
-rw-r--r--yaksh/models.py53
-rw-r--r--yaksh/static/yaksh/css/custom.css21
-rw-r--r--yaksh/templates/yaksh/course_detail_options.html5
-rw-r--r--yaksh/templates/yaksh/course_forum.html115
-rw-r--r--yaksh/templates/yaksh/course_modules.html1
-rw-r--r--yaksh/templates/yaksh/post_comments.html71
-rw-r--r--yaksh/test_models.py131
-rw-r--r--yaksh/test_views.py537
-rw-r--r--yaksh/urls.py12
-rw-r--r--yaksh/views.py109
14 files changed, 1066 insertions, 42 deletions
diff --git a/.travis.yml b/.travis.yml
index 892898e..deb5703 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,6 +19,7 @@ install:
- python setup.py develop
before_script:
+ - python manage.py makemigrations
- python manage.py migrate auth
- python manage.py migrate
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt
index d1fed93..80dadb3 100644
--- a/requirements/requirements-common.txt
+++ b/requirements/requirements-common.txt
@@ -10,3 +10,4 @@ coverage
ruamel.yaml==0.15.23
markdown==2.6.9
pygments==2.2.0
+Pillow \ No newline at end of file
diff --git a/yaksh/admin.py b/yaksh/admin.py
index 9c36a98..3d3ba89 100644
--- a/yaksh/admin.py
+++ b/yaksh/admin.py
@@ -1,7 +1,7 @@
from yaksh.models import Question, Quiz, QuestionPaper, Profile
from yaksh.models import (TestCase, StandardTestCase, StdIOBasedTestCase,
Course, AnswerPaper, CourseStatus, LearningModule,
- Lesson
+ Lesson, Post, Comment
)
from django.contrib import admin
@@ -48,6 +48,8 @@ class QuizAdmin(admin.ModelAdmin):
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Question)
admin.site.register(TestCase)
+admin.site.register(Post)
+admin.site.register(Comment)
admin.site.register(StandardTestCase)
admin.site.register(StdIOBasedTestCase)
admin.site.register(Course, CourseAdmin)
diff --git a/yaksh/forms.py b/yaksh/forms.py
index 81b067c..216f5c2 100644
--- a/yaksh/forms.py
+++ b/yaksh/forms.py
@@ -1,7 +1,7 @@
from django import forms
from yaksh.models import (
get_model_class, Profile, Quiz, Question, Course, QuestionPaper, Lesson,
- LearningModule, TestCase, languages, question_types
+ LearningModule, TestCase, languages, question_types, Post, Comment
)
from grades.models import GradingSystem
from django.contrib.auth import authenticate
@@ -32,7 +32,7 @@ test_case_types = (
)
status_types = (
- ('select','Select Status'),
+ ('select', 'Select Status'),
('active', 'Active'),
('closed', 'Inactive'),
)
@@ -381,7 +381,7 @@ class SearchFilterForm(forms.Form):
search_tags = forms.CharField(
label='Search Tags',
widget=forms.TextInput(attrs={'placeholder': 'Search',
- 'class': form_input_class,}),
+ 'class': form_input_class, }),
required=False
)
search_status = forms.ChoiceField(
@@ -571,3 +571,44 @@ class TestcaseForm(forms.ModelForm):
class Meta:
model = TestCase
fields = ["type"]
+
+
+class PostForm(forms.ModelForm):
+ class Meta:
+ model = Post
+ fields = ["title", "description", "image"]
+ widgets = {
+ 'title': forms.TextInput(
+ attrs={
+ 'class': 'form-control'
+ }
+ ),
+ 'description': forms.Textarea(
+ attrs={
+ 'class': 'form-control'
+ }
+ ),
+ 'image': forms.FileInput(
+ attrs={
+ 'class': 'form-control-file'
+ }
+ )
+ }
+
+
+class CommentForm(forms.ModelForm):
+ class Meta:
+ model = Comment
+ fields = ["description", "image"]
+ widgets = {
+ 'description': forms.Textarea(
+ attrs={
+ 'class': 'form-control'
+ }
+ ),
+ 'image': forms.FileInput(
+ attrs={
+ 'class': 'form-control-file'
+ }
+ )
+ }
diff --git a/yaksh/models.py b/yaksh/models.py
index 5d4d453..9bcb132 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals, division
from datetime import datetime, timedelta
+import uuid
import json
import random
import ruamel.yaml
@@ -10,6 +11,7 @@ from collections import Counter, defaultdict
from django.db import models
from django.contrib.auth.models import User, Group, Permission
+from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType
from taggit.managers import TaggableManager
from django.utils import timezone
@@ -232,6 +234,19 @@ def render_template(template_path, data=None):
return render
+def validate_image(image):
+ file_size = image.file.size
+ limit_mb = 30
+ if file_size > limit_mb * 1024 * 1024:
+ raise ValidationError("Max size of file is {0} MB".format(limit_mb))
+
+
+def get_image_dir(instance, filename):
+ return os.sep.join((
+ 'post_%s' % (instance.uid), filename
+ ))
+
+
###############################################################################
class CourseManager(models.Manager):
@@ -1162,7 +1177,9 @@ class CourseStatus(models.Model):
if self.is_course_complete():
self.calculate_percentage()
if self.course.grading_system is None:
- grading_system = GradingSystem.objects.get(name__contains='default')
+ grading_system = GradingSystem.objects.get(
+ name__contains='default'
+ )
else:
grading_system = self.course.grading_system
grade = grading_system.get_grade(self.percentage)
@@ -2632,4 +2649,38 @@ class TestCaseOrder(models.Model):
# Order of the test case for a question.
order = models.TextField()
+
##############################################################################
+class ForumBase(models.Model):
+ uid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
+ creator = models.ForeignKey(User, on_delete=models.CASCADE)
+ description = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ modified_at = models.DateTimeField(auto_now=True)
+ image = models.ImageField(upload_to=get_image_dir, blank=True,
+ null=True, validators=[validate_image])
+ active = models.BooleanField(default=True)
+
+
+class Post(ForumBase):
+ title = models.CharField(max_length=200)
+ course = models.ForeignKey(Course,
+ on_delete=models.CASCADE, related_name='post')
+
+ def __str__(self):
+ return self.title
+
+ def get_last_comment(self):
+ return self.comment.last()
+
+ def get_comments_count(self):
+ return self.comment.filter(active=True).count()
+
+
+class Comment(ForumBase):
+ post_field = models.ForeignKey(Post, on_delete=models.CASCADE,
+ related_name='comment')
+
+ def __str__(self):
+ return 'Comment by {0}: {1}'.format(self.creator.username,
+ self.post_field.title)
diff --git a/yaksh/static/yaksh/css/custom.css b/yaksh/static/yaksh/css/custom.css
index 63ee455..3979e3e 100644
--- a/yaksh/static/yaksh/css/custom.css
+++ b/yaksh/static/yaksh/css/custom.css
@@ -97,3 +97,24 @@ body, .dropdown-menu {
min-height: 100vh;
transition: all 0.3s;
}
+
+/* ---------------------------------------------------
+ FORUM STYLE
+----------------------------------------------------- */
+
+.brown-light {
+ background: #f4a460;
+ padding-left: 0.3em;
+ padding-right: 0.3em;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+
+.post_image, .comment_image {
+ width: 50%;
+ height: 50%;
+}
+
+.description {
+ font-size: 16px;
+} \ No newline at end of file
diff --git a/yaksh/templates/yaksh/course_detail_options.html b/yaksh/templates/yaksh/course_detail_options.html
index 90662d6..4dd4dda 100644
--- a/yaksh/templates/yaksh/course_detail_options.html
+++ b/yaksh/templates/yaksh/course_detail_options.html
@@ -30,6 +30,11 @@
</a>
</li>
<li class="nav-item">
+ <a href="{% url 'yaksh:course_forum' course.id %}" class="nav-link list-group-item" title="Discussion forum of this course" data-placement="top" data-toggle="tooltip">
+ Discussion Forum
+ </a>
+ </li>
+ <li class="nav-item">
<a class="nav-link list-group-item {% if is_add_teacher %} active {% endif %}" href="{% url 'yaksh:search_teacher' course.id %}" data-toggle="tooltip" title="Add Teachers/TAs to this course" data-placement="top">
Add Teachers/TAs
</a>
diff --git a/yaksh/templates/yaksh/course_forum.html b/yaksh/templates/yaksh/course_forum.html
new file mode 100644
index 0000000..e6b6a90
--- /dev/null
+++ b/yaksh/templates/yaksh/course_forum.html
@@ -0,0 +1,115 @@
+{% extends base_template %}
+{% load static %}
+{% block title %}
+ {{course.name}}: Discussion Forum
+{% endblock title %}
+{% block content %}
+ <div class="container">
+ <div>
+ <h2><center>{{course.name}}</center></h2>
+ <center>Discussion Forum</center>
+ </div>
+ <div class="d-flex p-2 bd-highlight">
+ <div class="col-md-4">
+ {% if moderator %}
+ <a href="{% url 'yaksh:course_detail' course.id %}" class="btn btn-primary">Back to Course</a>
+ {% else %}
+ <a href="{% url 'yaksh:course_modules' course.id %}" class="btn btn-primary">Back to Course</a>
+ {% endif %}
+ </div>
+ <div class="col-md-4">
+ <form class="my-2 my-lg-0" action="" method="GET">
+ <div class="input-group">
+ <input type="search" placeholder="Search" name="search" class="form-control">
+ <span class="input-group-append">
+ <button class="btn btn-outline-info" type="submit"><i class="fa fa-search"></i>&nbsp;Search</button>
+ </span>
+ </div>
+ </form>
+ </div>
+ <div class="col-md-4">
+ <button type="button" class="btn btn-primary pull-right" data-toggle="modal" data-target="#newPostModal">New Post</button>
+ </div>
+ </div>
+ <!-- Modal -->
+ <div id="newPostModal" class="modal fade" role="dialog">
+ <div class="modal-dialog">
+
+ <!-- Modal content-->
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title">Create a new Post</h4>
+ <button type="button" class="close" data-dismiss="modal">&times;</button>
+ </div>
+ <div class="modal-body">
+ <form action="." method="POST" enctype='multipart/form-data'>
+ <div class="form-group">
+ {% csrf_token %}
+ {{form}}
+ </div>
+ <input type="submit" class="btn btn-primary" value="Create Post">
+ </form>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ <br>
+ <br>
+ {% if posts %}
+ <table id="posts_table" class="tablesorter table">
+ <thead class="thread-inverse">
+ <tr>
+ <th width="700">Questions</th>
+ <th>Created by</th>
+ <th>Replies</th>
+ <th>Last reply</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for post in posts %}
+ <tr>
+ <td>
+ <a href="{% url 'yaksh:post_comments' course.id post.uid %}">{{post.title}}</a>
+ <small class="text-muted d-block">{{ post.description|truncatewords:30 }}</small>
+ <small class="text-muted"><strong>Last updated: {{post.modified_at}}</strong></small>
+ </td>
+ <td>{{post.creator.username}}</td>
+ <td>{{post.get_comments_count}}</td>
+ <td>
+ {% with post.get_last_comment as last_comment %}
+ {% if last_comment %}
+ {{last_comment.creator}}
+ {% else %}
+ None
+ {% endif %}
+ {% endwith %}
+ </td>
+ <td>
+ {% if user.profile.is_moderator %}
+ <small><a href="{% url 'yaksh:hide_post' course.id post.uid %}" class="pull-right btn btn-danger">Delete</i></a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ No discussion posts are there yet. Create one to start discussing.
+ {% endif %}
+ {% include "yaksh/paginator.html" %}
+ </div>
+{% endblock content %}
+{% block script %}
+ <script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML"></script>
+ <script type="text/javascript" src="{% static 'yaksh/js/jquery.tablesorter.min.js' %}"></script>
+ <script type="text/javascript">
+ $(document).ready(() => {
+ $("#posts_table").tablesorter();
+ });
+ </script>
+{% endblock script %} \ No newline at end of file
diff --git a/yaksh/templates/yaksh/course_modules.html b/yaksh/templates/yaksh/course_modules.html
index dd7b68d..b808562 100644
--- a/yaksh/templates/yaksh/course_modules.html
+++ b/yaksh/templates/yaksh/course_modules.html
@@ -7,6 +7,7 @@
<div class="card">
<div class="card-header">
{{ course.name }}
+ <a href="{% url "yaksh:course_forum" course.id %}" class="btn btn-info pull-right">Discussion Forum</a>
</div>
<div class="card-body">
{% if course.view_grade %}
diff --git a/yaksh/templates/yaksh/post_comments.html b/yaksh/templates/yaksh/post_comments.html
new file mode 100644
index 0000000..463103e
--- /dev/null
+++ b/yaksh/templates/yaksh/post_comments.html
@@ -0,0 +1,71 @@
+{% extends base_template %}
+{% load static %}
+{% block title %}
+ {{post.title}}
+{% endblock title %}
+
+{% block content %}
+ <div class="container">
+ <a class="btn btn-primary" href="{% url 'yaksh:course_forum' post.course.id %}">Back to Posts</a>
+ <br>
+ <br>
+ <div class="card mb-2 border-dark">
+ <div class="card-header text-white bg-dark py-2 px-3">
+ {{post.title}}
+ <br>
+ <small>
+ <strong>{{post.creator.username}}</strong>
+ {{post.created_at}}
+ {% if user.profile.is_moderator %}<a href="{% url 'yaksh:hide_post' post.course.id post.uid %}" class="pull-right btn btn-danger">Delete</a>{% endif %}
+ </small>
+
+ </div>
+ <div class="card-body">
+ <p class="card-text description">{{post.description}}</p>
+ {% if post.image %}
+ <a href="{{post.image.url}}" target="_blank">
+ <center><img src="{{post.image.url}}" class="post_image thumbnail" alt=""></center>
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ <br>
+ {% if comments %}
+ {% for comment in comments %}
+ <div class="card mb-2">
+ <div class="card-body p-3">
+ <div class="row mb-3">
+ <div class="col-6">
+ <strong class="text-muted">{{comment.creator.username}}</strong>
+ </div>
+ <div class="col-6 text-right">
+ <small class="text-muted">{{comment.created_at}} {% if user.profile.is_moderator %} <a href="{% url 'yaksh:hide_comment' post.course.id comment.uid %}" class="btn btn-danger">Delete</a>{% endif %}</small>
+ </div>
+ </div>
+ <p class="card-text description">{{comment.description}}</p>
+ <div>
+ {% if comment.image %}
+ <a href="{{comment.image.url}}" target="_blank">
+ <center><img src="{{comment.image.url}}" class="comment_image thumbnail" alt=""></center>
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ {% endfor %}
+ {% endif %}
+ <br>
+ <div>
+ <form action="{% url 'yaksh:post_comments' post.course.id post.uid %}" method="POST" enctype='multipart/form-data'>
+ <div class="form-group">
+ {% csrf_token %}
+ {{form}}
+ </div>
+ <input type="submit" value="Submit" class="btn btn-primary">
+ </form>
+ </div>
+ </div>
+{% endblock content %}
+{% block script %}
+ <script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML"></script>
+{% endblock script %} \ No newline at end of file
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index a60a1d6..4e6b1ae 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -5,7 +5,7 @@ from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\
StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload,\
LearningModule, LearningUnit, Lesson, LessonFile, CourseStatus, \
- create_group, legend_display_types
+ create_group, legend_display_types, Post, Comment
from yaksh.code_server import (
ServerPool, get_result as get_result_from_code_server
)
@@ -191,7 +191,6 @@ class LearningModuleTestCases(unittest.TestCase):
self.prereq_course.students.add(self.student)
self.prereq_course.save()
-
def tearDown(self):
# Remove unit from course status completed units
self.course_status.completed_units.remove(self.learning_unit_one)
@@ -495,7 +494,7 @@ class QuestionTestCases(unittest.TestCase):
self.assertEqual(self.question1.snippet, 'def myfunc()')
tag_list = []
for tag in self.question1.tags.all():
- tag_list.append(tag.name)
+ tag_list.append(tag.name)
for tag in tag_list:
self.assertIn(tag, ['python', 'function'])
@@ -2239,3 +2238,129 @@ class FileUploadTestCases(unittest.TestCase):
if os.path.isfile(self.file_upload.file.path):
os.remove(self.file_upload.file.path)
self.file_upload.delete()
+
+
+class PostModelTestCases(unittest.TestCase):
+ def setUp(self):
+ self.user1 = User.objects.create(
+ username='bart',
+ password='bart',
+ email='bart@test.com'
+ )
+ Profile.objects.create(
+ user=self.user1,
+ roll_number=1,
+ institute='IIT',
+ department='Chemical',
+ position='Student'
+ )
+
+ self.user2 = User.objects.create(
+ username='dart',
+ password='dart',
+ email='dart@test.com'
+ )
+ Profile.objects.create(
+ user=self.user2,
+ roll_number=2,
+ institute='IIT',
+ department='Chemical',
+ position='Student'
+ )
+
+ self.user3 = User.objects.create(
+ username='user3',
+ password='user3',
+ email='user3@test.com'
+ )
+ Profile.objects.create(
+ user=self.user3,
+ roll_number=3,
+ is_moderator=True,
+ department='Chemical',
+ position='Teacher'
+ )
+
+ self.course = Course.objects.create(
+ name='Python Course',
+ enrollment='Enroll Request',
+ creator=self.user3
+ )
+ self.post1 = Post.objects.create(
+ title='Post 1',
+ course=self.course,
+ creator=self.user1,
+ description='Post 1 description'
+ )
+ self.comment1 = Comment.objects.create(
+ post_field=self.post1,
+ creator=self.user2,
+ description='Post 1 comment 1'
+ )
+ self.comment2 = Comment.objects.create(
+ post_field=self.post1,
+ creator=self.user3,
+ description='Post 1 user3 comment 2'
+ )
+
+ def test_get_last_comment(self):
+ last_comment = self.post1.get_last_comment()
+ self.assertEquals(last_comment.description, 'Post 1 user3 comment 2')
+
+ def test_get_comments_count(self):
+ count = self.post1.get_comments_count()
+ self.assertEquals(count, 2)
+
+ def test__str__(self):
+ self.assertEquals(str(self.post1.title), self.post1.title)
+
+ def tearDown(self):
+ self.user1.delete()
+ self.user2.delete()
+ self.user3.delete()
+ self.course.delete()
+ self.post1.delete()
+
+
+class CommentModelTestCases(unittest.TestCase):
+ def setUp(self):
+ self.user1 = User.objects.create(
+ username='bart',
+ password='bart',
+ email='bart@test.com'
+ )
+ Profile.objects.create(
+ user=self.user1,
+ roll_number=1,
+ institute='IIT',
+ department='Chemical',
+ position='Student'
+ )
+ self.course = Course.objects.create(
+ name='Python Course',
+ enrollment='Enroll Request',
+ creator=self.user1
+ )
+ self.post1 = Post.objects.create(
+ title='Post 1',
+ course=self.course,
+ creator=self.user1,
+ description='Post 1 description'
+ )
+ self.comment1 = Comment.objects.create(
+ post_field=self.post1,
+ creator=self.user1,
+ description='Post 1 comment 1'
+ )
+
+ def test__str__(self):
+ self.assertEquals(
+ str(self.comment1.post_field.title),
+ self.comment1.post_field.title
+ )
+
+ def tearDown(self):
+ self.user1.delete()
+ self.course.delete()
+ self.post1.delete()
+ self.comment1.delete() \ No newline at end of file
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index ef7c52f..94b81ad 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -11,7 +11,7 @@ import shutil
from markdown import Markdown
from django.contrib.auth.models import Group
from django.contrib.auth import authenticate
-from django.urls import reverse
+from django.urls import reverse, resolve
from django.test import TestCase
from django.test import Client
from django.http import Http404
@@ -27,9 +27,10 @@ from yaksh.models import (
User, Profile, Question, Quiz, QuestionPaper, AnswerPaper, Answer, Course,
AssignmentUpload, McqTestCase, IntegerTestCase, StringTestCase,
FloatTestCase, FIXTURES_DIR_PATH, LearningModule, LearningUnit, Lesson,
- LessonFile, CourseStatus, dict_to_yaml
+ LessonFile, CourseStatus, dict_to_yaml, Post, Comment
)
-from yaksh.views import add_as_moderator
+from yaksh.views import add_as_moderator, course_forum, post_comments
+from yaksh.forms import PostForm, CommentForm
from yaksh.decorators import user_has_profile
@@ -1123,11 +1124,10 @@ class TestAddQuiz(TestCase):
If not logged in redirect to login page
"""
response = self.client.get(
- reverse('yaksh:add_quiz',
- kwargs={'course_id': self.course.id,
- 'module_id': self.module.id}),
- follow=True
- )
+ reverse('yaksh:add_quiz', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id
+ }), follow=True)
redirect_destination = (
'/exam/login/?next=/exam/manage/addquiz/{0}/{1}/'.format(
self.course.id, self.module.id
@@ -1144,11 +1144,10 @@ class TestAddQuiz(TestCase):
password=self.student_plaintext_pass
)
response = self.client.get(
- reverse('yaksh:add_quiz',
- kwargs={'course_id': self.course.id,
- 'module_id': self.module.id}),
- follow=True
- )
+ reverse('yaksh:add_quiz', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id
+ }), follow=True)
self.assertEqual(response.status_code, 404)
def test_add_quiz_get(self):
@@ -1160,11 +1159,10 @@ class TestAddQuiz(TestCase):
password=self.user_plaintext_pass
)
response = self.client.get(
- reverse('yaksh:add_quiz',
- kwargs={'course_id': self.course.id,
- 'module_id': self.module.id}
- )
- )
+ reverse('yaksh:add_quiz', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id
+ }))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/add_quiz.html')
self.assertIsNotNone(response.context['form'])
@@ -1179,10 +1177,11 @@ class TestAddQuiz(TestCase):
)
tzone = pytz.timezone('UTC')
response = self.client.post(
- reverse('yaksh:edit_quiz',
- kwargs={'course_id': self.course.id,
- 'module_id': self.module.id,
- 'quiz_id': self.quiz.id}),
+ reverse('yaksh:edit_quiz', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id,
+ 'quiz_id': self.quiz.id
+ }),
data={
'start_date_time': '2016-01-10 09:00:15',
'end_date_time': '2016-01-15 09:00:15',
@@ -1316,10 +1315,11 @@ class TestAddQuiz(TestCase):
password=self.user_plaintext_pass
)
response = self.client.post(
- reverse('yaksh:edit_exercise',
- kwargs={'course_id': self.course.id,
- 'module_id': self.module.id,
- 'quiz_id': self.exercise.id}),
+ reverse('yaksh:edit_exercise', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id,
+ 'quiz_id': self.exercise.id
+ }),
data={
'description': 'updated demo exercise',
'active': True
@@ -1344,9 +1344,10 @@ class TestAddQuiz(TestCase):
password=self.user_plaintext_pass
)
response = self.client.post(
- reverse('yaksh:add_exercise',
- kwargs={'course_id': self.course.id,
- 'module_id': self.module.id}),
+ reverse('yaksh:add_exercise', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id
+ }),
data={
'description': "Demo Exercise",
'active': True
@@ -2281,7 +2282,7 @@ class TestSearchFilters(TestCase):
# Create moderator group
self.mod_group = Group.objects.create(name="moderator")
- #Create user1 with profile
+ # Create user1 with profile
self.user1_plaintext_pass = "demo1"
self.user1 = User.objects.create_user(
username='demo_user1',
@@ -4359,7 +4360,9 @@ class TestDownloadCsv(TestCase):
kwargs={'course_id': self.course.id}),
follow=True
)
- file_name = "{0}.csv".format(self.course.name.lower().replace(" ", "_"))
+ file_name = "{0}.csv".format(
+ self.course.name.lower().replace(" ", "_")
+ )
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get('Content-Disposition'),
'attachment; filename="{0}"'.format(file_name))
@@ -6337,3 +6340,475 @@ class TestLessons(TestCase):
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['data'], '<p>test description</p>')
+
+
+class TestPost(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.mod_group = Group.objects.create(name='moderator')
+
+ self.student_plaintext_pass = 'student'
+ self.student = User.objects.create_user(
+ username='student',
+ password=self.student_plaintext_pass,
+ first_name='first_name',
+ last_name='last_name',
+ email='student@test.com'
+ )
+
+ Profile.objects.create(
+ user=self.student,
+ roll_number=10,
+ institute='IIT',
+ department='Chemical',
+ position='student',
+ timezone='UTC'
+ )
+
+ # moderator
+ self.user_plaintext_pass = 'demo'
+ self.user = User.objects.create_user(
+ username='demo_user',
+ password=self.user_plaintext_pass,
+ first_name='first_name',
+ last_name='last_name',
+ email='demo@test.com'
+ )
+
+ Profile.objects.create(
+ user=self.user,
+ roll_number=10,
+ institute='IIT',
+ department='Chemical',
+ position='Moderator',
+ timezone='UTC'
+ )
+
+ self.course = Course.objects.create(
+ name="Python Course",
+ enrollment="Enroll Request", creator=self.user
+ )
+
+ def test_csrf(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ response = self.client.get(url)
+ self.assertContains(response, 'csrfmiddlewaretoken')
+
+ def test_view_course_forum_denies_anonymous_user(self):
+ url = reverse('yaksh:course_forum', kwargs= {
+ 'course_id': self.course.id
+ })
+ response = self.client.get(url, follow=True)
+ self.assertEqual(response.status_code, 200)
+ redirection_url = '/exam/login/?next=/exam/forum/{0}/'.format(
+ str(self.course.id)
+ )
+ self.assertRedirects(response, redirection_url)
+
+ def test_view_course_forum(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ response = self.client.get(url, follow=True)
+ self.assertEquals(response.status_code, 200)
+ self.assertTemplateUsed(response, 'yaksh/course_forum.html')
+
+ def test_view_course_forum_not_found_status_code(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': 99
+ })
+ response = self.client.get(url)
+ self.assertEquals(response.status_code, 404)
+
+ def test_course_forum_url_resolves_course_forum_view(self):
+ view = resolve('/exam/forum/1/')
+ self.assertEqual(view.func, course_forum)
+
+ def test_course_forum_contains_link_to_post_comments_page(self):
+ # create a post in setup
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ post = Post.objects.create(
+ title='post 1',
+ description='post 1 description',
+ course=self.course,
+ creator=self.student
+ )
+ response = self.client.get(url)
+ post_comments_url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': post.uid
+ })
+ self.assertContains(response, 'href="{0}'.format(post_comments_url))
+
+
+ def test_new_post_valid_post_data(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ data = {
+ "title": 'Post 1',
+ "description": 'Post 1 description',
+ }
+ response = self.client.post(url, data)
+ # This shouldn't be 302. Check where does it redirects.
+ result = Post.objects.filter(title='Post 1',
+ creator=self.student,
+ course=self.course)
+ self.assertTrue(result.exists())
+
+ def test_new_post_invalid_post_data(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ data = {}
+ response = self.client.post(url, data)
+ self.assertEquals(response.status_code, 200)
+
+ def test_new_post_invalid_post_data_empty_fields(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ data = {
+ "title": '',
+ "description": '',
+ }
+ response = self.client.post(url, data)
+ self.assertEquals(response.status_code, 200)
+ self.assertFalse(Post.objects.exists())
+
+ def test_contains_form(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ response = self.client.get(url)
+ form = response.context.get('form')
+ self.assertIsInstance(form, PostForm)
+
+ def test_open_created_post_denies_anonymous_user(self):
+ post = Post.objects.create(
+ title='post 1',
+ description='post 1 description',
+ course=self.course,
+ creator=self.student
+ )
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': post.uid
+ })
+ response = self.client.get(url, follow=True)
+ self.assertEqual(response.status_code, 200)
+ redirection_url = '/exam/login/?next=/exam/forum/{0}/post/{1}/'.format(
+ str(self.course.id), str(post.uid)
+ )
+ self.assertRedirects(response, redirection_url)
+
+ def test_new_post_invalid_post_data(self):
+ """
+ Invalid post data should not redirect
+ The expected behavior is to show form again with validation errors
+ """
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ data = {}
+ response = self.client.post(url, data)
+ form = response.context.get('form')
+ self.assertEquals(response.status_code, 200)
+ self.assertTrue(form.errors)
+
+ def test_hide_post(self):
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ self.course.students.add(self.user)
+ post = Post.objects.create(
+ title='post 1',
+ description='post 1 description',
+ course=self.course,
+ creator=self.user
+ )
+ url = reverse('yaksh:hide_post', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': post.uid
+ })
+ response = self.client.get(url, follow=True)
+ self.assertEqual(response.status_code, 200)
+
+ def tearDown(self):
+ self.client.logout()
+ self.user.delete()
+ self.course.delete()
+ self.mod_group.delete()
+
+
+class TestPostComment(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.mod_group = Group.objects.create(name='moderator')
+
+ self.student_plaintext_pass = 'student'
+ self.student = User.objects.create_user(
+ username='student',
+ password=self.student_plaintext_pass,
+ first_name='first_name',
+ last_name='last_name',
+ email='student@test.com'
+ )
+
+ Profile.objects.create(
+ user=self.student,
+ roll_number=10,
+ institute='IIT',
+ department='Chemical',
+ position='student',
+ timezone='UTC'
+ )
+
+ # moderator
+ self.user_plaintext_pass = 'demo'
+ self.user = User.objects.create_user(
+ username='demo_user',
+ password=self.user_plaintext_pass,
+ first_name='first_name',
+ last_name='last_name',
+ email='demo@test.com'
+ )
+
+ Profile.objects.create(
+ user=self.user,
+ roll_number=10,
+ institute='IIT',
+ department='Chemical',
+ position='Moderator',
+ timezone='UTC'
+ )
+
+ self.course = Course.objects.create(
+ name="Python Course",
+ enrollment="Enroll Request", creator=self.user
+ )
+
+ self.post = Post.objects.create(
+ title='post 1',
+ description='post 1 description',
+ course=self.course,
+ creator=self.student
+ )
+
+ def test_csrf(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ response = self.client.get(url)
+ self.assertContains(response, 'csrfmiddlewaretoken')
+
+ def test_post_comments_view_success_status_code(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ response = self.client.get(url)
+ self.assertEquals(response.status_code, 200)
+
+ def test_post_comments_view_not_found_status_code(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': 99,
+ 'uuid': '90da38ad-06fa-451b-9e82-5035e839da90'
+ })
+ response = self.client.get(url)
+ self.assertEquals(response.status_code, 404)
+
+ def test_post_comments_url_resolves_post_comments_view(self):
+ view = resolve(
+ '/exam/forum/1/post/90da38ad-06fa-451b-9e82-5035e839da89/'
+ )
+ self.assertEquals(view.func, post_comments)
+
+ def test_post_comments_view_contains_link_back_to_course_forum_view(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ comment_url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ course_forum_url = reverse('yaksh:course_forum', kwargs={
+ 'course_id': self.course.id
+ })
+ response = self.client.get(comment_url)
+ self.assertContains(response, 'href="{0}"'.format(course_forum_url))
+
+ def test_post_comments_valid_post_data(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ data = {
+ 'post_field': self.post,
+ 'description': 'post 1 comment',
+ 'creator': self.user,
+ }
+ response = self.client.post(url, data)
+ self.assertEquals(response.status_code, 302)
+ result = Comment.objects.filter(post_field__uid=self.post.uid)
+ self.assertTrue(result.exists())
+
+ def test_post_comments_invalid_post_data(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ data = {}
+ response = self.client.post(url, data)
+ self.assertEquals(response.status_code, 200)
+
+ def test_post_comments_post_data_empty_fields(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ data = {
+ 'post_field': '',
+ 'description': '',
+ 'creator': '',
+ }
+ response = self.client.post(url, data)
+ self.assertEquals(response.status_code, 200)
+ self.assertFalse(Comment.objects.exists())
+
+ def test_contains_form(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ response = self.client.get(url)
+ form = response.context.get('form')
+ self.assertIsInstance(form, CommentForm)
+
+ def post_comment_invalid_post_data(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+ self.course.students.add(self.student)
+ url = reverse('yaksh:post_comments', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': self.post.uid
+ })
+ data = {}
+ response = self.client.post(url, data)
+ form = response.context.get('form')
+ self.assertEquals(response.status_code, 200)
+ self.assertTrue(form.errors)
+
+ def test_hide_post_comment(self):
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ self.course.students.add(self.user)
+ comment = Comment.objects.create(
+ post_field=self.post,
+ description='post 1 comment',
+ creator=self.user
+ )
+ url = reverse('yaksh:hide_comment', kwargs={
+ 'course_id': self.course.id,
+ 'uuid': comment.uid
+ })
+ response = self.client.get(url)
+ self.assertEquals(response.status_code, 302)
+
+ def tearDown(self):
+ self.client.logout()
+ self.user.delete()
+ self.course.delete()
+ self.mod_group.delete()
diff --git a/yaksh/urls.py b/yaksh/urls.py
index 6085c51..149e4d6 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -59,6 +59,18 @@ urlpatterns = [
views.get_next_unit, name='next_unit'),
url(r'^course_modules/(?P<course_id>\d+)/$',
views.course_modules, name='course_modules'),
+ url(r'^forum/(?P<course_id>\d+)/$',
+ views.course_forum,
+ name='course_forum'),
+ url(r'^forum/(?P<course_id>\d+)/post/(?P<uuid>[0-9a-f-]+)/$',
+ views.post_comments,
+ name='post_comments'),
+ url(r'^forum/(?P<course_id>\d+)/post/(?P<uuid>[0-9a-f-]+)/delete/',
+ views.hide_post,
+ name='hide_post'),
+ url(r'^forum/(?P<course_id>\d+)/comment/(?P<uuid>[0-9a-f-]+)/delete/',
+ views.hide_comment,
+ name='hide_comment'),
url(r'^manage/$', views.prof_manage, name='manage'),
url(r'^manage/addquestion/$', views.add_question, name="add_question"),
url(r'^manage/addquestion/(?P<question_id>\d+)/$', views.add_question,
diff --git a/yaksh/views.py b/yaksh/views.py
index 90d3d2b..397e7c8 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -1,6 +1,6 @@
import os
import csv
-from django.http import HttpResponse, JsonResponse
+from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
from django.contrib.auth import login, logout, authenticate
from django.shortcuts import render, get_object_or_404, redirect
from django.template import Context, Template
@@ -37,14 +37,14 @@ from yaksh.models import (
QuestionPaper, QuestionSet, Quiz, Question, StandardTestCase,
StdIOBasedTestCase, StringTestCase, TestCase, User,
get_model_class, FIXTURES_DIR_PATH, MOD_GROUP_NAME, Lesson, LessonFile,
- LearningUnit, LearningModule, CourseStatus, question_types
+ LearningUnit, LearningModule, CourseStatus, question_types, Post, Comment
)
from yaksh.forms import (
UserRegisterForm, UserLoginForm, QuizForm, QuestionForm,
QuestionFilterForm, CourseForm, ProfileForm,
UploadFileForm, FileForm, QuestionPaperForm, LessonForm,
LessonFileForm, LearningModuleForm, ExerciseForm, TestcaseForm,
- SearchFilterForm
+ SearchFilterForm, PostForm, CommentForm
)
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
from .settings import URL_ROOT
@@ -3276,3 +3276,106 @@ def download_course_progress(request, course_id):
for student in stud_details:
writer.writerow(student)
return response
+
+
+@login_required
+@email_verified
+def course_forum(request, course_id):
+ user = request.user
+ base_template = 'user.html'
+ moderator = False
+ if is_moderator(user):
+ base_template = 'manage.html'
+ moderator = True
+ course = get_object_or_404(Course, id=course_id)
+ if (not course.is_creator(user) and not course.is_teacher(user)
+ and not course.is_student(user)):
+ raise Http404('You are not enrolled in {0} course'.format(course.name))
+ if 'search' in request.GET:
+ search_term = request.GET['search']
+ posts = course.post.filter(active=True, title__icontains=search_term)
+ else:
+ posts = course.post.filter(active=True).order_by('-modified_at')
+ paginator = Paginator(posts, 10)
+ page = request.GET.get('page')
+ posts = paginator.get_page(page)
+ if request.method == "POST":
+ form = PostForm(request.POST, request.FILES)
+ if form.is_valid():
+ new_post = form.save(commit=False)
+ new_post.creator = user
+ new_post.course = course
+ new_post.save()
+ return redirect('yaksh:post_comments',
+ course_id=course.id, uuid=new_post.uid)
+ else:
+ form = PostForm()
+ return render(request, 'yaksh/course_forum.html', {
+ 'user': user,
+ 'course': course,
+ 'base_template': base_template,
+ 'posts': posts,
+ 'moderator': moderator,
+ 'objects': posts,
+ 'form': form,
+ 'user': user
+ })
+
+
+@login_required
+@email_verified
+def post_comments(request, course_id, uuid):
+ user = request.user
+ base_template = 'user.html'
+ if is_moderator(user):
+ base_template = 'manage.html'
+ post = get_object_or_404(Post, uid=uuid)
+ comments = post.comment.filter(active=True)
+ course = get_object_or_404(Course, id=course_id)
+ if (not course.is_creator(user) and not course.is_teacher(user)
+ and not course.is_student(user)):
+ raise Http404('You are not enrolled in {0} course'.format(course.name))
+ form = CommentForm()
+ if request.method == "POST":
+ form = CommentForm(request.POST, request.FILES)
+ if form.is_valid():
+ new_comment = form.save(commit=False)
+ new_comment.creator = request.user
+ new_comment.post_field = post
+ new_comment.save()
+ return redirect(request.path_info)
+ return render(request, 'yaksh/post_comments.html', {
+ 'post': post,
+ 'comments': comments,
+ 'base_template': base_template,
+ 'form': form,
+ 'user': user
+ })
+
+
+@login_required
+@email_verified
+def hide_post(request, course_id, uuid):
+ user = request.user
+ course = get_object_or_404(Course, id=course_id)
+ if (not course.is_creator(user) and not course.is_teacher(user)):
+ raise Http404('You are not enrolled in {0} course'.format(course.name))
+ post = get_object_or_404(Post, uid=uuid)
+ post.comment.active = False
+ post.active = False
+ post.save()
+ return redirect('yaksh:course_forum', course_id)
+
+
+@login_required
+@email_verified
+def hide_comment(request, course_id, uuid):
+ user = request.user
+ course = get_object_or_404(Course, id=course_id)
+ if (not course.is_creator(user) and not course.is_teacher(user)):
+ raise Http404('You are not enrolled in {0} course'.format(course.name))
+ comment = get_object_or_404(Comment, uid=uuid)
+ post_uid = comment.post_field.uid
+ comment.active = False
+ comment.save()
+ return redirect('yaksh:post_comments', course_id, post_uid)