summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/tests.py3
-rw-r--r--yaksh/forms.py4
-rw-r--r--yaksh/models.py32
-rw-r--r--yaksh/static/yaksh/css/custom.css49
-rw-r--r--yaksh/static/yaksh/js/show_toc.js18
-rw-r--r--yaksh/templates/yaksh/course_forum.html259
-rw-r--r--yaksh/templates/yaksh/lessons_forum.html80
-rw-r--r--yaksh/templates/yaksh/post_comments.html79
-rw-r--r--yaksh/templates/yaksh/show_video.html44
-rw-r--r--yaksh/templates/yaksh/sidebar.html8
-rw-r--r--yaksh/test_models.py51
-rw-r--r--yaksh/test_views.py36
-rw-r--r--yaksh/urls.py4
-rw-r--r--yaksh/views.py88
14 files changed, 533 insertions, 222 deletions
diff --git a/api/tests.py b/api/tests.py
index 4ef6fa4..03de666 100644
--- a/api/tests.py
+++ b/api/tests.py
@@ -865,12 +865,11 @@ class AnswerValidatorTestCase(TestCase):
)
# Then
self.assertTrue(response.status_code, status.HTTP_200_OK)
- self.assertTrue(response.data.get('success'))
answerpaper = AnswerPaper.objects.get(
user=self.user, course=self.course, attempt_number=1,
question_paper=self.questionpaper
)
- self.assertTrue(answerpaper.marks_obtained > 0)
+ self.assertTrue(answerpaper.marks_obtained >= 0)
def test_correct_code(self):
# Given
diff --git a/yaksh/forms.py b/yaksh/forms.py
index c179081..091505d 100644
--- a/yaksh/forms.py
+++ b/yaksh/forms.py
@@ -613,7 +613,7 @@ class TestcaseForm(forms.ModelForm):
class PostForm(forms.ModelForm):
class Meta:
model = Post
- fields = ["title", "description", "image"]
+ fields = ["title", "description", "image", "anonymous"]
widgets = {
'title': forms.TextInput(
attrs={
@@ -636,7 +636,7 @@ class PostForm(forms.ModelForm):
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
- fields = ["description", "image"]
+ fields = ["description", "image", "anonymous"]
widgets = {
'description': forms.Textarea(
attrs={
diff --git a/yaksh/models.py b/yaksh/models.py
index b172e79..da2327c 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -42,6 +42,7 @@ from django.template import Context, Template
from django.conf import settings
from django.forms.models import model_to_dict
from django.db.models import Count
+
# Local Imports
from yaksh.code_server import (
submit, get_result as get_result_from_code_server
@@ -1101,6 +1102,25 @@ class Course(models.Model):
learning_units.extend(module.get_learning_units())
return learning_units
+ def get_lesson_posts(self):
+ learning_units = self.get_learning_units()
+ comments = []
+ for unit in learning_units:
+ if unit.lesson is not None:
+ lesson_ct = ContentType.objects.get_for_model(unit.lesson)
+ title = unit.lesson.name
+ try:
+ post = Post.objects.get(
+ target_ct=lesson_ct,
+ target_id=unit.lesson.id,
+ active=True, title=title
+ )
+ except Post.DoesNotExist:
+ post = None
+ if post is not None:
+ comments.append(post)
+ return comments
+
def remove_trial_modules(self):
learning_modules = self.learning_module.all()
for module in learning_modules:
@@ -2749,12 +2769,20 @@ class ForumBase(models.Model):
image = models.ImageField(upload_to=get_image_dir, blank=True,
null=True, validators=[validate_image])
active = models.BooleanField(default=True)
+ anonymous = models.BooleanField(default=False)
class Post(ForumBase):
title = models.CharField(max_length=200)
- course = models.ForeignKey(Course,
- on_delete=models.CASCADE, related_name='post')
+ target_ct = models.ForeignKey(ContentType,
+ blank=True,
+ null=True,
+ related_name='target_obj',
+ on_delete=models.CASCADE)
+ target_id = models.PositiveIntegerField(null=True,
+ blank=True,
+ db_index=True)
+ target = GenericForeignKey('target_ct', 'target_id')
def __str__(self):
return self.title
diff --git a/yaksh/static/yaksh/css/custom.css b/yaksh/static/yaksh/css/custom.css
index 26efbed..edb9530 100644
--- a/yaksh/static/yaksh/css/custom.css
+++ b/yaksh/static/yaksh/css/custom.css
@@ -109,12 +109,49 @@ body, .dropdown-menu {
FORUM STYLE
----------------------------------------------------- */
-.brown-light {
- background: #f4a460;
- padding-left: 0.3em;
- padding-right: 0.3em;
- padding-top: 0.2em;
- padding-bottom: 0.2em;
+ #wrapper {
+ overflow-x: hidden;
+ }
+
+#sidebar-wrapper {
+ min-height: 100vh;
+ margin-left: -15rem;
+ -webkit-transition: margin .25s ease-out;
+ -moz-transition: margin .25s ease-out;
+ -o-transition: margin .25s ease-out;
+ transition: margin .25s ease-out;
+}
+
+#sidebar-wrapper .sidebar-heading {
+ padding: 0.875rem 1.25rem;
+ font-size: 1.2rem;
+}
+
+#sidebar-wrapper .list-group {
+ width: 15rem;
+}
+
+#page-content-wrapper {
+ min-width: 100vw;
+}
+
+#wrapper.toggled #sidebar-wrapper {
+ margin-left: 0;
+}
+
+@media (min-width: 768px) {
+ #sidebar-wrapper {
+ margin-left: 0;
+ }
+
+ #page-content-wrapper {
+ min-width: 0;
+ width: 100%;
+ }
+
+ #wrapper.toggled #sidebar-wrapper {
+ margin-left: -15rem;
+ }
}
.post_image, .comment_image {
diff --git a/yaksh/static/yaksh/js/show_toc.js b/yaksh/static/yaksh/js/show_toc.js
index b628eaa..7d9b68e 100644
--- a/yaksh/static/yaksh/js/show_toc.js
+++ b/yaksh/static/yaksh/js/show_toc.js
@@ -2,6 +2,24 @@ $(document).ready(function() {
$('#sidebarCollapse').on('click', function () {
$('#sidebar').toggleClass('active');
});
+
+ $(document).ready(() => {
+ $(function() {
+ tinymce.init({
+ selector: 'textarea#id_description',
+ setup : function(ed) {
+ ed.on('change', function(e) {
+ tinymce.triggerSave();
+ });
+ },
+ max_height: 400,
+ height: 400,
+ plugins: "image code link",
+ convert_urls: false
+ });
+ });
+ });
+
player = new Plyr('#player');
var totalSeconds;
store_video_time(contents_by_time);
diff --git a/yaksh/templates/yaksh/course_forum.html b/yaksh/templates/yaksh/course_forum.html
index 4724981..acd6861 100644
--- a/yaksh/templates/yaksh/course_forum.html
+++ b/yaksh/templates/yaksh/course_forum.html
@@ -4,132 +4,169 @@
{{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">
- <i class="fa fa-arrow-left"></i>&nbsp;Back
- </a>
- {% else %}
- <a href="{% url 'yaksh:course_modules' course.id %}" class="btn btn-primary">
- <i class="fa fa-arrow-left"></i>&nbsp;Back
- </a>
- {% endif %}
+ <div id="wrapper" class="d-flex">
+ {% include "yaksh/sidebar.html" %}
+ <div class="container" id="page-content-wrapper">
+ <div>
+ <h2><center>{{course.name}}</center></h2>
+ <center>Discussion Forum</center>
</div>
- <div class="col-md">
- <button type="button" class="btn btn-success pull-right" data-toggle="modal" data-target="#newPostModal">
- <i class="fa fa-plus-circle"></i>&nbsp;New Post
- </button>
+ <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">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
+ {% else %}
+ <a href="{% url 'yaksh:course_modules' course.id %}" class="btn btn-primary">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
+ {% endif %}
+ </div>
+ <div class="col-md">
+ <button type="button" class="btn btn-success pull-right" data-toggle="modal" data-target="#newPostModal">
+ <i class="fa fa-plus-circle"></i>&nbsp;New Post
+ </button>
+ </div>
</div>
- </div>
- <!-- Modal -->
- <div id="newPostModal" class="modal fade" role="dialog">
- <div class="modal-dialog">
+ {% if messages %}
+ <div>
+ <center>
+ {% for message in messages %}
+ <div class="alert alert-dismissible alert-{{ message.tags }}">
+ <button type="button" class="close" data-dismiss="alert">
+ <i class="fa fa-close"></i>
+ </button>
+ <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+ </center>
+ </div>
+ {% endif %}
+ <!-- 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>
+ <!-- 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>
-
- </div>
- </div>
- <br>
- <div class="row justify-content-center">
- <div class="col-md-6">
- <form class="my-2 my-lg-0" action="" method="GET">
- <div class="input-group">
- <input type="search" placeholder="Search Post" name="search_post" class="form-control">
- <span class="input-group-append">
- <button class="btn btn-outline-info">
- <i class="fa fa-search"></i>&nbsp;Search
- </button>
- </span>
+ <br>
+ {% with objects as posts %}
+ {% if posts %}
+ <div class="row justify-content-center">
+ <div class="col-md-6">
+ <form class="my-2 my-lg-0" action="" method="GET">
+ <div class="input-group">
+ <input type="search" placeholder="Search Post" name="search_post" class="form-control">
+ <span class="input-group-append">
+ <button class="btn btn-outline-info">
+ <i class="fa fa-search"></i>&nbsp;Search
+ </button>
+ </span>
+ </div>
+ </form>
+ </div>
+ <div class="col-md-4">
+ <a class="btn btn-outline-danger" href="{% url 'yaksh:course_forum' course.id %}">
+ <i class="fa fa-times"></i>&nbsp;Clear Search
+ </a>
+ </div>
</div>
- </form>
- </div>
- <div class="col-md-4">
- <a class="btn btn-outline-danger" href="{% url 'yaksh:course_forum' course.id %}">
- <i class="fa fa-times"></i>&nbsp;Clear Search
- </a>
- </div>
+ <br>
+ {% include "yaksh/paginator.html" %}
+ <br>
+ <table id="posts_table" class="tablesorter table">
+ <thead class="thread-inverse">
+ <tr>
+ <th width="700">Posts</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>
+ {% if post.anonymous %}
+ Anonymous
+ {% else %}
+ {{post.creator.username}}
+ {% endif %}
+ </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 == course.creator %}
+ <small><a href="{% url 'yaksh:hide_post' course.id post.uid %}" class="pull-right fa fa-trash"></i></a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <br>
+ {% include "yaksh/paginator.html" %}
+ {% else %}
+ <center>No discussion posts are there yet. Create one to start discussing.</center>
+ {% endif %}
+ {% endwith %}
</div>
- <br>
- {% with objects as posts %}
- {% if posts %}
- {% include "yaksh/paginator.html" %}
- <br>
- <table id="posts_table" class="tablesorter table">
- <thead class="thread-inverse">
- <tr>
- <th width="700">Posts</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>
- <br>
- {% include "yaksh/paginator.html" %}
- {% else %}
- No discussion posts are there yet. Create one to start discussing.
- {% endif %}
- {% endwith %}
</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" src="{% static 'yaksh/js/tinymce/js/tinymce/tinymce.min.js' %}"></script>
<script type="text/javascript">
$(document).ready(() => {
$("#posts_table").tablesorter();
+ $(function() {
+ tinymce.init({
+ selector: 'textarea#id_description',
+ setup : function(ed) {
+ ed.on('change', function(e) {
+ tinymce.triggerSave();
+ });
+ },
+ max_height: 400,
+ height: 400,
+ plugins: "image code link",
+ convert_urls: false
+ });
+ });
});
</script>
{% endblock script %} \ No newline at end of file
diff --git a/yaksh/templates/yaksh/lessons_forum.html b/yaksh/templates/yaksh/lessons_forum.html
new file mode 100644
index 0000000..250536d
--- /dev/null
+++ b/yaksh/templates/yaksh/lessons_forum.html
@@ -0,0 +1,80 @@
+{% extends base_template %}
+{% load static %}
+{% block title %}
+ {{course.name}}: Lessons Forum
+{% endblock title %}
+{% block content %}
+ <div id="wrapper" class="d-flex">
+ {% include "yaksh/sidebar.html" %}
+ <div class="container" id="page-content-wrapper">
+ <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">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
+ {% else %}
+ <a href="{% url 'yaksh:course_modules' course.id %}" class="btn btn-primary">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ {% if posts %}
+ <div>
+ <h2><center>{{course.name}}</center></h2>
+ <center>Discussion Forum</center>
+ </div>
+ <table class="table">
+ <thead class="thread-inverse">
+ <tr>
+ <th width="700">Posts</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>
+ {% if post.anonymouse %}
+ Anonymous
+ {% else %}
+ {{post.creator.username}}
+ {% endif %}
+ </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 == course.creator %}
+ <small><a href="{% url 'yaksh:hide_post' course.id post.uid %}" class="pull-right fa fa-trash"></i></a></small>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {% else %}
+ <div>
+ <center>No Lesson posts here. They will appear when someone comments on video lessons.</center>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+{% endblock content %}
+
diff --git a/yaksh/templates/yaksh/post_comments.html b/yaksh/templates/yaksh/post_comments.html
index b16b80c..bc452e0 100644
--- a/yaksh/templates/yaksh/post_comments.html
+++ b/yaksh/templates/yaksh/post_comments.html
@@ -6,24 +6,44 @@
{% block content %}
<div class="container">
- <a class="btn btn-primary" href="{% url 'yaksh:course_forum' post.course.id %}">
+ <a class="btn btn-primary" href="{% url 'yaksh:course_forum' course.id %}">
<i class="fa fa-arrow-left"></i>&nbsp;Back to Posts
</a>
<br>
<br>
+ {% if messages %}
+ <div>
+ <center>
+ {% for message in messages %}
+ <div class="alert alert-dismissible alert-{{ message.tags }}">
+ <button type="button" class="close" data-dismiss="alert">
+ <i class="fa fa-close"></i>
+ </button>
+ <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+ </center>
+ </div>
+ {% endif %}
<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>
+ <strong>
+ {% if post.anonymous %}
+ Anonymous
+ {% else %}
+ {{post.creator.username}}
+ {% endif %}
+ </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 %}
+ {% if user == course.creator %}<a href="{% url 'yaksh:hide_post' post.target.id post.uid %}" class="pull-right fa fa-trash"></a>{% endif %}
</small>
</div>
<div class="card-body">
- <p class="card-text description">{{post.description}}</p>
+ <p class="card-text description">{{post.description|safe}}</p>
{% if post.image %}
<a href="{{post.image.url}}" target="_blank">
<center><img src="{{post.image.url}}" class="post_image thumbnail" alt=""></center>
@@ -31,6 +51,16 @@
{% endif %}
</div>
</div>
+ <div>
+ <b><u>Add comment:</u></b>
+ <form action="" method="POST" enctype='multipart/form-data'>
+ <div class="form-group">
+ {% csrf_token %}
+ {{form}}
+ </div>
+ <input type="submit" value="Submit" class="btn btn-success">
+ </form>
+ </div>
<br>
{% if comments %}
{% for comment in comments %}
@@ -38,13 +68,19 @@
<div class="card-body p-3">
<div class="row mb-3">
<div class="col-6">
- <strong class="text-muted">{{comment.creator.username}}</strong>
+ <strong class="text-muted">
+ {% if comment.anonymous %}
+ Anonymous
+ {% else %}
+ {{comment.creator.username}}
+ {% endif %}
+ </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>
+ <small class="text-muted">{{comment.created_at}} {% if user == course.creator %} <a href="{% url 'yaksh:hide_comment' post.target.id comment.uid %}" class="fa fa-trash"></a>{% endif %}</small>
</div>
</div>
- <p class="card-text description">{{comment.description}}</p>
+ <p class="card-text description">{{comment.description|safe}}</p>
<div>
{% if comment.image %}
<a href="{{comment.image.url}}" target="_blank">
@@ -57,18 +93,27 @@
{% endfor %}
{% endif %}
<br>
- <div>
- <b><u>Add comment:</u></b>
- <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-success">
- </form>
- </div>
</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/tinymce/js/tinymce/tinymce.min.js' %}"></script>
+ <script type="text/javascript">
+ $(document).ready(() => {
+ $(function() {
+ tinymce.init({
+ selector: 'textarea#id_description',
+ setup : function(ed) {
+ ed.on('change', function(e) {
+ tinymce.triggerSave();
+ });
+ },
+ max_height: 400,
+ height: 400,
+ plugins: "image code link",
+ convert_urls: false
+ });
+ });
+ });
+ </script>
{% endblock script %} \ No newline at end of file
diff --git a/yaksh/templates/yaksh/show_video.html b/yaksh/templates/yaksh/show_video.html
index 9e9d0b4..6e3cabb 100644
--- a/yaksh/templates/yaksh/show_video.html
+++ b/yaksh/templates/yaksh/show_video.html
@@ -16,6 +16,7 @@
<script src="{% static 'yaksh/js/jquery-sortable.js' %}"></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/tinymce/js/tinymce/tinymce.min.js' %}"></script>
{% endblock %}
{% block css %}
<link rel="stylesheet" href="{% static 'yaksh/css/jquery-ui/jquery-ui.css' %}">
@@ -227,6 +228,49 @@
</div>
</div>
{% endif %}
+ {% if state == 'lesson' %}
+ <div>
+ <b><u>Add comment:</u></b>
+ <form action="" method="POST" enctype='multipart/form-data'>
+ <div class="form-group">
+ {% csrf_token %}
+ {{form}}
+ </div>
+ <input type="submit" value="Submit" class="btn btn-success">
+ </form>
+ </div>
+ {% endif %}
+ <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">
+ {% if comment.anonymous %}
+ Anonymous
+ {% else %}
+ {{comment.creator.username}}
+ {% endif %}
+ </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' course.id comment.uid %}" class="fa fa-trash"></a>{% endif %}</small>
+ </div>
+ </div>
+ <p class="card-text description">{{comment.description|safe}}</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 %}
</div>
</div>
<div id="dialog"></div>
diff --git a/yaksh/templates/yaksh/sidebar.html b/yaksh/templates/yaksh/sidebar.html
new file mode 100644
index 0000000..7d0ac74
--- /dev/null
+++ b/yaksh/templates/yaksh/sidebar.html
@@ -0,0 +1,8 @@
+<!-- Sidebar -->
+<div class="bg-light border-right" id="sidebar-wrapper">
+ <div class="list-group list-group-flush">
+ <a href="{% url 'yaksh:course_forum' course.id %}" class="list-group-item list-group-item-action {% if '/course_forum/' in request.path %}active{% endif %}">Course Forum</a>
+ <a href="{% url 'yaksh:lessons_forum' course.id %}" class="list-group-item list-group-item-action {% if '/lessons_forum/' in request.path %}active{% endif %}">Lessons Forum</a>
+ </div>
+</div>
+<!-- /#sidebar-wrapper -->
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index cd8776b..67da7d1 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -1,5 +1,6 @@
import unittest
from django.contrib.auth.models import Group
+from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\
@@ -2431,9 +2432,10 @@ class PostModelTestCases(unittest.TestCase):
enrollment='Enroll Request',
creator=self.user3
)
+ course_ct = ContentType.objects.get_for_model(self.course)
self.post1 = Post.objects.create(
title='Post 1',
- course=self.course,
+ target_ct=course_ct, target_id=self.course.id,
creator=self.user1,
description='Post 1 description'
)
@@ -2456,56 +2458,9 @@ class PostModelTestCases(unittest.TestCase):
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()
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index 9cb454e..e7bbd91 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -22,6 +22,7 @@ from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.files import File
from django.contrib.messages import get_messages
+from django.contrib.contenttypes.models import ContentType
from celery.contrib.testing.worker import start_worker
from django.test import SimpleTestCase
@@ -6807,7 +6808,7 @@ class TestPost(TestCase):
})
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
- redirection_url = '/exam/login/?next=/exam/forum/{0}/'.format(
+ redirection_url = '/exam/login/?next=/exam/forum/course_forum/{0}/'.format(
str(self.course.id)
)
self.assertRedirects(response, redirection_url)
@@ -6838,7 +6839,7 @@ class TestPost(TestCase):
self.assertEquals(response.status_code, 404)
def test_course_forum_url_resolves_course_forum_view(self):
- view = resolve('/exam/forum/1/')
+ view = resolve('/exam/forum/course_forum/1/')
self.assertEqual(view.func, course_forum)
def test_course_forum_contains_link_to_post_comments_page(self):
@@ -6851,10 +6852,11 @@ class TestPost(TestCase):
url = reverse('yaksh:course_forum', kwargs={
'course_id': self.course.id
})
+ course_ct = ContentType.objects.get_for_model(self.course)
post = Post.objects.create(
title='post 1',
description='post 1 description',
- course=self.course,
+ target_ct=course_ct, target_id=self.course.id,
creator=self.student
)
response = self.client.get(url)
@@ -6879,9 +6881,11 @@ class TestPost(TestCase):
}
response = self.client.post(url, data)
# This shouldn't be 302. Check where does it redirects.
+ course_ct = ContentType.objects.get_for_model(self.course)
result = Post.objects.filter(title='Post 1',
creator=self.student,
- course=self.course)
+ target_ct=course_ct,
+ target_id=self.course.id)
self.assertTrue(result.exists())
def test_new_post_invalid_post_data(self):
@@ -6914,24 +6918,12 @@ class TestPost(TestCase):
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):
+ course_ct = ContentType.objects.get_for_model(self.course)
post = Post.objects.create(
title='post 1',
description='post 1 description',
- course=self.course,
+ target_ct=course_ct, target_id=self.course.id,
creator=self.student
)
url = reverse('yaksh:post_comments', kwargs={
@@ -6970,11 +6962,12 @@ class TestPost(TestCase):
password=self.user_plaintext_pass
)
self.course.students.add(self.user)
+ course_ct = ContentType.objects.get_for_model(self.course)
post = Post.objects.create(
title='post 1',
description='post 1 description',
- course=self.course,
- creator=self.user
+ target_ct=course_ct, target_id=self.course.id,
+ creator=self.student
)
url = reverse('yaksh:hide_post', kwargs={
'course_id': self.course.id,
@@ -7037,10 +7030,11 @@ class TestPostComment(TestCase):
enrollment="Enroll Request", creator=self.user
)
+ course_ct = ContentType.objects.get_for_model(self.course)
self.post = Post.objects.create(
title='post 1',
description='post 1 description',
- course=self.course,
+ target_ct=course_ct, target_id=self.course.id,
creator=self.student
)
diff --git a/yaksh/urls.py b/yaksh/urls.py
index b60b5f5..82785ca 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -60,9 +60,11 @@ 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+)/$',
+ url(r'^forum/course_forum/(?P<course_id>\d+)/$',
views.course_forum,
name='course_forum'),
+ url(r'^forum/lessons_forum/(?P<course_id>\d+)/$',
+ views.lessons_forum, name='lessons_forum'),
url(r'^forum/(?P<course_id>\d+)/post/(?P<uuid>[0-9a-f-]+)/$',
views.post_comments,
name='post_comments'),
diff --git a/yaksh/views.py b/yaksh/views.py
index 73979da..69a7414 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -10,6 +10,7 @@ from django.db import models
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Group
+from django.contrib.contenttypes.models import ContentType
from django.forms.models import inlineformset_factory
from django.forms import fields
from django.utils import timezone
@@ -2803,11 +2804,40 @@ def show_lesson(request, lesson_id, module_id, course_id):
if not learn_unit.is_prerequisite_complete(user, learn_module, course):
msg = "You have not completed previous Lesson/Quiz/Exercise"
return view_module(request, learn_module.id, course_id, msg=msg)
+
+ lesson_ct = ContentType.objects.get_for_model(learn_unit.lesson)
+ title = learn_unit.lesson.name
+ try:
+ post = Post.objects.get(
+ target_ct=lesson_ct, target_id=learn_unit.lesson.id,
+ active=True, title=title
+ )
+ except Post.DoesNotExist:
+ post = Post.objects.create(
+ target_ct=lesson_ct, target_id=learn_unit.lesson.id,
+ active=True, title=title, creator=user,
+ description=f'Discussion on {title} lesson',
+ )
+ 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.anonymous = request.POST.get('anonymous', '') == 'on'
+ new_comment.save()
+ return redirect(request.path_info)
+ else:
+ raise Http404(f'Post does not exist for lesson {title}')
+ else:
+ form = CommentForm()
+ comments = post.comment.filter(active=True)
context = {'lesson': learn_unit.lesson, 'user': user,
'course': course, 'state': "lesson", "all_modules": all_modules,
'learning_units': learning_units, "current_unit": learn_unit,
'learning_module': learn_module, 'toc': toc,
- 'contents_by_time': contents_by_time}
+ 'contents_by_time': contents_by_time,
+ 'comments': comments, 'form': form, 'post': post}
return my_render_to_response(request, 'yaksh/show_video.html', context)
@@ -3447,17 +3477,22 @@ def course_forum(request, course_id):
base_template = 'manage.html'
moderator = True
course = get_object_or_404(Course, id=course_id)
+ course_ct = ContentType.objects.get_for_model(course)
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))
search_term = request.GET.get('search_post')
if search_term:
- posts = course.post.get_queryset().filter(
- active=True, title__icontains=search_term)
+ posts = Post.objects.filter(
+ Q(title__icontains=search_term) |
+ Q(description__icontains=search_term),
+ target_ct=course_ct, target_id=course.id, active=True
+ )
else:
- posts = course.post.get_queryset().filter(
- active=True).order_by('-modified_at')
- paginator = Paginator(posts, 1)
+ posts = Post.objects.filter(
+ target_ct=course_ct, target_id=course.id, active=True
+ ).order_by('-modified_at')
+ paginator = Paginator(posts, 10)
page = request.GET.get('page')
posts = paginator.get_page(page)
if request.method == "POST":
@@ -3465,7 +3500,8 @@ def course_forum(request, course_id):
if form.is_valid():
new_post = form.save(commit=False)
new_post.creator = user
- new_post.course = course
+ new_post.target = course
+ new_post.anonymous = request.POST.get('anonymous', '') == 'on'
new_post.save()
return redirect('yaksh:post_comments',
course_id=course.id, uuid=new_post.uid)
@@ -3484,6 +3520,27 @@ def course_forum(request, course_id):
@login_required
@email_verified
+def lessons_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)
+ course_ct = ContentType.objects.get_for_model(course)
+ lesson_posts = course.get_lesson_posts()
+ return render(request, 'yaksh/lessons_forum.html', {
+ 'user': user,
+ 'base_template': base_template,
+ 'moderator': moderator,
+ 'course': course,
+ 'posts': lesson_posts,
+ })
+
+
+@login_required
+@email_verified
def post_comments(request, course_id, uuid):
user = request.user
base_template = 'user.html'
@@ -3502,6 +3559,7 @@ def post_comments(request, course_id, uuid):
new_comment = form.save(commit=False)
new_comment.creator = request.user
new_comment.post_field = post
+ new_comment.anonymous = request.POST.get('anonymous', '') == 'on'
new_comment.save()
return redirect(request.path_info)
return render(request, 'yaksh/post_comments.html', {
@@ -3509,7 +3567,8 @@ def post_comments(request, course_id, uuid):
'comments': comments,
'base_template': base_template,
'form': form,
- 'user': user
+ 'user': user,
+ 'course': course
})
@@ -3519,7 +3578,9 @@ 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))
+ raise Http404(
+ 'Only a course creator or a teacher can delete the post.'
+ )
post = get_object_or_404(Post, uid=uuid)
post.comment.active = False
post.active = False
@@ -3531,9 +3592,12 @@ def hide_post(request, course_id, uuid):
@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))
+ if course_id:
+ course = get_object_or_404(Course, id=course_id)
+ if (not course.is_creator(user) and not course.is_teacher(user)):
+ raise Http404(
+ 'Only a course creator or a teacher can delete the comments'
+ )
comment = get_object_or_404(Comment, uid=uuid)
post_uid = comment.post_field.uid
comment.active = False