diff options
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | requirements/requirements-common.txt | 1 | ||||
-rw-r--r-- | yaksh/admin.py | 4 | ||||
-rw-r--r-- | yaksh/forms.py | 47 | ||||
-rw-r--r-- | yaksh/models.py | 53 | ||||
-rw-r--r-- | yaksh/static/yaksh/css/custom.css | 21 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_detail_options.html | 5 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_forum.html | 115 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_modules.html | 1 | ||||
-rw-r--r-- | yaksh/templates/yaksh/post_comments.html | 71 | ||||
-rw-r--r-- | yaksh/test_models.py | 131 | ||||
-rw-r--r-- | yaksh/test_views.py | 537 | ||||
-rw-r--r-- | yaksh/urls.py | 12 | ||||
-rw-r--r-- | yaksh/views.py | 109 |
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> 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">×</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) |