diff options
51 files changed, 1359 insertions, 532 deletions
diff --git a/requirements/requirements-codeserver.txt b/requirements/requirements-codeserver.txt index e9585fa..004e45b 100644 --- a/requirements/requirements-codeserver.txt +++ b/requirements/requirements-codeserver.txt @@ -2,5 +2,5 @@ pytest python-decouple six requests -tornado +tornado==4.5.3 psutil diff --git a/yaksh/documentation/conf.py b/yaksh/documentation/conf.py index 10ad210..1a2c50f 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'2016, FOSSEE' +copyright = u'2018, 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.1.2' +version = u'0.7' # The full version, including alpha/beta/rc tags. -release = u'July 2016' +release = u'Feb 2018' # 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 Binary files differnew file mode 100644 index 0000000..2512f1a --- /dev/null +++ b/yaksh/documentation/images/add_exercise.jpg diff --git a/yaksh/documentation/images/add_lesson.jpg b/yaksh/documentation/images/add_lesson.jpg Binary files differnew file mode 100644 index 0000000..6de272c --- /dev/null +++ b/yaksh/documentation/images/add_lesson.jpg diff --git a/yaksh/documentation/images/add_question.jpg b/yaksh/documentation/images/add_question.jpg Binary files differindex 791ba79..b9b5bc7 100644 --- a/yaksh/documentation/images/add_question.jpg +++ b/yaksh/documentation/images/add_question.jpg diff --git a/yaksh/documentation/images/add_quiz.jpg b/yaksh/documentation/images/add_quiz.jpg Binary files differindex 780a179..3264684 100644 --- a/yaksh/documentation/images/add_quiz.jpg +++ b/yaksh/documentation/images/add_quiz.jpg diff --git a/yaksh/documentation/images/course_details_features.jpg b/yaksh/documentation/images/course_details_features.jpg Binary files differindex 5e820f9..63b4b2e 100644 --- a/yaksh/documentation/images/course_details_features.jpg +++ b/yaksh/documentation/images/course_details_features.jpg diff --git a/yaksh/documentation/images/course_features.jpg b/yaksh/documentation/images/course_features.jpg Binary files differindex 12853af..2da356e 100644 --- a/yaksh/documentation/images/course_features.jpg +++ b/yaksh/documentation/images/course_features.jpg diff --git a/yaksh/documentation/images/cpp_standard_testcase.jpg b/yaksh/documentation/images/cpp_standard_testcase.jpg Binary files differindex 8d3161f..cfb1d89 100644 --- a/yaksh/documentation/images/cpp_standard_testcase.jpg +++ b/yaksh/documentation/images/cpp_standard_testcase.jpg diff --git a/yaksh/documentation/images/create_course.jpg b/yaksh/documentation/images/create_course.jpg Binary files differnew file mode 100644 index 0000000..bcf1eff --- /dev/null +++ b/yaksh/documentation/images/create_course.jpg diff --git a/yaksh/documentation/images/design_course.jpg b/yaksh/documentation/images/design_course.jpg Binary files differnew file mode 100644 index 0000000..287ebea --- /dev/null +++ b/yaksh/documentation/images/design_course.jpg diff --git a/yaksh/documentation/images/design_module.jpg b/yaksh/documentation/images/design_module.jpg Binary files differnew file mode 100644 index 0000000..eda8825 --- /dev/null +++ b/yaksh/documentation/images/design_module.jpg diff --git a/yaksh/documentation/images/design_questionpaper.jpg b/yaksh/documentation/images/design_questionpaper.jpg Binary files differindex fdadcdb..05da597 100644 --- a/yaksh/documentation/images/design_questionpaper.jpg +++ b/yaksh/documentation/images/design_questionpaper.jpg diff --git a/yaksh/documentation/images/embed_video.jpg b/yaksh/documentation/images/embed_video.jpg Binary files differnew file mode 100644 index 0000000..84d18a2 --- /dev/null +++ b/yaksh/documentation/images/embed_video.jpg diff --git a/yaksh/documentation/images/float_testcase.jpg b/yaksh/documentation/images/float_testcase.jpg Binary files differindex 2b6827c..70b8a9f 100644 --- a/yaksh/documentation/images/float_testcase.jpg +++ b/yaksh/documentation/images/float_testcase.jpg diff --git a/yaksh/documentation/images/hook_testcase.jpg b/yaksh/documentation/images/hook_testcase.jpg Binary files differindex 3018050..4d404f6 100644 --- a/yaksh/documentation/images/hook_testcase.jpg +++ b/yaksh/documentation/images/hook_testcase.jpg diff --git a/yaksh/documentation/images/integer_testcase.jpg b/yaksh/documentation/images/integer_testcase.jpg Binary files differindex ca70a41..58ff6be 100644 --- a/yaksh/documentation/images/integer_testcase.jpg +++ b/yaksh/documentation/images/integer_testcase.jpg diff --git a/yaksh/documentation/images/java_standard_testcase.jpg b/yaksh/documentation/images/java_standard_testcase.jpg Binary files differindex a5af3fc..c13f618 100644 --- a/yaksh/documentation/images/java_standard_testcase.jpg +++ b/yaksh/documentation/images/java_standard_testcase.jpg diff --git a/yaksh/documentation/images/moderator_dashboard.jpg b/yaksh/documentation/images/moderator_dashboard.jpg Binary files differindex 13ce524..739d221 100644 --- a/yaksh/documentation/images/moderator_dashboard.jpg +++ b/yaksh/documentation/images/moderator_dashboard.jpg diff --git a/yaksh/documentation/images/python_standard_testcase.jpg b/yaksh/documentation/images/python_standard_testcase.jpg Binary files differindex 992e805..a4d2ac8 100644 --- a/yaksh/documentation/images/python_standard_testcase.jpg +++ b/yaksh/documentation/images/python_standard_testcase.jpg diff --git a/yaksh/documentation/images/questions.jpg b/yaksh/documentation/images/questions.jpg Binary files differindex 780d729..1935541 100644 --- a/yaksh/documentation/images/questions.jpg +++ b/yaksh/documentation/images/questions.jpg diff --git a/yaksh/documentation/images/stdio_testcase.jpg b/yaksh/documentation/images/stdio_testcase.jpg Binary files differindex 41a1694..9a041ba 100644 --- a/yaksh/documentation/images/stdio_testcase.jpg +++ b/yaksh/documentation/images/stdio_testcase.jpg diff --git a/yaksh/documentation/images/string_testcase.jpg b/yaksh/documentation/images/string_testcase.jpg Binary files differindex 7286eff..6cd2b72 100644 --- a/yaksh/documentation/images/string_testcase.jpg +++ b/yaksh/documentation/images/string_testcase.jpg diff --git a/yaksh/documentation/images/view_lessons.jpg b/yaksh/documentation/images/view_lessons.jpg Binary files differnew file mode 100644 index 0000000..4229afe --- /dev/null +++ b/yaksh/documentation/images/view_lessons.jpg diff --git a/yaksh/documentation/images/view_modules.jpg b/yaksh/documentation/images/view_modules.jpg Binary files differnew file mode 100644 index 0000000..5b535d3 --- /dev/null +++ b/yaksh/documentation/images/view_modules.jpg diff --git a/yaksh/documentation/images/view_quizzes.jpg b/yaksh/documentation/images/view_quizzes.jpg Binary files differnew file mode 100644 index 0000000..43bb36f --- /dev/null +++ b/yaksh/documentation/images/view_quizzes.jpg diff --git a/yaksh/documentation/index.rst b/yaksh/documentation/index.rst index a790357..db48544 100644 --- a/yaksh/documentation/index.rst +++ b/yaksh/documentation/index.rst @@ -2,10 +2,6 @@ Welcome to Yaksh's documentation! ================================= Yaksh lets user create and take online programming quiz. Yaksh is an open source project developed by FOSSEE. The code is available on `github <https://github.com/fossee/online_test>`_. - -.. note:: - - This is a basic documentation for users to get comfortable with the interface. The documentation is still under progress. The user documentation for the site is organized into a few sections: diff --git a/yaksh/documentation/installation.rst b/yaksh/documentation/installation.rst index e4ec581..1c90997 100644 --- a/yaksh/documentation/installation.rst +++ b/yaksh/documentation/installation.rst @@ -12,67 +12,311 @@ Installing Yaksh **For installing Yaksh** - 1. **Clone the repository**:: + 1. **Clone the repository**:: $ git clone https://github.com/FOSSEE/online_test.git - 2. **Go to the online_test directory**:: + 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** - + * For Python 2 use:: - $ pip install -r ./requirements/requirements-py2.txt + $ pip install -r ./requirements/requirements-py2.txt - * For Python 3 (recommended) use:: + * For Python 3 (recommended) use:: - $ pip install -r ./requirements/requirements-py3.txt + $ pip install -r ./requirements/requirements-py3.txt Quick Start ----------- 1. **Start up the code server that executes the user code safely**: - * To run the code server in a sandboxed docker environment, run the command:: + * To run the code server in a sandboxed docker environment, run the command:: - $ invoke start + $ invoke start - .. note:: + .. note:: - Make sure that you have Docker installed on your system beforehand. - Find docker installation guide `here <https://docs.docker.com/engine/installation/#desktop>`_. + Make sure that you have Docker installed on your system beforehand. + Find docker installation guide `here <https://docs.docker.com/engine/installation/#desktop>`_. - * To run the code server without docker, locally use:: + * To run the code server without docker, locally use:: - $ invoke start --unsafe + $ invoke start --unsafe - .. note:: + .. note:: - Note this command will run the yaksh code server locally on your machine and is susceptible to malicious code. You will have to install the code server requirements in sudo mode. + Note this command will run the yaksh code server locally on your machine and is susceptible to malicious code. You will have to install the code server requirements in sudo mode. 2. **On another terminal, run the application using the following command** - * To start the django server:: + * To start the django server:: - $ invoke serve + $ invoke serve - .. note:: + .. note:: - The serve command will run the django application server on the 8000 port and hence this port will be unavailable to other processes. + The serve command will run the django application server on the 8000 port and hence this port will be unavailable to other processes. 3. **Open your browser and open the URL** - ``http://localhost:8000/exam`` 4. **Login as a teacher to edit the quiz or as a student to take the quiz** - - * Credentials: - For Student: - * Username: student - * Password: student - For Teacher: - * Username: teacher - * Password: teacher + + * Credentials: + For Student: + * Username: student + * Password: student + For Teacher: + * Username: teacher + * Password: teacher 5. **User can also login to the Default Django admin by going to URL and entering the following admin credentials** ``http://localhost:8000/admin`` - For admin: - * Username: admin - * Password: admin + For admin: + * Username: admin + * Password: admin + + +Production Deployment +--------------------- + +* **Deploying Locally** + + Follow these steps to deploy locally on the server. + + * **Pre-Requisite** + + 1. Ensure `pip <https://pip.pypa.io/en/latest/installing.html>`__ is + installed + 2. Install dependencies, Run; + + :: + + pip install -r requirements/requirements-py2.txt # For Python 2 + + pip3 install -r requirements/requirements-py3.txt # For Python 3 + + 3. Install MySql Server + 4. Install Python MySql support + 5. Install Apache Server for deployment + + 6. Create a database named ``yaksh`` by following the steps below + + :: + + $> mysql -u root -p + $> mysql> create database yaksh + + 7. Add a user named ``yaksh_user`` and give access to it on the database + ``yaksh`` by following the steps below + + :: + + mysql> grant usage on yaksh to yaksh_user@localhost identified + by 'mysecretpassword'; + + mysql> grant all privileges on yaksh to yaksh_user@localhost; + + 8. Add ``DATABASE_PASSWORD = 'mysecretpassword'`` and + ``DATABASE_USER = 'yaksh_user'`` to online\_test/settings.py + + + * **Installation & Usage** + + To install this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + :: + + $ git clone https://github.com/FOSSEE/online_test.git + + 2. Rename the ``.sampleenv`` to ``.env`` + + 3. In the ``.env`` file, uncomment the following and replace the values (please keep the remaining settings as is); + + :: + + DB_ENGINE=mysql # Or psycopg (postgresql), sqlite3 (SQLite) + DB_NAME=yaksh + DB_USER=root + DB_PASSWORD=mypassword # Or the password used while creating a Database + DB_PORT=3306 + + 4. Run: + + :: + + $ python manage.py makemigrations yaksh + + $ python manage.py migrate yaksh + + 5. Run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so: + + :: + + $ 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 + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is necessary + since the server is run as the user "nobody". This runs the number + ports configured in the settings.py file in the variable + "N\_CODE\_SERVERS". The "SERVER\_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. Note + that this will likely spawn multiple processes as "nobody" depending + on the number of server ports specified. + + You can also use a Dockerized code server, see `Dockerized Code Server` + + + 6. The ``wsgi.py`` script should make it easy to deploy this using + mod\_wsgi. You will need to add a line of the form: + + :: + + WSGIScriptAlias / "/online_test/wsgi.py" + + to your apache.conf. For more details see the Django docs here: + + https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ + + 7. Create a Superuser/Administrator: + + :: + + python manage.py createsuperuser + + 8. Go to http://desired\_host\_or\_ip:desired\_port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for + each user. Users can potentially write output into these that can be + used for checking later. + + 10. As a moderator you can visit http://desired\_host\_or\_ip/exam/monitor to view results and user data interactively. You could also "grade" the papers manually if needed. + +.. _dockerized-code-server: + +* **Using Dockerized Code Server** + + 1. Install + `Docker <https://docs.docker.com/engine/installation/>`__ + + 2. Go to the directory where the project is located + + :: + + cd /path/to/online_test + + 3. Create a docker image. This may take a few minutes, + + :: + + docker build -t yaksh_code_server -f ./docker/Dockerfile_codeserver + + 4. Check if the image has been created using the output of ``docker + images`` + + 5. Run the invoke script using the command ``invoke start`` The command + will create and run a new docker container (that is running the + code\_server.py within it), it will also bind the ports of the host + with those of the container + + 6. You can use ``invoke --list`` to get a list of all the available commands + + + .. _deploying-multiple-dockers: + + +* **Deploying Multiple Dockers** + + Follow these steps to deploy and run the Django Server, MySQL instance and Code Server in seperate Docker instances. + + 1. Install `Docker <https://docs.docker.com/engine/installation/>`__ + + 2. Install `Docker Compose <https://docs.docker.com/compose/install/>`__ + + 3. Rename the ``.sampleenv`` to ``.env`` + + 4. In the ``.env`` file, uncomment all the values and keep the default values as is. + + 5. Go to the ``docker`` directory where the project is located: + + :: + + cd /path/to/online_test/docker + + 6. Build the docker images + + :: + + invoke build + + 7. Run the containers and scripts necessary to deploy the web + application + + :: + + invoke begin + + 8. Make sure that all the containers are ``Up`` and stable + + :: + + invoke status + + 8. Run the containers and scripts necessary to deploy the web + application, ``--fixtures`` allows you to load fixtures. + + :: + + invoke deploy --fixtures + + 10. Stop the containers, you can use ``invoke restart`` to restart the containers without removing them + + :: + + invoke halt + + 11. Remove the containers + + :: + + invoke remove + + 12. You can use ``invoke --list`` to get a list of all the available commands + + + .. _add-commands: + +* **Additional commands available** + + We provide several convenient commands for you to use: + + - 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. + + - 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: + + :: + + $ python manage.py help [command] + + where [command] is one of the above. diff --git a/yaksh/documentation/moderator_dashboard.rst b/yaksh/documentation/moderator_dashboard.rst index a93ea3c..4b5bfea 100644 --- a/yaksh/documentation/moderator_dashboard.rst +++ b/yaksh/documentation/moderator_dashboard.rst @@ -14,4 +14,5 @@ The following pages explain the various functions available for moderators moderator_docs/creating_course.rst moderator_docs/creating_quiz.rst moderator_docs/creating_question.rst + moderator_docs/creating_lessons_modules.rst moderator_docs/other_features.rst
\ No newline at end of file diff --git a/yaksh/documentation/moderator_docs/creating_course.rst b/yaksh/documentation/moderator_docs/creating_course.rst index d4dc5f8..5aaddf5 100644 --- a/yaksh/documentation/moderator_docs/creating_course.rst +++ b/yaksh/documentation/moderator_docs/creating_course.rst @@ -3,26 +3,30 @@ Courses ======= For students to take a quiz, it is imperative for the moderator to create a course first. +A course can contain several modules and a module can contain several lessons and/or quizzes. + +To create modules, lessons and quizzes go to the :doc:`creating_lessons_modules` +and :doc:`creating_quiz` section of the documentation. Setting up a new course ----------------------- To create a course, click on the Add New Course button on the moderator's dashboard. This will lead you to a create add course page, where you need to fill in the following fields. - .. image:: ../images/create_course.png + .. image:: ../images/create_course.jpg - * Name + * **Name** Name of the Course - * Enrollment + * **Enrollment** Open enrollment is open to all students. Enroll Request requires students to send a request which the moderator can accept or reject. - * Active + * **Active** If the course should be active for students to take the quiz. The status of the course can be edited later. - * Code + * **Code** If the course should be hidden and only accessible to students possessing the correct course code. - * Instructions + * **Instructions** Instructions for the course - * Start Date and Time for enrollment of 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 - * End Date and Time for enrollment of course + * **End Date and Time for enrollment of course** If the enrollment of the course should be available only before a set date and time @@ -37,21 +41,77 @@ Features in Courses The following features are available for courses - * Course Name - Clicking on course name link will display all the enrolled, rejected and requested students list. Moderator can accept or reject the student. - * Quiz Name - Clicking on the quiz name will let you edit the quiz. - * Question Paper - Click on the **Add** link to create a Question Paper for associated Quiz. - If a question paper is already created, click on the Question Paper link to edit question paper. - * Add Teacher - Clicking on Add teacher can let you add teachers for the course. The teachers can edit and modify only the specific course that are allotted to them. - * Teachers added to the course + * **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. + * **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 + * **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 - Clicking on the edit course button will let you edit the details of an existing 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 +--------------- + + Clicking on **Design Course** will show the below page. + + .. image:: ../images/design_course.jpg + + **Available Modules** contains all the modules that are not added to a course. + + 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** - 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**. + + **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. + + To remove a module from the course select the checkbox beside every module and click **Remove from course** button. + Features in Course Details -------------------------- @@ -62,17 +122,15 @@ Features in Course Details Following are the features for course details - - * Requests + * **Requests** This is a list of students who have requested to be enrolled in the course. Moderator can enroll or reject selected students. - * Enrolled + * **Enrolled** This is a list of students who have been enrolled in the course. Moderator can reject enrolled students. - * Rejected + * **Rejected** This is a list of students who have been rejected for enrollment in a course. Moderator can enroll rejected students. - * Deactivate/Activate Course - Clicking on this will deactivate or activate the course. - * Upload Users + * **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. - * Clone Course - This will create a clone of the course for the moderator. - * Send Mail + * **Send Mail** Moderator can send mail to all enrolled students or selected students. + * **View Course Status** + View students' progress through the course. diff --git a/yaksh/documentation/moderator_docs/creating_lessons_modules.rst b/yaksh/documentation/moderator_docs/creating_lessons_modules.rst new file mode 100644 index 0000000..5131dd1 --- /dev/null +++ b/yaksh/documentation/moderator_docs/creating_lessons_modules.rst @@ -0,0 +1,91 @@ +.. _creating_lessons_modules: + +=================== +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 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. + + +Setting up a Lesson +----------------------- + + To create a new lesson or edit any existing lesson click on **Add/View Lessons** from courses page. + + .. image:: ../images/view_lessons.jpg + + This page shows all the lessons created by you. + + Click on **Add new Lesson** to add new lesson. Click on the **lesson name** to edit a lesson. + + .. 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 + * **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 **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. + + Click on **Embed Video Link** to embed a video. On clicking a pop-up will be shown. + + .. 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. + + +Setting up a Module +----------------------- + + To create a new module or edit any existing module click on **Add/View Modules** from courses page. + + .. image:: ../images/view_modules.jpg + + This page shows all the modules created by you. + + 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. + + +Design a Module +--------------- + + To add lessons or quizzes to a module click on **Add Quizzes/Lessons for <module-name>**. + + .. image:: ../images/design_module.jpg + + **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. + + **Choosen 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: + + **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**. + + **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**. + + 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. + + + + + diff --git a/yaksh/documentation/moderator_docs/creating_question.rst b/yaksh/documentation/moderator_docs/creating_question.rst index 78b6f2c..82bb6e5 100644 --- a/yaksh/documentation/moderator_docs/creating_question.rst +++ b/yaksh/documentation/moderator_docs/creating_question.rst @@ -5,315 +5,356 @@ Questions Setting up questions -------------------- - Setting up questions is the most important part of the Yaksh experience. Questions can be of multiple types i.e Multiple choice questions (MCQ), Multiple correct choices (MCC), Coding questions and assignment upload types. + Setting up questions is the most important part of the Yaksh experience. Questions can be of multiple types i.e Multiple choice questions (MCQ), Multiple correct choices (MCC), Coding questions, assignment upload, fill in the blanks. - To set up a question click on the questions link in the navigation bar. + To set up a question click on the questions link in the navigation bar. - .. image:: ../images/questions.jpg - - To add a question click on the **Add Question** button + .. image:: ../images/questions.jpg + + To add a question click on the **Add Question** button - .. image:: ../images/add_question.jpg + .. image:: ../images/add_question.jpg - * **Summary**- Summary or the name of the question. + * **Summary**- Summary or the name of the question. - * **Language** - Programming language on which the question is based. + * **Language** - Programming language on which the question is based. - * **Type** - Type of the question. i.e Multiple Choice, Multiple Correct Choice, Code and Assignment Upload. + * **Type** - Type of the question. i.e Multiple Choice, Multiple Correct Choice, Code, Assignment Upload etc. - * **Points** - Points is the marks for a question. + * **Points** - Points is the marks for a question. - * **Description** - The actual question description is to be written. + * **Description** - The actual question description in HTML format. - .. note:: To add code snippets in questions please use html <code> and <br> tags. + .. note:: To add code snippets in questions please use html <code> and <br> tags. - * **Tags** - Type of label or metadata tag making it easier to find specific type of questions. + * **Tags** - Type of label or metadata tag making it easier to find specific type of questions. - * **Snippet** - Snippet is used to give any default value or default code or command. This will be displayed in the students answer form. This is used only for code questions. + * **Solution** - Add solution for the question. - * **Partial Grading** - Click this checkbox to enable partial grading feature. + * **Snippet** - Snippet is used to give any default value or default code or command. This will be displayed in the students answer form. This is used only for code questions. - * **File** - File field is used to upload files if there is any file based question. - For e.g. The question is reading a file say **dummy.txt** and print its content. - You can then upload a file **dummy.txt** which will be available to the student while attempting the quiz. + * **Minimum time(in minutes)** - This value can be set for questions which will be added to a Exercise. Exercise time will depend on this time. - * Some file features: - 1. To delete a file click the delete checkbox and click on Delete Selected Files button. - 2. To extract a file for e.g. say **dummy.zip** click the extract checkbox and click on Save button. - If **extract** is selected, the file will be extracted while checking - the student submitted code. - 3. To hide any file from student click the hide checkbox and click on Save button. + * **Partial Grading** - Click this checkbox to enable partial grading feature. - .. Note:: We only support **zip** extension for **extract file** feature. + * **Grade Assignment Upload** - Click this checkbox if the assignment upload based question needs evaluation. Evaluation is done with **Hook based TestCase** only. + + * **File** - File field is used to upload files if there is any file based question. + For e.g. The question is reading a file say **dummy.txt** and print its content. + You can then upload a file **dummy.txt** which will be available to the student while attempting the quiz. + + * Some file features: + 1. To delete a file click the delete checkbox and click on **Delete Selected Files button**. + 2. To extract a file for e.g. say **dummy.zip** click the extract checkbox and click on Save button. + If **extract** is selected, the file will be extracted while checking + the student submitted code. + 3. To hide any file from student click the hide checkbox and click on Save button. + + .. Note:: We only support **zip** extension for **extract file** feature. How to write Test cases ----------------------- - - The following explains different methods to write test cases. - - * **Create Standard Test Case** - - Select Standard from Add Test Case field. - - * For Python: - .. image:: ../images/python_standard_testcase.jpg - :width: 80% - - In the test case field write a python assert to check the user code. - For e.g. :: - - assert add(1, 2) == 3 - - for program of addition. - - * For C, C++, Java and Bash: - Sample Moderator code - - For C and C++: - .. image:: ../images/cpp_standard_testcase.jpg - :width: 80% - - Consider a Program to add three numbers. - The code in the Test case field should be as follows: :: - - #include <stdio.h> - #include <stdlib.h> - - extern int add(int, int, int); - - template <class T> - void check(T expect,T result) - { - if (expect == result) - { - printf("\nCorrect:\n Expected %d got %d \n",expect,result); - } - else - { - printf("\nIncorrect:\n Expected %d got %d \n",expect,result); - exit (1); - } - } - - int main(void) - { - int result; - result = add(0,0,0); - printf("Input submitted to the function: 0, 0, 0"); - check(0, result); - result = add(2,3,3); - printf("Input submitted to the function: 2, 3, 3"); - check(8,result); - printf("All Correct\n"); - } - - Assuming Students answer to be as below: :: - - int add(int a, int b, int c) - { - return a+b+c; - } - - .. Note:: 1. In the above example, **add** in the main function is obtained from student code. - 2. Please make sure that the student code function and testcase calling function should be same which in this case is **add**. - - For Java: - .. image:: ../images/java_standard_testcase.jpg - :width: 80% - - Consider a Program to find square of a number. - The code in the Test case Field should be as follows: :: - class main - { - public static <E> void check(E expect, E result) - { - if(result.equals(expect)) - { - System.out.println("Correct:\nOutput expected "+expect+" and got "+result); - } - else - { - System.out.println("Incorrect:\nOutput expected "+expect+" but got "+result); - System.exit(1); - } - } - public static void main(String arg[]) - { - Test t = new Test(); - int result, input, output; - input = 0; output = 0; - result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); - check(output, result); - input = 5; output = 25; - result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); - check(output, result); - input = 6; output = 36; - result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); - check(output, result); - } - } - - Assuming Students answer to be as below: :: - - class Test - { - int square_num(int num) - { - return num*num; - } - } - - .. Note:: 1. For Java, class name should always be **main** in testcase. - - 2. In the above example, **Test** is the class of student's code. - 3. Please make sure that the student's code class and calling class in testcase is always **Test**. (square_num is the function inside Test class.) - - For Bash: - .. image:: ../images/bash_standard_testcase.jpg - :width: 80% - - In **Test case** Field write your bash script. - For e.g. the question is to move to a particular directory and read a file - **test.txt** - The Test case code shown is: :: - - #!/bin/bash - cd $1 - cat $2 - - In **Test case args** Field type your Command line arguments. - - In this case the test case args are: :: - - somedata/ test.txt - - .. Note:: 1. **Test case args** field is used only for bash. - 2. Each argument should be separated by **space**. - 3. This field can be left blank. - - - 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** + + The following explains different methods to write test cases. + + * **Create Standard Test Case** + + Select Standard from Add Test Case field. Sample Testcases are given for all + languages. + + * **For Python:** + .. image:: ../images/python_standard_testcase.jpg + :width: 80% + + In the test case field write a python assert to check the user code. + For e.g. :: + + assert add(1, 2) == 3 + + for program of addition. + + * **For C, C++:** + + .. image:: ../images/cpp_standard_testcase.jpg + :width: 80% + + Consider a Program to add three numbers. + The code in the Test case field should be as follows: :: + + #include <stdio.h> + #include <stdlib.h> + + extern int add(int, int, int); + + template <class T> + void check(T expect,T result) + { + if (expect == result) + { + printf("\nCorrect:\n Expected %d got %d \n",expect,result); + } + else + { + printf("\nIncorrect:\n Expected %d got %d \n",expect,result); + exit (1); + } + } + + int main(void) + { + int result; + result = add(0,0,0); + printf("Input submitted to the function: 0, 0, 0"); + check(0, result); + result = add(2,3,3); + printf("Input submitted to the function: 2, 3, 3"); + check(8,result); + printf("All Correct\n"); + } + + Assuming Students answer to be as below: :: + + int add(int a, int b, int c) + { + return a+b+c; + } + + .. Note:: 1. In the above example, **add** in the main function is obtained from student code. + 2. Please make sure that the student code function and testcase calling function should be same which in this case is **add**. + + * **For Java:** + .. image:: ../images/java_standard_testcase.jpg + :width: 80% + + Consider a Program to find square of a number. + The code in the Test case Field should be as follows: :: + + class main + { + public static <E> void check(E expect, E result) + { + if(result.equals(expect)) + { + System.out.println("Correct:\nOutput expected "+expect+" and got "+result); + } + else + { + System.out.println("Incorrect:\nOutput expected "+expect+" but got "+result); + System.exit(1); + } + } + public static void main(String arg[]) + { + Test t = new Test(); + int result, input, output; + input = 0; output = 0; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+input); + check(output, result); + input = 5; output = 25; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+input); + check(output, result); + input = 6; output = 36; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+input); + check(output, result); + } + } + + Assuming Students answer to be as below: :: + + class Test + { + int square_num(int num) + { + return num*num; + } + } + + .. Note:: 1. For Java, class name should always be **main** in testcase. + + 2. In the above example, **Test** is the class of student's code. + 3. Please make sure that the student's code class and calling class in testcase is always **Test**. (square_num is the function inside Test class.) + + * **For Bash:** + .. image:: ../images/bash_standard_testcase.jpg + :width: 80% + + In **Test case** Field write your bash script. + For e.g. the question is to move to a particular directory and read a file + **test.txt** + The Test case code shown is: :: + + cd $1 + cat $2 + + In **Test case args** Field type your Command line arguments. + + In this case the test case args are: :: + + somedata/ test.txt + + .. Note:: 1. **Test case args** field is used only for bash. + 2. Each argument should be separated by **space**. + 3. This field can be left blank. + + + 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** - Select StdIO from Add Test Case field. + Select StdIO from Add Test Case field. + + .. image:: ../images/stdio_testcase.jpg + :width: 80% + + In Expected input field, enter the value(s) that will be passed to the students' code through a standard I/O stream. + + .. note:: If there are multiple input values in a test case, enter the values in new line. + + In Expected Output Field, enter the expected output for that test case. For e.g type 3 if the output of the user code is 3. + + Setting up Standard Input/Output Based questions is same for all languages. - .. image:: ../images/stdio_testcase.jpg - :width: 80% + * **Create MCQ or MCC Based Test Case** - In Expected input field, enter the value(s) that will be passed to the students' code through a standard I/O stream. + Select MCQ/MCC from Add Test Case field. - .. note:: If there are multiple input values in a test case, enter the values in new line. + Fig (a) showing MCQ based testcase - In Expected Output Field, enter the expected output for that test case. For e.g type 3 if the output of the user code is 3. + .. image:: ../images/mcq_testcase.jpg + :width: 80% - Setting up Standard Input/Output Based questions is same for all languages. + Fig (b) showing MCC based testcase - * **Create MCQ or MCC Based Test Case** + .. image:: ../images/mcc_testcase.jpg + :width: 80% - Select MCQ/MCC from Add Test Case field. + 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. - Fig (a) showing MCQ based testcase + For MCC based question, check the correct checkbox for multiple correct options. - .. image:: ../images/mcq_testcase.jpg - :width: 80% + * **Create Hook based Test Case** - Fig (b) showing MCC based testcase + Select Hook from Add Test Case field. - .. image:: ../images/mcc_testcase.jpg - :width: 80% + In Hook based test case type, moderator is provided with a evaluator function + called **check_answer** which is provided with a parameter called **user_answer**. - 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. + **user_answer** is the code of the student in string format. - For MCC based question, check the correct checkbox for multiple correct options. + .. note :: For assignment upload type question there will be no **user answer** File uploaded by student will be the answer. - * **Create Hook based Test Case** + Suppose the student needs to upload a file say **new.txt** as assignment. + Sample Hook code for this will be as shown below. :: - Select Hook from Add Test Case field. + def check_answer(user_answer): + ''' Evaluates user answer to return - + success - Boolean, indicating if code was executed correctly + mark_fraction - Float, indicating fraction of the weight to a test case + error - String, error message if success is false - In Hook based test case type, moderator is provided with a evaluator function - called **check_answer** which is provided with a parameter called **user_answer**. + In case of assignment upload there will be no user answer ''' - **user_answer** is the code of the student in string format. + success = False + err = "Incorrect Answer" # Please make this more specific + mark_fraction = 0.0 - A moderator can check the string for specific words in the user answer - and/or compile and execute the user answer (using standard python libraries) to - evaluate and hence return the mark fraction. + try: + with open('new.txt', 'r') as f: + if "Hello, World!" in f.read(): + success = True + err = "Correct Answer" + mark_fraction = 1.0 + else: + err = "Did not found string Hello, World! in file." + except IOError: + err = "File new.txt not found." + return success, err, mark_fraction - .. image:: ../images/hook_testcase.jpg - :width: 80% + A moderator can check the string for specific words in the user answer + and/or compile and execute the user answer (using standard python libraries) to + evaluate and hence return the mark fraction. - * **Create Integer Based Test Case** - Select **Answer in Integer** from Type field. + .. image:: ../images/hook_testcase.jpg + :width: 80% - Select Integer from Add Test Case field. + * **Create Integer Based Test Case** - In the Correct field, add the correct integer value for the question. + Select **Answer in Integer** from Type field. - .. image:: ../images/integer_testcase.jpg - :width: 80% + Select Integer from Add Test Case field. - * **Create String Based Test Case** + In the Correct field, add the correct integer value for the question. - Select **Answer in String** from Type field. + .. image:: ../images/integer_testcase.jpg + :width: 80% - Select **String** from Add Test Case field. + * **Create String Based Test Case** - In the **Correct** field, add the exact string answer for the question. + Select **Answer in String** from Type field. - In **String Check** field, select if the checking of the string answer - should be case sensitive or not. + Select **String** from Add Test Case field. - .. image:: ../images/string_testcase.jpg - :width: 80% + In the **Correct** field, add the exact string answer for the question. - * **Create Float Based Test Case** + In **String Check** field, select if the checking of the string answer + should be case sensitive or not. - Select **Answer in Float** from Type field. + .. image:: ../images/string_testcase.jpg + :width: 80% - Select **Float** from Add Test Case field. + * **Create Float Based Test Case** - In the **Correct** field, add the correct float value for the question. + Select **Answer in Float** from Type field. - In the **Error Margin** field, add the margin of error that will be allowed. + Select **Float** from Add Test Case field. - .. image:: ../images/float_testcase.jpg - :width: 80% + In the **Correct** field, add the correct float value for the question. + + In the **Error Margin** field, add the margin of error that will be allowed. + + .. image:: ../images/float_testcase.jpg + :width: 80% Features in Question -------------------- - - * **Download Questions** + + * **Download Questions** - Select questions from the list of questions displayed on the Questions page. Click on the Download Selected button to download the questions. This will create a zip file of the Questions selected. + Select questions from the list of questions displayed on the Questions page. Click on the Download Selected button to download the questions. This will create a zip file of the Questions selected. - * **Upload Questions** - - Click on the browse button. This will open up a window. Select the zip file of questions and click Ok and then click on Upload file button, questions will be uploaded and displayed on the Questions page. + * **Upload Questions** + + Click on the browse button. This will open up a window. Select the zip file of questions and click Ok and then click on Upload file button, questions will be uploaded and displayed on the Questions page. - Zip file should contain **questions_dump.yaml** from which questions will be loaded. - Zip file can contain files related to questions. + Zip file should contain **questions_dump.yaml** from which questions will be loaded. + Zip file can contain files related to questions. - * **Test Questions** - - 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. + * **Test Questions** + + 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. + + * **Filter Questions** + + You can filter questions based on type of question, language of question or marks of question. + 1. Click Select Question Type to filter question based on type of the question. + 2. Click Select Language to filter question based on language of the question. + 3. Click Select marks to filter question based on mark of the question. - .. 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. + * **Search by tags** - * **Filter Questions** - - You can filter questions based on type of question, language of question or marks of question. - 1. Click Select Question Type to filter question based on type of the question. - 2. Click Select Language to filter question based on language of the question. - 3. Click Select marks to filter question based on mark of the 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. diff --git a/yaksh/documentation/moderator_docs/creating_quiz.rst b/yaksh/documentation/moderator_docs/creating_quiz.rst index 3f227ef..8b93188 100644 --- a/yaksh/documentation/moderator_docs/creating_quiz.rst +++ b/yaksh/documentation/moderator_docs/creating_quiz.rst @@ -1,44 +1,68 @@ +.. _creating_quiz: + ======= Quizzes ======= Quizzes are intrinsically associated with a course, hence to view and/or edit a quiz, we need to navigate to the courses page. -In courses page click on **Add Quiz** button to create a new quiz. +Clicking on Add/View Quizzes from courses page will open the page as shown below + +.. image:: ../images/view_quizzes.jpg +This page shows all the quizzes and exercise created. Creating a Quiz --------------- + + Click on **Add New 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. - In courses click on **Add Quiz** button to add a quiz. - - * **Course** - Select a course from Course field. This field is mandatory. * **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** - If the quiz is active or not. + * **Active** - Check the checkbox to activate/deactivate quiz. * **Description** - Description or name of the quiz. * **Passing Percentage** - Minimum percentage required to pass the test. - * **Prerequisite** - Set a prerequisite quiz to be passed before attempting the current quiz. - * **Language** - Programming language on which the quiz is based. * **Attempts allowed** - Number of attempts that a student can take of the current quiz. - * **Number of Days** - Number of days between attempts. + * **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. -To create a Question paper, Click on **Add** link located besides the created quiz in courses page. +To create a Question paper, Click on **Add** link located besides the created quiz. + +Creating a Exercise +------------------- + + Click on **Add New Exercise** button to add a exercise. + + .. 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. + + 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. + Designing Question Paper ------------------------ .. image:: ../images/design_questionpaper.jpg - A quiz 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 @@ -48,25 +72,17 @@ Designing Question Paper * Click on save question paper to save it or preview question paper to preview it. -Editing a Quiz --------------- - - In Courses page, click on the quiz link to edit the quiz. Then change the parameters and click on design question paper to save it. This will redirect you to the moderator dashboard. +Editing a Quiz/Exercise +----------------------- - 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. - * You will be notified about quiz prerequisites.(You can still attempt the quiz though) + Click on the quiz/exercise link to edit, change the parameters and click on Save. Editing a QuestionPaper ----------------------- - Click on the Question Paper for a Quiz link besides Quiz in courses page and follow steps from Design Question Paper. + Click on the Question Paper for a <Quiz-name/Exercise-name> 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. diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index b86a9d8..cbf2abd 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -4,49 +4,56 @@ from django.utils import timezone import pytz from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ QuestionSet, AnswerPaper, Answer, Course, IntegerTestCase, FloatTestCase,\ - StringTestCase + StringTestCase, McqTestCase def setUpModule(): - # create user profile + # Create user profile + # Create User 1 user = User.objects.create_user(username='demo_user_100', password='demo', email='demo@test.com') + Profile.objects.create(user=user, roll_number=1, institute='IIT', department='Aerospace', position='Student') + # Create User 2 + user2 = User.objects.create_user(username='demo_user_101', + password='demo', + email='demo@test.com') - # create a course + Profile.objects.create(user=user2, roll_number=2, + institute='IIT', department='Aerospace', + position='Student') + + # Create a course course = Course.objects.create(name="Python Course 100", enrollment="Enroll Request", creator=user) - quiz = Quiz.objects.create(start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc), - end_date_time=datetime(2199, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc), + quiz = Quiz.objects.create(start_date_time=datetime\ + (2015, 10, 9, 10, 8, 15, 0, + tzinfo=pytz.utc), + end_date_time=datetime\ + (2199, 10, 9, 10, 8, 15, 0, + tzinfo=pytz.utc), duration=30, active=True, attempts_allowed=1, - time_between_attempts=0, description='demo quiz 100', - pass_criteria=0, + time_between_attempts=0, pass_criteria=0, + description='demo quiz 100', instructions="Demo Instructions" ) question_paper = QuestionPaper.objects.create(quiz=quiz, total_marks=1.0) - answerpaper = AnswerPaper.objects.create(user=user, user_ip='101.0.0.1', - start_time=timezone.now(), - question_paper=question_paper, - end_time=timezone.now() - +timedelta(minutes=5), - attempt_number=1, - course=course - ) - + def tearDownModule(): User.objects.get(username="demo_user_100").delete() + User.objects.get(username="demo_user_101").delete() class IntegerQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): + # Creating Course + self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper @@ -65,9 +72,16 @@ class IntegerQuestionTestCases(unittest.TestCase): self.question1.save() #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + + self.answerpaper = AnswerPaper.objects.create(user=self.user, + user_ip='101.0.0.1', + start_time=timezone.now(), + question_paper=self.question_paper, + end_time=timezone.now() + +timedelta(minutes=5), + attempt_number=1, + course=self.course + ) self.answerpaper.questions.add(self.question1) self.answerpaper.save() # For question @@ -80,6 +94,7 @@ class IntegerQuestionTestCases(unittest.TestCase): @classmethod def tearDownClass(self): self.question1.delete() + self.answerpaper.delete() def test_validate_regrade_integer_correct_answer(self): # Given @@ -158,6 +173,8 @@ class IntegerQuestionTestCases(unittest.TestCase): class StringQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): + # Creating Course + self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper @@ -182,9 +199,16 @@ class StringQuestionTestCases(unittest.TestCase): self.question2.save() #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + + self.answerpaper = AnswerPaper.objects.create(user=self.user, + user_ip='101.0.0.1', + start_time=timezone.now(), + question_paper=self.question_paper, + end_time=timezone.now() + +timedelta(minutes=5), + attempt_number=1, + course=self.course + ) self.answerpaper.questions.add(*[self.question1, self.question2]) self.answerpaper.save() @@ -207,6 +231,7 @@ class StringQuestionTestCases(unittest.TestCase): def tearDownClass(self): self.question1.delete() self.question2.delete() + self.answerpaper.delete() def test_validate_regrade_case_insensitive_string_correct_answer(self): # Given @@ -346,6 +371,8 @@ class StringQuestionTestCases(unittest.TestCase): class FloatQuestionTestCases(unittest.TestCase): @classmethod def setUpClass(self): + # Creating Course + self.course = Course.objects.get(name="Python Course 100") # Creating Quiz self.quiz = Quiz.objects.get(description="demo quiz 100") # Creating Question paper @@ -362,9 +389,16 @@ class FloatQuestionTestCases(unittest.TestCase): self.question1.save() #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + + self.answerpaper = AnswerPaper.objects.create(user=self.user, + user_ip='101.0.0.1', + start_time=timezone.now(), + question_paper=self.question_paper, + end_time=timezone.now() + +timedelta(minutes=5), + attempt_number=1, + course=self.course + ) self.answerpaper.questions.add(self.question1) self.answerpaper.save() # For question @@ -378,6 +412,7 @@ class FloatQuestionTestCases(unittest.TestCase): @classmethod def tearDownClass(self): self.question1.delete() + self.answerpaper.delete() def test_validate_regrade_float_correct_answer(self): # Given @@ -450,3 +485,105 @@ class FloatQuestionTestCases(unittest.TestCase): self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 1) self.assertTrue(self.answer.correct) +class MCQQuestionTestCases(unittest.TestCase): + @classmethod + def setUpClass(self): + #Creating User + self.user = User.objects.get(username='demo_user_100') + self.user2 = User.objects.get(username='demo_user_101') + self.user_ip = '127.0.0.1' + + #Creating Course + self.course = Course.objects.get(name="Python Course 100") + # Creating Quiz + self.quiz = Quiz.objects.get(description="demo quiz 100") + # Creating Question paper + self.question_paper = QuestionPaper.objects.get(quiz=self.quiz) + self.question_paper.shuffle_testcases = True + self.question_paper.save() + #Creating Question + self.question1 = Question.objects.create(summary='mcq1', points=1, + type='code', user=self.user, + ) + self.question1.language = 'python' + self.question1.type = "mcq" + self.question1.test_case_type = 'Mcqtestcase' + self.question1.description = 'Which option is Correct?' + self.question1.save() + + # For questions + self.mcq_based_testcase_1 = McqTestCase(question=self.question1, + options="Correct", + correct=True, + type='mcqtestcase', + ) + self.mcq_based_testcase_1.save() + + self.mcq_based_testcase_2 = McqTestCase(question=self.question1, + options="Incorrect", + correct=False, + type='mcqtestcase', + ) + self.mcq_based_testcase_2.save() + + self.mcq_based_testcase_3 = McqTestCase(question=self.question1, + options="Incorrect", + correct=False, + type='mcqtestcase', + ) + self.mcq_based_testcase_3.save() + + self.mcq_based_testcase_4 = McqTestCase(question=self.question1, + options="Incorrect", + correct=False, + type='mcqtestcase', + ) + self.mcq_based_testcase_4.save() + + self.question_paper.fixed_questions.add(self.question1) + + self.answerpaper = self.question_paper.make_answerpaper( + user=self.user, ip=self.user_ip, + attempt_num=1, + course_id=self.course.id + ) + + # Answerpaper for user 2 + self.answerpaper2 = self.question_paper.make_answerpaper( + user=self.user2, ip=self.user_ip, + attempt_num=1, + course_id=self.course.id + ) + @classmethod + def tearDownClass(self): + self.question1.delete() + self.answerpaper.delete() + self.answerpaper2.delete() + + def test_shuffle_test_cases(self): + # Given + # When + + user_testcase = self.question1.get_ordered_test_cases( + self.answerpaper + ) + order1 = [tc.id for tc in user_testcase] + user2_testcase = self.question1.get_ordered_test_cases( + self.answerpaper2 + ) + order2 = [tc.id for tc in user2_testcase] + self.question_paper.shuffle_testcases = False + self.question_paper.save() + answerpaper3 = self.question_paper.make_answerpaper( + user=self.user2, ip=self.user_ip, + attempt_num=self.answerpaper.attempt_number+1, + course_id=self.course.id + ) + not_ordered_testcase = self.question1.get_ordered_test_cases( + answerpaper3 + ) + get_test_cases = self.question1.get_test_cases() + # Then + self.assertNotEqual(order1, order2) + self.assertEqual(get_test_cases, not_ordered_testcase) + answerpaper3.delete() diff --git a/yaksh/forms.py b/yaksh/forms.py index 258a1ee..97b3108 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -85,10 +85,12 @@ class UserRegisterForm(forms.Form): department = forms.CharField( max_length=64, help_text='Department you work/study at') position = forms.CharField( - max_length=64, help_text='Student/Faculty/Researcher/Industry/etc.') + max_length=64, + help_text='Student/Faculty/Researcher/Industry/Fellowship/etc.') timezone = forms.ChoiceField( choices=[(tz, tz) for tz in pytz.common_timezones], - initial=pytz.utc) + help_text='Course timings are shown based on the selected timezone', + initial=pytz.country_timezones['IN'][0]) def clean_username(self): u_name = self.cleaned_data["username"] @@ -308,7 +310,7 @@ class UploadFileForm(forms.Form): class QuestionPaperForm(forms.ModelForm): class Meta: model = QuestionPaper - fields = ['shuffle_questions'] + fields = ['shuffle_questions', 'shuffle_testcases'] class LessonForm(forms.ModelForm): diff --git a/yaksh/models.py b/yaksh/models.py index f065190..d9eea57 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -163,6 +163,23 @@ class Lesson(models.Model): def get_files(self): return LessonFile.objects.filter(lesson=self) + def _create_lesson_copy(self, user): + lesson_files = self.get_files() + new_lesson = self + new_lesson.id = None + new_lesson.name = "Copy of {0}".format(self.name) + new_lesson.creator = user + new_lesson.save() + for _file in lesson_files: + file_name = os.path.basename(_file.file.name) + if os.path.exists(_file.file.path): + lesson_file = open(_file.file.path, "rb") + django_file = File(lesson_file) + lesson_file_obj = LessonFile() + lesson_file_obj.lesson = new_lesson + lesson_file_obj.file.save(file_name, django_file, save=True) + return new_lesson + ############################################################################# class LessonFile(models.Model): @@ -366,6 +383,17 @@ class Quiz(models.Model): course=course, passed=False ).values_list("user", flat=True).distinct().count() + def _create_quiz_copy(self, user): + question_papers = self.questionpaper_set.all() + new_quiz = self + new_quiz.id = None + new_quiz.description = "Copy of {0}".format(self.description) + new_quiz.creator = user + new_quiz.save() + for qp in question_papers: + qp._create_duplicate_questionpaper(new_quiz) + return new_quiz + def __str__(self): desc = self.description or 'Quiz' return '%s: on %s for %d minutes' % (desc, self.start_date_time, @@ -416,6 +444,17 @@ class LearningUnit(models.Model): success = False return success + def _create_unit_copy(self, user): + if self.type == "quiz": + new_quiz = self.quiz._create_quiz_copy(user) + new_unit = LearningUnit.objects.create( + order=self.order, type="quiz", quiz=new_quiz) + else: + new_lesson = self.lesson._create_lesson_copy(user) + new_unit = LearningUnit.objects.create( + order=self.order, type="lesson", lesson=new_lesson) + return new_unit + ############################################################################### class LearningModule(models.Model): @@ -511,6 +550,18 @@ class LearningModule(models.Model): percent = round((count / len(units)) * 100) return percent + def _create_module_copy(self, user, module_name): + learning_units = self.learning_unit.order_by("order") + new_module = self + new_module.id = None + new_module.name = module_name + new_module.creator = user + new_module.save() + for unit in learning_units: + new_unit = unit._create_unit_copy(user) + new_module.learning_unit.add(new_unit) + return new_module + def __str__(self): return self.name @@ -562,14 +613,13 @@ class Course(models.Model): return new_course def create_duplicate_course(self, user): - learning_modules = self.learning_module.all() - - new_course_name = "Copy Of {0}".format(self.name) - new_course = self._create_duplicate_instance(user, new_course_name) - - new_course.learning_module.add(*learning_modules) - - return new_course + learning_modules = self.learning_module.order_by("order") + copy_course_name = "Copy Of {0}".format(self.name) + new_course = self._create_duplicate_instance(user, copy_course_name) + for module in learning_modules: + copy_module_name = "Copy of {0}".format(module.name) + new_module = module._create_module_copy(user, copy_module_name) + new_course.learning_module.add(new_module) def request(self, *users): self.requests.add(*users) @@ -722,6 +772,14 @@ class Course(models.Model): percent = round((count / len(modules))) return percent + def days_before_start(self): + """ Get the days remaining for the start of the course """ + if timezone.now() < self.start_enroll_time: + remaining_days = (self.start_enroll_time - timezone.now()).days + 1 + else: + remaining_days = 0 + return remaining_days + def __str__(self): return self.name @@ -815,8 +873,10 @@ class Question(models.Model): min_time = models.IntegerField("time in minutes", default=0) + #Solution for the question. solution = models.TextField(blank=True) + def consolidate_answer_data(self, user_answer, user=None): question_data = {} metadata = {} @@ -928,6 +988,17 @@ class Question(models.Model): return test_case + def get_ordered_test_cases(self, answerpaper): + try: + order = TestCaseOrder.objects.get(answer_paper=answerpaper, + question = self + ).order.split(",") + return [self.get_test_case(id=int(tc_id)) + for tc_id in order + ] + except TestCaseOrder.DoesNotExist: + return self.get_test_cases() + def get_maximum_test_case_weight(self, **kwargs): max_weight = 0.0 for test_case in self.get_test_cases(): @@ -1134,6 +1205,11 @@ class QuestionPaper(models.Model): # Sequence or Order of fixed questions fixed_question_order = models.CharField(max_length=255, blank=True) + # Shuffle testcase order. + shuffle_testcases = models.BooleanField("Shuffle testcase for each user", + default=True + ) + objects = QuestionPaperManager() def get_question_bank(self): @@ -1144,8 +1220,8 @@ class QuestionPaper(models.Model): return questions def _create_duplicate_questionpaper(self, quiz): - new_questionpaper = QuestionPaper.objects.create(quiz=quiz, - shuffle_questions=self.shuffle_questions, + new_questionpaper = QuestionPaper.objects.create( + quiz=quiz, shuffle_questions=self.shuffle_questions, total_marks=self.total_marks, fixed_question_order=self.fixed_question_order ) @@ -1197,7 +1273,20 @@ class QuestionPaper(models.Model): ans_paper.save() questions = self._get_questions_for_answerpaper() ans_paper.questions.add(*questions) - question_ids = [str(que.id) for que in questions] + question_ids = [] + for question in questions: + question_ids.append(str(question.id)) + if self.shuffle_testcases and \ + question.type in ["mcq", "mcc"]: + testcases = question.get_test_cases() + random.shuffle(testcases) + testcases_ids = ",".join([str(tc.id) for tc in testcases] + ) + testcases_order = TestCaseOrder.objects.create( + answer_paper=ans_paper, + question=question, + order=testcases_ids) + ans_paper.questions_order = ",".join(question_ids) ans_paper.save() ans_paper.questions_unanswered.add(*questions) @@ -1829,7 +1918,7 @@ class AnswerPaper(models.Model): .format(u.first_name, u.last_name, q.description) -################################################################################ +############################################################################## class AssignmentUploadManager(models.Manager): def get_assignments(self, qp, que_id=None, user_id=None): @@ -1851,7 +1940,7 @@ class AssignmentUploadManager(models.Manager): return assignment_files, file_name -################################################################################ +############################################################################## class AssignmentUpload(models.Model): user = models.ForeignKey(User) assignmentQuestion = models.ForeignKey(Question) @@ -1860,7 +1949,7 @@ class AssignmentUpload(models.Model): objects = AssignmentUploadManager() -############################################################################### +############################################################################## class TestCase(models.Model): question = models.ForeignKey(Question, blank=True, null=True) type = models.CharField(max_length=24, choices=test_case_types, null=True) @@ -1978,3 +2067,19 @@ class FloatTestCase(TestCase): return u'Testcase | Correct: {0} | Error Margin: +or- {1}'.format( self.correct, self.error_margin ) + + +############################################################################## +class TestCaseOrder(models.Model): + """Testcase order contains a set of ordered test cases for a given question + for each user. + """ + + # Answerpaper of the user. + answer_paper = models.ForeignKey(AnswerPaper, related_name="answer_paper") + + # Question in an answerpaper. + question = models.ForeignKey(Question) + + #Order of the test case for a question. + order = models.TextField() diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js index 346991a..0f02aab 100644 --- a/yaksh/static/yaksh/js/add_question.js +++ b/yaksh/static/yaksh/js/add_question.js @@ -126,8 +126,9 @@ function textareaformat() document.getElementById('my').innerHTML = document.getElementById('id_description').value ; document.getElementById('rend_solution').innerHTML = document.getElementById('id_solution').value ; + var question_type = document.getElementById('id_type').value if (document.getElementById('id_grade_assignment_upload').checked || - document.getElementById('id_type').value == 'upload'){ + question_type == 'upload'){ $("#id_grade_assignment_upload").prop("disabled", false); } else{ diff --git a/yaksh/templates/yaksh/complete.html b/yaksh/templates/yaksh/complete.html index 3d6cadc..0881bfe 100644 --- a/yaksh/templates/yaksh/complete.html +++ b/yaksh/templates/yaksh/complete.html @@ -33,9 +33,6 @@ width="80" alt="YAKSH"></img>{% endblock %} <center><h3>{{message}}</h3></center> <center> <br> - {% if not module_id %} - <br><center><h4>You may now close the browser.</h4></center><br> - {% endif %} {% if module_id and not user == "moderator" %} {% if first_unit %} <a href="{{URL_ROOT}}/exam/next_unit/{{course_id}}/{{module_id}}/{{learning_unit.id}}/1" class="btn btn-info" id="Next"> Next diff --git a/yaksh/templates/yaksh/course_modules.html b/yaksh/templates/yaksh/course_modules.html index fad1be0..afbae75 100644 --- a/yaksh/templates/yaksh/course_modules.html +++ b/yaksh/templates/yaksh/course_modules.html @@ -18,102 +18,100 @@ </div> {% endif %} {% if learning_modules %} - {% for module in learning_modules %} - <div class="row well"> - <table class="table"> - <tr> - <td> - <a href="{{URL_ROOT}}/exam/quizzes/view_module/{{module.id}}/{{course.id}}"> - {{module.name|title}}</a> - </td> - <td> - <span class="glyphicon glyphicon-chevron-down" id="learning_units{{module.id}}{{course.id}}_down"> - </span> - <span class="glyphicon glyphicon-chevron-up" id="learning_units{{module.id}}{{course.id}}_up" style="display: none;"></span> - <a data-toggle="collapse" data-target="#learning_units{{module.id}}{{course.id}}" onclick="view_unit('learning_units{{module.id}}{{course.id}}');"> - View Lessons/Quizzes/Exercises</a> - </td> - <td> - {% get_module_status user module course as module_status %} - Status: - {% if module_status == "completed" %} - <span class="label label-success"> - {{module_status|title}} - </span> - {% elif module_status == "inprogress" %} - <span class="label label-info"> - {{module_status|title}} - </span> - {% else %} - <span class="label label-warning"> - {{module_status|title}} - </span> - {% endif %} - </td> - </tr> - </table> - </div> - <div id="learning_units{{module.id}}{{course.id}}" class="collapse"> - <table class="table"> - <tr> - <th>Lesson/Quiz/Exercise</th> - <th>Status</th> - <th>Type</th> - <th>View AnswerPaper</th> - </tr> - {% for unit in module.get_learning_units %} + <table class="table"> + {% for module in learning_modules %} <tr> - <ul class="inputs-list"> <td> - {% if unit.type == "quiz" %} - {{unit.quiz.description}} - {% else %} - {{unit.lesson.name}} - {% endif %} + <a href="{{URL_ROOT}}/exam/quizzes/view_module/{{module.id}}/{{course.id}}"> + {{module.name|title}}</a> </td> <td> - {% get_unit_status course module unit user as status %} - {% if status == "completed" %} - <span class="label label-success">{{status|title}} + <span class="glyphicon glyphicon-chevron-down" id="learning_units{{module.id}}{{course.id}}_down"> </span> - {% elif status == "inprogress" %} - <span class="label label-info">{{status|title}} + <span class="glyphicon glyphicon-chevron-up" id="learning_units{{module.id}}{{course.id}}_up" style="display: none;"> </span> - {% else %} - <span class="label label-warning">{{status|title}} - </span> - {% endif %} - </td> - <td> - {% if unit.type == "quiz" %} - {% if unit.quiz.is_exercise %} - Exercise - {% else %} - Quiz - {% endif %} - {% else %} - Lesson - {% endif %} + <a data-toggle="collapse" data-target="#learning_units{{module.id}}{{course.id}}" onclick="view_unit('learning_units{{module.id}}{{course.id}}');"> + View Lessons/Quizzes/Exercises</a> + <div id="learning_units{{module.id}}{{course.id}}" class="collapse"> + <table class="table"> + <tr> + <th>Lesson/Quiz/Exercise</th> + <th>Status</th> + <th>Type</th> + <th>View AnswerPaper</th> + </tr> + {% for unit in module.get_learning_units %} + <tr> + <td> + {% if unit.type == "quiz" %} + {{unit.quiz.description}} + {% else %} + {{unit.lesson.name}} + {% endif %} + </td> + <td> + {% get_unit_status course module unit user as status %} + {% if status == "completed" %} + <span class="label label-success">{{status|title}} + </span> + {% elif status == "inprogress" %} + <span class="label label-info">{{status|title}} + </span> + {% else %} + <span class="label label-warning">{{status|title}} + </span> + {% endif %} + </td> + <td> + {% if unit.type == "quiz" %} + {% if unit.quiz.is_exercise %} + Exercise + {% else %} + Quiz + {% endif %} + {% else %} + Lesson + {% endif %} + </td> + <td> + {% if unit.type == "quiz" %} + {% if unit.quiz.view_answerpaper %} + <a href="{{ URL_ROOT }}/exam/view_answerpaper/{{ unit.quiz.questionpaper_set.get.id }}/{{course.id}}"> + <i class="fa fa-eye" aria-hidden="true"></i> Can View </a> + {% else %} + <a> + <i class="fa fa-eye-slash" aria-hidden="true"> + </i> Cannot view now </a> + {% endif %} + {% else %} + ------ + {% endif %} + </td> + </tr> + {% endfor %} + </table> + </div> </td> <td> - {% if unit.type == "quiz" %} - {% if unit.quiz.view_answerpaper %} - <a href="{{ URL_ROOT }}/exam/view_answerpaper/{{ unit.quiz.questionpaper_set.get.id }}/{{course.id}}"><i class="fa fa-eye" aria-hidden="true"></i> Can View </a> + {% get_module_status user module course as module_status %} + Status: + {% if module_status == "completed" %} + <span class="label label-success"> + {{module_status|title}} + </span> + {% elif module_status == "inprogress" %} + <span class="label label-info"> + {{module_status|title}} + </span> {% else %} - <a> - <i class="fa fa-eye-slash" aria-hidden="true"> - </i> Cannot view now </a> + <span class="label label-warning"> + {{module_status|title}} + </span> {% endif %} - {% else %} - ------ - {% endif %} </td> - </ul> </tr> - {% endfor %} - </table> - </div> - {% endfor %} + {% endfor %} + </table> {% else %} <h3> No lectures found </h3> {% endif %} diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html index bc96bf5..78d21bf 100644 --- a/yaksh/templates/yaksh/courses.html +++ b/yaksh/templates/yaksh/courses.html @@ -4,6 +4,7 @@ {% block script %} <script> $(document).ready(function(){ + $('[data-toggle="tooltip"]').tooltip(); $("#created_courses").toggle(); $("#link_created_courses").click(function() { if ($("#allotted_courses").is(":visible")){ @@ -24,6 +25,14 @@ }); </script> {% endblock %} +{% block css %} +<style> + .test + .tooltip.top > .tooltip-inner { + padding: 15px; + font-size: 12px; + } +</style> +{% endblock %} {% block content %} <div class="row"> <div class="col-sm-3 col-md-2 sidebar"> @@ -99,7 +108,8 @@ <br><br> <ul> <li> - <a href="{{URL_ROOT}}/exam/manage/courses/designcourse/{{course.id}}/">Design Course + <a href="{{URL_ROOT}}/exam/manage/courses/designcourse/{{course.id}}/" data-toggle="tooltip" title="Add/Remove/Change course modules" data-placement="top"> + Design Course </a> </li> <br> @@ -123,7 +133,7 @@ </li> <br> <li> - <a href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/"> + <a class="test" href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/" data-toggle="tooltip" title="Creates Copy of selected Course as well as its Modules, Lessons/Quizzes" data-placement="top"> Clone Course</a> </li> </ul> @@ -259,7 +269,7 @@ </li> <br> <li> - <a href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/"> + <a class="test" href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/" data-toggle="tooltip" title="Creates Copy of selected Course as well as its Modules, Lessons/Quizzes" data-placement="top"> Clone Course</a> </li> </ul> diff --git a/yaksh/templates/yaksh/design_course_session.html b/yaksh/templates/yaksh/design_course_session.html index ee530e0..6542e3c 100644 --- a/yaksh/templates/yaksh/design_course_session.html +++ b/yaksh/templates/yaksh/design_course_session.html @@ -23,7 +23,7 @@ <div class="row"> <div class="col-md-8 col-md-offset-2 available-list"> <div id="fixed-available-wrapper"> - <p><u><b>Available Lessons and quizzes: (Add Lessons and Quizzes)</b></u></p> + <p><u><b>Available Modules:</b></u></p> <div id="fixed-available"> <table id="course-details" class="table table-bordered"> <tr> @@ -64,7 +64,7 @@ </div> <div class="col-md-8 col-md-offset-2"> <div id="fixed-added-wrapper"> - <p><u><b>Choosen Lessons and quizzes:</b></u></p> + <p><u><b>Choosen Modules:</b></u></p> <div id="fixed-added"> <table id="course-details" class="table table-bordered"> <tr> diff --git a/yaksh/templates/yaksh/design_questionpaper.html b/yaksh/templates/yaksh/design_questionpaper.html index 1656e2b..d982d27 100644 --- a/yaksh/templates/yaksh/design_questionpaper.html +++ b/yaksh/templates/yaksh/design_questionpaper.html @@ -192,10 +192,14 @@ select <div class="tab-pane" id="finish"> <center> - <h5>Almost finished creating your question paper</h5> + <h5><u>Almost finished creating your question paper</u></h5> <label style="float: none;"> {{ qpaper_form.shuffle_questions }} - <span>Auto shuffle.</span> + <span>Shuffle questions' order for each student</span> + </label> <br><br> + <label style="float: none;"> + {{ qpaper_form.shuffle_testcases }} + <span>Shuffle MCQ/MCC options for each student</span> </label> <br><br> <input class ="btn primary large" type="submit" name="save" id="save" value="Save question paper"> <br> diff --git a/yaksh/templates/yaksh/quizzes_user.html b/yaksh/templates/yaksh/quizzes_user.html index cf08752..49f8d2d 100644 --- a/yaksh/templates/yaksh/quizzes_user.html +++ b/yaksh/templates/yaksh/quizzes_user.html @@ -39,25 +39,44 @@ No Courses to display </b></h4> </div> <div class="col-md-4"> - {% if not course.active %} - <span class="label label-danger">Closed</span> - {% endif %} {% if user in course.requests.all %} <span class="label label-warning">Request Pending </span> {% elif user in course.rejected.all %}<span class="label label-danger">Request Rejected</span> {% elif user in course.students.all %}<span class="label label-info">Enrolled</span> {% else %} - {% if course.is_active_enrollment %} - {% if course.is_self_enroll %} - <a class="btn btn-success" href="{{ URL_ROOT }}/exam/self_enroll/{{ course.id }}">Enroll</a> + {% if course.active %} + {% if course.is_active_enrollment %} + {% if course.is_self_enroll %} + <a class="btn btn-success" href="{{ URL_ROOT }}/exam/self_enroll/{{ course.id }}">Enroll</a> + {% else %} + <a class="btn btn-success" href="{{ URL_ROOT }}/exam/enroll_request/{{ course.id }}">Enroll</a> + {% endif %} {% else %} - <a class="btn btn-success" href="{{ URL_ROOT }}/exam/enroll_request/{{ course.id }}">Enroll</a> + <span class="label label-danger" style="font-size: 15px"> + Enrollment Closed + </span> {% endif %} {% else %} - <span class="label label-danger">Enrollment Closed</span> + <span class="label label-danger" style="font-size: 15px"> + Course is not activated + </span> {% endif %} {% endif %} </div> + <div class="col-md-4"> + {% if course.days_before_start != 0 %} + <span class="label label-info" style="font-size: 15px"> + {{course.days_before_start}} day(s) to start + </span> + {% endif %} + </div> </div> + {% if course.is_active_enrollment %} + <div class="alert alert-info"> + Start Date : {{course.start_enroll_time}} + <br> + End Date : {{course.end_enroll_time}} + </div> + {% endif %} {% if course.instructions %} <div class="row"> diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html index 45867d2..ce2533e 100644 --- a/yaksh/templates/yaksh/user_data.html +++ b/yaksh/templates/yaksh/user_data.html @@ -74,8 +74,7 @@ User IP address: {{ paper.user_ip }} {% endif %} {% endfor %} - {% elif question.type == "integer" or question.type == "string" - or question.type == "float" %} + {% elif question.type == "integer" or question.type == "string" or question.type == "float" %} <h5> <u>Correct Answer:</u></h5> {% for testcase in question.get_test_cases %} <strong>{{ testcase.correct|safe }}</strong> diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html index 410b578..971ef77 100644 --- a/yaksh/templates/yaksh/view_answerpaper.html +++ b/yaksh/templates/yaksh/view_answerpaper.html @@ -34,7 +34,7 @@ Start time: {{ paper.start_time }} <br/> End time : {{ paper.end_time }} <br/> Percentage obtained: {{ paper.percent }}% <br/> - {% if paper.passed == 0 %} + {% if paper.passed %} Status : <b style="color: red;"> Failed </b><br/> {% else %} Status : <b style="color: green;"> Passed </b><br/> @@ -55,7 +55,8 @@ <h5><u>Question:</u></h5> <strong>{{ question.description|safe }}</strong> {% if question.type == "mcq" or question.type == "mcc" %} <h5> <u>Choices:</u></h5> - {% for testcase in question.get_test_cases %} + {% get_ordered_testcases question paper as testcases %} + {% for testcase in testcases %} {% if testcase.correct %} <br/> <strong>{{ forloop.counter }}. {{ testcase.options|safe }}</strong> diff --git a/yaksh/templates/yaksh/view_profile.html b/yaksh/templates/yaksh/view_profile.html index 5f06135..ce95226 100644 --- a/yaksh/templates/yaksh/view_profile.html +++ b/yaksh/templates/yaksh/view_profile.html @@ -31,6 +31,10 @@ <th><label for="id_position"><h5>Position:</h5></label></th> <th><label for="id_position"><h5>{{ user.profile.position }}</h5></label></th> </tr> + <tr> + <th><label for="id_position"><h5>Timezone:</h5></label></th> + <th><label for="id_position"><h5>{{ user.profile.timezone }}</h5></label></th> + </tr> </table> <a class="btn btn-primary pull-right" href="{{ URL_ROOT }}/exam/editprofile/">Edit Profile</a> {% endblock %} diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py index 3c2c6fd..fa0802f 100644 --- a/yaksh/templatetags/custom_filters.py +++ b/yaksh/templatetags/custom_filters.py @@ -62,3 +62,8 @@ def module_completion_percent(course, module, user): @register.simple_tag def course_completion_percent(course, user): return course.percent_completed(user) + + +@register.simple_tag +def get_ordered_testcases(question, answerpaper): + return question.get_ordered_test_cases(answerpaper)
\ No newline at end of file diff --git a/yaksh/test_models.py b/yaksh/test_models.py index cd4279b..49bba00 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -113,6 +113,7 @@ def tearDownModule(): Lesson.objects.all().delete() LearningUnit.objects.all().delete() LearningModule.objects.all().delete() + AnswerPaper.objects.all().delete() ############################################################################### @@ -625,8 +626,9 @@ class QuestionPaperTestCases(unittest.TestCase): @classmethod def setUpClass(self): self.course = Course.objects.get(name="Python Course") + self.user= User.objects.get(username='creator') # All active questions - self.questions = Question.objects.filter(active=True) + self.questions = Question.objects.filter(active=True, user=self.user) self.quiz = Quiz.objects.get(description="demo quiz 1") # create question paper with only fixed questions @@ -848,7 +850,7 @@ class AnswerPaperTestCases(unittest.TestCase): ) self.qtn_paper_with_single_question.save() - all_questions = Question.objects.all() + all_questions = Question.objects.filter(user=self.user).order_by("id") self.questions = all_questions[0:3] self.start_time = timezone.now() self.end_time = self.start_time + timedelta(minutes=20) @@ -874,6 +876,9 @@ class AnswerPaperTestCases(unittest.TestCase): self.answerpaper.attempt_number = already_attempted + 1 self.answerpaper.save() self.answerpaper.questions.add(*self.questions) + self.answerpaper.questions_order = ",".join( + [str(q.id) for q in self.questions] + ) self.answerpaper.questions_unanswered.add(*self.questions) self.answerpaper.save() # answers for the Answer Paper @@ -928,17 +933,17 @@ class AnswerPaperTestCases(unittest.TestCase): self.question1.language = 'python' self.question1.test_case_type = 'standardtestcase' - self.question1.summary = "Question1" + self.question1.summary = "Q1" self.question1.save() self.question2.language = 'python' self.question2.type = 'mcq' self.question2.test_case_type = 'mcqtestcase' - self.question2.summary = "Question2" + self.question2.summary = "Q2" self.question2.save() self.question3.language = 'python' self.question3.type = 'mcc' self.question3.test_case_type = 'mcqtestcase' - self.question3.summary = "Question3" + self.question3.summary = "Q3" self.question3.save() self.assertion_testcase = StandardTestCase( question=self.question1, @@ -1097,7 +1102,8 @@ class AnswerPaperTestCases(unittest.TestCase): details = self.answerpaper.regrade(self.question3.id) # Then - self.answer = self.answerpaper.answers.filter(question=self.question3).last() + self.answer = self.answerpaper.answers.filter( + question=self.question3).last() self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 0) self.assertFalse(self.answer.correct) @@ -1237,9 +1243,9 @@ class AnswerPaperTestCases(unittest.TestCase): """ Test Answer Paper""" self.assertEqual(self.answerpaper.user.username, 'creator') self.assertEqual(self.answerpaper.user_ip, self.ip) - questions = self.answerpaper.get_questions() + questions = [q.id for q in self.answerpaper.get_questions()] num_questions = len(questions) - self.assertSequenceEqual(list(questions), list(self.questions)) + self.assertEqual(set(questions), set([q.id for q in self.questions])) self.assertEqual(num_questions, 3) self.assertEqual(self.answerpaper.question_paper, self.question_paper) self.assertEqual(self.answerpaper.start_time, self.start_time) @@ -1250,7 +1256,7 @@ class AnswerPaperTestCases(unittest.TestCase): self.assertEqual(self.answerpaper.questions_left(), 3) # Test current_question() method of Answer Paper current_question = self.answerpaper.current_question() - self.assertEqual(current_question.summary, "Question1") + self.assertEqual(current_question.summary, "Q1") # Test completed_question() method of Answer Paper question = self.answerpaper.add_completed_question(self.question1.id) @@ -1259,14 +1265,14 @@ class AnswerPaperTestCases(unittest.TestCase): # Test next_question() method of Answer Paper current_question = self.answerpaper.current_question() - self.assertEqual(current_question.summary, "Question2") + self.assertEqual(current_question.summary, "Q2") # When next_question_id = self.answerpaper.next_question(current_question.id) # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question3") + self.assertEqual(next_question_id.summary, "Q3") # Given, here question is already answered current_question_id = self.question1.id @@ -1276,7 +1282,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question2") + self.assertEqual(next_question_id.summary, "Q2") # Given, wrong question id current_question_id = 12 @@ -1286,7 +1292,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question1") + self.assertEqual(next_question_id.summary, "Q1") # Given, last question in the list current_question_id = self.question3.id @@ -1297,7 +1303,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question1") + self.assertEqual(next_question_id.summary, "Q1") # Test get_questions_answered() method # When @@ -1312,8 +1318,11 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(questions_unanswered.count(), 2) - self.assertSequenceEqual(questions_unanswered, - [self.questions[1], self.questions[2]]) + self.assertEqual(set([q.id for q in questions_unanswered]), + set([self.questions[1].id, + self.questions[2].id] + ) + ) # Test completed_question and next_question # When all questions are answered @@ -1325,7 +1334,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(self.answerpaper.questions_left(), 1) self.assertIsNotNone(current_question) - self.assertEqual(current_question.summary, "Question3") + self.assertEqual(current_question.summary, "Q3") # When current_question = self.answerpaper.add_completed_question( @@ -1335,7 +1344,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(self.answerpaper.questions_left(), 0) self.assertIsNotNone(current_question) - self.assertTrue(current_question == self.answerpaper.questions.all()[0]) + self.assertTrue(current_question == self.answerpaper.get_all_ordered_questions()[0]) # When next_question_id = self.answerpaper.next_question(current_question_id) @@ -1426,7 +1435,9 @@ class CourseTestCases(unittest.TestCase): self.student2 = User.objects.get(username="demo_user3") self.quiz1 = Quiz.objects.get(description='demo quiz 1') self.quiz2 = Quiz.objects.get(description='demo quiz 2') - self.questions = Question.objects.filter(active=True) + self.questions = Question.objects.filter(active=True, + user=self.creator + ) self.modules = LearningModule.objects.filter(creator=self.creator) # create courses with disabled enrollment @@ -1665,6 +1676,24 @@ class CourseTestCases(unittest.TestCase): updated_percent = self.course.percent_completed(self.student1) self.assertEqual(updated_percent, 25) + def test_course_time_remaining_to_start(self): + # check if course has 0 days left to start + self.assertEqual(self.course.days_before_start(), 0) + + # check if course has some days left to start + course_time = self.course.start_enroll_time + self.course.start_enroll_time = datetime( + 2199, 12, 31, 10, 8, 15, 0, + tzinfo=pytz.utc + ) + self.course.save() + updated_course = Course.objects.get(id=self.course.id) + time_diff = updated_course.start_enroll_time - timezone.now() + actual_days = time_diff.days + 1 + self.assertEqual(updated_course.days_before_start(), actual_days) + self.course.start_enroll_time = course_time + self.course.save() + ############################################################################### class TestCaseTestCases(unittest.TestCase): diff --git a/yaksh/test_views.py b/yaksh/test_views.py index 3b27338..faac617 100644 --- a/yaksh/test_views.py +++ b/yaksh/test_views.py @@ -20,6 +20,7 @@ from django.utils import timezone from django.core import mail from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.files import File from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\ @@ -1668,13 +1669,38 @@ class TestCourses(TestCase): order=0, name="test module", description="module", check_prerequisite=False, creator=self.teacher) - self.user1_course = Course.objects.create(name="Python Course", + self.user1_course = Course.objects.create( + name="Python Course", enrollment="Enroll Request", creator=self.user1) + # Create Learning Module for Python Course + self.learning_module1 = LearningModule.objects.create( + order=0, name="demo module", description="module", + check_prerequisite=False, creator=self.user1) + + self.quiz = Quiz.objects.create( + time_between_attempts=0, description='demo quiz', + creator=self.user1) + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0) + self.lesson = Lesson.objects.create( + name="demo lesson", description="test description", + creator=self.user1) + + self.lesson_unit = LearningUnit.objects.create( + order=1, type="lesson", lesson=self.lesson) + self.quiz_unit = LearningUnit.objects.create( + order=2, type="quiz", quiz=self.quiz) + + # Add units to module + self.learning_module1.learning_unit.add(self.lesson_unit) + self.learning_module1.learning_unit.add(self.quiz_unit) + # Add teacher to user1 course self.user1_course.teachers.add(self.teacher) - self.user2_course = Course.objects.create(name="Java Course", + self.user2_course = Course.objects.create( + name="Java Course", enrollment="Enroll Request", creator=self.user2) self.user2_course.learning_module.add(self.learning_module) @@ -1683,10 +1709,7 @@ class TestCourses(TestCase): self.user1.delete() self.user2.delete() self.student.delete() - self.user1_course.delete() - self.user2_course.delete() self.teacher.delete() - self.learning_module.delete() def test_courses_denies_anonymous(self): """ @@ -1837,7 +1860,7 @@ class TestCourses(TestCase): self.learning_module) def test_duplicate_course(self): - """ Test To clone/duplicate course """ + """ Test To clone/duplicate course and link modules""" # Student Login self.client.login( @@ -1869,27 +1892,64 @@ class TestCourses(TestCase): self.assertTemplateUsed(response, "yaksh/complete.html") self.assertIn(err_msg, response.context['message']) - # Moderator/Course creator login + # Test clone/duplicate courses and create copies of modules and units + + # Teacher Login + # Given + # Add files to a lesson + lesson_file = SimpleUploadedFile("file1.txt", b"Test") + django_file = File(lesson_file) + lesson_file_obj = LessonFile() + lesson_file_obj.lesson = self.lesson + lesson_file_obj.file.save(lesson_file.name, django_file, save=True) + + # Add module to Python Course + self.user1_course.learning_module.add(self.learning_module1) self.client.login( - username=self.user2.username, - password=self.user2_plaintext_pass + username=self.teacher.username, + password=self.teacher_plaintext_pass ) - - # Allows creator to duplicate the course response = self.client.get( reverse('yaksh:duplicate_course', - kwargs={"course_id": self.user2_course.id}), + kwargs={"course_id": self.user1_course.id}), follow=True ) - self.assertEqual(response.status_code, 200) + # When courses = Course.objects.filter( - creator=self.user2).order_by("id") - self.assertEqual(courses.count(), 2) - self.assertEqual(courses.last().creator, self.user2) - self.assertEqual(courses.last().name, "Copy Of Java Course") - self.assertEqual(courses.last().get_learning_modules()[0].id, - self.user2_course.get_learning_modules()[0].id) + creator=self.teacher).order_by("id") + module = courses.last().get_learning_modules()[0] + units = module.get_learning_units() + cloned_lesson = units[0].lesson + cloned_quiz = units[1].quiz + expected_lesson_files = cloned_lesson.get_files() + actual_lesson_files = self.lesson.get_files() + cloned_qp = cloned_quiz.questionpaper_set.get() + self.all_files = LessonFile.objects.filter( + lesson_id__in=[self.lesson.id, cloned_lesson.id]) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(courses.last().creator, self.teacher) + self.assertEqual(courses.last().name, "Copy Of Python Course") + self.assertEqual(module.name, "Copy of demo module") + self.assertEqual(module.creator, self.teacher) + self.assertEqual(module.order, 0) + self.assertEqual(len(units), 2) + self.assertEqual(cloned_lesson.name, "Copy of demo lesson") + self.assertEqual(cloned_lesson.creator, self.teacher) + self.assertEqual(cloned_quiz.description, "Copy of demo quiz") + self.assertEqual(cloned_quiz.creator, self.teacher) + self.assertEqual(cloned_qp.__str__(), + "Question Paper for Copy of demo quiz") + self.assertEqual(os.path.basename(expected_lesson_files[0].file.name), + os.path.basename(actual_lesson_files[0].file.name)) + + for lesson_file in self.all_files: + file_path = lesson_file.file.path + if os.path.exists(file_path): + os.remove(file_path) + shutil.rmtree(os.path.dirname(file_path)) class TestAddCourse(TestCase): diff --git a/yaksh/urls.py b/yaksh/urls.py index 08c2091..b81c7b2 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -86,8 +86,8 @@ urlpatterns = [ views.show_statistics, name="show_statistics"), url(r'^manage/download_quiz_csv/(?P<course_id>\d+)/(?P<quiz_id>\d+)/$', views.download_quiz_csv, name="download_quiz_csv"), - url(r'^manage/duplicate_course/(?P<course_id>\d+)/$', views.duplicate_course, - name='duplicate_course'), + url(r'^manage/duplicate_course/(?P<course_id>\d+)/$', + views.duplicate_course, name='duplicate_course'), url(r'manage/courses/$', views.courses, name='courses'), url(r'manage/add_course/$', views.add_course, name='add_course'), url(r'manage/edit_course/(?P<course_id>\d+)$', views.add_course, name='edit_course'), diff --git a/yaksh/views.py b/yaksh/views.py index 011b417..ec1807c 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -390,8 +390,8 @@ def prof_manage(request, msg=None): return my_redirect('/exam/login') if not is_moderator(user): return my_redirect('/exam/') - courses = Course.objects.filter(creator=user, is_trial=False) - + courses = Course.objects.filter(Q(creator=user) | Q(teachers=user), + is_trial=False) trial_paper = AnswerPaper.objects.filter( user=user, question_paper__quiz__is_trial=True, course__is_trial=True @@ -410,6 +410,7 @@ def prof_manage(request, msg=None): qpaper.quiz.delete() else: answerpaper.delete() + context = {'user': user, 'courses': courses, 'trial_paper': trial_paper, 'msg': msg } @@ -616,7 +617,10 @@ def show_question(request, question, paper, error_message=None, notification=Non if question.type == "code" else 'You have already attempted this question' ) - test_cases = question.get_test_cases() + if question.type in ['mcc', 'mcq']: + test_cases = question.get_ordered_test_cases(paper) + else: + test_cases = question.get_test_cases() files = FileUpload.objects.filter(question_id=question.id, hide=False) course = Course.objects.get(id=course_id) module = course.learning_module.get(id=module_id) @@ -2252,10 +2256,15 @@ def duplicate_course(request, course_id): raise Http404('You are not allowed to view this page!') if course.is_teacher(user) or course.is_creator(user): + # Create new entries of modules, lessons/quizzes + # from current course to copied course course.create_duplicate_course(user) else: - msg = 'You do not have permissions to clone this course, please contact your '\ - 'instructor/administrator.' + msg = dedent( + '''\ + You do not have permissions to clone {0} course, please contact + your instructor/administrator.'''.format(course.name) + ) return complete(request, msg, attempt_num=None, questionpaper_id=None) return my_redirect('/exam/manage/courses/') |