summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--CHANGELOG.txt11
-rw-r--r--README.rst64
-rw-r--r--README_production.rst30
-rw-r--r--grades/templates/add_grades.html18
-rw-r--r--grades/templates/grading_systems.html18
-rw-r--r--online_test/__init__.py2
-rw-r--r--online_test/settings.py1
-rw-r--r--requirements/requirements-common.txt1
-rw-r--r--yaksh/admin.py4
-rw-r--r--yaksh/documentation/conf.py6
-rw-r--r--yaksh/documentation/images/add_exercise.jpgbin56803 -> 28485 bytes
-rw-r--r--yaksh/documentation/images/add_lesson.jpgbin74708 -> 39153 bytes
-rw-r--r--yaksh/documentation/images/add_module.jpgbin0 -> 31048 bytes
-rw-r--r--yaksh/documentation/images/add_question.jpgbin72155 -> 63387 bytes
-rw-r--r--yaksh/documentation/images/add_quiz.jpgbin110571 -> 73655 bytes
-rw-r--r--yaksh/documentation/images/bash_standard_testcase.jpgbin25121 -> 20440 bytes
-rw-r--r--yaksh/documentation/images/course_details_features.jpgbin104513 -> 46197 bytes
-rw-r--r--yaksh/documentation/images/course_features.jpgbin85643 -> 41292 bytes
-rw-r--r--yaksh/documentation/images/course_modules.jpgbin0 -> 57662 bytes
-rw-r--r--yaksh/documentation/images/cpp_standard_testcase.jpgbin35558 -> 36713 bytes
-rw-r--r--yaksh/documentation/images/create_course.jpgbin83748 -> 48943 bytes
-rw-r--r--yaksh/documentation/images/design_course.jpgbin68198 -> 75429 bytes
-rw-r--r--yaksh/documentation/images/design_module.jpgbin75652 -> 55246 bytes
-rw-r--r--yaksh/documentation/images/design_questionpaper.jpgbin89567 -> 62444 bytes
-rw-r--r--yaksh/documentation/images/embed_video.jpgbin15227 -> 11253 bytes
-rw-r--r--yaksh/documentation/images/float_testcase.jpgbin63320 -> 15697 bytes
-rw-r--r--yaksh/documentation/images/hook_testcase.jpgbin63401 -> 28047 bytes
-rw-r--r--yaksh/documentation/images/integer_testcase.jpgbin71280 -> 21616 bytes
-rw-r--r--yaksh/documentation/images/java_standard_testcase.jpgbin33812 -> 39025 bytes
-rw-r--r--yaksh/documentation/images/mcc_testcase.jpgbin33782 -> 36265 bytes
-rw-r--r--yaksh/documentation/images/mcq_testcase.jpgbin30914 -> 36252 bytes
-rw-r--r--yaksh/documentation/images/moderator_dashboard.jpgbin99088 -> 42100 bytes
-rw-r--r--yaksh/documentation/images/python_standard_testcase.jpgbin28411 -> 19746 bytes
-rw-r--r--yaksh/documentation/images/questions.jpgbin101605 -> 66138 bytes
-rw-r--r--yaksh/documentation/images/r_standard_testcase.jpgbin0 -> 42217 bytes
-rw-r--r--yaksh/documentation/images/scilab_standard_testcase.jpgbin0 -> 34651 bytes
-rw-r--r--yaksh/documentation/images/stdio_testcase.jpgbin29389 -> 20130 bytes
-rw-r--r--yaksh/documentation/images/string_testcase.jpgbin66369 -> 14758 bytes
-rw-r--r--yaksh/documentation/images/view_lessons.jpgbin61713 -> 0 bytes
-rw-r--r--yaksh/documentation/images/view_modules.jpgbin63372 -> 0 bytes
-rw-r--r--yaksh/documentation/images/view_quizzes.jpgbin72866 -> 0 bytes
-rw-r--r--yaksh/documentation/installation.rst61
-rw-r--r--yaksh/documentation/moderator_dashboard.rst15
-rw-r--r--yaksh/documentation/moderator_docs/creating_course.rst170
-rw-r--r--yaksh/documentation/moderator_docs/creating_lessons_modules.rst81
-rw-r--r--yaksh/documentation/moderator_docs/creating_question.rst135
-rw-r--r--yaksh/documentation/moderator_docs/creating_quiz.rst109
-rw-r--r--yaksh/documentation/moderator_docs/other_features.rst17
-rw-r--r--yaksh/evaluator_tests/test_r_evaluation.py197
-rw-r--r--yaksh/forms.py120
-rw-r--r--yaksh/middleware/one_session_per_user.py15
-rw-r--r--yaksh/middleware/user_time_zone.py11
-rw-r--r--yaksh/migrations/0017_release_0_14_0.py18
-rw-r--r--yaksh/models.py64
-rw-r--r--yaksh/r_code_evaluator.py17
-rw-r--r--yaksh/static/yaksh/css/custom.css21
-rw-r--r--yaksh/static/yaksh/js/codemirror/mode/r/index.html85
-rw-r--r--yaksh/static/yaksh/js/codemirror/mode/r/r.js164
-rw-r--r--yaksh/static/yaksh/js/design_course.js10
-rw-r--r--yaksh/static/yaksh/js/question_filter.js47
-rw-r--r--yaksh/static/yaksh/js/requesthandler.js3
-rw-r--r--yaksh/static/yaksh/js/show_question.js17
-rw-r--r--yaksh/templates/yaksh/add_course.html18
-rw-r--r--yaksh/templates/yaksh/add_exercise.html14
-rw-r--r--yaksh/templates/yaksh/add_lesson.html10
-rw-r--r--yaksh/templates/yaksh/add_module.html5
-rw-r--r--yaksh/templates/yaksh/add_question.html30
-rw-r--r--yaksh/templates/yaksh/add_quiz.html14
-rw-r--r--yaksh/templates/yaksh/ajax_question_filter.html57
-rw-r--r--yaksh/templates/yaksh/course_added_modules.html141
-rw-r--r--yaksh/templates/yaksh/course_detail_options.html19
-rw-r--r--yaksh/templates/yaksh/course_forum.html115
-rw-r--r--yaksh/templates/yaksh/course_modules.html9
-rw-r--r--yaksh/templates/yaksh/courses.html35
-rw-r--r--yaksh/templates/yaksh/design_course_session.html8
-rw-r--r--yaksh/templates/yaksh/design_questionpaper.html9
-rw-r--r--yaksh/templates/yaksh/paginator.html7
-rw-r--r--yaksh/templates/yaksh/post_comments.html71
-rw-r--r--yaksh/templates/yaksh/question.html1
-rw-r--r--yaksh/templates/yaksh/showquestions.html373
-rw-r--r--yaksh/test_models.py131
-rw-r--r--yaksh/test_views.py785
-rw-r--r--yaksh/urls.py61
-rw-r--r--yaksh/views.py534
85 files changed, 2829 insertions, 1152 deletions
diff --git a/.travis.yml b/.travis.yml
index 1e735b2..daf3773 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,6 +9,7 @@ services:
before_install:
- sudo apt-get update -qq
- sudo apt-get install -y scilab
+ - sudo apt-get install -y r-base
- export DISPLAY=:99.0
# command to install dependencies
@@ -18,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/CHANGELOG.txt b/CHANGELOG.txt
index 18063e5..68290e7 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,14 @@
+=== 0.14.0 (07-04-2020) ===
+
+* Added search bar for searching course
+* Added codemirror for R language
+* Fixed a bug which did not allow to save the empty expected input field for
+Standard input output based test case
+* Improved UX for creating course modules, lessons, quizzes and practice exercises
+* Fixed bug in the custom middleware to get proper user timezone and manage
+concurrent user logins
+* Prettify error message for R language
+
=== 0.13.0 (11-03-2020) ===
* Revamped the UI and UX for the interface and standardised with Bootstrap 4 (Lumen theme from https://bootswatch.com/)
diff --git a/README.rst b/README.rst
index 9361243..c730fdf 100644
--- a/README.rst
+++ b/README.rst
@@ -29,41 +29,65 @@ Features
- Scales to over 500+ simultaneous users.
- Distributed under the BSD license.
-Quick Start
-===========
+Requirements
+============
+
+Python 3.6, 3.7, 3.8
-Pre-Requisites
-^^^^^^^^^^^^^^
+Django 3.0.3
-1. Ensure you have Python available.
-2. Ensure `pip <https://pip.pypa.io/en/latest/installing.html>`__ is
- installed.
Installation
-^^^^^^^^^^^^
+============
-1. Install yaksh
+**Note**: Currently, only Linux and MacOS is supported for the project.
- - Clone the repository
+If Python 3.6 and above is not available in the system, then we recommend using
+miniconda. Download miniconda with Python 3.6 and above.
- ::
+**Installing Miniconda**
- $ git clone https://github.com/FOSSEE/online_test.git
+1. Download miniconda from https://docs.conda.io/en/latest/miniconda.html according to the OS version.
- - Go to the online\_test directory
+2. Follow the installation instructions as given in https://conda.io/projects/conda/en/latest/user-guide/install/index.html#regular-installation
- ::
+3. Restart the Terminal.
+
+**Pre-Requisite**
+
+* Ensure `pip <https://pip.pypa.io/en/latest/installing.html>`_ is installed
+
+**Installing Yaksh**
+
+* **Clone the repository**
+
+ ::
- $ cd ./online_test
+ git clone https://github.com/FOSSEE/online_test.git
- - Install the dependencies for local setup
+* **Go to the online_test directory**
- ::
+ ::
- $ pip install -r ./requirements/requirements-common.txt
+ cd online_test
-Short instructions
-^^^^^^^^^^^^^^^^^^
+* **Install the dependencies**:
+
+ * Install Django and dependencies
+
+ ::
+
+ pip3 install -r requirements/requirements-common.txt
+
+ * Install Code Server dependencies
+
+ ::
+
+ sudo pip3 install -r requirements/requirements-codeserver.txt
+
+
+Quick Start
+^^^^^^^^^^^
1. Start up the code server that executes the user code safely:
diff --git a/README_production.rst b/README_production.rst
index 1126c41..b29fa4f 100644
--- a/README_production.rst
+++ b/README_production.rst
@@ -5,6 +5,15 @@ This README provides documentation to help deploy Yaksh in a production
environment. If you wish to take Yaksh on a trial run, here is a
`Quickstart Guide <https://github.com/FOSSEE/online\_test/blob/master/README.rst>`__
+
+Requirements
+============
+
+Python 3.6, 3.7, 3.8
+
+Django 3.0.3
+
+
###################
Deploying Locally
###################
@@ -50,16 +59,26 @@ To install this app follow the steps below:
::
- $ git clone https://github.com/FOSSEE/online_test.git
- $ cd online_test
+ git clone https://github.com/FOSSEE/online_test.git
+
+ ::
+
+ cd online_test
2. Install Yaksh dependencies, Run
::
- pip install -r requirements/requirements-py2.txt # For Python 2
+ pip3 install -r requirements/requirements-common.txt
+
+ ::
+
+ pip3 install -r requirements/requirements-py3.txt
+
+ ::
+
+ sudo pip3 install -r requirements/requirements-codeserver.txt
- pip3 install -r requirements/requirements-py3.txt # For Python 3
3. Rename the ``.sampleenv`` to ``.env``
@@ -85,9 +104,6 @@ To install this app follow the steps below:
::
- $ sudo python -m yaksh.code_server # For Python 2.x
-
-
$ sudo python3 -m yaksh.code_server # For Python 3.x
Put this in the background once it has started since this will not
diff --git a/grades/templates/add_grades.html b/grades/templates/add_grades.html
index f53a337..198eb4b 100644
--- a/grades/templates/add_grades.html
+++ b/grades/templates/add_grades.html
@@ -17,22 +17,10 @@
Add/Edit Course
</a>
</li>
- <li class="nav-item dropdown hide">
- <a class="nav-link dropdown-toggle active" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="true">More</a>
- <div class="dropdown-menu hide" x-placement="bottom-start" style="position: absolute; transform: translate3d(0px, 37px, 0px); top: 0px; left: 0px; will-change: transform;">
- <a class="dropdown-item" href="{% url 'yaksh:show_all_quizzes' %}">
- View Quizzes
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_lessons' %}">
- View Lessons
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_modules' %}">
- View Modules
- </a>
- <a href="{% url 'grades:grading_systems'%}" class="dropdown-item active" >
- View Grading Systems
+ <li class="nav-item">
+ <a href="{% url 'grades:grading_systems'%}" class="active nav-link" >
+ Add/View Grading Systems
</a>
- </div>
</li>
</ul>
</div>
diff --git a/grades/templates/grading_systems.html b/grades/templates/grading_systems.html
index 88adfa0..46b3d30 100644
--- a/grades/templates/grading_systems.html
+++ b/grades/templates/grading_systems.html
@@ -16,22 +16,10 @@
Add/Edit Course
</a>
</li>
- <li class="nav-item dropdown hide">
- <a class="nav-link dropdown-toggle active" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="true">More</a>
- <div class="dropdown-menu hide" x-placement="bottom-start" style="position: absolute; transform: translate3d(0px, 37px, 0px); top: 0px; left: 0px; will-change: transform;">
- <a class="dropdown-item" href="{% url 'yaksh:show_all_quizzes' %}">
- View Quizzes
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_lessons' %}">
- View Lessons
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_modules' %}">
- View Modules
- </a>
- <a href="{% url 'grades:grading_systems'%}" class="dropdown-item active" >
- View Grading Systems
+ <li class="nav-item">
+ <a href="{% url 'grades:grading_systems'%}" class="active nav-link" >
+ Add/View Grading Systems
</a>
- </div>
</li>
</ul>
</div>
diff --git a/online_test/__init__.py b/online_test/__init__.py
index 2d7893e..ef91994 100644
--- a/online_test/__init__.py
+++ b/online_test/__init__.py
@@ -1 +1 @@
-__version__ = '0.13.0'
+__version__ = '0.14.0'
diff --git a/online_test/settings.py b/online_test/settings.py
index 284ec64..565b7b7 100644
--- a/online_test/settings.py
+++ b/online_test/settings.py
@@ -169,6 +169,7 @@ TEMPLATES = [
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
'django.contrib.messages.context_processors.messages',
+ 'django.template.context_processors.request',
],
'debug': True, # make this False in production
}
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt
index ff5d2f8..ca0ea4f 100644
--- a/requirements/requirements-common.txt
+++ b/requirements/requirements-common.txt
@@ -12,3 +12,4 @@ markdown==2.6.9
pygments==2.2.0
djangorestframework==3.11.0
django-cors-headers==3.1.0
+Pillow
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/documentation/conf.py b/yaksh/documentation/conf.py
index 39481c7..627217f 100644
--- a/yaksh/documentation/conf.py
+++ b/yaksh/documentation/conf.py
@@ -59,7 +59,7 @@ master_doc = 'index'
# General information about the project.
project = u'Yaksh'
-copyright = u'2018, FOSSEE'
+copyright = u'2020, FOSSEE'
author = u'FOSSEE'
# The version info for the project you're documenting, acts as replacement for
@@ -67,9 +67,9 @@ author = u'FOSSEE'
# built documents.
#
# The short X.Y version.
-version = u'0.7'
+version = u'0.13'
# The full version, including alpha/beta/rc tags.
-release = u'Feb 2018'
+release = u'April 2020'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/yaksh/documentation/images/add_exercise.jpg b/yaksh/documentation/images/add_exercise.jpg
index 2512f1a..47e7c49 100644
--- a/yaksh/documentation/images/add_exercise.jpg
+++ b/yaksh/documentation/images/add_exercise.jpg
Binary files differ
diff --git a/yaksh/documentation/images/add_lesson.jpg b/yaksh/documentation/images/add_lesson.jpg
index 6de272c..819d035 100644
--- a/yaksh/documentation/images/add_lesson.jpg
+++ b/yaksh/documentation/images/add_lesson.jpg
Binary files differ
diff --git a/yaksh/documentation/images/add_module.jpg b/yaksh/documentation/images/add_module.jpg
new file mode 100644
index 0000000..5f3f890
--- /dev/null
+++ b/yaksh/documentation/images/add_module.jpg
Binary files differ
diff --git a/yaksh/documentation/images/add_question.jpg b/yaksh/documentation/images/add_question.jpg
index b9b5bc7..3a4c73e 100644
--- a/yaksh/documentation/images/add_question.jpg
+++ b/yaksh/documentation/images/add_question.jpg
Binary files differ
diff --git a/yaksh/documentation/images/add_quiz.jpg b/yaksh/documentation/images/add_quiz.jpg
index 3264684..d2e7fd8 100644
--- a/yaksh/documentation/images/add_quiz.jpg
+++ b/yaksh/documentation/images/add_quiz.jpg
Binary files differ
diff --git a/yaksh/documentation/images/bash_standard_testcase.jpg b/yaksh/documentation/images/bash_standard_testcase.jpg
index a017445..3bbbdea 100644
--- a/yaksh/documentation/images/bash_standard_testcase.jpg
+++ b/yaksh/documentation/images/bash_standard_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/course_details_features.jpg b/yaksh/documentation/images/course_details_features.jpg
index 63b4b2e..34ea117 100644
--- a/yaksh/documentation/images/course_details_features.jpg
+++ b/yaksh/documentation/images/course_details_features.jpg
Binary files differ
diff --git a/yaksh/documentation/images/course_features.jpg b/yaksh/documentation/images/course_features.jpg
index 2da356e..d911188 100644
--- a/yaksh/documentation/images/course_features.jpg
+++ b/yaksh/documentation/images/course_features.jpg
Binary files differ
diff --git a/yaksh/documentation/images/course_modules.jpg b/yaksh/documentation/images/course_modules.jpg
new file mode 100644
index 0000000..4c16eb3
--- /dev/null
+++ b/yaksh/documentation/images/course_modules.jpg
Binary files differ
diff --git a/yaksh/documentation/images/cpp_standard_testcase.jpg b/yaksh/documentation/images/cpp_standard_testcase.jpg
index cfb1d89..59b801f 100644
--- a/yaksh/documentation/images/cpp_standard_testcase.jpg
+++ b/yaksh/documentation/images/cpp_standard_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/create_course.jpg b/yaksh/documentation/images/create_course.jpg
index bcf1eff..61d8329 100644
--- a/yaksh/documentation/images/create_course.jpg
+++ b/yaksh/documentation/images/create_course.jpg
Binary files differ
diff --git a/yaksh/documentation/images/design_course.jpg b/yaksh/documentation/images/design_course.jpg
index 287ebea..b6701db 100644
--- a/yaksh/documentation/images/design_course.jpg
+++ b/yaksh/documentation/images/design_course.jpg
Binary files differ
diff --git a/yaksh/documentation/images/design_module.jpg b/yaksh/documentation/images/design_module.jpg
index eda8825..3757586 100644
--- a/yaksh/documentation/images/design_module.jpg
+++ b/yaksh/documentation/images/design_module.jpg
Binary files differ
diff --git a/yaksh/documentation/images/design_questionpaper.jpg b/yaksh/documentation/images/design_questionpaper.jpg
index 05da597..d748754 100644
--- a/yaksh/documentation/images/design_questionpaper.jpg
+++ b/yaksh/documentation/images/design_questionpaper.jpg
Binary files differ
diff --git a/yaksh/documentation/images/embed_video.jpg b/yaksh/documentation/images/embed_video.jpg
index 84d18a2..e441a31 100644
--- a/yaksh/documentation/images/embed_video.jpg
+++ b/yaksh/documentation/images/embed_video.jpg
Binary files differ
diff --git a/yaksh/documentation/images/float_testcase.jpg b/yaksh/documentation/images/float_testcase.jpg
index 70b8a9f..a5c9ab0 100644
--- a/yaksh/documentation/images/float_testcase.jpg
+++ b/yaksh/documentation/images/float_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/hook_testcase.jpg b/yaksh/documentation/images/hook_testcase.jpg
index 4d404f6..49650fc 100644
--- a/yaksh/documentation/images/hook_testcase.jpg
+++ b/yaksh/documentation/images/hook_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/integer_testcase.jpg b/yaksh/documentation/images/integer_testcase.jpg
index 58ff6be..8d32cfd 100644
--- a/yaksh/documentation/images/integer_testcase.jpg
+++ b/yaksh/documentation/images/integer_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/java_standard_testcase.jpg b/yaksh/documentation/images/java_standard_testcase.jpg
index c13f618..949c783 100644
--- a/yaksh/documentation/images/java_standard_testcase.jpg
+++ b/yaksh/documentation/images/java_standard_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/mcc_testcase.jpg b/yaksh/documentation/images/mcc_testcase.jpg
index 7101833..11b3b0b 100644
--- a/yaksh/documentation/images/mcc_testcase.jpg
+++ b/yaksh/documentation/images/mcc_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/mcq_testcase.jpg b/yaksh/documentation/images/mcq_testcase.jpg
index afb51ca..dc705df 100644
--- a/yaksh/documentation/images/mcq_testcase.jpg
+++ b/yaksh/documentation/images/mcq_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/moderator_dashboard.jpg b/yaksh/documentation/images/moderator_dashboard.jpg
index 739d221..4486083 100644
--- a/yaksh/documentation/images/moderator_dashboard.jpg
+++ b/yaksh/documentation/images/moderator_dashboard.jpg
Binary files differ
diff --git a/yaksh/documentation/images/python_standard_testcase.jpg b/yaksh/documentation/images/python_standard_testcase.jpg
index a4d2ac8..5536aa8 100644
--- a/yaksh/documentation/images/python_standard_testcase.jpg
+++ b/yaksh/documentation/images/python_standard_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/questions.jpg b/yaksh/documentation/images/questions.jpg
index 1935541..4afa888 100644
--- a/yaksh/documentation/images/questions.jpg
+++ b/yaksh/documentation/images/questions.jpg
Binary files differ
diff --git a/yaksh/documentation/images/r_standard_testcase.jpg b/yaksh/documentation/images/r_standard_testcase.jpg
new file mode 100644
index 0000000..db6bf9b
--- /dev/null
+++ b/yaksh/documentation/images/r_standard_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/scilab_standard_testcase.jpg b/yaksh/documentation/images/scilab_standard_testcase.jpg
new file mode 100644
index 0000000..f7598e3
--- /dev/null
+++ b/yaksh/documentation/images/scilab_standard_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/stdio_testcase.jpg b/yaksh/documentation/images/stdio_testcase.jpg
index 9a041ba..1526e6b 100644
--- a/yaksh/documentation/images/stdio_testcase.jpg
+++ b/yaksh/documentation/images/stdio_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/string_testcase.jpg b/yaksh/documentation/images/string_testcase.jpg
index 6cd2b72..5c6a1e8 100644
--- a/yaksh/documentation/images/string_testcase.jpg
+++ b/yaksh/documentation/images/string_testcase.jpg
Binary files differ
diff --git a/yaksh/documentation/images/view_lessons.jpg b/yaksh/documentation/images/view_lessons.jpg
deleted file mode 100644
index 4229afe..0000000
--- a/yaksh/documentation/images/view_lessons.jpg
+++ /dev/null
Binary files differ
diff --git a/yaksh/documentation/images/view_modules.jpg b/yaksh/documentation/images/view_modules.jpg
deleted file mode 100644
index 5b535d3..0000000
--- a/yaksh/documentation/images/view_modules.jpg
+++ /dev/null
Binary files differ
diff --git a/yaksh/documentation/images/view_quizzes.jpg b/yaksh/documentation/images/view_quizzes.jpg
deleted file mode 100644
index 43bb36f..0000000
--- a/yaksh/documentation/images/view_quizzes.jpg
+++ /dev/null
Binary files differ
diff --git a/yaksh/documentation/installation.rst b/yaksh/documentation/installation.rst
index 1c90997..e74a6f0 100644
--- a/yaksh/documentation/installation.rst
+++ b/yaksh/documentation/installation.rst
@@ -2,33 +2,50 @@
Installation
============
+Requirements
+------------
+
+Python 3.6, 3.7, 3.8
+
+Django 3.0.3
+
Installing Yaksh
----------------
+If Python 3.6 and above is not available in the system, then we recommend using
+miniconda
+
+**Installing Miniconda**
+
+1. Download miniconda from https://docs.conda.io/en/latest/miniconda.html according to the OS version.
+
+2. Follow the installation instructions as given in https://conda.io/projects/conda/en/latest/user-guide/install/index.html#regular-installation
+
+3. Restart the Terminal.
**Pre-Requisite**
* Ensure `pip <https://pip.pypa.io/en/latest/installing.html>`_ is installed
-**For installing Yaksh**
+**Installing Yaksh**
1. **Clone the repository**::
- $ git clone https://github.com/FOSSEE/online_test.git
+ git clone https://github.com/FOSSEE/online_test.git
2. **Go to the online_test directory**::
- $ cd ./online_test
+ cd ./online_test
- 3. **Install the dependencies** -
- * For Python 2 use::
+ 3. **Install the dependencies**:
- $ pip install -r ./requirements/requirements-py2.txt
+ * Install Django and dependencies::
- * For Python 3 (recommended) use::
+ pip install -r ./requirements/requirements-common.txt
- $ pip install -r ./requirements/requirements-py3.txt
+ * Install Code Server dependencies::
+ sudo pip3 install -r requirements/requirements-codeserver.txt
Quick Start
-----------
@@ -93,9 +110,7 @@ Production Deployment
::
- pip install -r requirements/requirements-py2.txt # For Python 2
-
- pip3 install -r requirements/requirements-py3.txt # For Python 3
+ pip3 install -r requirements/requirements-py3.txt # For Python 3.x
3. Install MySql Server
4. Install Python MySql support
@@ -157,9 +172,6 @@ Production Deployment
::
- $ sudo python -m yaksh.code_server # For Python 2.x
-
-
$ sudo python3 -m yaksh.code_server # For Python 3.x
Put this in the background once it has started since this will not
@@ -298,25 +310,14 @@ Production Deployment
* **Additional commands available**
- We provide several convenient commands for you to use:
+ * **create_moderator** : Use this command to make a user as moderator.
- - load\_exam : load questions and a quiz from a python file. See
- docs/sample\_questions.py
+ ::
- - load\_questions\_xml : load questions from XML file, see
- docs/sample\_questions.xml use of this is deprecated in favor of
- load\_exam.
+ python manage.py create_moderator <username>
- - results2csv : Dump the quiz results into a CSV file for further
- processing.
-
- - dump\_user\_data : Dump out relevalt user data for either all users
- or specified users.
-
- For more information on these do this:
+ For more information on the command:
::
- $ python manage.py help [command]
-
- where [command] is one of the above.
+ python manage.py help [command-name]
diff --git a/yaksh/documentation/moderator_dashboard.rst b/yaksh/documentation/moderator_dashboard.rst
index 4b5bfea..441106a 100644
--- a/yaksh/documentation/moderator_dashboard.rst
+++ b/yaksh/documentation/moderator_dashboard.rst
@@ -6,6 +6,21 @@ On logging in moderators see the following dashboard.
.. image:: images/moderator_dashboard.jpg
+There are two options available:
+ * **Add Course**
+ It allows to create a new course.
+ * **Create Demo Course**
+ It creates a demo course contaning sample lesson and quiz with questions.
+
+The dashboard contains all the courses. Each course provides two options
+
+ * **Manage Course**
+ Click on this button to manage the course. See the Manage course section
+ in the :doc:`moderator_docs/creating_course` page for more details.
+ * **Details**
+ Clicking on the Details button shows all the quizzes in the course.
+ Click on the quiz link to monitor the quiz.
+
The following pages explain the various functions available for moderators
diff --git a/yaksh/documentation/moderator_docs/creating_course.rst b/yaksh/documentation/moderator_docs/creating_course.rst
index 5aaddf5..78e9907 100644
--- a/yaksh/documentation/moderator_docs/creating_course.rst
+++ b/yaksh/documentation/moderator_docs/creating_course.rst
@@ -23,11 +23,15 @@ Setting up a new course
* **Code**
If the course should be hidden and only accessible to students possessing the correct course code.
* **Instructions**
- Instructions for the course
+ Instructions for the course.
* **Start Date and Time for enrollment of course**
- If the enrollment of the course should be available only after a set date and time
+ If the enrollment of the course should be available only after a set date and time.
* **End Date and Time for enrollment of course**
- If the enrollment of the course should be available only before a set date and time
+ If the enrollment of the course should be available only before a set date and time.
+ * **Grading System**
+ Add a grading system to the course.
+ * **View Grade**
+ This field allows the student to view the grade if checked else grade is not visible to student.
Features in Courses
@@ -39,98 +43,114 @@ Features in Courses
This page shows all the courses created by a moderator and all the courses allotted to a moderator.
- The following features are available for courses
+ The following options are available in the courses page
+ * **My Courses**
+ Click to show all the courses created by you.
+ * **Add/Edit Course**
+ Click to add the details of a new course.
+ * **Add/View Grading Systems**
+ Add or view grading systems. More info on creating grading system
+ * **Search/Filter Courses**
+ Search the courses by name or filter the course with active and inactive status.
* **Course Name**
- Click on course name link to view all the enrolled, rejected and requested students list. Moderator can accept or reject the student.
- * **Module Name**
- Click to edit a module added to the course
- * **Lesson or Quiz Name**
- Click to edit a Lesson or Quiz added to the course
-
- In edit quiz you can also attempt the quiz in two modes -
- * **God Mode** - In God mode you can attempt quiz without any time or eligibilty constraints.
- * **User Mode** - In user mode you can attempt quiz the way normal users will attempt i.e.
-
- * Quiz will have the same duration as that of the original quiz.
- * Quiz won't start if the course is inactive or the quiz time has expired.
- * **Add Quizzes/Lessons for <module-name>**
- Click to add/delete lessons or quizzes.
- * **Design Course**
- Click to add/delete modules of a course.
- * **Add Teacher**
- Click to add teachers for the course. The teachers can edit and modify only the specific course that are allotted to them.
+ Shows course name of all the created and allotted courses.
+ * **Edit Course**
+ Click this button to edit the corresponding course details.
+ * **Manage Course**
+ This provides more options for the course. For e.g. setting up modules,
+ lessons, quizzes, practice exercises, students enrollments etc.
+ * **Download**
+ This button provides two options. One is to download the course CSV containing student data, Other is to download entire course for offline viewing.
* **Clone Course**
Click to create a copy of a course along with its modules, lessons and quizzes.
- * **Teachers added to the course**
- This shows all the teachers added to a particular course.
- * **Download CSV for the entire course**
- This downloads the CSV file containing the performance of all students in every quiz for a given course.
- * **Edit Course**
- Click to edit the details of an existing course.
- * **Deactivate/Activate Course**
- Click to deactivate or activate the course.
- * **My Courses**
- Click to show all the courses created by you.
- * **Allotted courses**
- Click to view all the courses allotted to you.
- * **Add New Course**
- Click to open course form to create new course.
- * **Add/View Quizzes**
- Click to view all the quizzes created by you or add new quiz.
- * **Add/View Lessons**
- Click to view all the lessons created by you or add new lesson.
- * **Add/View Modules**
- Click to view all the modules created by you or add new module.
-
-
-Design a Course
----------------
+ * **Activate/Deactivate Course**
+ Toogle to activate or deactivate the course.
- Clicking on **Design Course** will show the below page.
- .. image:: ../images/design_course.jpg
+Manage Course
+--------------------------
- **Available Modules** contains all the modules that are not added to a course.
+ Click on the Manage course button to view the course details page.
- To add a module to the course select the checkbox besides the desired module to be added and click **Add to course** button.
+ .. image:: ../images/course_details_features.jpg
- **Chosen Modules** contains all the modules that are added to a course.
+ Following are the features for course details -
- Following parameters can be changed while designing a course:
+ * **Enroll Students**
+ * **Upload Users**
+ Create and enroll users automatically by uploading a csv of the users. The mandatory fields for this csv are - **firstname, lastname, email**. Other fields like **username, password, institute, roll_no, department, remove** fields are optionals.
+ * **Requests**
+ This is a list of students who have requested to be enrolled in the course. Moderator can enroll or reject selected students.
+ * **Enrolled**
+ This is a list of students who have been enrolled in the course. Moderator can reject enrolled students.
+ * **Rejected**
+ This is a list of students who have been rejected for enrollment in a course. Moderator can enroll rejected students.
+ * **Course Modules**
+ Moderator can send mail to all enrolled students or selected students.
- **Order** - Order in which modules are shown to a student.
+ .. image:: ../images/course_modules.jpg
- To change a module's order change the value to a desired order in the textbox under **Order** column and click **Change order**.
+ * **Add Module**
+ Click on this button to add a module to the course. Fill the details
+ of the module and save it.
- **Check Prerequisite** - Check if previous module is completed. Default value is **Yes**.
- For e.g., Assuming a course contains modules **Demo Module** and **Python module** in the given order; a student has to first complete **Demo module** to attempt **Python Module** if the **Check Prerequisite** value for **Python Module** is checked **Yes**.
+ After creating a module for the course, following options are available:
- **Currently** column shows the current value of **Check Prerequisite** which in this case is **Yes**.
+ * **Add Lesson**
+ Add lesson to the corresponding module.
- Select the checkbox from **Change** column under **Check Prerequisite** and click **Change Prerequisite** button to change the value.
+ * **Add Quiz**
+ Add a graded quiz to the correspoding module.
- To remove a module from the course select the checkbox beside every module and click **Remove from course** button.
-
+ * **Add Exercise**
+ Add a ungraded practice exercise to the corresponding module.
-Features in Course Details
---------------------------
+ * **Design Module**
+ This option allows you to change the order of the units added to
+ the module, check for prerequisites of the module and remove a unit from the module.
+ * **Design Course**
+ Clicking on **Design Course** will show the below page.
- Click on a given course name to go to the course details page.
+ .. image:: ../images/design_course.jpg
- .. image:: ../images/course_details_features.jpg
+ * **Available Modules** contains all the modules that are not added to a course.
- Following are the features for course details -
+ To add a module to the course select the checkbox besides the desired module to be added and click **Add to course** button.
+
+ * **Chosen Modules** contains all the modules that are added to a course.
+
+ Following parameters can be changed while designing a course:
+
+ * **Order**
+ Order in which modules are shown to a student.
+
+ To change a module's order change the value to a desired order in the textbox under **Order** column and click **Change order**.
+
+ * **Check Prerequisite Completion**
+ Check if previous module is completed. Default value is **Yes**.
+
+ For e.g., Assuming a course contains modules **Demo Module** and **Trial for trial_course** in the given order; a student has to first complete **Demo module** to attempt **Trial for trial_course** if the **Check Prerequisite** value for **Trial for trial_course** is checked **Yes**.
+
+ **Currently** column shows the current value of **Change Prerequisite Completion** which in this case is **Yes**.
+
+ Select the checkbox from **Change** column under **Check Prerequisite Completion** and click **Change Prerequisite Completion** button to change the value.
+
+ * **Check Prerequisite Passing**
+ Check if previous module is completed. Default value is **Yes**. This is similar to **Check Prerequisite Completion** except that it checks if all the quizzes in the module are passed or not.
+
+ **Currently** column shows the current value of **Change Prerequisite Passing** which in this case is **Yes**.
+
+ Select the checkbox from **Change** column under **Check Prerequisite Passing** and click **Change Prerequisite Passing** button to change the value.
- * **Requests**
- This is a list of students who have requested to be enrolled in the course. Moderator can enroll or reject selected students.
- * **Enrolled**
- This is a list of students who have been enrolled in the course. Moderator can reject enrolled students.
- * **Rejected**
- This is a list of students who have been rejected for enrollment in a course. Moderator can enroll rejected students.
- * **Upload Users**
- Create and enroll users automatically by uploading a csv of the users. The mandatory fields for this csv are - **firstname, lastname, email**. Other fields like **username, password, institute, roll_no, department, remove** fields are optionals.
+ * **Remove Module**
+ To remove a module from the course select the checkbox beside every module and click **Remove from course** button.
+ * **Course Progress**
+ It shows progress made by the students in the course. Moderator can also
+ download the course progress data.
* **Send Mail**
Moderator can send mail to all enrolled students or selected students.
- * **View Course Status**
- View students' progress through the course.
+ * **Add Teachers/TAs**
+ Moderator can search for the users by username, email, first name and last name to add as Teacher/TA to the course.
+ * **Current Teachers/TAs**
+ It shows all the added Teachers/TAs to the course. Added users can view and edit the course, modules, lessons and quizzes available in the course.
diff --git a/yaksh/documentation/moderator_docs/creating_lessons_modules.rst b/yaksh/documentation/moderator_docs/creating_lessons_modules.rst
index e057be0..b61789e 100644
--- a/yaksh/documentation/moderator_docs/creating_lessons_modules.rst
+++ b/yaksh/documentation/moderator_docs/creating_lessons_modules.rst
@@ -6,84 +6,89 @@ Lessons and Modules
Courses can have lessons and quizzes encapsulated using a module.
- * **What is a lesson?**
- A lesson can be any markdown text with/or an embedded video of a particular topic.
+ * **What is a lesson?**
+ A lesson can be any markdown text with/or an embedded video of a particular topic.
- * **What is a module?**
- A Module is a collection of lessons and courses clubbed together by similar idea/content. A module can have its own description as a markdown text with/or an embedded video.
+ * **What is a module?**
+ A Module is a collection of lessons, quizzes and practice exercises clubbed together by similar idea/content. A module can have its own description as a markdown text with/or an embedded video.
Setting up a Lesson
-----------------------
- To create a new lesson or edit any existing lesson click on **Add/View Lessons** from courses page.
+ This page shows all the lessons created by you and added to a module.
- .. image:: ../images/view_lessons.jpg
+ .. image:: ../images/course_modules.jpg
- This page shows all the lessons created by you.
+ To create a new lesson click on **Add Lesson** button in the module.
- Click on **Add new Lesson** to add new lesson. Click on the **lesson name** to edit a lesson.
+ .. image:: ../images/add_lesson.jpg
- .. image:: ../images/add_lesson.jpg
+ * **Name** - Name of the lesson.
+ * **Description** - Description can be any markdown text or embedded video link.
+ * **Active** - Activate/Deactivate a lesson
+ * **Video File** - Upload a video file for the lesson
+ * **Lesson files** - Add files to the lesson which will be available for students to view and download. All the uploaded files will be shown below.
- * **Name** - Name of the lesson.
- * **Description** - Description can be any markdown text or embedded video link.
- * **Active** - Activate/Deactivate a lesson
- * **Lesson files** - Add files to the lesson which will be available for students to view and download. All the uploaded files will be shown below.
+ Click on **Save** to save a lesson.
- Click on **Save** to save a lesson.
+ Click on **Preview Lesson Description** to preview lesson description. Markdown text from the description is converted to html and is displayed below.
- Click on **Preview Lesson Description** to preview lesson description. Markdown text from the description is converted to html and is displayed below.
+ Select the checkbox beside each uploaded file and click on **Delete files** to remove files from the lesson.
- Select the checkbox beside each uploaded file and click on **Delete files** to remove files from the lesson.
+ Click on **Embed Video Link** to embed a video. On clicking a pop-up will be shown.
- Click on **Embed Video Link** to embed a video. On clicking a pop-up will be shown.
+ .. image:: ../images/embed_video.jpg
- .. image:: ../images/embed_video.jpg
-
- Enter the url and click on **Submit** a html div is generated in the text area below.
- Click on the button below the textarea to copy the textarea content. This html div can then be added in the lesson description.
+ Enter the url and click on **Submit** a html div is generated in the text area below.
+ Click on the button below the textarea to copy the textarea content. This html div can then be added in the lesson description.
Setting up a Module
-----------------------
- To create a new module or edit any existing module click on **Add/View Modules** from courses page.
+ To create a new module click on **Add Module** button from course modules
+ section of course details page.
+
+ .. image:: ../images/add_module.jpg
+
+ * **Name** - Name of the module.
+ * **Description** - Description can be any markdown text or embedded video link.
- .. image:: ../images/view_modules.jpg
+ Click on **Save** to save a module.
- This page shows all the modules created by you.
+ Click on **Preview Lesson Description** to preview lesson description. Markdown text from the description is converted to html and is displayed below.
- Creating a new module or editing an existing module is similar to a lesson creation with a difference that a module has no option to upload files.
+ Click on **Embed Video Link** to embed a video.
Design a Module
---------------
- To add lessons or quizzes to a module click on **Add Quizzes/Lessons for <module-name>**.
+ To add lessons or quizzes to a module click on **Design Module**.
- .. image:: ../images/design_module.jpg
+ .. image:: ../images/design_module.jpg
- **Available Lessons and quizzes** contains all the lessons and quizzes that are not added to a module.
+ **Available Lessons and quizzes** contains all the lessons and quizzes that are not added to a module.
- To add a lesson or a quiz to the module select the checkbox beside every lesson or quiz and click **Add to Module** button.
+ To add a lesson or a quiz to the module select the checkbox beside every lesson or quiz and click **Add to Module** button.
- **Chosen Lesson and quizzes** contains all the lessons and quizzes that are added to a module.
+ **Chosen Lesson and quizzes** contains all the lessons and quizzes that are added to a module.
- A lesson or quiz added to a module becomes a unit. A unit has following parameters to change:
+ A lesson or quiz added to a module becomes a unit. A unit has following parameters to change:
- **Order** - Order in which units are shown to a student.
+ **Order** - Order in which units are shown to a student.
- To change a unit's order change the value in the textbox under **Order** column and click **Change order**.
+ To change a unit's order change the value in the textbox under **Order** column and click **Change order**.
- **Check Prerequisite** - Check if previous unit is completed. Default value is **Yes**.
- For e.g. A student has to first complete **Yaksh Demo quiz** to attempt **Demo Lesson** if the **Check Prerequisite** value for **Demo Lesson** is checked **Yes**.
+ **Check Prerequisite** - Check if previous unit is completed. Default value is **Yes**.
+ For e.g. A student has to first complete **Yaksh Demo quiz** to attempt **Demo Lesson** if the **Check Prerequisite** value for **Demo Lesson** is checked **Yes**.
- **Currently** column shows the current value of **Check Prerequisite** which in this case is **Yes**.
+ **Currently** column shows the current value of **Check Prerequisite** which in this case is **Yes**.
- Select the checkbox from **Change** column under **Check Prerequisite** and click **Change Prerequisite** button to change the value.
+ Select the checkbox from **Change** column under **Check Prerequisite** and click **Change Prerequisite** button to change the value.
- To remove a lesson or a quiz from the module select the checkbox beside every lesson or quiz and click **Remove from Module** button.
+ To remove a lesson or a quiz from the module select the checkbox beside every lesson or quiz and click **Remove from Module** button.
diff --git a/yaksh/documentation/moderator_docs/creating_question.rst b/yaksh/documentation/moderator_docs/creating_question.rst
index 3e878ea..ea2d610 100644
--- a/yaksh/documentation/moderator_docs/creating_question.rst
+++ b/yaksh/documentation/moderator_docs/creating_question.rst
@@ -23,9 +23,7 @@ Setting up questions
* **Points** - Points is the marks for a question.
- * **Description** - The actual question description in HTML format.
-
- .. note:: To add code snippets in questions please use html <code> and <br> tags.
+ * **Description** - The actual question description.
* **Tags** - Type of label or metadata tag making it easier to find specific type of questions.
@@ -55,7 +53,9 @@ Setting up questions
How to write Test cases
-----------------------
-
+ After saving the question with the necessary details, you will be able to add
+ the test cases. A drop down **Add Test case** will be available to add the test case in the Test Cases section.
+
The following explains different methods to write test cases.
* **Create Standard Test Case**
@@ -200,10 +200,91 @@ How to write Test cases
2. Each argument should be separated by **space**.
3. This field can be left blank.
+ * **For Scilab**
+ .. image:: ../images/scilab_standard_testcase.jpg
+ :width: 80%
+
+ Consider a Program to add two numbers.
+ The code in the Test case Field should be as follows: ::
+
+ mode(-1)
+ exec("function.sci",-1);
+ i = 0
+ p = add(3,5);
+ correct = (p == 8);
+ if correct then
+ i=i+1
+ end
+ disp("Input submitted 3 and 5")
+ disp("Expected output 8 got " + string(p))
+ p = add(22,-20);
+ correct = (p==2);
+ if correct then
+ i=i+1
+ end
+ disp("Input submitted 22 and -20")
+ disp("Expected output 2 got " + string(p))
+ p =add(91,0);
+ correct = (p==91);
+ if correct then
+ i=i+1
+ end
+ disp("Input submitted 91 and 0")
+ disp("Expected output 91 got " + string(p))
+ if i==3 then
+ exit(5);
+ else
+ exit(3);
+ end
+
+ Assuming Students answer to be as below: ::
+
+ funcprot(0)
+ function[c]=add(a,b)
+ c=a+b;
+ endfunction
+
+ * **For R**
+ .. image:: ../images/r_standard_testcase.jpg
+ :width: 80%
+
+ Consider a Program to print even or odd number.
+ The code in the Test case Field should be as follows: ::
+
+ source("function.r")
+ check_empty = function(obj){
+ stopifnot(is.null(obj) == FALSE)
+ }
+ check = function(input, output){
+ stopifnot(input == output)
+ }
+ is_correct = function(){
+ if (count == 3){
+ quit("no", 31)
+ }
+ }
+ check_empty(odd_or_even(3))
+ check(odd_or_even(6), "EVEN")
+ check(odd_or_even(1), "ODD")
+ check(odd_or_even(10), "EVEN")
+ check(odd_or_even(777), "ODD")
+ check(odd_or_even(778), "EVEN")
+ count = 3
+ is_correct()
+
+ Assuming Students answer to be as below: ::
+
+ odd_or_even <- function(n){
+ if(n %% 2 == 0){
+ return("EVEN")
+ }
+ return("ODD")
+ }
- Check Delete Field if a test case is to be removed.
- Finally click on Save to save the test case.
+ Check **Delete** Field if a test case is to be removed.
+
+ Finally click on **Save** to save the test case.
* **Create Standard Input/Output Based Test Case**
@@ -221,23 +302,7 @@ How to write Test cases
Setting up Standard Input/Output Based questions is same for all languages.
- * **Create MCQ or MCC Based Test Case**
-
- Select MCQ/MCC from Add Test Case field.
-
- Fig (a) showing MCQ based testcase
-
- .. image:: ../images/mcq_testcase.jpg
- :width: 80%
-
- Fig (b) showing MCC based testcase
-
- .. image:: ../images/mcc_testcase.jpg
- :width: 80%
-
- In Options Field type the option check the correct checkbox if the current option is correct and click on Save button to save each option.
-
- For MCC based question, check the correct checkbox for multiple correct options.
+ .. note:: Standard Input/Output Based questions is available only for the languages Python, C, C++, Java, Bash.
* **Create Hook based Test Case**
@@ -286,6 +351,24 @@ How to write Test cases
.. image:: ../images/hook_testcase.jpg
:width: 80%
+ * **Create MCQ or MCC Based Test Case**
+
+ Select MCQ/MCC from Add Test Case field.
+
+ Fig (a) showing MCQ based testcase
+
+ .. image:: ../images/mcq_testcase.jpg
+ :width: 80%
+
+ Fig (b) showing MCC based testcase
+
+ .. image:: ../images/mcc_testcase.jpg
+ :width: 80%
+
+ In Options Field type the option check the correct checkbox if the current option is correct and click on Save button to save each option.
+
+ For MCC based question, check the correct checkbox for multiple correct options.
+
* **Create Integer Based Test Case**
Select **Answer in Integer** from Type field.
@@ -340,7 +423,7 @@ Features in Question
* **Upload Questions**
- Click on the **Upload and Download questions** tab in the
+ Click on the **Upload Questions** tab in the
**Question Page**.
One can upload Yaml file with extensions .yaml or .yml.
Please note that you cannot upload files associated to a question.
@@ -363,7 +446,7 @@ Features in Question
Select questions from the list of question displayed on the Questions page. Click on Test selected button. This will take you to a quiz with the selected questions.
- .. Note:: This will not create an actual quiz but a trial quiz. This quiz is hidden from the students and only for moderator to view. You can delete the quiz from moderator's dashboard.
+ .. Note:: This will not create an actual quiz but a trial quiz. This quiz is hidden from the students and only for moderator to view.
* **Filter Questions**
@@ -376,5 +459,5 @@ Features in Question
1. You can search the questions by tags added during question creation.
2. Click on the Available tags to view all the available tags. Select any tag from available tags and click **Search**.
- 3. Enter the tag in the search bar and click on **Search** respective questions will be displayed.
+ 3. Enter the tag in the search bar and click on **Search Icon** respective questions will be displayed.
diff --git a/yaksh/documentation/moderator_docs/creating_quiz.rst b/yaksh/documentation/moderator_docs/creating_quiz.rst
index 8b93188..6c7cb93 100644
--- a/yaksh/documentation/moderator_docs/creating_quiz.rst
+++ b/yaksh/documentation/moderator_docs/creating_quiz.rst
@@ -4,85 +4,104 @@
Quizzes
=======
-Quizzes are intrinsically associated with a course, hence to view and/or edit a quiz, we need to navigate to the courses page.
+Quizzes are intrinsically associated with a course, hence to view and/or edit a quiz, we need to navigate to the course details page by clicking on **Manage Course** button.
-Clicking on Add/View Quizzes from courses page will open the page as shown below
+Clicking on **Course Modules** in the course details page will open the page as shown below
-.. image:: ../images/view_quizzes.jpg
+.. image:: ../images/course_modules.jpg
-This page shows all the quizzes and exercise created.
+This page shows all the modules with quizzes and exercises.
Creating a Quiz
----------------
+---------------
- Click on **Add New Quiz** button to add a quiz.
+ Click on **Add Quiz** button to add a quiz.
- .. image:: ../images/add_quiz.jpg
-
- .. note :: It is important to have created or uploaded questions before creating a quiz.
+ .. image:: ../images/add_quiz.jpg
+
+ .. note :: It is important to have created or uploaded questions before creating a quiz.
- * **Start Date and Time of quiz** - The date and time after which the quiz can be taken.
- * **End Date and Time of quiz** - The date and time after which the quiz is deactivated and cannot be attempted.
- * **Duration** - Duration of quiz to be written in minutes.
- * **Active** - Check the checkbox to activate/deactivate quiz.
- * **Description** - Description or name of the quiz.
- * **Passing Percentage** - Minimum percentage required to pass the test.
- * **Attempts allowed** - Number of attempts that a student can take of the current quiz.
- * **Time Between Quiz Attempts in hours** - For a quiz with multiple attempts this value can be set so that student can attempt again after the specified time.
- * **Instructions for students** - Additional instructions for students can be added. Some default instructions are already provided.
- * **Allow student to view answer paper** - Click on this checkbox to allow student to view their answer paper.
- * **Allow student to skip questions** - Click on this checkbox to allow/disallow student to skip questions for a quiz. Value defaults to allow skipping questions.
- * **Weightage** - Every quiz will have weightage depending on which grades will be calculated.
+ * **Start Date and Time of quiz** - The date and time after which the quiz can be taken.
+ * **End Date and Time of quiz** - The date and time after which the quiz is deactivated and cannot be attempted.
+ * **Duration** - Duration of quiz to be written in minutes.
+ * **Active** - Check the checkbox to activate/deactivate quiz.
+ * **Description** - Description or name of the quiz.
+ * **Passing Percentage** - Minimum percentage required to pass the test.
+ * **Attempts allowed** - Number of attempts that a student can take of the current quiz.
+ * **Time Between Quiz Attempts in hours** - For a quiz with multiple attempts this value can be set so that student can attempt again after the specified time.
+ * **Instructions for students** - Additional instructions for students can be added. Some default instructions are already provided.
+ * **Allow student to view answer paper** - Click on this checkbox to allow student to view their answer paper.
+ * **Allow student to skip questions** - Click on this checkbox to allow/disallow student to skip questions for a quiz. Value defaults to allow skipping questions.
+ * **Weightage** - Every quiz will have weightage depending on which grades will be calculated.
- Once a quiz parameters have been set click on **Save** button to save the quiz.
+ Once a quiz parameters have been set click on **Save** button to save the quiz.
-To create a Question paper, Click on **Add** link located besides the created quiz.
+ To create a Question paper, Click on **Add Question Paper** link located
+ besides the created quiz.
Creating a Exercise
--------------------
+-------------------
- Click on **Add New Exercise** button to add a exercise.
+ Click on **Add New Exercise** button to add a exercise.
- .. image:: ../images/add_exercise.jpg
+ .. image:: ../images/add_exercise.jpg
- Exercise is similar to quiz with a difference that exercise has infinite attempts and
- infinite time. It also does not allow a student to skip the question.
- Each question in an exercise can be timed i.e. time to solve a particular question.
- Once the question time expires, question solution is shown to the student.
+ Exercise is similar to quiz with a difference that exercise has infinite attempts and
+ infinite time. It also does not allow a student to skip the question.
+ Each question in an exercise can be timed i.e. time to solve a particular question.
+ Once the question time expires, question solution is shown to the student.
- All the parameters are set by default only below parameters can be changed.
+ All the parameters are set by default only below parameters can be changed.
- * **Description** - Description or name of the exercise.
- * **Allow student to view answer paper** - Click on this checkbox to allow student to view their answer paper.
- * **Active** - Select the checkbox to activate/deactivate exercise. Default value is active.
+ * **Description** - Description or name of the exercise.
+ * **Allow student to view answer paper** - Click on this checkbox to allow student to view their answer paper.
+ * **Active** - Select the checkbox to activate/deactivate exercise. Default value is active.
+
+ To create a Question paper, Click on **Add Question Paper** link located besides the created exercise.
Designing Question Paper
------------------------
- .. image:: ../images/design_questionpaper.jpg
+ .. image:: ../images/design_questionpaper.jpg
- A quiz/exercise can have fixed as well as random questions. Fixed questions are those question that are bound to appear for every student taking the quiz. In random questions a pool of questions is given and number of questions to be picked from the pool is set. Hence for different students, different questions from the pool will appear.
+ A quiz/exercise can have fixed as well as random questions. Fixed questions are those question that are bound to appear for every student taking the quiz. In random questions a pool of questions is given and number of questions to be picked from the pool is set. Hence for different students, different questions from the pool will appear.
- To add questions to a questionpaper
+ To add questions to a questionpaper
- * Select Question type and marks and a list of questions will be displayed will be in the **select questions to add** section. Do this for both fixed questions and random questions.
- * After adding questions click on **Next** button to go to **Step 3 Finish**.
- * Select shuffle paper if you want to jumble up the question sequence for every student and for every attempt.
- * Click on save question paper to save it or preview question paper to preview it.
+ * Select Question type and marks and a list of questions will be displayed will be in the **select questions to add** section. Do this for both fixed questions and random questions.
+ * You can also search for questions using the tags added while creating the question. All the available tags are shown.
+ * After adding questions click on **Next** button to go to **Step 3 Finish**.
+ * Select **Shuffle questions' order for each student** if you want to jumble up the question sequence for every student and for every attempt.
+ * Select **Shuffle MCQ/MCC options for each student** if you want to jumble up the MCQ/MCC question options for every student and for every attempt.
+ * Click on **Save** to save it.
Editing a Quiz/Exercise
-----------------------
- Click on the quiz/exercise link to edit, change the parameters and click on Save.
+ Click on the quiz/exercise link to edit, change the parameters and click on Save.
+ Options Available once the Quiz is created.
+
+ * **Preview Question Paper**
+ Click on the Preview Question Paper button located at the bottom of the
+ page to preview all the questions available in the question paper of the
+ quiz.
+ * **User Mode**
+ Attempt quiz the way student will attempt i.e. -
+ Quiz will have the same duration as that of the original quiz.
+
+ Quiz won't start if the course is inactive or the quiz time has expired.
+
+ * **God Mode**
+ Attempt quiz without any time or eligibilty constraints.
Editing a QuestionPaper
-----------------------
- Click on the Question Paper for a <Quiz-name/Exercise-name> besides Quiz/Exercise and follow steps from Design Question Paper.
+ Click on the **Edit Question Paper** besides Quiz/Exercise and follow steps from Design Question Paper.
- If the questions are already added to a Question Paper then they are shown in the
- **Fixed Questions currently in the paper** section.
+ If the questions are already added to a Question Paper then they are shown in the
+ **Fixed Questions currently in the paper** section.
diff --git a/yaksh/documentation/moderator_docs/other_features.rst b/yaksh/documentation/moderator_docs/other_features.rst
index 0b39788..bbd60dc 100644
--- a/yaksh/documentation/moderator_docs/other_features.rst
+++ b/yaksh/documentation/moderator_docs/other_features.rst
@@ -7,17 +7,26 @@ Grade User
Grade User is a feature of Yaksh to access students' answer papers for each quiz and grade them where necessary.
+ Clicking on the **Grade User** link from the nav bar will show all the courses.
+
+ Click on the **Details** button to show the quizzes associated to a particular course.
+
+ Click on the **Grade User** button next to the quiz name to view all the students who have attempted the quiz.
+
+ Click on the student name to view their submissions.
+
Monitor
-------
Monitor is a feature of Yaksh where the moderator can monitor a quiz and view statistics.
-Trial Papers
-------------
+ Clicking on the **Monitor** link from the nav bar will show all the courses.
+
+ Click on the **Details** button to show the quizzes associated to a particular course.
- When a moderator attempts a quiz in User or God mode or tests questions, a trial answer paper is created. Moderator can check the answer paper.
+ Click on the **Monitor** button next to the quiz name to view all the students who are attempting the quiz.
- .. note:: It is advisable to delete these trial answer papers.
+ Click on the student name to view their submissions.
Grader
------
diff --git a/yaksh/evaluator_tests/test_r_evaluation.py b/yaksh/evaluator_tests/test_r_evaluation.py
new file mode 100644
index 0000000..b4b81ae
--- /dev/null
+++ b/yaksh/evaluator_tests/test_r_evaluation.py
@@ -0,0 +1,197 @@
+from __future__ import unicode_literals
+import unittest
+from textwrap import dedent
+import os
+import tempfile
+import shutil
+from psutil import Process
+
+from yaksh.grader import Grader
+from yaksh.settings import SERVER_TIMEOUT
+from yaksh.evaluator_tests.test_python_evaluation import EvaluatorBaseTest
+
+
+class RAssertionEvaluationTestCase(EvaluatorBaseTest):
+ def setUp(self):
+ self.tmp_file = os.path.join(tempfile.gettempdir(), 'test.txt')
+ with open(self.tmp_file, 'wb') as f:
+ f.write('2'.encode('ascii'))
+ tmp_in_dir_path = tempfile.mkdtemp()
+ self.in_dir = tmp_in_dir_path
+ self.test_case = dedent(
+ '''
+ source("function.r")
+ check_empty = function(obj){
+ stopifnot(is.null(obj) == FALSE)
+ }
+ check = function(input, output){
+ stopifnot(input == output)
+ }
+ is_correct = function(){
+ if (count == 3){
+ quit("no", 31)
+ }
+ }
+ check_empty(odd_or_even(3))
+ check(odd_or_even(6), "EVEN")
+ check(odd_or_even(1), "ODD")
+ check(odd_or_even(10), "EVEN")
+ check(odd_or_even(777), "ODD")
+ check(odd_or_even(778), "EVEN")
+ count = 3
+ is_correct()
+ '''
+ )
+ self.test_case_data = [{"test_case": self.test_case,
+ "test_case_type": "standardtestcase",
+ "weight": 0.0
+ }]
+ self.timeout_msg = ("Code took more than {0} seconds to run. "
+ "You probably have an infinite loop in"
+ " your code.").format(SERVER_TIMEOUT)
+ self.file_paths = None
+
+ def tearDown(self):
+ os.remove(self.tmp_file)
+ shutil.rmtree(self.in_dir)
+
+ def test_correct_answer(self):
+ # Given
+ user_answer = dedent(
+ '''
+ odd_or_even <- function(n){
+ if(n %% 2 == 0){
+ return("EVEN")
+ }
+ return("ODD")
+ }
+ '''
+ )
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'r'},
+ 'test_case_data': self.test_case_data,
+ }
+
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+
+ # Then
+ self.assertTrue(result.get('success'))
+
+ def test_incorrect_answer(self):
+ # Given
+ user_answer = dedent(
+ '''
+ odd_or_even <- function(n){
+ if(n %% 2 == 0){
+ return("ODD")
+ }
+ return("EVEN")
+ }
+ '''
+ )
+ err = 'input == output is not TRUE\nExecution halted\n'
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'r'},
+ 'test_case_data': self.test_case_data,
+ }
+
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+ errors = result.get('error')
+ # Then
+ self.assertFalse(result.get('success'))
+ self.assertEqual(errors[0]['message'], err)
+
+ def test_error_code(self):
+ # Given
+ user_answer = dedent(
+ '''
+ odd_or_even <- function(n){
+ a
+ }
+ '''
+ )
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'r'},
+ 'test_case_data': self.test_case_data,
+ }
+
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+ errors = result.get('error')
+
+ # Then
+ self.assertFalse(result.get("success"))
+ self.assertIn("object 'a' not found", errors[0]['message'])
+
+ def test_empty_function(self):
+ # Given
+ user_answer = dedent(
+ '''
+ odd_or_even <- function(n){
+ }
+ '''
+ )
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'r'},
+ 'test_case_data': self.test_case_data,
+ }
+
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+ errors = result.get('error')
+
+ # Then
+ self.assertFalse(result.get("success"))
+ err = errors[0]['message']
+ self.assertIn("is.null(obj) == FALSE is not TRUE", err)
+
+ def test_infinite_loop(self):
+ # Given
+ user_answer = dedent(
+ '''
+ odd_or_even <- function(n){
+ while(0 == 0){
+ a <- 1
+ }
+ }
+ '''
+ )
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'r'},
+ 'test_case_data': self.test_case_data,
+ }
+
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+
+ # Then
+ self.assertFalse(result.get("success"))
+ self.assert_correct_output(self.timeout_msg,
+ result.get("error")[0]["message"]
+ )
+ parent_proc = Process(os.getpid()).children()
+ if parent_proc:
+ children_procs = Process(parent_proc[0].pid)
+ self.assertFalse(any(children_procs.children(recursive=True)))
diff --git a/yaksh/forms.py b/yaksh/forms.py
index c0f40ea..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
+ LearningModule, TestCase, languages, question_types, Post, Comment
)
from grades.models import GradingSystem
from django.contrib.auth import authenticate
@@ -17,27 +17,9 @@ from string import punctuation, digits
import pytz
from .send_emails import generate_activation_key
-languages = (
- ("select", "Select Language"),
- ("python", "Python"),
- ("bash", "Bash"),
- ("c", "C Language"),
- ("cpp", "C++ Language"),
- ("java", "Java Language"),
- ("scilab", "Scilab"),
-)
+languages = (("", "Select Language"),) + languages
-question_types = (
- ("select", "Select Question Type"),
- ("mcq", "Multiple Choice"),
- ("mcc", "Multiple Correct Choices"),
- ("code", "Code"),
- ("upload", "Assignment Upload"),
- ("integer", "Answer in Integer"),
- ("string", "Answer in String"),
- ("float", "Answer in Float"),
- ("arrange", "Arrange in Correct Order"),
-)
+question_types = (("", "Select Question Type"),) + question_types
test_case_types = (
("standardtestcase", "Standard Testcase"),
@@ -50,7 +32,7 @@ test_case_types = (
)
status_types = (
- ('select','Select Status'),
+ ('select', 'Select Status'),
('active', 'Active'),
('closed', 'Inactive'),
)
@@ -362,39 +344,58 @@ class RandomQuestionForm(forms.Form):
class QuestionFilterForm(forms.Form):
+
+ language = forms.ChoiceField(
+ choices=languages,
+ widget=forms.Select(attrs={'class': 'custom-select'}),
+ required=False
+ )
+ question_type = forms.ChoiceField(
+ choices=question_types,
+ widget=forms.Select(attrs={'class': 'custom-select'}),
+ required=False
+ )
+
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
+ lang = kwargs.pop("language") if "language" in kwargs else None
+ que_type = kwargs.pop("type") if "type" in kwargs else None
+ marks = kwargs.pop("marks") if "marks" in kwargs else None
super(QuestionFilterForm, self).__init__(*args, **kwargs)
- questions = Question.objects.filter(user_id=user.id)
- points_list = questions.values_list('points', flat=True).distinct()
- points_options = [(None, 'Select Marks')]
- points_options.extend([(point, point) for point in points_list])
- self.fields['marks'] = forms.FloatField(
- widget=forms.Select(choices=points_options,
- attrs={'class': 'custom-select'})
+ points = Question.objects.filter(
+ user_id=user.id).values_list('points', flat=True).distinct()
+ points_options = [('', 'Select Marks')]
+ points_options.extend([(point, point) for point in points])
+ self.fields['marks'] = forms.ChoiceField(
+ choices=points_options,
+ widget=forms.Select(attrs={'class': 'custom-select'}),
+ required=False
)
self.fields['marks'].required = False
- language = forms.CharField(
- max_length=8, widget=forms.Select(choices=languages,
- attrs={'class': 'custom-select'}))
- question_type = forms.CharField(
- max_length=8, widget=forms.Select(choices=question_types,
- attrs={'class': 'custom-select'})
- )
+ self.fields['language'].initial = lang
+ self.fields['question_type'].initial = que_type
+ self.fields['marks'].initial = marks
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.CharField(max_length=16, widget=forms.Select(
+ search_status = forms.ChoiceField(
choices=status_types,
- attrs={'class': 'custom-select'}),
+ widget=forms.Select(attrs={'class': 'custom-select'})
)
+ def __init__(self, *args, **kwargs):
+ status = kwargs.pop("status") if "status" in kwargs else None
+ tags = kwargs.pop("tags") if "tags" in kwargs else None
+ super(SearchFilterForm, self).__init__(*args, **kwargs)
+ self.fields["search_status"].initial = status
+ self.fields["search_tags"].initial = tags
+
class CourseForm(forms.ModelForm):
""" course form for moderators """
@@ -570,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/middleware/one_session_per_user.py b/yaksh/middleware/one_session_per_user.py
index 3b8d302..114c92b 100644
--- a/yaksh/middleware/one_session_per_user.py
+++ b/yaksh/middleware/one_session_per_user.py
@@ -25,14 +25,8 @@ class OneSessionPerUserMiddleware(object):
self.get_response = get_response
def __call__(self, request):
- return self.get_response(request)
-
- def process_request(self, request):
- """
- # Documentation:
- # https://docs.djangoproject.com/en/1.5/topics/auth/customizing/
- #extending-the-existing-user-model
- """
+ # Code to be executed for each request before
+ # the view (and later middleware) are called.
if isinstance(request.user, User):
current_key = request.session.session_key
if hasattr(request.user, 'concurrentuser'):
@@ -46,3 +40,8 @@ class OneSessionPerUserMiddleware(object):
concurrent_user=request.user,
session_key=current_key,
)
+
+ response = self.get_response(request)
+ # Code to be executed for each request/response after
+ # the view is called.
+ return response
diff --git a/yaksh/middleware/user_time_zone.py b/yaksh/middleware/user_time_zone.py
index 92035e8..8140851 100644
--- a/yaksh/middleware/user_time_zone.py
+++ b/yaksh/middleware/user_time_zone.py
@@ -12,12 +12,17 @@ class TimezoneMiddleware(object):
self.get_response = get_response
def __call__(self, request):
- return self.get_response(request)
-
- def process_request(self, request):
+ # Code to be executed for each request before
+ # the view (and later middleware) are called.
user = request.user
user_tz = 'Asia/Kolkata'
if hasattr(user, 'profile'):
if user.profile.timezone:
user_tz = user.profile.timezone
timezone.activate(pytz.timezone(user_tz))
+
+ response = self.get_response(request)
+
+ # Code to be executed for each request/response after
+ # the view is called.
+ return response
diff --git a/yaksh/migrations/0017_release_0_14_0.py b/yaksh/migrations/0017_release_0_14_0.py
new file mode 100644
index 0000000..334ab8c
--- /dev/null
+++ b/yaksh/migrations/0017_release_0_14_0.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.0.3 on 2020-04-07 08:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('yaksh', '0016_release_0_12_0'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='profile',
+ name='timezone',
+ field=models.CharField(choices=[('Africa/Abidjan', 'Africa/Abidjan'), ('Africa/Accra', 'Africa/Accra'), ('Africa/Addis_Ababa', 'Africa/Addis_Ababa'), ('Africa/Algiers', 'Africa/Algiers'), ('Africa/Asmara', 'Africa/Asmara'), ('Africa/Bamako', 'Africa/Bamako'), ('Africa/Bangui', 'Africa/Bangui'), ('Africa/Banjul', 'Africa/Banjul'), ('Africa/Bissau', 'Africa/Bissau'), ('Africa/Blantyre', 'Africa/Blantyre'), ('Africa/Brazzaville', 'Africa/Brazzaville'), ('Africa/Bujumbura', 'Africa/Bujumbura'), ('Africa/Cairo', 'Africa/Cairo'), ('Africa/Casablanca', 'Africa/Casablanca'), ('Africa/Ceuta', 'Africa/Ceuta'), ('Africa/Conakry', 'Africa/Conakry'), ('Africa/Dakar', 'Africa/Dakar'), ('Africa/Dar_es_Salaam', 'Africa/Dar_es_Salaam'), ('Africa/Djibouti', 'Africa/Djibouti'), ('Africa/Douala', 'Africa/Douala'), ('Africa/El_Aaiun', 'Africa/El_Aaiun'), ('Africa/Freetown', 'Africa/Freetown'), ('Africa/Gaborone', 'Africa/Gaborone'), ('Africa/Harare', 'Africa/Harare'), ('Africa/Johannesburg', 'Africa/Johannesburg'), ('Africa/Juba', 'Africa/Juba'), ('Africa/Kampala', 'Africa/Kampala'), ('Africa/Khartoum', 'Africa/Khartoum'), ('Africa/Kigali', 'Africa/Kigali'), ('Africa/Kinshasa', 'Africa/Kinshasa'), ('Africa/Lagos', 'Africa/Lagos'), ('Africa/Libreville', 'Africa/Libreville'), ('Africa/Lome', 'Africa/Lome'), ('Africa/Luanda', 'Africa/Luanda'), ('Africa/Lubumbashi', 'Africa/Lubumbashi'), ('Africa/Lusaka', 'Africa/Lusaka'), ('Africa/Malabo', 'Africa/Malabo'), ('Africa/Maputo', 'Africa/Maputo'), ('Africa/Maseru', 'Africa/Maseru'), ('Africa/Mbabane', 'Africa/Mbabane'), ('Africa/Mogadishu', 'Africa/Mogadishu'), ('Africa/Monrovia', 'Africa/Monrovia'), ('Africa/Nairobi', 'Africa/Nairobi'), ('Africa/Ndjamena', 'Africa/Ndjamena'), ('Africa/Niamey', 'Africa/Niamey'), ('Africa/Nouakchott', 'Africa/Nouakchott'), ('Africa/Ouagadougou', 'Africa/Ouagadougou'), ('Africa/Porto-Novo', 'Africa/Porto-Novo'), ('Africa/Sao_Tome', 'Africa/Sao_Tome'), ('Africa/Tripoli', 'Africa/Tripoli'), ('Africa/Tunis', 'Africa/Tunis'), ('Africa/Windhoek', 'Africa/Windhoek'), ('America/Adak', 'America/Adak'), ('America/Anchorage', 'America/Anchorage'), ('America/Anguilla', 'America/Anguilla'), ('America/Antigua', 'America/Antigua'), ('America/Araguaina', 'America/Araguaina'), ('America/Argentina/Buenos_Aires', 'America/Argentina/Buenos_Aires'), ('America/Argentina/Catamarca', 'America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'America/Argentina/La_Rioja'), ('America/Argentina/Mendoza', 'America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'America/Argentina/Rio_Gallegos'), ('America/Argentina/Salta', 'America/Argentina/Salta'), ('America/Argentina/San_Juan', 'America/Argentina/San_Juan'), ('America/Argentina/San_Luis', 'America/Argentina/San_Luis'), ('America/Argentina/Tucuman', 'America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'America/Argentina/Ushuaia'), ('America/Aruba', 'America/Aruba'), ('America/Asuncion', 'America/Asuncion'), ('America/Atikokan', 'America/Atikokan'), ('America/Bahia', 'America/Bahia'), ('America/Bahia_Banderas', 'America/Bahia_Banderas'), ('America/Barbados', 'America/Barbados'), ('America/Belem', 'America/Belem'), ('America/Belize', 'America/Belize'), ('America/Blanc-Sablon', 'America/Blanc-Sablon'), ('America/Boa_Vista', 'America/Boa_Vista'), ('America/Bogota', 'America/Bogota'), ('America/Boise', 'America/Boise'), ('America/Cambridge_Bay', 'America/Cambridge_Bay'), ('America/Campo_Grande', 'America/Campo_Grande'), ('America/Cancun', 'America/Cancun'), ('America/Caracas', 'America/Caracas'), ('America/Cayenne', 'America/Cayenne'), ('America/Cayman', 'America/Cayman'), ('America/Chicago', 'America/Chicago'), ('America/Chihuahua', 'America/Chihuahua'), ('America/Costa_Rica', 'America/Costa_Rica'), ('America/Creston', 'America/Creston'), ('America/Cuiaba', 'America/Cuiaba'), ('America/Curacao', 'America/Curacao'), ('America/Danmarkshavn', 'America/Danmarkshavn'), ('America/Dawson', 'America/Dawson'), ('America/Dawson_Creek', 'America/Dawson_Creek'), ('America/Denver', 'America/Denver'), ('America/Detroit', 'America/Detroit'), ('America/Dominica', 'America/Dominica'), ('America/Edmonton', 'America/Edmonton'), ('America/Eirunepe', 'America/Eirunepe'), ('America/El_Salvador', 'America/El_Salvador'), ('America/Fort_Nelson', 'America/Fort_Nelson'), ('America/Fortaleza', 'America/Fortaleza'), ('America/Glace_Bay', 'America/Glace_Bay'), ('America/Godthab', 'America/Godthab'), ('America/Goose_Bay', 'America/Goose_Bay'), ('America/Grand_Turk', 'America/Grand_Turk'), ('America/Grenada', 'America/Grenada'), ('America/Guadeloupe', 'America/Guadeloupe'), ('America/Guatemala', 'America/Guatemala'), ('America/Guayaquil', 'America/Guayaquil'), ('America/Guyana', 'America/Guyana'), ('America/Halifax', 'America/Halifax'), ('America/Havana', 'America/Havana'), ('America/Hermosillo', 'America/Hermosillo'), ('America/Indiana/Indianapolis', 'America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'America/Indiana/Knox'), ('America/Indiana/Marengo', 'America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'America/Indiana/Tell_City'), ('America/Indiana/Vevay', 'America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'America/Indiana/Winamac'), ('America/Inuvik', 'America/Inuvik'), ('America/Iqaluit', 'America/Iqaluit'), ('America/Jamaica', 'America/Jamaica'), ('America/Juneau', 'America/Juneau'), ('America/Kentucky/Louisville', 'America/Kentucky/Louisville'), ('America/Kentucky/Monticello', 'America/Kentucky/Monticello'), ('America/Kralendijk', 'America/Kralendijk'), ('America/La_Paz', 'America/La_Paz'), ('America/Lima', 'America/Lima'), ('America/Los_Angeles', 'America/Los_Angeles'), ('America/Lower_Princes', 'America/Lower_Princes'), ('America/Maceio', 'America/Maceio'), ('America/Managua', 'America/Managua'), ('America/Manaus', 'America/Manaus'), ('America/Marigot', 'America/Marigot'), ('America/Martinique', 'America/Martinique'), ('America/Matamoros', 'America/Matamoros'), ('America/Mazatlan', 'America/Mazatlan'), ('America/Menominee', 'America/Menominee'), ('America/Merida', 'America/Merida'), ('America/Metlakatla', 'America/Metlakatla'), ('America/Mexico_City', 'America/Mexico_City'), ('America/Miquelon', 'America/Miquelon'), ('America/Moncton', 'America/Moncton'), ('America/Monterrey', 'America/Monterrey'), ('America/Montevideo', 'America/Montevideo'), ('America/Montserrat', 'America/Montserrat'), ('America/Nassau', 'America/Nassau'), ('America/New_York', 'America/New_York'), ('America/Nipigon', 'America/Nipigon'), ('America/Nome', 'America/Nome'), ('America/Noronha', 'America/Noronha'), ('America/North_Dakota/Beulah', 'America/North_Dakota/Beulah'), ('America/North_Dakota/Center', 'America/North_Dakota/Center'), ('America/North_Dakota/New_Salem', 'America/North_Dakota/New_Salem'), ('America/Ojinaga', 'America/Ojinaga'), ('America/Panama', 'America/Panama'), ('America/Pangnirtung', 'America/Pangnirtung'), ('America/Paramaribo', 'America/Paramaribo'), ('America/Phoenix', 'America/Phoenix'), ('America/Port-au-Prince', 'America/Port-au-Prince'), ('America/Port_of_Spain', 'America/Port_of_Spain'), ('America/Porto_Velho', 'America/Porto_Velho'), ('America/Puerto_Rico', 'America/Puerto_Rico'), ('America/Punta_Arenas', 'America/Punta_Arenas'), ('America/Rainy_River', 'America/Rainy_River'), ('America/Rankin_Inlet', 'America/Rankin_Inlet'), ('America/Recife', 'America/Recife'), ('America/Regina', 'America/Regina'), ('America/Resolute', 'America/Resolute'), ('America/Rio_Branco', 'America/Rio_Branco'), ('America/Santarem', 'America/Santarem'), ('America/Santiago', 'America/Santiago'), ('America/Santo_Domingo', 'America/Santo_Domingo'), ('America/Sao_Paulo', 'America/Sao_Paulo'), ('America/Scoresbysund', 'America/Scoresbysund'), ('America/Sitka', 'America/Sitka'), ('America/St_Barthelemy', 'America/St_Barthelemy'), ('America/St_Johns', 'America/St_Johns'), ('America/St_Kitts', 'America/St_Kitts'), ('America/St_Lucia', 'America/St_Lucia'), ('America/St_Thomas', 'America/St_Thomas'), ('America/St_Vincent', 'America/St_Vincent'), ('America/Swift_Current', 'America/Swift_Current'), ('America/Tegucigalpa', 'America/Tegucigalpa'), ('America/Thule', 'America/Thule'), ('America/Thunder_Bay', 'America/Thunder_Bay'), ('America/Tijuana', 'America/Tijuana'), ('America/Toronto', 'America/Toronto'), ('America/Tortola', 'America/Tortola'), ('America/Vancouver', 'America/Vancouver'), ('America/Whitehorse', 'America/Whitehorse'), ('America/Winnipeg', 'America/Winnipeg'), ('America/Yakutat', 'America/Yakutat'), ('America/Yellowknife', 'America/Yellowknife'), ('Antarctica/Casey', 'Antarctica/Casey'), ('Antarctica/Davis', 'Antarctica/Davis'), ('Antarctica/DumontDUrville', 'Antarctica/DumontDUrville'), ('Antarctica/Macquarie', 'Antarctica/Macquarie'), ('Antarctica/Mawson', 'Antarctica/Mawson'), ('Antarctica/McMurdo', 'Antarctica/McMurdo'), ('Antarctica/Palmer', 'Antarctica/Palmer'), ('Antarctica/Rothera', 'Antarctica/Rothera'), ('Antarctica/Syowa', 'Antarctica/Syowa'), ('Antarctica/Troll', 'Antarctica/Troll'), ('Antarctica/Vostok', 'Antarctica/Vostok'), ('Arctic/Longyearbyen', 'Arctic/Longyearbyen'), ('Asia/Aden', 'Asia/Aden'), ('Asia/Almaty', 'Asia/Almaty'), ('Asia/Amman', 'Asia/Amman'), ('Asia/Anadyr', 'Asia/Anadyr'), ('Asia/Aqtau', 'Asia/Aqtau'), ('Asia/Aqtobe', 'Asia/Aqtobe'), ('Asia/Ashgabat', 'Asia/Ashgabat'), ('Asia/Atyrau', 'Asia/Atyrau'), ('Asia/Baghdad', 'Asia/Baghdad'), ('Asia/Bahrain', 'Asia/Bahrain'), ('Asia/Baku', 'Asia/Baku'), ('Asia/Bangkok', 'Asia/Bangkok'), ('Asia/Barnaul', 'Asia/Barnaul'), ('Asia/Beirut', 'Asia/Beirut'), ('Asia/Bishkek', 'Asia/Bishkek'), ('Asia/Brunei', 'Asia/Brunei'), ('Asia/Chita', 'Asia/Chita'), ('Asia/Choibalsan', 'Asia/Choibalsan'), ('Asia/Colombo', 'Asia/Colombo'), ('Asia/Damascus', 'Asia/Damascus'), ('Asia/Dhaka', 'Asia/Dhaka'), ('Asia/Dili', 'Asia/Dili'), ('Asia/Dubai', 'Asia/Dubai'), ('Asia/Dushanbe', 'Asia/Dushanbe'), ('Asia/Famagusta', 'Asia/Famagusta'), ('Asia/Gaza', 'Asia/Gaza'), ('Asia/Hebron', 'Asia/Hebron'), ('Asia/Ho_Chi_Minh', 'Asia/Ho_Chi_Minh'), ('Asia/Hong_Kong', 'Asia/Hong_Kong'), ('Asia/Hovd', 'Asia/Hovd'), ('Asia/Irkutsk', 'Asia/Irkutsk'), ('Asia/Jakarta', 'Asia/Jakarta'), ('Asia/Jayapura', 'Asia/Jayapura'), ('Asia/Jerusalem', 'Asia/Jerusalem'), ('Asia/Kabul', 'Asia/Kabul'), ('Asia/Kamchatka', 'Asia/Kamchatka'), ('Asia/Karachi', 'Asia/Karachi'), ('Asia/Kathmandu', 'Asia/Kathmandu'), ('Asia/Khandyga', 'Asia/Khandyga'), ('Asia/Kolkata', 'Asia/Kolkata'), ('Asia/Krasnoyarsk', 'Asia/Krasnoyarsk'), ('Asia/Kuala_Lumpur', 'Asia/Kuala_Lumpur'), ('Asia/Kuching', 'Asia/Kuching'), ('Asia/Kuwait', 'Asia/Kuwait'), ('Asia/Macau', 'Asia/Macau'), ('Asia/Magadan', 'Asia/Magadan'), ('Asia/Makassar', 'Asia/Makassar'), ('Asia/Manila', 'Asia/Manila'), ('Asia/Muscat', 'Asia/Muscat'), ('Asia/Nicosia', 'Asia/Nicosia'), ('Asia/Novokuznetsk', 'Asia/Novokuznetsk'), ('Asia/Novosibirsk', 'Asia/Novosibirsk'), ('Asia/Omsk', 'Asia/Omsk'), ('Asia/Oral', 'Asia/Oral'), ('Asia/Phnom_Penh', 'Asia/Phnom_Penh'), ('Asia/Pontianak', 'Asia/Pontianak'), ('Asia/Pyongyang', 'Asia/Pyongyang'), ('Asia/Qatar', 'Asia/Qatar'), ('Asia/Qostanay', 'Asia/Qostanay'), ('Asia/Qyzylorda', 'Asia/Qyzylorda'), ('Asia/Riyadh', 'Asia/Riyadh'), ('Asia/Sakhalin', 'Asia/Sakhalin'), ('Asia/Samarkand', 'Asia/Samarkand'), ('Asia/Seoul', 'Asia/Seoul'), ('Asia/Shanghai', 'Asia/Shanghai'), ('Asia/Singapore', 'Asia/Singapore'), ('Asia/Srednekolymsk', 'Asia/Srednekolymsk'), ('Asia/Taipei', 'Asia/Taipei'), ('Asia/Tashkent', 'Asia/Tashkent'), ('Asia/Tbilisi', 'Asia/Tbilisi'), ('Asia/Tehran', 'Asia/Tehran'), ('Asia/Thimphu', 'Asia/Thimphu'), ('Asia/Tokyo', 'Asia/Tokyo'), ('Asia/Tomsk', 'Asia/Tomsk'), ('Asia/Ulaanbaatar', 'Asia/Ulaanbaatar'), ('Asia/Urumqi', 'Asia/Urumqi'), ('Asia/Ust-Nera', 'Asia/Ust-Nera'), ('Asia/Vientiane', 'Asia/Vientiane'), ('Asia/Vladivostok', 'Asia/Vladivostok'), ('Asia/Yakutsk', 'Asia/Yakutsk'), ('Asia/Yangon', 'Asia/Yangon'), ('Asia/Yekaterinburg', 'Asia/Yekaterinburg'), ('Asia/Yerevan', 'Asia/Yerevan'), ('Atlantic/Azores', 'Atlantic/Azores'), ('Atlantic/Bermuda', 'Atlantic/Bermuda'), ('Atlantic/Canary', 'Atlantic/Canary'), ('Atlantic/Cape_Verde', 'Atlantic/Cape_Verde'), ('Atlantic/Faroe', 'Atlantic/Faroe'), ('Atlantic/Madeira', 'Atlantic/Madeira'), ('Atlantic/Reykjavik', 'Atlantic/Reykjavik'), ('Atlantic/South_Georgia', 'Atlantic/South_Georgia'), ('Atlantic/St_Helena', 'Atlantic/St_Helena'), ('Atlantic/Stanley', 'Atlantic/Stanley'), ('Australia/Adelaide', 'Australia/Adelaide'), ('Australia/Brisbane', 'Australia/Brisbane'), ('Australia/Broken_Hill', 'Australia/Broken_Hill'), ('Australia/Currie', 'Australia/Currie'), ('Australia/Darwin', 'Australia/Darwin'), ('Australia/Eucla', 'Australia/Eucla'), ('Australia/Hobart', 'Australia/Hobart'), ('Australia/Lindeman', 'Australia/Lindeman'), ('Australia/Lord_Howe', 'Australia/Lord_Howe'), ('Australia/Melbourne', 'Australia/Melbourne'), ('Australia/Perth', 'Australia/Perth'), ('Australia/Sydney', 'Australia/Sydney'), ('Canada/Atlantic', 'Canada/Atlantic'), ('Canada/Central', 'Canada/Central'), ('Canada/Eastern', 'Canada/Eastern'), ('Canada/Mountain', 'Canada/Mountain'), ('Canada/Newfoundland', 'Canada/Newfoundland'), ('Canada/Pacific', 'Canada/Pacific'), ('Europe/Amsterdam', 'Europe/Amsterdam'), ('Europe/Andorra', 'Europe/Andorra'), ('Europe/Astrakhan', 'Europe/Astrakhan'), ('Europe/Athens', 'Europe/Athens'), ('Europe/Belgrade', 'Europe/Belgrade'), ('Europe/Berlin', 'Europe/Berlin'), ('Europe/Bratislava', 'Europe/Bratislava'), ('Europe/Brussels', 'Europe/Brussels'), ('Europe/Bucharest', 'Europe/Bucharest'), ('Europe/Budapest', 'Europe/Budapest'), ('Europe/Busingen', 'Europe/Busingen'), ('Europe/Chisinau', 'Europe/Chisinau'), ('Europe/Copenhagen', 'Europe/Copenhagen'), ('Europe/Dublin', 'Europe/Dublin'), ('Europe/Gibraltar', 'Europe/Gibraltar'), ('Europe/Guernsey', 'Europe/Guernsey'), ('Europe/Helsinki', 'Europe/Helsinki'), ('Europe/Isle_of_Man', 'Europe/Isle_of_Man'), ('Europe/Istanbul', 'Europe/Istanbul'), ('Europe/Jersey', 'Europe/Jersey'), ('Europe/Kaliningrad', 'Europe/Kaliningrad'), ('Europe/Kiev', 'Europe/Kiev'), ('Europe/Kirov', 'Europe/Kirov'), ('Europe/Lisbon', 'Europe/Lisbon'), ('Europe/Ljubljana', 'Europe/Ljubljana'), ('Europe/London', 'Europe/London'), ('Europe/Luxembourg', 'Europe/Luxembourg'), ('Europe/Madrid', 'Europe/Madrid'), ('Europe/Malta', 'Europe/Malta'), ('Europe/Mariehamn', 'Europe/Mariehamn'), ('Europe/Minsk', 'Europe/Minsk'), ('Europe/Monaco', 'Europe/Monaco'), ('Europe/Moscow', 'Europe/Moscow'), ('Europe/Oslo', 'Europe/Oslo'), ('Europe/Paris', 'Europe/Paris'), ('Europe/Podgorica', 'Europe/Podgorica'), ('Europe/Prague', 'Europe/Prague'), ('Europe/Riga', 'Europe/Riga'), ('Europe/Rome', 'Europe/Rome'), ('Europe/Samara', 'Europe/Samara'), ('Europe/San_Marino', 'Europe/San_Marino'), ('Europe/Sarajevo', 'Europe/Sarajevo'), ('Europe/Saratov', 'Europe/Saratov'), ('Europe/Simferopol', 'Europe/Simferopol'), ('Europe/Skopje', 'Europe/Skopje'), ('Europe/Sofia', 'Europe/Sofia'), ('Europe/Stockholm', 'Europe/Stockholm'), ('Europe/Tallinn', 'Europe/Tallinn'), ('Europe/Tirane', 'Europe/Tirane'), ('Europe/Ulyanovsk', 'Europe/Ulyanovsk'), ('Europe/Uzhgorod', 'Europe/Uzhgorod'), ('Europe/Vaduz', 'Europe/Vaduz'), ('Europe/Vatican', 'Europe/Vatican'), ('Europe/Vienna', 'Europe/Vienna'), ('Europe/Vilnius', 'Europe/Vilnius'), ('Europe/Volgograd', 'Europe/Volgograd'), ('Europe/Warsaw', 'Europe/Warsaw'), ('Europe/Zagreb', 'Europe/Zagreb'), ('Europe/Zaporozhye', 'Europe/Zaporozhye'), ('Europe/Zurich', 'Europe/Zurich'), ('GMT', 'GMT'), ('Indian/Antananarivo', 'Indian/Antananarivo'), ('Indian/Chagos', 'Indian/Chagos'), ('Indian/Christmas', 'Indian/Christmas'), ('Indian/Cocos', 'Indian/Cocos'), ('Indian/Comoro', 'Indian/Comoro'), ('Indian/Kerguelen', 'Indian/Kerguelen'), ('Indian/Mahe', 'Indian/Mahe'), ('Indian/Maldives', 'Indian/Maldives'), ('Indian/Mauritius', 'Indian/Mauritius'), ('Indian/Mayotte', 'Indian/Mayotte'), ('Indian/Reunion', 'Indian/Reunion'), ('Pacific/Apia', 'Pacific/Apia'), ('Pacific/Auckland', 'Pacific/Auckland'), ('Pacific/Bougainville', 'Pacific/Bougainville'), ('Pacific/Chatham', 'Pacific/Chatham'), ('Pacific/Chuuk', 'Pacific/Chuuk'), ('Pacific/Easter', 'Pacific/Easter'), ('Pacific/Efate', 'Pacific/Efate'), ('Pacific/Enderbury', 'Pacific/Enderbury'), ('Pacific/Fakaofo', 'Pacific/Fakaofo'), ('Pacific/Fiji', 'Pacific/Fiji'), ('Pacific/Funafuti', 'Pacific/Funafuti'), ('Pacific/Galapagos', 'Pacific/Galapagos'), ('Pacific/Gambier', 'Pacific/Gambier'), ('Pacific/Guadalcanal', 'Pacific/Guadalcanal'), ('Pacific/Guam', 'Pacific/Guam'), ('Pacific/Honolulu', 'Pacific/Honolulu'), ('Pacific/Kiritimati', 'Pacific/Kiritimati'), ('Pacific/Kosrae', 'Pacific/Kosrae'), ('Pacific/Kwajalein', 'Pacific/Kwajalein'), ('Pacific/Majuro', 'Pacific/Majuro'), ('Pacific/Marquesas', 'Pacific/Marquesas'), ('Pacific/Midway', 'Pacific/Midway'), ('Pacific/Nauru', 'Pacific/Nauru'), ('Pacific/Niue', 'Pacific/Niue'), ('Pacific/Norfolk', 'Pacific/Norfolk'), ('Pacific/Noumea', 'Pacific/Noumea'), ('Pacific/Pago_Pago', 'Pacific/Pago_Pago'), ('Pacific/Palau', 'Pacific/Palau'), ('Pacific/Pitcairn', 'Pacific/Pitcairn'), ('Pacific/Pohnpei', 'Pacific/Pohnpei'), ('Pacific/Port_Moresby', 'Pacific/Port_Moresby'), ('Pacific/Rarotonga', 'Pacific/Rarotonga'), ('Pacific/Saipan', 'Pacific/Saipan'), ('Pacific/Tahiti', 'Pacific/Tahiti'), ('Pacific/Tarawa', 'Pacific/Tarawa'), ('Pacific/Tongatapu', 'Pacific/Tongatapu'), ('Pacific/Wake', 'Pacific/Wake'), ('Pacific/Wallis', 'Pacific/Wallis'), ('US/Alaska', 'US/Alaska'), ('US/Arizona', 'US/Arizona'), ('US/Central', 'US/Central'), ('US/Eastern', 'US/Eastern'), ('US/Hawaii', 'US/Hawaii'), ('US/Mountain', 'US/Mountain'), ('US/Pacific', 'US/Pacific'), ('UTC', 'UTC')], default='UTC', max_length=64),
+ ),
+ ]
diff --git a/yaksh/models.py b/yaksh/models.py
index 813453d..64489b8 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):
@@ -841,6 +856,15 @@ class LearningModule(models.Model):
write_templates_to_zip(zip_file, module_file_path, module_data,
module_name, folder_name)
+ def get_unit_order(self, type, unit):
+ if type == "lesson":
+ order = self.get_learning_units().get(
+ type=type, lesson=unit).order
+ else:
+ order = self.get_learning_units().get(
+ type=type, quiz=unit).order
+ return order
+
def __str__(self):
return self.name
@@ -1153,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)
@@ -1376,7 +1402,7 @@ class Question(models.Model):
testcases.append(case.get_field_value())
q_dict['testcase'] = testcases
q_dict['files'] = file_names
- q_dict['tags'] = [tags.tag.name for tags in q_dict['tags']]
+ q_dict['tags'] = [tag.name for tag in q_dict['tags']]
questions_dict.append(q_dict)
question._add_yaml_to_zip(zip_file, questions_dict)
return zip_file_name
@@ -2637,4 +2663,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/r_code_evaluator.py b/yaksh/r_code_evaluator.py
index ca4c94a..8eaeb38 100644
--- a/yaksh/r_code_evaluator.py
+++ b/yaksh/r_code_evaluator.py
@@ -7,6 +7,7 @@ import re
# Local imports
from .base_evaluator import BaseEvaluator
from .file_utils import copy_files, delete_files
+from .error_messages import prettify_exceptions
class RCodeEvaluator(BaseEvaluator):
@@ -49,9 +50,8 @@ class RCodeEvaluator(BaseEvaluator):
# Throw message if there are commmands that terminates scilab
add_err = ""
if terminate_commands:
- add_err = "Please do not use quit() in your\
- code.\n Otherwise your code will not be evaluated\
- correctly.\n"
+ add_err = "Please do not use quit() q() in your code.\
+ \n Otherwise your code will not be evaluated.\n"
cmd = 'Rscript main.r'
ret = self._run_command(cmd, shell=True, stdout=subprocess.PIPE,
@@ -66,10 +66,12 @@ class RCodeEvaluator(BaseEvaluator):
success, err = True, None
mark_fraction = 1.0 if self.partial_grading else 0.0
else:
- err = add_err + stdout
+ err = stdout + add_err
else:
- err = add_err + stderr
-
+ err = stderr + add_err
+ if err:
+ err = re.sub(r'.*?: ', '', err, count=1)
+ err = prettify_exceptions('Error', err)
return success, err, mark_fraction
def _remove_r_quit(self, string):
@@ -79,7 +81,8 @@ class RCodeEvaluator(BaseEvaluator):
new_string = ""
terminate_commands = False
for line in string.splitlines():
- new_line = re.sub(r"quit.*$", "", line)
+ new_line = re.sub(r'quit(.*$)', "", line)
+ new_line = re.sub(r'q(.*$)', "", new_line)
if line != new_line:
terminate_commands = True
new_string = new_string + '\n' + new_line
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/static/yaksh/js/codemirror/mode/r/index.html b/yaksh/static/yaksh/js/codemirror/mode/r/index.html
new file mode 100644
index 0000000..6dd9634
--- /dev/null
+++ b/yaksh/static/yaksh/js/codemirror/mode/r/index.html
@@ -0,0 +1,85 @@
+<!doctype html>
+
+<title>CodeMirror: R mode</title>
+<meta charset="utf-8"/>
+<link rel=stylesheet href="../../doc/docs.css">
+
+<link rel="stylesheet" href="../../lib/codemirror.css">
+<script src="../../lib/codemirror.js"></script>
+<script src="r.js"></script>
+<style>
+ .CodeMirror { border-top: 1px solid silver; border-bottom: 1px solid silver; }
+ .cm-s-default span.cm-semi { color: blue; font-weight: bold; }
+ .cm-s-default span.cm-dollar { color: orange; font-weight: bold; }
+ .cm-s-default span.cm-arrow { color: brown; }
+ .cm-s-default span.cm-arg-is { color: brown; }
+ </style>
+<div id=nav>
+ <a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
+
+ <ul>
+ <li><a href="../../index.html">Home</a>
+ <li><a href="../../doc/manual.html">Manual</a>
+ <li><a href="https://github.com/codemirror/codemirror">Code</a>
+ </ul>
+ <ul>
+ <li><a href="../index.html">Language modes</a>
+ <li><a class=active href="#">R</a>
+ </ul>
+</div>
+
+<article>
+<h2>R mode</h2>
+<form><textarea id="code" name="code">
+# Code from http://www.mayin.org/ajayshah/KB/R/
+
+# FIRST LEARN ABOUT LISTS --
+X = list(height=5.4, weight=54)
+print("Use default printing --")
+print(X)
+print("Accessing individual elements --")
+cat("Your height is ", X$height, " and your weight is ", X$weight, "\n")
+
+# FUNCTIONS --
+square <- function(x) {
+ return(x*x)
+}
+cat("The square of 3 is ", square(3), "\n")
+
+ # default value of the arg is set to 5.
+cube <- function(x=5) {
+ return(x*x*x);
+}
+cat("Calling cube with 2 : ", cube(2), "\n") # will give 2^3
+cat("Calling cube : ", cube(), "\n") # will default to 5^3.
+
+# LEARN ABOUT FUNCTIONS THAT RETURN MULTIPLE OBJECTS --
+powers <- function(x) {
+ parcel = list(x2=x*x, x3=x*x*x, x4=x*x*x*x);
+ return(parcel);
+}
+
+X = powers(3);
+print("Showing powers of 3 --"); print(X);
+
+# WRITING THIS COMPACTLY (4 lines instead of 7)
+
+powerful <- function(x) {
+ return(list(x2=x*x, x3=x*x*x, x4=x*x*x*x));
+}
+print("Showing powers of 3 --"); print(powerful(3));
+
+# In R, the last expression in a function is, by default, what is
+# returned. So you could equally just say:
+powerful <- function(x) {list(x2=x*x, x3=x*x*x, x4=x*x*x*x)}
+</textarea></form>
+ <script>
+ var editor = CodeMirror.fromTextArea(document.getElementById("code"), {});
+ </script>
+
+ <p><strong>MIME types defined:</strong> <code>text/x-rsrc</code>.</p>
+
+ <p>Development of the CodeMirror R mode was kindly sponsored
+ by <a href="https://twitter.com/ubalo">Ubalo</a>.</p>
+
+ </article>
diff --git a/yaksh/static/yaksh/js/codemirror/mode/r/r.js b/yaksh/static/yaksh/js/codemirror/mode/r/r.js
new file mode 100644
index 0000000..d41d1c5
--- /dev/null
+++ b/yaksh/static/yaksh/js/codemirror/mode/r/r.js
@@ -0,0 +1,164 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("wordChars", "r", /[\w.]/);
+
+CodeMirror.defineMode("r", function(config) {
+ function wordObj(str) {
+ var words = str.split(" "), res = {};
+ for (var i = 0; i < words.length; ++i) res[words[i]] = true;
+ return res;
+ }
+ var atoms = wordObj("NULL NA Inf NaN NA_integer_ NA_real_ NA_complex_ NA_character_");
+ var builtins = wordObj("list quote bquote eval return call parse deparse");
+ var keywords = wordObj("if else repeat while function for in next break");
+ var blockkeywords = wordObj("if else repeat while function for");
+ var opChars = /[+\-*\/^<>=!&|~$:]/;
+ var curPunc;
+
+ function tokenBase(stream, state) {
+ curPunc = null;
+ var ch = stream.next();
+ if (ch == "#") {
+ stream.skipToEnd();
+ return "comment";
+ } else if (ch == "0" && stream.eat("x")) {
+ stream.eatWhile(/[\da-f]/i);
+ return "number";
+ } else if (ch == "." && stream.eat(/\d/)) {
+ stream.match(/\d*(?:e[+\-]?\d+)?/);
+ return "number";
+ } else if (/\d/.test(ch)) {
+ stream.match(/\d*(?:\.\d+)?(?:e[+\-]\d+)?L?/);
+ return "number";
+ } else if (ch == "'" || ch == '"') {
+ state.tokenize = tokenString(ch);
+ return "string";
+ } else if (ch == "." && stream.match(/.[.\d]+/)) {
+ return "keyword";
+ } else if (/[\w\.]/.test(ch) && ch != "_") {
+ stream.eatWhile(/[\w\.]/);
+ var word = stream.current();
+ if (atoms.propertyIsEnumerable(word)) return "atom";
+ if (keywords.propertyIsEnumerable(word)) {
+ // Block keywords start new blocks, except 'else if', which only starts
+ // one new block for the 'if', no block for the 'else'.
+ if (blockkeywords.propertyIsEnumerable(word) &&
+ !stream.match(/\s*if(\s+|$)/, false))
+ curPunc = "block";
+ return "keyword";
+ }
+ if (builtins.propertyIsEnumerable(word)) return "builtin";
+ return "variable";
+ } else if (ch == "%") {
+ if (stream.skipTo("%")) stream.next();
+ return "variable-2";
+ } else if (ch == "<" && stream.eat("-")) {
+ return "arrow";
+ } else if (ch == "=" && state.ctx.argList) {
+ return "arg-is";
+ } else if (opChars.test(ch)) {
+ if (ch == "$") return "dollar";
+ stream.eatWhile(opChars);
+ return "operator";
+ } else if (/[\(\){}\[\];]/.test(ch)) {
+ curPunc = ch;
+ if (ch == ";") return "semi";
+ return null;
+ } else {
+ return null;
+ }
+ }
+
+ function tokenString(quote) {
+ return function(stream, state) {
+ if (stream.eat("\\")) {
+ var ch = stream.next();
+ if (ch == "x") stream.match(/^[a-f0-9]{2}/i);
+ else if ((ch == "u" || ch == "U") && stream.eat("{") && stream.skipTo("}")) stream.next();
+ else if (ch == "u") stream.match(/^[a-f0-9]{4}/i);
+ else if (ch == "U") stream.match(/^[a-f0-9]{8}/i);
+ else if (/[0-7]/.test(ch)) stream.match(/^[0-7]{1,2}/);
+ return "string-2";
+ } else {
+ var next;
+ while ((next = stream.next()) != null) {
+ if (next == quote) { state.tokenize = tokenBase; break; }
+ if (next == "\\") { stream.backUp(1); break; }
+ }
+ return "string";
+ }
+ };
+ }
+
+ function push(state, type, stream) {
+ state.ctx = {type: type,
+ indent: state.indent,
+ align: null,
+ column: stream.column(),
+ prev: state.ctx};
+ }
+ function pop(state) {
+ state.indent = state.ctx.indent;
+ state.ctx = state.ctx.prev;
+ }
+
+ return {
+ startState: function() {
+ return {tokenize: tokenBase,
+ ctx: {type: "top",
+ indent: -config.indentUnit,
+ align: false},
+ indent: 0,
+ afterIdent: false};
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ if (state.ctx.align == null) state.ctx.align = false;
+ state.indent = stream.indentation();
+ }
+ if (stream.eatSpace()) return null;
+ var style = state.tokenize(stream, state);
+ if (style != "comment" && state.ctx.align == null) state.ctx.align = true;
+
+ var ctype = state.ctx.type;
+ if ((curPunc == ";" || curPunc == "{" || curPunc == "}") && ctype == "block") pop(state);
+ if (curPunc == "{") push(state, "}", stream);
+ else if (curPunc == "(") {
+ push(state, ")", stream);
+ if (state.afterIdent) state.ctx.argList = true;
+ }
+ else if (curPunc == "[") push(state, "]", stream);
+ else if (curPunc == "block") push(state, "block", stream);
+ else if (curPunc == ctype) pop(state);
+ state.afterIdent = style == "variable" || style == "keyword";
+ return style;
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != tokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), ctx = state.ctx,
+ closing = firstChar == ctx.type;
+ if (ctx.type == "block") return ctx.indent + (firstChar == "{" ? 0 : config.indentUnit);
+ else if (ctx.align) return ctx.column + (closing ? 0 : 1);
+ else return ctx.indent + (closing ? 0 : config.indentUnit);
+ },
+
+ lineComment: "#"
+ };
+});
+
+CodeMirror.defineMIME("text/x-rsrc", "r");
+
+});
diff --git a/yaksh/static/yaksh/js/design_course.js b/yaksh/static/yaksh/js/design_course.js
index dbff9fd..4e2dc9d 100644
--- a/yaksh/static/yaksh/js/design_course.js
+++ b/yaksh/static/yaksh/js/design_course.js
@@ -20,7 +20,11 @@ $(document).ready(function(){
$(this).append('<input type="hidden" name="ordered_list" value='+order_list+'>');
return true;
});
- var msg = "Check Prerequisite is set to Yes by default \n" +
- "To change, select the Change checkbox and Click Change Prerequisite button \n";
- $("#prereq_msg").attr("title", msg);
+ var completion_msg = "This will check if the previous module is completed " +
+ "before viewing the next module."
+ $("#prereq_msg").attr("title", completion_msg);
+ $("#prereq_msg").tooltip();
+ var completion_msg = "This will check if the previous module is completed " +
+ "before viewing the next module based on quiz passing status."
+ $("#prereq_passing_msg").attr("title", completion_msg);
}); \ No newline at end of file
diff --git a/yaksh/static/yaksh/js/question_filter.js b/yaksh/static/yaksh/js/question_filter.js
deleted file mode 100644
index aa3a229..0000000
--- a/yaksh/static/yaksh/js/question_filter.js
+++ /dev/null
@@ -1,47 +0,0 @@
-$(document).ready(function(){
- $question_type = $("#id_question_type");
- $marks = $("#id_marks");
- $language = $("#id_language");
-
- function question_filter() {
- $.ajax({
- url: "/exam/ajax/questions/filter/",
- type: "POST",
- data: {
- question_type: $question_type.val(),
- marks: $marks.val(),
- language: $language.val()
- },
- dataType: "html",
- success: function(output) {
- var questions = $(output).filter("#questions").html();
- $("#filtered-questions").html(questions);
- }
- });
- }
-
- $question_type.change(function() {
- question_filter()
- });
-
- $language.change(function() {
- question_filter()
- });
-
- $marks.change(function() {
- question_filter()
- });
-
- $("#checkall").change(function(){
- if($(this).prop("checked")) {
- $("#filtered-questions input:checkbox").each(function(index, element) {
- $(this).prop('checked', true);
- });
- }
- else {
- $("#filtered-questions input:checkbox").each(function(index, element) {
- $(this).prop('checked', false);
- });
- }
- });
-}); \ No newline at end of file
diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js
index 7ccdef0..80b67fb 100644
--- a/yaksh/static/yaksh/js/requesthandler.js
+++ b/yaksh/static/yaksh/js/requesthandler.js
@@ -160,7 +160,8 @@ $(document).ready(function(){
'cpp': 'text/x-c++src',
'java': 'text/x-java',
'bash': 'text/x-sh',
- 'scilab': 'text/x-csrc'
+ 'scilab': 'text/x-csrc',
+ 'r':'text/x-rsrc',
}
// Code mirror Options
diff --git a/yaksh/static/yaksh/js/show_question.js b/yaksh/static/yaksh/js/show_question.js
index e6825a0..d7b6a44 100644
--- a/yaksh/static/yaksh/js/show_question.js
+++ b/yaksh/static/yaksh/js/show_question.js
@@ -47,7 +47,18 @@ function append_tag(tag){
tag_name.value = tag.value;
}
}
-$(document).ready(function()
- {
- $("#questions-table").tablesorter({sortList: [[0,0], [4,0]]});
+$(document).ready(function() {
+ $("#questions-table").tablesorter({});
+ $("#checkall").change(function(){
+ if($(this).prop("checked")) {
+ $("#filtered-questions input:checkbox").each(function(index, element) {
+ $(this).prop('checked', true);
+ });
+ }
+ else {
+ $("#filtered-questions input:checkbox").each(function(index, element) {
+ $(this).prop('checked', false);
+ });
+ }
});
+});
diff --git a/yaksh/templates/yaksh/add_course.html b/yaksh/templates/yaksh/add_course.html
index 97c6f56..0072a95 100644
--- a/yaksh/templates/yaksh/add_course.html
+++ b/yaksh/templates/yaksh/add_course.html
@@ -26,22 +26,10 @@
Add/Edit Course
</a>
</li>
- <li class="nav-item dropdown hide">
- <a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="true">More</a>
- <div class="dropdown-menu hide" x-placement="bottom-start" style="position: absolute; transform: translate3d(0px, 37px, 0px); top: 0px; left: 0px; will-change: transform;">
- <a class="dropdown-item" href="{% url 'yaksh:show_all_quizzes' %}">
- View Quizzes
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_lessons' %}">
- View Lessons
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_modules' %}">
- View Modules
- </a>
- <a href="{% url 'grades:grading_systems'%}" class="dropdown-item" >
- View Grading Systems
+ <li class="nav-item">
+ <a href="{% url 'grades:grading_systems'%}" class="nav-link" >
+ Add/View Grading Systems
</a>
- </div>
</li>
</ul>
</div>
diff --git a/yaksh/templates/yaksh/add_exercise.html b/yaksh/templates/yaksh/add_exercise.html
index d3d9068..542d1c4 100644
--- a/yaksh/templates/yaksh/add_exercise.html
+++ b/yaksh/templates/yaksh/add_exercise.html
@@ -23,17 +23,9 @@
</div>
{% endfor %}
{% endif %}
- {% if course_id %}
- <a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
- <i class="fa fa-arrow-left"></i>
- Back
- </a>
- {% else %}
- <a class="btn btn-primary" href="{% url 'yaksh:show_all_quizzes' %}">
- <i class="fa fa-arrow-left"></i>
- Back
- </a>
- {% endif %}
+ <a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
<br><br>
<form name=frm id=frm action="" method="post" >
{% csrf_token %}
diff --git a/yaksh/templates/yaksh/add_lesson.html b/yaksh/templates/yaksh/add_lesson.html
index 99fc31a..b984db0 100644
--- a/yaksh/templates/yaksh/add_lesson.html
+++ b/yaksh/templates/yaksh/add_lesson.html
@@ -26,17 +26,9 @@
<div class="container">
<div class="row justify-content-center form-group">
<div class="col-md-9 col-md-offset-4">
- {% if course_id %}
<a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
- <i class="fa fa-arrow-left"></i>
- Back
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
</a>
- {% else %}
- <a class="btn btn-primary" href="{% url 'yaksh:show_all_lessons' %}">
- <i class="fa fa-arrow-left"></i>
- Back
- </a>
- {% endif %}
<br>
{% if messages %}
<br>
diff --git a/yaksh/templates/yaksh/add_module.html b/yaksh/templates/yaksh/add_module.html
index edbfaa2..262c009 100644
--- a/yaksh/templates/yaksh/add_module.html
+++ b/yaksh/templates/yaksh/add_module.html
@@ -110,7 +110,7 @@
<!-- Add learning Units -->
{% if status == "design" %}
<div class="container">
-<center><h3><u>Add/Edit Learning Units</h3></u></center>
+<center><h2><u>{{module.name}}</u></h2></center>
{% if course_id %}
<form action="{% url 'yaksh:design_module' module_id course_id %}" method="POST" id="design_course_form">
{% else %}
@@ -161,8 +161,7 @@
<th width="25%" colspan="2">Check Prerequisite
<br>
<a href="#" data-toggle="tooltip" id="prereq_msg">
- <span class="glyphicon glyphicon-question-sign">
- </span> What's This
+ What's This&nbsp;<i class="fa fa-question-circle"></i>
</a>
</th>
</tr>
diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html
index 17cdcfe..0c846d0 100644
--- a/yaksh/templates/yaksh/add_question.html
+++ b/yaksh/templates/yaksh/add_question.html
@@ -74,18 +74,6 @@
</div>
</td>
</tr>
- {% if question %}
- <tr><td>Add Test Case:</td>
- <td>
- <select id="case_type" class="form-control" name="case_type" onchange="frm.submit()">
- <option value="" selected="selected">Select Testcase</option>
- {% for key, value in testcase_options %}
- <option value="{{key}}">{{value}}</option>
- {% endfor %}
- </select>
- </td>
- </tr>
- {% endif %}
</table>
{% if uploaded_files %}
<div class="card">
@@ -127,6 +115,22 @@
<h3>Test Cases</h3>
</div>
<div class="card-body">
+ {% if question %}
+ <div class="row">
+ <div class="col pb-4">
+ <tr><td>Add Test Case:</td>
+ <td>
+ <select id="case_type" class="form-control" name="case_type" onchange="frm.submit()">
+ <option value="" selected="selected">Select Testcase</option>
+ {% for key, value in testcase_options %}
+ <option value="{{key}}">{{value}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ </tr>
+ </div>
+ </div>
+ {% endif %}
{% for formset in formsets %}
{{ formset.management_form }}
<div id="accordion">
@@ -141,7 +145,7 @@
</div>
<div class="ml-auto">
<a class="card-link" data-toggle="collapse" href="#collapse{{form.instance.id}}">
- <i class="fa fa-toggle-down"></i>
+ Details&nbsp;<i class="fa fa-angle-down"></i></i>
</a>
</div>
</div>
diff --git a/yaksh/templates/yaksh/add_quiz.html b/yaksh/templates/yaksh/add_quiz.html
index 5497eeb..55e3bd6 100644
--- a/yaksh/templates/yaksh/add_quiz.html
+++ b/yaksh/templates/yaksh/add_quiz.html
@@ -35,17 +35,9 @@
</div>
{% endfor %}
{% endif %}
- {% if course_id %}
- <a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
- <i class="fa fa-arrow-left"></i>
- Back
- </a>
- {% else %}
- <a class="btn btn-primary" href="{% url 'yaksh:show_all_quizzes' %}">
- <i class="fa fa-arrow-left"></i>
- Back
- </a>
- {% endif %}
+ <a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
<br><br>
<form name=frm id=frm action="" method="post" >
{% csrf_token %}
diff --git a/yaksh/templates/yaksh/ajax_question_filter.html b/yaksh/templates/yaksh/ajax_question_filter.html
deleted file mode 100644
index 18f14ff..0000000
--- a/yaksh/templates/yaksh/ajax_question_filter.html
+++ /dev/null
@@ -1,57 +0,0 @@
-<div id="questions">
- <script>
- $(document).ready(function(){
- $("#checkall").change(function(){
- if($(this).prop("checked")) {
- $("#filtered-questions input:checkbox").each(function(index, element) {
- $(this).prop('checked', true);
- });
- }
- else {
- $("#filtered-questions input:checkbox").each(function(index, element) {
- $(this).prop('checked', false);
- });
- }
- });
- });
- </script>
- <br>
- <a class="btn btn-lg btn-success" href="{% url 'yaksh:add_question' %}">
- <i class="fa fa-plus-circle"></i>&nbsp;Add Question
- </a>
- <br><br>
- {% if questions %}
- {% include "yaksh/paginator.html" %}
-
- <h5 class="highlight"><input type="checkbox" id="checkall">
- Select All
- </h5>
- <ul class="inputs-list">
- <table id="questions-table" class="tablesorter table table table-striped">
- <thead>
- <tr>
- <th> Select </th>
- <th> Summary </th>
- <th> Language </th>
- <th> Type </th>
- <th> Marks </th>
- </tr>
- </thead>
- <tbody>
- {% for question in questions %}
- <tr>
- <td>
- <input type="checkbox" name="question" value="{{ question.id }}">
- </td>
- <td><a href="{% url 'yaksh:add_question' question.id %}">{{question.summary|capfirst}}</a></td>
- <td>{{question.language|capfirst}}</td>
- <td>{{question.type|capfirst}}</td>
- <td>{{question.points}}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </ul>
- {% include "yaksh/paginator.html" %}
- {% endif %}
-</div>
diff --git a/yaksh/templates/yaksh/course_added_modules.html b/yaksh/templates/yaksh/course_added_modules.html
index c70eb7a..2d194b9 100644
--- a/yaksh/templates/yaksh/course_added_modules.html
+++ b/yaksh/templates/yaksh/course_added_modules.html
@@ -1,46 +1,113 @@
{% if is_modules %}
{% block pagetitle %} <center> <h3>Course Modules</h3> </center> {% endblock %}
+ <a href="{% url 'yaksh:add_module' course.id %}" class="btn btn-primary btn-lg">
+ <i class="fa fa-plus-circle"></i>&nbsp;Add Module
+ </a>
+ <br><br>
{% if modules %}
- <table class="table table-responsive">
- <tr>
- <th>Module</th>
- <th>Module Design</th>
- <th>Lessons/Quizzes</th>
- </tr>
+ <center>
+ <div class="alert alert-dismissible alert-info">
+ <strong>
+ For additional module settings, Click on Design Module
+ </strong>
+ </div>
+ </center>
{% for module in modules %}
- <tr>
- <td>
- <a href="{% url 'yaksh:edit_module' module.id course.id %}">
- {{module.name}}</a>
- </td>
- <td>
- <a href="{% url 'yaksh:design_module' module.id course.id %}">
- Add Quizzes/Lessons for {{module.name}}
- </a>
- </td>
- <td>
- {% for unit in module.get_learning_units %}
- <ul class="inputs-list">
- <li>
- {% if unit.type == "quiz" %}
- {% if unit.quiz.is_exercise %}
- <a href="{% url 'yaksh:edit_exercise' unit.quiz.id course.id %}">
- {{unit.quiz.description}}</a>
+ <div class="card">
+ <div class="card-header">
+ <a href="{% url 'yaksh:edit_module' course.id module.id %}">
+ <i class="fa fa-edit"></i>&nbsp;{{module.name}}
+ </a>
+ </div>
+ <div class="card-body">
+ <div class="row">
+ <div class="col">
+ <a href="{% url 'yaksh:edit_lesson' course.id module.id %}" class="btn btn-info">
+ <i class="fa fa-plus-circle"></i>&nbsp;Add Lesson
+ </a>
+ </div>
+ <div class="col">
+ <a href="{% url 'yaksh:add_quiz' course.id module.id %}" class="btn btn-success">
+ <i class="fa fa-plus-circle"></i>&nbsp;Add Quiz
+ </a>
+ </div>
+ <div class="col">
+ <a href="{% url 'yaksh:add_exercise' course.id module.id %}" class="btn btn-dark">
+ <i class="fa fa-plus-circle"></i>&nbsp;Add Exercise
+ </a>
+ </div>
+ <div class="col">
+ <a href="{% url 'yaksh:design_module' module.id course.id %}" class="btn btn-secondary">
+ Design Module
+ </a>
+ </div>
+ </div>
+ <br>
+ {% with module.get_learning_units as units %}
+ {% if units %}
+ <p><b><u>Lessons/Quizzes/Exercise</u></b><p>
+ <table class="table table-responsive-sm">
+ {% for unit in units %}
+ <tr>
+ <td>
+ {% if unit.type == "quiz" %}
+ {% if unit.quiz.is_exercise %}
+ <a href="{% url 'yaksh:edit_exercise' course.id module.id unit.quiz.id %}">
+ {{unit.quiz.description}}</a>
+ {% else %}
+ <a href="{% url 'yaksh:edit_quiz' course.id module.id unit.quiz.id %}">
+ {{unit.quiz.description}}</a>
+ {% endif %}
+ {% else %}
+ <a href="{% url 'yaksh:edit_lesson' course.id module.id unit.lesson.id %}">
+ {{unit.lesson.name}}</a>
+ {% endif %}
+ </td>
+ <td>
+ {% if unit.type == "quiz" %}
+ {% with unit.quiz as quiz %}
+ {% if quiz.questionpaper_set.get.id %}
+ <a href="{% url 'yaksh:designquestionpaper' course.id quiz.id quiz.questionpaper_set.get.id %}" class="btn btn-primary">
+ <i class="fa fa-edit"></i>
+ Edit Question Paper
+ </a>
+ {% else %}
+ <a href="{% url 'yaksh:designquestionpaper' course.id quiz.id %}" class="btn btn-success">
+ <i class="fa fa-plus-circle"></i>
+ Add Question Paper
+ </a>
+ {% endif %}
+ {% endwith %}
+ {% else %}
+ -------
+ {% endif %}
+ </td>
+ <td>
+ {% if unit.type == "quiz" %}
+ {% if unit.quiz.is_exercise %}
+ Exercise
+ {% else %}
+ Quiz
+ {% endif %}
+ {% else %}
+ Lesson
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
{% else %}
- <a href="{% url 'yaksh:edit_quiz' unit.quiz.id course.id %}">
- {{unit.quiz.description}}</a>
+ <center>
+ <span class="badge badge-warning">
+ <big>No lesson/quiz added</big>
+ </span>
+ </center>
{% endif %}
- {% else %}
- <a href="{% url 'yaksh:edit_lesson' unit.lesson.id course.id %}">
- {{unit.lesson.name}}</a>
- {% endif %}
- </li>
- </ul>
- {% endfor %}
- </td>
- </tr>
- {% endfor %} <!-- end for modules -->
- </table>
+ {% endwith %}
+ </div>
+ </div>
+ <br>
+ {% endfor %}
{% else %}
<center>
<span class="badge badge-warning"><big>No learning modules</big></span>
diff --git a/yaksh/templates/yaksh/course_detail_options.html b/yaksh/templates/yaksh/course_detail_options.html
index 6f9a711..4dd4dda 100644
--- a/yaksh/templates/yaksh/course_detail_options.html
+++ b/yaksh/templates/yaksh/course_detail_options.html
@@ -5,13 +5,18 @@
</a>
</li>
<li class="nav-item">
- <a href="{% url 'yaksh:course_students' course.id %}" id="enroll-students" class="nav-link list-group-item {% if is_students %} active {% endif %}" title="View the course requested, rejected and added students" data-placement="top" data-toggle="tooltip">
+ <a href="{% url 'yaksh:course_students' course.id %}" id="enroll-students" class="nav-link list-group-item {% if is_students %} active {% endif %}" title="View the course requested, rejected and enrolled students" data-placement="top" data-toggle="tooltip">
Enroll Students
</a>
</li>
<li class="nav-item">
- <a href="{% url 'yaksh:send_mail' course.id %}" class="nav-link list-group-item {% if is_mail %} active {% endif %}" title="Send mail to course students" data-placement="top" data-toggle="tooltip">
- Send Mail
+ <a class="nav-link list-group-item {% if is_modules %} active {% endif %}" href="{% url 'yaksh:get_course_modules' course.id %}" title="View modules added to the course" data-placement="top" data-toggle="tooltip">
+ Course Modules
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link list-group-item {% if is_design_course %} active {% endif %}" href="{% url 'yaksh:design_course' course.id %}" title="Additional course settings" data-placement="top" data-toggle="tooltip">
+ Design Course
</a>
</li>
<li class="nav-item">
@@ -20,13 +25,13 @@
</a>
</li>
<li class="nav-item">
- <a class="nav-link list-group-item {% if is_design_course %} active {% endif %}" href="{% url 'yaksh:design_course' course.id %}" title="Add modules to this course" data-placement="top" data-toggle="tooltip">
- Design Course
+ <a href="{% url 'yaksh:send_mail' course.id %}" class="nav-link list-group-item {% if is_mail %} active {% endif %}" title="Send mail to course students" data-placement="top" data-toggle="tooltip">
+ Send Mail
</a>
</li>
<li class="nav-item">
- <a class="nav-link list-group-item {% if is_modules %} active {% endif %}" href="{% url 'yaksh:get_course_modules' course.id %}" title="View modules added to the course" data-placement="top" data-toggle="tooltip">
- Course Modules
+ <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">
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 214f8c7..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 %}
@@ -128,9 +129,11 @@
View
</a>
{% else %}
- <a href="{% url 'yaksh:start_quiz' unit.quiz.questionpaper_set.get.id module.id course.id %}" class="btn btn-outline-info">
- View
- </a>
+ {% if unit.quiz.questionpaper_set.get %}
+ <a href="{% url 'yaksh:start_quiz' unit.quiz.questionpaper_set.get.id module.id course.id %}" class="btn btn-outline-info">
+ View
+ </a>
+ {% endif %}
{% endif %}
</td>
<td>
diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html
index 084d0f6..151e1fb 100644
--- a/yaksh/templates/yaksh/courses.html
+++ b/yaksh/templates/yaksh/courses.html
@@ -8,14 +8,6 @@
</script>
{% endblock %}
-{% block css %}
-<style>
- .test + .tooltip.top > .tooltip-inner {
- padding: 15px;
- font-size: 12px;
- }
-</style>
-{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="container">
@@ -32,22 +24,10 @@
Add/Edit Course
</a>
</li>
- <li class="nav-item dropdown hide">
- <a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="true">More</a>
- <div class="dropdown-menu hide" x-placement="bottom-start" style="position: absolute; transform: translate3d(0px, 37px, 0px); top: 0px; left: 0px; will-change: transform;">
- <a class="dropdown-item" href="{% url 'yaksh:show_all_quizzes' %}">
- Add/View Quizzes
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_lessons' %}">
- Add/View Lessons
- </a>
- <a class="dropdown-item" href="{% url 'yaksh:show_all_modules' %}">
- Add/View Modules
- </a>
- <a href="{% url 'grades:grading_systems'%}" class="dropdown-item" >
+ <li class="nav-item">
+ <a href="{% url 'grades:grading_systems'%}" class="nav-link" >
Add/View Grading Systems
</a>
- </div>
</li>
</ul>
</div>
@@ -61,8 +41,7 @@
</div>
{% else %}
<hr>
- <form name=frm action="" method="post">
- {% csrf_token %}
+ <form name=frm action="" method="get">
<div class="card">
<div class="card-header">
<h3>Search/Filter Courses</h3>
@@ -77,9 +56,11 @@
</div>
</div>
<br>
- <button class="btn btn-success" type="submit">Search</button>
- <a class="btn btn-primary" href="{% url 'yaksh:courses' %}">
- Clear Search
+ <button class="btn btn-outline-success" type="submit">
+ <i class="fa fa-search"></i>&nbsp;Search
+ </button>
+ <a class="btn btn-outline-danger" href="{% url 'yaksh:courses' %}">
+ <i class="fa fa-times"></i>&nbsp;Clear
</a>
</div>
</div>
diff --git a/yaksh/templates/yaksh/design_course_session.html b/yaksh/templates/yaksh/design_course_session.html
index a15f4b1..88ecc16 100644
--- a/yaksh/templates/yaksh/design_course_session.html
+++ b/yaksh/templates/yaksh/design_course_session.html
@@ -68,16 +68,14 @@
<th width="25%" colspan="2">Check Prerequisite Completion
<br>
<a href="#" data-toggle="tooltip" id="prereq_msg">
- <span class="glyphicon glyphicon-question-sign">
- </span> What's This
+ What's This&nbsp;<i class="fa fa-question-circle"></i>
</a>
</th>
<th width="25%" colspan="2">Check Prerequisite Passing
<br>
<a href="#" data-toggle="tooltip" id="prereq_passing_msg">
- <span class="glyphicon glyphicon-question-sign">
- </span> What's This
- </a>
+ What's This&nbsp;<i class="fa fa-question-circle"></i>
+ </a>
</th>
</tr>
<tr>
diff --git a/yaksh/templates/yaksh/design_questionpaper.html b/yaksh/templates/yaksh/design_questionpaper.html
index 6e916a3..ffbdf5f 100644
--- a/yaksh/templates/yaksh/design_questionpaper.html
+++ b/yaksh/templates/yaksh/design_questionpaper.html
@@ -17,17 +17,10 @@
{% block content %}
<div class="container">
<input type=hidden id="url_root" value={{ URL_ROOT }}>
-{% if course_id %}
- <form action="{% url 'yaksh:designquestionpaper' qpaper.quiz.id qpaper.id course_id %}" method="POST" id="design_q">
+ <form action="{% url 'yaksh:designquestionpaper' course_id qpaper.quiz.id qpaper.id %}" method="POST" id="design_q">
<a href="{% url 'yaksh:get_course_modules' course_id %}" class="btn btn-primary">
<i class="fa fa-arrow-left"></i>&nbsp;Back
</a>
-{% else %}
- <form action="{% url 'yaksh:designquestionpaper' qpaper.quiz.id qpaper.id %}" method="POST" id="design_q">
- <a href="{% url 'yaksh:show_all_quizzes' %}" class="btn btn-primary">
- <i class="fa fa-arrow-left"></i>&nbsp;Back
- </a>
-{% endif %}
{% csrf_token %}
<input type=hidden name="is_active" id="is_active" value="{{ state }}">
<center><b>Manual mode to design the {{lang}} Question Paper</center><br>
diff --git a/yaksh/templates/yaksh/paginator.html b/yaksh/templates/yaksh/paginator.html
index 5f0df7a..e958519 100644
--- a/yaksh/templates/yaksh/paginator.html
+++ b/yaksh/templates/yaksh/paginator.html
@@ -1,7 +1,8 @@
<ul class="pagination pagination">
{% if objects.has_previous %}
<li class="page-item">
- <a class="page-link" href="?page=1" aria-label="Previous">
+ <a class="page-link" href="?page=1{% if request.GET.question_type %}&question_type={{ request.GET.question_type }}{% endif %}{% if request.GET.language %}&language={{ request.GET.language }}{% endif %}{% if request.GET.marks %}&marks={{ request.GET.marks }}{% endif %}{% if request.GET.question_tags %}&question_tags={{ request.GET.question_tags }}{% endif %}
+ {% if request.GET.search_tags %}&search_tags={{ request.GET.search_tags }}{% endif %}{% if request.GET.search_status %}&search_status={{ request.GET.search_status }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">
<i class="fa fa-angle-double-left"></i>
</span>
@@ -16,13 +17,13 @@
<span class="page-link">{{ n }}<span class="sr-only">(current)</span></span>
</li>
{% elif n > objects.number|add:'-5' and n < objects.number|add:'5' %}
- <li class="page-item"><a class="page-link" href="?page={{ n }}">{{ n }}</a></li>
+ <li class="page-item"><a class="page-link" href="?page={{ n }}{% if request.GET.question_type %}&question_type={{ request.GET.question_type }}{% endif %}{% if request.GET.language %}&language={{ request.GET.language }}{% endif %}{% if request.GET.marks %}&marks={{ request.GET.marks }}{% endif %}{% if request.GET.question_tags %}&question_tags={{ request.GET.question_tags }}{% endif %}{% if request.GET.search_tags %}&search_tags={{ request.GET.search_tags }}{% endif %}{% if request.GET.search_status %}&search_status={{ request.GET.search_status }}{% endif %}">{{ n }}</a></li>
{% endif %}
{% endfor %}
{% if objects.has_next %}
<li class="page-item">
- <a class="page-link" href="?page={{ objects.paginator.num_pages }}" aria-label="Next">
+ <a class="page-link" href="?page={{ objects.paginator.num_pages }}{% if request.GET.question_type %}&question_type={{ request.GET.question_type }}{% endif %}{% if request.GET.language %}&language={{ request.GET.language }}{% endif %}{% if request.GET.marks %}&marks={{ request.GET.marks }}{% endif %}{% if request.GET.question_tags %}&question_tags={{ request.GET.question_tags }}{% endif %}{% if request.GET.search_tags %}&search_tags={{ request.GET.search_tags }}{% endif %}{% if request.GET.search_status %}&search_status={{ request.GET.search_status }}{% endif %}" aria-label="Next">
<span aria-hidden="true">
<i class="fa fa-angle-double-right"></i>
</span>
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/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index 74343f8..92d591f 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -26,6 +26,7 @@
<script src="{% static 'yaksh/js/codemirror/mode/python/python.js' %}"></script>
<script src="{% static 'yaksh/js/codemirror/mode/clike/clike.js' %}"></script>
<script src="{% static 'yaksh/js/codemirror/mode/shell/shell.js' %}"></script>
+<script src="{% static 'yaksh/js/codemirror/mode/r/r.js' %}"></script>
<script type="text/javascript" src="{% static 'yaksh/js/mathjax/MathJax.js' %}?config=TeX-MML-AM_CHTML"></script>
<script src="{% static 'yaksh/js/jquery-sortable.js' %}"></script>
<script>
diff --git a/yaksh/templates/yaksh/showquestions.html b/yaksh/templates/yaksh/showquestions.html
index fdfcc60..daeaea7 100644
--- a/yaksh/templates/yaksh/showquestions.html
+++ b/yaksh/templates/yaksh/showquestions.html
@@ -7,207 +7,218 @@
{% block script %}
<script type="text/javascript" src="{% static 'yaksh/js/show_question.js' %}"></script>
-<script type="text/javascript" src="{% static 'yaksh/js/question_filter.js' %}"></script>
<script type="text/javascript" src="{% static 'yaksh/js/jquery.tablesorter.min.js' %}"></script>
{% endblock %}
{% block content %}
-<div class="container">
- <!-- Side bar -->
- <div class="nav nav-pills" role="tablist" aria-orientation="vertical">
- <a href="#show" id="showbar" class="nav-link active" data-toggle="pill" role="tab" aria-controls="show" aria-selected="true"> Show all Questions</a>
- <a href="#updown" id="updownbar" class="nav-link" data-toggle="pill" role="tab" aria-controls="updown" aria-selected="false" > Upload Questions</a>
- </div>
- <!-- End of side bar -->
- <div class="tab-content">
+ <div class="container-fluid">
+ <div class="nav nav-pills" role="tablist" aria-orientation="vertical">
+ <a href="#show" id="showbar" class="nav-link active" data-toggle="pill" role="tab" aria-controls="show" aria-selected="true"> All Questions</a>
+ <a href="#updown" id="updownbar" class="nav-link" data-toggle="pill" role="tab" aria-controls="updown" aria-selected="false" > Upload Questions</a>
+ </div>
<br>
- <!-- Upload Questions -->
- <div id="updown" class="card tab-pane fade" role="tabpanel" aria-labelledby="updownbar">
- <div class="col" role="alert">
- <p>You can upload question files the following ways -
- <li><b><u>Yaml File</u></b>
- <p>One can upload Yaml file with extensions .yaml or .yml. Please note
- that you cannot upload files associated to a question. Yaml file can
- have any name.
- </p>
- </li>
- <li><b><u>Zip File</u></b>
- <p> One can also upload zip with the following zip structure - </p>
- <pre>
- .zip
- |-- .yaml or .yml
- |-- .yaml or .yml
- |-- folder1
- | |-- Files required by questions
- |-- folder2
- | |-- Files required by questions
- </pre>
- </li>
- </p>
- </div>
- <div class="card-body">
- <form action="" method="post" enctype="multipart/form-data">
- {% csrf_token %}
- <div class="form-group col-md-6">
- <a class="btn btn-info" href="{% url 'yaksh:download_yaml_template' %}">
- <i class="fa fa-download"></i>&nbsp;Download Template</a>
- <br><br>
- <h4> Or </h4>
- <br>
- <div class="input-group mb-3">
- <div class="custom-file">
- {{ upload_form }}
- <label class="custom-file-label" for="id_file">
- Choose file
- </label>
- </div>
- <div class="input-group-append">
- <button class="btn btn-outline-primary" type="submit" name="upload" value="upload"><i class="fa fa-upload"></i>&nbsp;Upload File</button>
- </div>
- </div>
- </div>
- <script>
- $('#id_file').on('change',function(){
- //get the file name
- var fileName = $(this).val();
- //replace the "Choose a file" label
- $(this).next('.custom-file-label').html(fileName);
- })
- </script>
- </form>
+ <div class="tab-content">
+ <!-- Upload Questions -->
+ <div id="updown" class="card tab-pane fade" role="tabpanel" aria-labelledby="updownbar">
+ <div class="col" role="alert">
+ <p>You can upload question files the following ways -
+ <li><b><u>Yaml File</u></b>
+ <p>One can upload Yaml file with extensions .yaml or .yml. Please note
+ that you cannot upload files associated to a question. Yaml file can
+ have any name.
+ </p>
+ </li>
+ <li><b><u>Zip File</u></b>
+ <p> One can also upload zip with the following zip structure - </p>
+ <pre>
+ .zip
+ |-- .yaml or .yml
+ |-- .yaml or .yml
+ |-- folder1
+ | |-- Files required by questions
+ |-- folder2
+ | |-- Files required by questions
+ </pre>
+ </li>
+ </p>
+ </div>
+ <div class="card-body">
+ <form action="" method="post" enctype="multipart/form-data">
+ {% csrf_token %}
+ <div class="form-group col-md-6">
+ <a class="btn btn-info" href="{% url 'yaksh:download_yaml_template' %}">
+ <i class="fa fa-download"></i>&nbsp;Download Template</a>
+ <br><br>
+ <h4> Or </h4>
+ <br>
+ <div class="input-group mb-3">
+ <div class="custom-file">
+ {{ upload_form }}
+ <label class="custom-file-label" for="id_file">
+ Choose file
+ </label>
+ </div>
+ <div class="input-group-append">
+ <button class="btn btn-outline-primary" type="submit" name="upload" value="upload"><i class="fa fa-upload"></i>&nbsp;Upload File</button>
+ </div>
+ </div>
+ </div>
+ <script>
+ $('#id_file').on('change',function(){
+ //get the file name
+ var fileName = $(this).val();
+ //replace the "Choose a file" label
+ $(this).next('.custom-file-label').html(fileName);
+ })
+ </script>
+ </form>
+ </div>
</div>
- </div>
- <!-- End of upload questions -->
+ <!-- End of upload questions -->
- <!-- Show questions -->
- <div id="show" class="tab-pane fade show active" role="tabpanel" aria-labelledby="showbar">
- {% if messages %}
- {% for message in messages %}
- <div class="alert alert-dismissible alert-info">
- <button type="button" class="close" data-dismiss="alert">
- <i class="fa fa-close"></i>
- </button>
- <strong>{{ message }}</strong>
- </div>
- {% endfor %}
- {% endif %}
- <form name=frm action="" method="post">
+ <div id="show" class="tab-pane fade show active" role="tabpanel" aria-labelledby="showbar">
+ {% if messages %}
+ {% for message in messages %}
+ <div class="alert alert-dismissible alert-info">
+ <button type="button" class="close" data-dismiss="alert">
+ <i class="fa fa-close"></i>
+ </button>
+ <strong>{{ message }}</strong>
+ </div>
+ {% endfor %}
+ {% endif %}
<div class="card">
<div class="card-body">
- <!-- Filtering Questions -->
- <div id="selectors">
- <h4>Filters Questions: </h4>
- <div class="dropdown">
- <div class="col-md-4">
- {{ form.question_type }}
- </div>
- <div class="col-md-4">
- {{ form.language }}
- </div>
- <div class="col-md-4">
- {{ form.marks }}
- </div>
+ <!-- Filter Questions -->
+ <h4>Filters Questions: </h4>
+ <form method="GET" action="{% url 'yaksh:questions_filter' %}">
+
+ <div class="row">
+ <div class="col-md-4">{{ form.question_type }}</div>
+ <div class="col-md-4">{{ form.language }}</div>
+ <div class="col-md-4">{{ form.marks }}</div>
+ <br><br>
+ <div class="col">
+ <button class="btn btn-outline-success">
+ <i class="fa fa-filter"></i>&nbsp;Filter
+ </button>
+ </div>
</div>
- </div>
+ </form>
+ <!-- End Filter Questions -->
<hr>
- <h4 >Or Search using Tags: </h4>
- <!-- Searching Tags -->
- {% csrf_token %}
- <div class="col-md-14">
- <div class="input-group">
- <div class="col-md-6">
+ <!-- Search by Tags -->
+ <h4 >Search using Tags: </h4>
+ <div class="row">
+ <div class="col">
+ <form method="GET" action="{% url 'yaksh:search_questions_by_tags' %}">
<div class="input-group">
- <div class="input-group-prepend">
- <span class="input-group-text" id="basic-addon1">Search Questions</span>
- </div>
- <input type="text" name="question_tags" id="question_tags" class="form-control" type="search" placeholder="Search using comma separated Tags">
+ <input type="text" name="question_tags" id="question_tags" class="form-control" type="search" placeholder="Search questions using comma separated Tags">
<span class="input-group-append">
- <a class="btn btn-outline-secondary" type="submit"><i class="fa fa-search yakshred"></i></a>
+ <button class="btn btn-outline-success" type="submit">
+ <i class="fa fa-search"></i>&nbsp;Search
+ </button>
</span>
</div>
- </div>
- <div class="col-md-6">
- <select class="form-control" id="sel1" onchange="append_tag(this);">
- {% if all_tags %}
- <option value="" disabled selected>Available Tags</option>
- {% for tag in all_tags %}
- <option>
- {{tag}}
- </option>
- {% endfor %}
- {% else %}
- <option value="" disabled selected>No Available Tags</option>
- {% endif %}
- </select>
- </div>
- <br><br>
- <div class="col-md-6">
- <a class="btn btn-primary" href="{% url 'yaksh:show_questions' %}">
- Clear Filters
- </a>
- </div>
+ </form>
+ </div>
+ <div class="col">
+ <select class="form-control" id="sel1" onchange="append_tag(this);">
+ {% if all_tags %}
+ <option value="" disabled selected>Available Tags</option>
+ {% for tag in all_tags %}
+ <option>
+ {{tag}}
+ </option>
+ {% endfor %}
+ {% else %}
+ <option value="" disabled selected>No Available Tags</option>
+ {% endif %}
+ </select>
</div>
</div>
+ <br>
+ <!-- End Search by Tags -->
+ <a class="btn btn-outline-danger" href="{% url 'yaksh:show_questions' %}">
+ <i class="fa fa-times"></i>&nbsp;Clear
+ </a>
</div>
+ <!-- End Card body -->
</div>
- <div id="filtered-questions">
+ <!-- End card filters and search -->
+ <form name=frm action="{% url 'yaksh:show_questions' %}" method="post">
+ {% csrf_token %}
+ <div id="filtered-questions">
+ <br>
+ <a class="btn btn-lg btn-success" href="{% url 'yaksh:add_question' %}">
+ <i class="fa fa-plus-circle"></i>&nbsp;Add Question</a>
+ {% if objects %}
+ <div>
+ <br>
+ {% include "yaksh/paginator.html" %}
+ <br>
+ <h5><input id="checkall" type="checkbox"> Select All </h5>
+ <div class="table-wrapper-2">
+ <table id="questions-table" class="tablesorter table table-striped table-responsive-sm">
+ <thead>
+ <tr>
+ <th> Select </th>
+ <th> Sr No. </th>
+ <th> Summary&nbsp;<i class="fa fa-sort"></i> </th>
+ <th> Language&nbsp;<i class="fa fa-sort"></i> </th>
+ <th> Type&nbsp;<i class="fa fa-sort"></i> </th>
+ <th> Marks&nbsp;<i class="fa fa-sort"></i> </th>
+ <th>Test</th>
+ <th>Download</th>
+ <th>Delete</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for question in objects %}
+ <tr>
+ <td>
+ <input type="checkbox" name="question" value="{{ question.id }}">
+ </td>
+ <td>{{forloop.counter}}</td>
+ <td><a href="{% url 'yaksh:add_question' question.id %}">{{question.summary|capfirst}}</a></td>
+ <td>{{question.language|capfirst}}</td>
+ <td>{{question.type|capfirst}}</td>
+ <td>{{question.points}}</td>
+ <td>
+ <a href="{% url 'yaksh:test_question' question.id %}" class="btn btn-info">
+ Test
+ </a>
+ </td>
+ <td><a href="{% url 'yaksh:download_question' question.id %}" class="btn btn-primary">
+ <i class="fa fa-download"></i>&nbsp;Download</a></td>
+ <td><a href="{% url 'yaksh:delete_question' question.id %}" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete {{question.summary|capfirst}}?')">
+ <i class="fa fa-trash"></i>&nbsp;Delete</a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ {% include "yaksh/paginator.html" %}
+ {% else %}
+ <br><br>
+ <div class="alert alert-info">
+ <center><h3>No Questions found</h3></center>
+ </div>
+ {% endif %}
+ </div>
<br>
- <a class="btn btn-lg btn-success" href="{% url 'yaksh:add_question' %}">
- <i class="fa fa-plus-circle"></i>&nbsp;Add Question</a>
- {% if questions %}
- <div>
- <br>
- {% include "yaksh/paginator.html" %}
- <br>
- <h5><input id="checkall" type="checkbox"> Select All </h5>
- <div class="table-wrapper-2">
- <table id="questions-table" class="tablesorter table table-striped table-responsive-sm">
- <thead>
- <tr class="yakshred">
- <th> Select </th>
- <th> Summary&nbsp;<i class="fa fa-sort"></i> </th>
- <th> Language&nbsp;<i class="fa fa-sort"></i> </th>
- <th> Type&nbsp;<i class="fa fa-sort"></i> </th>
- <th> Marks&nbsp;<i class="fa fa-sort"></i> </th>
- </tr>
- </thead>
- <tbody>
- {% for question in questions %}
- <tr>
- <td>
- <input type="checkbox" name="question" value="{{ question.id }}">
- </td>
- <td><a href="{% url 'yaksh:add_question' question.id %}">{{question.summary|capfirst}}</a></td>
- <td>{{question.language|capfirst}}</td>
- <td>{{question.type|capfirst}}</td>
- <td>{{question.points}}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
- </div>
- {% include "yaksh/paginator.html" %}
- {% else %}
- <br><br>
- <div class="alert alert-info">
- <center><h3>No Questions created</h3></center>
- </div>
- {% endif %}
- </div>
- <br>
- <center>
- {% if questions %}
- <button class="btn btn-lg btn-primary" type="submit" name='download' value='download'><i class="fa fa-download"></i>&nbsp;Download Selected</button>
- <button class="btn btn-lg btn-primary" type="submit" name="test" value="test">Test Selected</button>
- <button class="btn btn-lg btn-danger" type="submit" onClick="return confirm_delete(frm);" name='delete' value='delete'>
- <i class="fa fa-trash"></i>&nbsp;Delete Selected</button>
- {% endif %}
- </center>
- </form>
- </div>
- <!-- End of Show questions -->
+ <center>
+ {% if objects %}
+ <button class="btn btn-lg btn-primary" type="submit" name='download' value='download'><i class="fa fa-download"></i>&nbsp;Download Selected</button>
+ <button class="btn btn-lg btn-primary" type="submit" name="test" value="test">Test Selected</button>
+ <button class="btn btn-lg btn-danger" type="submit" onClick="return confirm_delete(frm);" name='delete' value='delete'>
+ <i class="fa fa-trash"></i>&nbsp;Delete Selected</button>
+ {% endif %}
+ </center>
+ </form>
+ </div>
</div>
-</div>
+ </div>
{% endblock %}
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 569d4d7..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
@@ -1083,6 +1084,10 @@ class TestAddQuiz(TestCase):
enrollment="Enroll Request", creator=self.user
)
+ self.module = LearningModule.objects.create(
+ name="My test module", creator=self.user, description="Test"
+ )
+
self.quiz = Quiz.objects.create(
start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone),
end_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, tzone),
@@ -1099,6 +1104,12 @@ class TestAddQuiz(TestCase):
is_exercise=True, description='demo exercise', creator=self.user
)
+ unit1 = LearningUnit.objects.create(
+ type="quiz", quiz=self.quiz, order=1)
+ unit2 = LearningUnit.objects.create(
+ type="quiz", quiz=self.exercise, order=2)
+ self.module.learning_unit.add(*[unit1.id, unit2.id])
+
def tearDown(self):
self.client.logout()
self.user.delete()
@@ -1112,10 +1123,16 @@ class TestAddQuiz(TestCase):
"""
If not logged in redirect to login page
"""
- response = self.client.get(reverse('yaksh:add_quiz'),
- follow=True
- )
- redirect_destination = '/exam/login/?next=/exam/manage/addquiz/'
+ response = self.client.get(
+ 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
+ )
+ )
self.assertRedirects(response, redirect_destination)
def test_add_quiz_denies_non_moderator(self):
@@ -1126,9 +1143,11 @@ class TestAddQuiz(TestCase):
username=self.student.username,
password=self.student_plaintext_pass
)
- response = self.client.get(reverse('yaksh:add_quiz'),
- follow=True
- )
+ response = self.client.get(
+ 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):
@@ -1139,8 +1158,11 @@ class TestAddQuiz(TestCase):
username=self.user.username,
password=self.user_plaintext_pass
)
- response = self.client.get(reverse('yaksh:add_quiz')
- )
+ response = self.client.get(
+ 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'])
@@ -1155,8 +1177,11 @@ class TestAddQuiz(TestCase):
)
tzone = pytz.timezone('UTC')
response = self.client.post(
- reverse('yaksh:edit_quiz',
- kwargs={'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',
@@ -1198,7 +1223,9 @@ class TestAddQuiz(TestCase):
tzone = pytz.timezone('UTC')
response = self.client.post(
- reverse('yaksh:add_quiz'),
+ reverse('yaksh:add_quiz',
+ kwargs={'course_id': self.course.id,
+ 'module_id': self.module.id}),
data={
'start_date_time': '2016-01-10 09:00:15',
'end_date_time': '2016-01-15 09:00:15',
@@ -1233,11 +1260,18 @@ class TestAddQuiz(TestCase):
"""
If not logged in redirect to login page
"""
- response = self.client.get(reverse('yaksh:add_exercise'),
- follow=True
- )
- redirect_destination = '/exam/login/?next=/exam/manage/add_exercise/'
- self.assertRedirects(response, redirect_destination)
+ response = self.client.get(
+ reverse('yaksh:add_exercise',
+ kwargs={'course_id': self.course.id,
+ 'module_id': self.module.id}),
+ follow=True
+ )
+ redirect = (
+ '/exam/login/?next=/exam/manage/add_exercise/{0}/{1}/'.format(
+ self.course.id, self.module.id
+ )
+ )
+ self.assertRedirects(response, redirect)
def test_add_exercise_denies_non_moderator(self):
"""
@@ -1247,9 +1281,12 @@ class TestAddQuiz(TestCase):
username=self.student.username,
password=self.student_plaintext_pass
)
- response = self.client.get(reverse('yaksh:add_exercise'),
- follow=True
- )
+ response = self.client.get(
+ reverse('yaksh:add_exercise',
+ kwargs={'course_id': self.course.id,
+ 'module_id': self.module.id}),
+ follow=True
+ )
self.assertEqual(response.status_code, 404)
def test_add_exercise_get(self):
@@ -1260,8 +1297,11 @@ class TestAddQuiz(TestCase):
username=self.user.username,
password=self.user_plaintext_pass
)
- response = self.client.get(reverse('yaksh:add_exercise')
- )
+ response = self.client.get(
+ reverse('yaksh:add_exercise',
+ kwargs={'course_id': self.course.id,
+ 'module_id': self.module.id})
+ )
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/add_exercise.html')
self.assertIsNotNone(response.context['form'])
@@ -1275,8 +1315,11 @@ class TestAddQuiz(TestCase):
password=self.user_plaintext_pass
)
response = self.client.post(
- reverse('yaksh:edit_exercise',
- kwargs={'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
@@ -1301,7 +1344,10 @@ class TestAddQuiz(TestCase):
password=self.user_plaintext_pass
)
response = self.client.post(
- reverse('yaksh:add_exercise'),
+ reverse('yaksh:add_exercise', kwargs={
+ 'course_id': self.course.id,
+ 'module_id': self.module.id
+ }),
data={
'description': "Demo Exercise",
'active': True
@@ -1317,19 +1363,6 @@ class TestAddQuiz(TestCase):
self.assertEqual(new_exercise.pass_criteria, 0)
self.assertTrue(new_exercise.is_exercise)
- def test_show_all_quizzes(self):
- self.client.login(
- username=self.user.username,
- password=self.user_plaintext_pass
- )
- response = self.client.get(
- reverse('yaksh:show_all_quizzes'),
- follow=True
- )
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['quizzes'][0], self.quiz)
- self.assertTemplateUsed(response, "yaksh/quizzes.html")
-
class TestAddAsModerator(TestCase):
def setUp(self):
@@ -2249,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',
@@ -2335,60 +2368,15 @@ class TestSearchFilters(TestCase):
username=self.user1.username,
password=self.user1_plaintext_pass
)
- response = self.client.post(
+ response = self.client.get(
reverse('yaksh:courses'),
- data={'course_tags': 'demo', 'course_status': 'active'}
+ data={'search_tags': 'demo', 'search_status': 'active'}
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/courses.html')
self.assertIsNotNone(response.context['form'])
self.assertIn(self.user1_course1, response.context['courses'])
- def test_quizzes_search_filter(self):
- """ Test to check if quizzes are obtained with tags and status """
- self.client.login(
- username=self.user1.username,
- password=self.user1_plaintext_pass
- )
- response = self.client.post(
- reverse('yaksh:show_all_quizzes'),
- data={'quiz_tags': 'demo', 'quiz_status': 'active'}
- )
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'yaksh/quizzes.html')
- self.assertIsNotNone(response.context['form'])
- self.assertIn(self.quiz1, response.context['quizzes'])
-
- def test_lessons_search_filter(self):
- """ Test to check if lessons are obtained with tags and status """
- self.client.login(
- username=self.user1.username,
- password=self.user1_plaintext_pass
- )
- response = self.client.post(
- reverse('yaksh:show_all_lessons'),
- data={'lesson_tags': 'demo', 'lesson_status': 'active'}
- )
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'yaksh/lessons.html')
- self.assertIsNotNone(response.context['form'])
- self.assertIn(self.lesson1, response.context['lessons'])
-
- def test_learning_modules_search_filter(self):
- """ Test to check if learning modules are obtained with tags and status """
- self.client.login(
- username=self.user1.username,
- password=self.user1_plaintext_pass
- )
- response = self.client.post(
- reverse('yaksh:show_all_modules'),
- data={'module_tags': 'demo', 'module_status': 'active'}
- )
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'yaksh/modules.html')
- self.assertIsNotNone(response.context['form'])
- self.assertIn(self.learning_module1, response.context['modules'])
-
class TestAddCourse(TestCase):
def setUp(self):
@@ -4372,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))
@@ -4448,6 +4438,7 @@ class TestShowQuestions(TestCase):
points=2.0, language="python", type="code", user=self.user,
active=True
)
+ self.question.tags.add("question1")
self.question1 = Question.objects.create(
summary="Test_question2", description="Add two numbers",
points=1.0, language="python", type="mcq", user=self.user,
@@ -4517,7 +4508,7 @@ class TestShowQuestions(TestCase):
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/showquestions.html')
- self.assertEqual(response.context['questions'][0], self.question)
+ self.assertEqual(response.context['objects'][0], self.question1)
def test_download_questions(self):
"""
@@ -4668,13 +4659,13 @@ class TestShowQuestions(TestCase):
)
trial_course = Course.objects.get(name="trial_course")
trial_module = trial_course.learning_module.all()[0]
- redirection_url = "/exam/start/1/{0}/{1}/{2}".format(
+ redirection_url = "/exam/start/1/{0}/{1}/{2}/".format(
trial_module.id, trial_que_paper.id, trial_course.id
)
self.assertEqual(response.status_code, 302)
- self.assertRedirects(response, redirection_url, target_status_code=301)
+ self.assertRedirects(response, redirection_url, target_status_code=200)
- def test_ajax_questions_filter(self):
+ def test_questions_filter(self):
"""
Check for filter questions based type, marks and
language of a question
@@ -4683,15 +4674,15 @@ class TestShowQuestions(TestCase):
username=self.user.username,
password=self.user_plaintext_pass
)
- response = self.client.post(
+ response = self.client.get(
reverse('yaksh:questions_filter'),
data={'question_type': 'mcq',
'marks': '1.0', 'language': 'python'
}
)
self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, 'yaksh/ajax_question_filter.html')
- self.assertEqual(response.context['questions'][0], self.question1)
+ self.assertTemplateUsed(response, 'yaksh/showquestions.html')
+ self.assertEqual(response.context['objects'][0], self.question1)
def test_download_question_yaml_template(self):
""" Test to check download question yaml template """
@@ -4737,13 +4728,63 @@ class TestShowQuestions(TestCase):
password=self.user_plaintext_pass
)
self.question.tags.add('code')
- response = self.client.post(
- reverse('yaksh:show_questions'),
- data={'question_tags': ['code']}
+ response = self.client.get(
+ reverse('yaksh:search_questions_by_tags'),
+ data={'question_tags': ['question1']}
)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'yaksh/showquestions.html')
- self.assertEqual(response.context['questions'][0], self.question)
+ self.assertEqual(response.context['objects'][0], self.question)
+
+ def test_single_question_attempt(self):
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ response = self.client.get(
+ reverse('yaksh:test_question', args=[self.question.id])
+ )
+ trial_que_paper = QuestionPaper.objects.get(
+ quiz__description="trial_questions"
+ )
+ trial_course = Course.objects.get(name="trial_course")
+ trial_module = trial_course.learning_module.all()[0]
+ redirection_url = "/exam/start/1/{0}/{1}/{2}/".format(
+ trial_module.id, trial_que_paper.id, trial_course.id
+ )
+ self.assertEqual(response.status_code, 302)
+ self.assertRedirects(response, redirection_url, target_status_code=200)
+
+ def test_single_question_download(self):
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ response = self.client.get(
+ reverse('yaksh:download_question', args=[self.question.id])
+ )
+ file_name = "{0}_question.zip".format(self.user)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.get('Content-Disposition'),
+ "attachment; filename={0}".format(file_name))
+ zip_file = string_io(response.content)
+ zipped_file = zipfile.ZipFile(zip_file, 'r')
+ self.assertIsNone(zipped_file.testzip())
+ self.assertIn('questions_dump.yaml', zipped_file.namelist())
+ zip_file.close()
+ zipped_file.close()
+
+ def test_single_question_delete(self):
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ response = self.client.get(
+ reverse('yaksh:delete_question', args=[self.question.id])
+ )
+ self.assertEqual(response.status_code, 302)
+ updated_que = Question.objects.get(id=self.question.id)
+ self.assertFalse(updated_que.active)
class TestShowStatistics(TestCase):
@@ -5491,14 +5532,17 @@ class TestQuestionPaper(TestCase):
response = self.client.get(
reverse('yaksh:designquestionpaper',
- kwargs={"quiz_id": self.demo_quiz.id,
- "questionpaper_id": self.question_paper.id}))
+ kwargs={
+ "course_id": self.course.id,
+ "quiz_id": self.demo_quiz.id,
+ "questionpaper_id": self.question_paper.id}))
self.assertEqual(response.status_code, 404)
# Design question paper for a quiz
response = self.client.post(
reverse('yaksh:designquestionpaper',
- kwargs={"quiz_id": self.quiz_without_qp.id}),
+ kwargs={"course_id": self.course.id,
+ "quiz_id": self.quiz_without_qp.id}),
data={"marks": "1.0", "question_type": "code"})
self.assertEqual(response.status_code, 200)
self.assertIsNotNone(response.context['questions'])
@@ -5511,7 +5555,8 @@ class TestQuestionPaper(TestCase):
response = self.client.get(
reverse('yaksh:designquestionpaper',
- kwargs={"quiz_id": self.demo_quiz.id,
+ kwargs={"course_id": self.course.id,
+ "quiz_id": self.demo_quiz.id,
"questionpaper_id": self.question_paper.id}))
self.assertEqual(response.status_code, 404)
@@ -5523,7 +5568,8 @@ class TestQuestionPaper(TestCase):
# Should not allow teacher to view question paper
response = self.client.get(
reverse('yaksh:designquestionpaper',
- kwargs={"quiz_id": self.quiz.id,
+ kwargs={"course_id": self.course.id,
+ "quiz_id": self.quiz.id,
"questionpaper_id": self.question_paper.id}))
self.assertEqual(response.status_code, 404)
@@ -5792,14 +5838,14 @@ class TestLearningModule(TestCase):
)
# Student tries to add learning module
- response = self.client.post(reverse('yaksh:add_module'),
- data={"name": "test module1",
- "description": "my test1",
- "Save": "Save"})
+ response = self.client.post(
+ reverse('yaksh:add_module', kwargs={"course_id": self.course.id}),
+ data={"name": "test module1",
+ "description": "my test1",
+ "Save": "Save"})
self.assertEqual(response.status_code, 404)
# Student tries to view learning modules
- response = self.client.get(reverse('yaksh:show_all_modules'))
self.assertEqual(response.status_code, 404)
def test_add_new_module(self):
@@ -5810,10 +5856,11 @@ class TestLearningModule(TestCase):
)
# Test add new module
- response = self.client.post(reverse('yaksh:add_module'),
- data={"name": "test module1",
- "description": "my test1",
- "Save": "Save"})
+ response = self.client.post(
+ reverse('yaksh:add_module', kwargs={"course_id": self.course.id}),
+ data={"name": "test module1",
+ "description": "my test1",
+ "Save": "Save"})
self.assertEqual(response.status_code, 200)
learning_module = LearningModule.objects.get(name="test module1")
@@ -5833,7 +5880,8 @@ class TestLearningModule(TestCase):
# Test add new module
response = self.client.post(
reverse('yaksh:edit_module',
- kwargs={"module_id": self.learning_module.id}),
+ kwargs={"course_id": self.course.id,
+ "module_id": self.learning_module.id}),
data={"name": "test module2",
"description": "my test2",
"Save": "Save"})
@@ -5846,17 +5894,6 @@ class TestLearningModule(TestCase):
self.assertEqual(learning_module.html_data,
Markdown().convert("my test2"))
- def test_show_all_modules(self):
- """Try to get all learning modules"""
- self.client.login(
- username=self.user.username,
- password=self.user_plaintext_pass
- )
- response = self.client.get(reverse('yaksh:show_all_modules'))
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['modules'][0],
- self.learning_module)
-
def test_teacher_can_edit_module(self):
""" Check if teacher can edit the module """
self.client.login(
@@ -6178,7 +6215,8 @@ class TestLessons(TestCase):
response = self.client.get(
reverse('yaksh:edit_lesson',
kwargs={"lesson_id": self.lesson.id,
- "course_id": self.course.id}))
+ "course_id": self.course.id,
+ "module_id": self.learning_module.id}))
self.assertEqual(response.status_code, 404)
def test_teacher_can_edit_lesson(self):
@@ -6192,7 +6230,8 @@ class TestLessons(TestCase):
response = self.client.post(
reverse('yaksh:edit_lesson',
kwargs={"lesson_id": self.lesson.id,
- "course_id": self.course.id}),
+ "course_id": self.course.id,
+ "module_id": self.learning_module.id}),
data={"name": "updated lesson",
"description": "updated description",
"Lesson_files": dummy_file,
@@ -6201,7 +6240,7 @@ class TestLessons(TestCase):
)
# Teacher edits existing lesson and adds file
- self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.status_code, 302)
updated_lesson = Lesson.objects.get(name="updated lesson")
self.assertEqual(updated_lesson.description, "updated description")
self.assertEqual(updated_lesson.creator, self.user)
@@ -6216,7 +6255,8 @@ class TestLessons(TestCase):
response = self.client.post(
reverse('yaksh:edit_lesson',
kwargs={"lesson_id": self.lesson.id,
- "course_id": self.course.id}),
+ "course_id": self.course.id,
+ "module_id": self.learning_module.id}),
data={"delete_files": [str(lesson_files.id)],
"Delete": "Delete"}
)
@@ -6287,17 +6327,6 @@ class TestLessons(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["msg"], err_msg)
- def test_show_all_lessons(self):
- """ Moderator should be able to see all created lessons"""
- self.client.login(
- username=self.user.username,
- password=self.user_plaintext_pass
- )
- response = self.client.get(reverse('yaksh:show_all_lessons'))
- self.assertEqual(response.status_code, 200)
- self.assertTemplateUsed(response, "yaksh/lessons.html")
- self.assertEqual(response.context["lessons"][0], self.lesson)
-
def test_preview_lesson_description(self):
""" Test preview lesson description converted from md to html"""
self.client.login(
@@ -6311,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 b53d335..149e4d6 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -59,19 +59,29 @@ 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,
name="add_question"),
- url(r'^manage/addquiz/$', views.add_quiz, name='add_quiz'),
- url(r'^manage/add_exercise/$', views.add_exercise, name='add_exercise'),
- url(r'^manage/add_exercise/(?P<quiz_id>\d+)/$', views.add_exercise,
- name='edit_exercise'),
- url(r'^manage/add_exercise/(?P<quiz_id>\d+)/(?P<course_id>\d+)/$',
+ url(r'^manage/add_exercise/(?P<course_id>\d+)/(?P<module_id>\d+)/$',
+ views.add_exercise, name='add_exercise'),
+ url(r'^manage/add_exercise/(?P<course_id>\d+)/(?P<module_id>\d+)/(?P<quiz_id>\d+)$',
views.add_exercise, name='edit_exercise'),
- url(r'^manage/addquiz/(?P<quiz_id>\d+)/$',
- views.add_quiz, name='edit_quiz'),
- url(r'^manage/addquiz/(?P<quiz_id>\d+)/(?P<course_id>\d+)$',
+ url(r'^manage/addquiz/(?P<course_id>\d+)/(?P<module_id>\d+)/$',
+ views.add_quiz, name='add_quiz'),
+ url(r'^manage/addquiz/(?P<course_id>\d+)/(?P<module_id>\d+)/(?P<quiz_id>\d+)$',
views.add_quiz, name='edit_quiz'),
url(r'^manage/gradeuser/$', views.grade_user, name="grade_user"),
url(r'^manage/gradeuser/(?P<quiz_id>\d+)/(?P<course_id>\d+)/$',
@@ -91,13 +101,10 @@ urlpatterns = [
'(?P<course_id>\d+)/$',
views.user_data, name="user_data"),
url(r'^manage/user_data/(?P<user_id>\d+)/$', views.user_data),
- url(r'^manage/quiz/designquestionpaper/(?P<quiz_id>\d+)/$',
- views.design_questionpaper, name='designquestionpaper'),
- url(r'^manage/designquestionpaper/(?P<quiz_id>\d+)/'
+ url(r'^manage/designquestionpaper/(?P<course_id>\d+)/(?P<quiz_id>\d+)/'
'(?P<questionpaper_id>\d+)/$',
views.design_questionpaper, name='designquestionpaper'),
- url(r'^manage/designquestionpaper/(?P<quiz_id>\d+)/'
- '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$',
+ url(r'^manage/designquestionpaper/(?P<course_id>\d+)/(?P<quiz_id>\d+)/$',
views.design_questionpaper, name='designquestionpaper'),
url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/'
'(?P<course_id>\d+)/$',
@@ -129,7 +136,7 @@ urlpatterns = [
views.reject, {'was_enrolled': True}, name="reject_user"),
url(r'manage/toggle_status/(?P<course_id>\d+)/$',
views.toggle_course_status, name="toggle_course_status"),
- url(r'^ajax/questions/filter/$', views.ajax_questions_filter,
+ url(r'^questions/filter$', views.questions_filter,
name="questions_filter"),
url(r'^editprofile/$', views.edit_profile, name='edit_profile'),
url(r'^viewprofile/$', views.view_profile, name='view_profile'),
@@ -176,29 +183,19 @@ urlpatterns = [
views.download_yaml_template, name="download_yaml_template"),
url(r'^manage/download_sample_csv/',
views.download_sample_csv, name="download_sample_csv"),
- url(r'^manage/courses/edit_lesson/$',
+ url(r'^manage/courses/edit_lesson/(?P<course_id>\d+)/(?P<module_id>\d+)/$',
views.edit_lesson, name="edit_lesson"),
- url(r'^manage/courses/edit_lesson/(?P<lesson_id>\d+)/$',
- views.edit_lesson, name="edit_lesson"),
- url(r'^manage/courses/edit_lesson/(?P<lesson_id>\d+)/(?P<course_id>\d+)/$',
+ url(r'^manage/courses/edit_lesson/(?P<course_id>\d+)/(?P<module_id>\d+)/(?P<lesson_id>\d+)/$',
views.edit_lesson, name="edit_lesson"),
url(r'^manage/courses/designmodule/(?P<module_id>\d+)/$',
views.design_module, name="design_module"),
url(r'^manage/courses/designmodule/(?P<module_id>\d+)/'
'(?P<course_id>\d+)/$', views.design_module, name="design_module"),
- url(r'^manage/courses/all_quizzes/$',
- views.show_all_quizzes, name="show_all_quizzes"),
- url(r'^manage/courses/all_lessons/$',
- views.show_all_lessons, name="show_all_lessons"),
url(r'^manage/courses/lesson/preview/$',
views.preview_html_text, name="preview_html_text"),
- url(r'^manage/courses/all_learning_module/$',
- views.show_all_modules, name="show_all_modules"),
- url(r'^manage/courses/add_module/$',
+ url(r'^manage/courses/add_module/(?P<course_id>\d+)/$',
views.add_module, name="add_module"),
- url(r'^manage/courses/add_module/(?P<module_id>\d+)/$',
- views.add_module, name="edit_module"),
- url(r'^manage/courses/add_module/(?P<module_id>\d+)/(?P<course_id>\d+)/$',
+ url(r'^manage/courses/add_module/(?P<course_id>\d+)/(?P<module_id>\d+)/$',
views.add_module, name="edit_module"),
url(r'^manage/courses/designcourse/(?P<course_id>\d+)/$',
views.design_course, name="design_course"),
@@ -220,4 +217,12 @@ urlpatterns = [
views.course_teachers, name="course_teachers"),
url(r'^manage/download/course/progress/(?P<course_id>\d+)',
views.download_course_progress, name="download_course_progress"),
+ url(r'^manage/question/download/(?P<question_id>\d+)',
+ views.download_question, name="download_question"),
+ url(r'^manage/question/test/(?P<question_id>\d+)',
+ views.test_question, name="test_question"),
+ url(r'^manage/question/delete/(?P<question_id>\d+)',
+ views.delete_question, name="delete_question"),
+ url(r'^manage/search/questions', views.search_questions_by_tags,
+ name="search_questions_by_tags"),
]
diff --git a/yaksh/views.py b/yaksh/views.py
index b54461f..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
@@ -103,8 +103,9 @@ def get_html_text(md_text):
def formfield_callback(field):
- if (isinstance(field, models.TextField) and field.name == 'expected_output'
- or field.name == 'expected_input'):
+ if (isinstance(field, models.TextField) and field.name == 'expected_input'):
+ return fields.CharField(strip=False, required = False)
+ if (isinstance(field, models.TextField) and field.name == 'expected_output'):
return fields.CharField(strip=False)
return field.formfield()
@@ -176,15 +177,14 @@ def quizlist_user(request, enrolled=None, msg=None):
courses = hidden_courses
title = 'Search Results'
else:
- courses = list(Course.objects.filter(
- active=True, is_trial=False,
+ enrolled_courses = user.students.filter(is_trial=False).order_by('-id')
+ remaining_courses = list(Course.objects.filter(
+ active=True, is_trial=False, hidden=False
).exclude(
- ~Q(requests=user), ~Q(rejected=user), hidden=True
- ).order_by('-id'))
- enrolled_course = list(
- user.students.filter(is_trial=False).order_by('-id')
- )
- courses.extend(enrolled_course)
+ id__in=enrolled_courses.values_list("id", flat=True)
+ ).order_by('-id'))
+ courses = list(enrolled_courses)
+ courses.extend(remaining_courses)
title = 'All Courses'
for course in courses:
@@ -319,7 +319,7 @@ def add_question(request, question_id=None):
@login_required
@email_verified
-def add_quiz(request, quiz_id=None, course_id=None):
+def add_quiz(request, course_id=None, module_id=None, quiz_id=None):
"""To add a new quiz in the database.
Create a new quiz and store it."""
user = request.user
@@ -331,6 +331,8 @@ def add_quiz(request, quiz_id=None, course_id=None):
raise Http404('This quiz does not belong to you')
else:
quiz = None
+ if module_id:
+ module = get_object_or_404(LearningModule, id=module_id)
if course_id:
course = get_object_or_404(Course, pk=course_id)
if not course.is_creator(user) and not course.is_teacher(user):
@@ -341,9 +343,22 @@ def add_quiz(request, quiz_id=None, course_id=None):
form = QuizForm(request.POST, instance=quiz)
if form.is_valid():
if quiz is None:
+ last_unit = module.get_learning_units().last()
+ order = last_unit.order + 1 if last_unit else 1
form.instance.creator = user
- form.save()
+ else:
+ order = module.get_unit_order("quiz", quiz)
+ added_quiz = form.save()
+ unit, created = LearningUnit.objects.get_or_create(
+ type="quiz", quiz=added_quiz, order=order
+ )
+ if created:
+ module.learning_unit.add(unit.id)
messages.success(request, "Quiz saved successfully")
+ return redirect(
+ reverse("yaksh:edit_quiz",
+ args=[course_id, module_id, added_quiz.id])
+ )
else:
form = QuizForm(instance=quiz)
context["course_id"] = course_id
@@ -354,7 +369,7 @@ def add_quiz(request, quiz_id=None, course_id=None):
@login_required
@email_verified
-def add_exercise(request, quiz_id=None, course_id=None):
+def add_exercise(request, course_id=None, module_id=None, quiz_id=None):
user = request.user
if not is_moderator(user):
raise Http404('You are not allowed to view this course !')
@@ -364,6 +379,8 @@ def add_exercise(request, quiz_id=None, course_id=None):
raise Http404('This quiz does not belong to you')
else:
quiz = None
+ if module_id:
+ module = get_object_or_404(LearningModule, id=module_id)
if course_id:
course = get_object_or_404(Course, pk=course_id)
if not course.is_creator(user) and not course.is_teacher(user):
@@ -374,7 +391,11 @@ def add_exercise(request, quiz_id=None, course_id=None):
form = ExerciseForm(request.POST, instance=quiz)
if form.is_valid():
if quiz is None:
+ last_unit = module.get_learning_units().last()
+ order = last_unit.order + 1 if last_unit else 1
form.instance.creator = user
+ else:
+ order = module.get_unit_order("quiz", quiz)
quiz = form.save(commit=False)
quiz.is_exercise = True
quiz.time_between_attempts = 0
@@ -384,9 +405,18 @@ def add_exercise(request, quiz_id=None, course_id=None):
quiz.duration = 1000
quiz.pass_criteria = 0
quiz.save()
+ unit, created = LearningUnit.objects.get_or_create(
+ type="quiz", quiz=quiz, order=order
+ )
+ if created:
+ module.learning_unit.add(unit.id)
messages.success(
request, "{0} saved successfully".format(quiz.description)
)
+ return redirect(
+ reverse("yaksh:edit_exercise",
+ args=[course_id, module_id, quiz.id])
+ )
else:
form = ExerciseForm(instance=quiz)
context["exercise"] = quiz
@@ -1040,21 +1070,20 @@ def courses(request):
Q(creator=user) | Q(teachers=user),
is_trial=False).order_by('-active').distinct()
- form = SearchFilterForm()
+ tags = request.GET.get('search_tags')
+ status = request.GET.get('search_status')
- if request.method == 'POST':
- course_tags = request.POST.get('search_tags')
- course_status = request.POST.get('search_status')
-
- if course_status == 'select' :
- courses = courses.filter(
- name__contains=course_tags)
- elif course_status == 'active' :
- courses = courses.filter(
- name__contains=course_tags, active=True)
- elif course_status == 'closed':
- courses = courses.filter(
- name__contains=course_tags, active=False)
+ form = SearchFilterForm(tags=tags, status=status)
+
+ if status == 'select' and tags:
+ courses = courses.filter(
+ name__icontains=tags)
+ elif status == 'active':
+ courses = courses.filter(
+ name__icontains=tags, active=True)
+ elif status == 'closed':
+ courses = courses.filter(
+ name__icontains=tags, active=False)
paginator = Paginator(courses, 30)
page = request.GET.get('page')
@@ -1299,36 +1328,6 @@ def monitor(request, quiz_id=None, course_id=None):
return my_render_to_response(request, 'yaksh/monitor.html', context)
-@csrf_exempt
-def ajax_questions_filter(request):
- """Ajax call made when filtering displayed questions."""
-
- user = request.user
- filter_dict = {"user_id": user.id, "active": True}
- question_type = request.POST.get('question_type')
- marks = request.POST.get('marks')
- language = request.POST.get('language')
- if question_type:
- filter_dict['type'] = str(question_type)
-
- if marks:
- filter_dict['points'] = marks
-
- if language:
- filter_dict['language'] = str(language)
- questions = Question.objects.get_queryset().filter(
- **filter_dict).order_by('id')
- paginator = Paginator(questions, 30)
- page = request.GET.get('page')
- questions = paginator.get_page(page)
- return my_render_to_response(
- request, 'yaksh/ajax_question_filter.html', {
- 'questions': questions,
- 'objects': questions
- }
- )
-
-
def _get_questions(user, question_type, marks):
if question_type is None and marks is None:
return None
@@ -1355,18 +1354,21 @@ def _remove_already_present(questionpaper_id, questions):
return questions
-def _get_questions_from_tags(question_tags, user, active=True):
+def _get_questions_from_tags(question_tags, user, active=True, questions=None):
search_tags = []
for tags in question_tags:
search_tags.extend(re.split('[; |, |\*|\n]', tags))
- return Question.objects.filter(tags__name__in=search_tags,
- user=user, active=active).distinct()
+ if questions:
+ search = questions.filter(tags__name__in=search_tags)
+ else:
+ search = Question.objects.get_queryset().filter(
+ tags__name__in=search_tags, user=user, active=active).distinct()
+ return search
@login_required
@email_verified
-def design_questionpaper(request, quiz_id, questionpaper_id=None,
- course_id=None):
+def design_questionpaper(request, course_id, quiz_id, questionpaper_id=None):
user = request.user
que_tags = Question.objects.filter(
active=True, user=user).values_list('tags', flat=True).distinct()
@@ -1506,23 +1508,6 @@ def show_all_questions(request):
if not is_moderator(user):
raise Http404("You are not allowed to view this page !")
- questions = Question.objects.get_queryset().filter(
- user_id=user.id, active=True).order_by('id')
- form = QuestionFilterForm(user=user)
- user_tags = questions.values_list('tags', flat=True).distinct()
- all_tags = Tag.objects.filter(id__in=user_tags)
- upload_form = UploadFileForm()
- paginator = Paginator(questions, 30)
- page = request.GET.get('page')
- questions = paginator.get_page(page)
- context['questions'] = questions
- context['objects'] = questions
- context['all_tags'] = all_tags
- context['papers'] = []
- context['question'] = None
- context['form'] = form
- context['upload_form'] = upload_form
-
if request.method == 'POST':
if request.POST.get('delete') == 'delete':
data = request.POST.getlist('question')
@@ -1547,7 +1532,7 @@ def show_all_questions(request):
questions = questions_file.read()
message = ques.load_questions(questions, user)
else:
- message = "Please Upload a ZIP file"
+ message = "Please Upload a ZIP file or YAML file"
if request.POST.get('download') == 'download':
question_ids = request.POST.getlist('question')
@@ -1571,21 +1556,150 @@ def show_all_questions(request):
user, False, question_ids, None)
trial_paper.update_total_marks()
trial_paper.save()
- return my_redirect("/exam/start/1/{0}/{1}/{2}".format(
- trial_module.id, trial_paper.id, trial_course.id))
+ return my_redirect(
+ reverse("yaksh:start_quiz",
+ args=[1, trial_module.id, trial_paper.id,
+ trial_course.id]
+ )
+ )
else:
message = "Please select atleast one question to test"
- if request.POST.get('question_tags'):
- question_tags = request.POST.getlist("question_tags")
- search_result = _get_questions_from_tags(question_tags, user)
- context['questions'] = search_result
+ questions = Question.objects.get_queryset().filter(
+ user_id=user.id, active=True).order_by('-id')
+ form = QuestionFilterForm(user=user)
+ user_tags = questions.values_list('tags', flat=True).distinct()
+ all_tags = Tag.objects.filter(id__in=user_tags)
+ upload_form = UploadFileForm()
+ paginator = Paginator(questions, 30)
+ page = request.GET.get('page')
+ questions = paginator.get_page(page)
+ context['objects'] = questions
+ context['all_tags'] = all_tags
+ context['form'] = form
+ context['upload_form'] = upload_form
+
messages.info(request, message)
return my_render_to_response(request, 'yaksh/showquestions.html', context)
@login_required
@email_verified
+def questions_filter(request):
+ """Filter questions by type, language or marks."""
+
+ user = request.user
+ if not is_moderator(user):
+ raise Http404('You are not allowed to view this page!')
+
+ questions = Question.objects.get_queryset().filter(
+ user_id=user.id, active=True).order_by('-id')
+ user_tags = questions.values_list('tags', flat=True).distinct()
+ all_tags = Tag.objects.filter(id__in=user_tags)
+ upload_form = UploadFileForm()
+ filter_dict = {}
+ question_type = request.GET.get('question_type')
+ marks = request.GET.get('marks')
+ language = request.GET.get('language')
+ form = QuestionFilterForm(
+ user=user, language=language, marks=marks, type=question_type
+ )
+ if question_type:
+ filter_dict['type'] = str(question_type)
+ if marks:
+ filter_dict['points'] = marks
+ if language:
+ filter_dict['language'] = str(language)
+ questions = questions.filter(**filter_dict).order_by('-id')
+ paginator = Paginator(questions, 30)
+ page = request.GET.get('page')
+ questions = paginator.get_page(page)
+ context = {'form': form, 'upload_form': upload_form,
+ 'all_tags': all_tags, 'objects': questions}
+ return my_render_to_response(
+ request, 'yaksh/showquestions.html', context
+ )
+
+
+@login_required
+@email_verified
+def delete_question(request, question_id):
+ user = request.user
+ if not is_moderator(user):
+ raise Http404("You are not allowed to view this page !")
+
+ question = get_object_or_404(Question, pk=question_id)
+ question.active = False
+ question.save()
+ messages.success(request, "Deleted Question Successfully")
+
+ return my_redirect(reverse("yaksh:show_questions"))
+
+
+@login_required
+@email_verified
+def download_question(request, question_id):
+ user = request.user
+ if not is_moderator(user):
+ raise Http404("You are not allowed to view this page !")
+
+ question = Question()
+ zip_file = question.dump_questions([question_id], user)
+ response = HttpResponse(content_type='application/zip')
+ response['Content-Disposition'] = dedent(
+ '''attachment; filename={0}_question.zip'''.format(user)
+ )
+ zip_file.seek(0)
+ response.write(zip_file.read())
+ return response
+
+
+@login_required
+@email_verified
+def test_question(request, question_id):
+ user = request.user
+ if not is_moderator(user):
+ raise Http404("You are not allowed to view this page !")
+
+ trial_paper, trial_course, trial_module = test_mode(
+ user, False, [question_id], None)
+ trial_paper.update_total_marks()
+ trial_paper.save()
+ return my_redirect(
+ reverse("yaksh:start_quiz",
+ args=[1, trial_module.id, trial_paper.id, trial_course.id]
+ )
+ )
+
+
+@login_required
+@email_verified
+def search_questions_by_tags(request):
+ user = request.user
+ if not is_moderator(user):
+ raise Http404("You are not allowed to view this page !")
+
+ questions = Question.objects.get_queryset().filter(
+ user_id=user.id, active=True).order_by('-id')
+ form = QuestionFilterForm(user=user)
+ user_tags = questions.values_list('tags', flat=True).distinct()
+ all_tags = Tag.objects.filter(id__in=user_tags)
+ form = QuestionFilterForm(user=user)
+ upload_form = UploadFileForm()
+ question_tags = request.GET.getlist("question_tags")
+ questions = _get_questions_from_tags(
+ question_tags, user, questions=questions
+ )
+ paginator = Paginator(questions, 30)
+ page = request.GET.get('page')
+ questions = paginator.get_page(page)
+ context = {'form': form, 'upload_form': upload_form,
+ 'all_tags': all_tags, 'objects': questions}
+ return my_render_to_response(request, 'yaksh/showquestions.html', context)
+
+
+@login_required
+@email_verified
def user_data(request, user_id, questionpaper_id=None, course_id=None):
"""Render user data."""
current_user = request.user
@@ -2444,7 +2558,7 @@ def download_yaml_template(request):
@login_required
@email_verified
-def edit_lesson(request, lesson_id=None, course_id=None):
+def edit_lesson(request, course_id=None, module_id=None, lesson_id=None):
user = request.user
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
@@ -2454,13 +2568,13 @@ def edit_lesson(request, lesson_id=None, course_id=None):
raise Http404('This Lesson does not belong to you')
else:
lesson = None
+ if module_id:
+ module = get_object_or_404(LearningModule, id=module_id)
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('This Lesson does not belong to you')
- redirect_url = reverse("yaksh:get_course_modules", args=[course_id])
- else:
- redirect_url = reverse("yaksh:show_all_lessons")
+
context = {}
if request.method == "POST":
if "Save" in request.POST:
@@ -2476,7 +2590,11 @@ def edit_lesson(request, lesson_id=None, course_id=None):
lesson.remove_file()
if lesson_form.is_valid():
if lesson is None:
+ last_unit = module.get_learning_units().last()
+ order = last_unit.order + 1 if last_unit else 1
lesson_form.instance.creator = user
+ else:
+ order = module.get_unit_order("lesson", lesson)
lesson = lesson_form.save()
lesson.html_data = get_html_text(lesson.description)
lesson.save()
@@ -2485,9 +2603,18 @@ def edit_lesson(request, lesson_id=None, course_id=None):
LessonFile.objects.get_or_create(
lesson=lesson, file=les_file
)
+ unit, created = LearningUnit.objects.get_or_create(
+ type="lesson", lesson=lesson, order=order
+ )
+ if created:
+ module.learning_unit.add(unit.id)
messages.success(
request, "Saved {0} successfully".format(lesson.name)
)
+ return redirect(
+ reverse("yaksh:edit_lesson",
+ args=[course_id, module_id, lesson.id])
+ )
else:
context['lesson_form'] = lesson_form
context['error'] = lesson_form["video_file"].errors
@@ -2591,14 +2718,14 @@ def design_module(request, module_id, course_id=None):
for order, value in enumerate(add_values, start_val):
learning_id, type = value.split(":")
if type == "quiz":
- learning_unit = LearningUnit.objects.create(
+ unit, status = LearningUnit.objects.get_or_create(
order=order, quiz_id=learning_id,
type=type)
else:
- learning_unit = LearningUnit.objects.create(
+ unit, status = LearningUnit.objects.get_or_create(
order=order, lesson_id=learning_id,
type=type)
- to_add_list.append(learning_unit)
+ to_add_list.append(unit)
learning_module.learning_unit.add(*to_add_list)
messages.success(request, "Lesson/Quiz added successfully")
else:
@@ -2661,21 +2788,20 @@ def design_module(request, module_id, course_id=None):
context['status'] = 'design'
context['module_id'] = module_id
context['course_id'] = course_id
+ context['module'] = learning_module
return my_render_to_response(request, 'yaksh/add_module.html', context)
@login_required
@email_verified
-def add_module(request, module_id=None, course_id=None):
+def add_module(request, course_id=None, module_id=None):
user = request.user
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
- redirect_url = reverse("yaksh:show_all_modules")
if course_id:
course = Course.objects.get(id=course_id)
if not course.is_creator(user) and not course.is_teacher(user):
raise Http404('This course does not belong to you')
- redirect_url = reverse("yaksh:get_course_modules", args=[course_id])
if module_id:
module = LearningModule.objects.get(id=module_id)
if not module.creator == user and not course_id:
@@ -2688,10 +2814,14 @@ def add_module(request, module_id=None, course_id=None):
module_form = LearningModuleForm(request.POST, instance=module)
if module_form.is_valid():
if module is None:
+ last_module = course.get_learning_modules().last()
module_form.instance.creator = user
+ if last_module:
+ module_form.instance.order = last_module.order + 1
module = module_form.save()
module.html_data = get_html_text(module.description)
module.save()
+ course.learning_module.add(module.id)
messages.success(
request,
"Saved {0} successfully".format(module.name)
@@ -2708,99 +2838,6 @@ def add_module(request, module_id=None, course_id=None):
@login_required
@email_verified
-def show_all_quizzes(request):
- user = request.user
- if not is_moderator(user):
- raise Http404('You are not allowed to view this page!')
- quizzes = Quiz.objects.filter(creator=user, is_trial=False)
-
- form = SearchFilterForm()
-
- if request.method == 'POST':
- quiz_tags = request.POST.get('search_tags')
- quiz_status = request.POST.get('search_status')
-
- if quiz_status == 'select' :
- quizzes = quizzes.filter(
- description__contains=quiz_tags)
- elif quiz_status == 'active' :
- quizzes = quizzes.filter(
- description__contains=quiz_tags, active=True)
- elif quiz_status == 'closed':
- quizzes = quizzes.filter(
- description__contains=quiz_tags, active=False)
- quizzes_found = quizzes.count()
-
- context = {"quizzes": quizzes, "form": form,
- "quizzes_found": quizzes_found}
- return my_render_to_response(request, 'yaksh/quizzes.html', context)
-
-
-@login_required
-@email_verified
-def show_all_lessons(request):
- user = request.user
- if not is_moderator(user):
- raise Http404('You are not allowed to view this page!')
- lessons = Lesson.objects.filter(creator=user)
-
- form = SearchFilterForm()
-
- if request.method == 'POST':
- lesson_tags = request.POST.get('search_tags')
- lesson_status = request.POST.get('search_status')
-
- if lesson_status == 'select' :
- lessons = lessons.filter(
- description__contains=lesson_tags)
- elif lesson_status == 'active' :
- lessons = lessons.filter(
- description__contains=lesson_tags, active=True)
- elif lesson_status == 'closed':
- lessons = lessons.filter(
- description__contains=lesson_tags, active=False)
- lessons_found = lessons.count()
-
- context = {"lessons": lessons, "form": form,
- "lessons_found": lessons_found}
- return my_render_to_response(request, 'yaksh/lessons.html', context)
-
-
-@login_required
-@email_verified
-def show_all_modules(request):
- user = request.user
- if not is_moderator(user):
- raise Http404('You are not allowed to view this page!')
- learning_modules = LearningModule.objects.filter(
- creator=user, is_trial=False)
-
- form = SearchFilterForm()
-
- if request.method == 'POST':
- module_tags = request.POST.get('search_tags')
- module_status = request.POST.get('search_status')
-
- if module_status == 'select' :
- learning_modules = learning_modules.filter(
- name__contains=module_tags)
- elif module_status == 'active' :
- learning_modules = learning_modules.filter(
- name__contains=module_tags, active=True)
- elif module_status == 'closed':
- learning_modules = learning_modules.filter(
- name__contains=module_tags, active=False)
- learning_modules_found = learning_modules.count()
-
- context = {"modules": learning_modules, "form": form,
- "modules_found": learning_modules_found}
- return my_render_to_response(
- request, 'yaksh/modules.html', context
- )
-
-
-@login_required
-@email_verified
def preview_html_text(request):
user = request.user
if not is_moderator(user):
@@ -2885,10 +2922,12 @@ def design_course(request, course_id):
else:
start_val = 1
for order, value in enumerate(add_values, start_val):
- learning_module = LearningModule.objects.get(id=int(value))
- learning_module.order = order
- learning_module.save()
- to_add_list.append(learning_module)
+ module, created = LearningModule.objects.get_or_create(
+ id=int(value)
+ )
+ module.order = order
+ module.save()
+ to_add_list.append(module)
course.learning_module.add(*to_add_list)
messages.success(request, "Modules added successfully")
else:
@@ -3237,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)