diff options
52 files changed, 1279 insertions, 802 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e259117..d3364e0 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +=== 0.8.0 === + +* Added a management command to add users to moderator group +* Renamed add_group management command to create_mod_group +* Deprecated multiple management commands: dump_user_data, load_exam, load_question_xml, results2csv + === 0.7.0 (15-11-2017) === * Simplified Standard I/O type question error output. 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/forms.py b/yaksh/forms.py index cbf3033..9a44cfa 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -86,10 +86,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"] diff --git a/yaksh/management/commands/add_group.py b/yaksh/management/commands/add_group.py deleted file mode 100644 index 624ff3c..0000000 --- a/yaksh/management/commands/add_group.py +++ /dev/null @@ -1,32 +0,0 @@ -''' - This command adds moderator group with permissions to add, change and delete - the objects in the exam app. - We can modify this command to add more groups by providing arguments. - Arguments like group-name, app-name can be passed. -''' - -# django imports -from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.db.utils import IntegrityError - -class Command(BaseCommand): - help = 'Adds the moderator group' - - def handle(self, *args, **options): - app_label = 'yaksh' - group = Group(name='moderator') - try: - group.save() - except IntegrityError: - raise CommandError("The group already exits") - else: - # Get the models for the given app - content_types = ContentType.objects.filter(app_label=app_label) - # Get list of permissions for the models - permission_list = Permission.objects.filter(content_type__in=content_types) - group.permissions.add(*permission_list) - group.save() - - self.stdout.write('Moderator group added successfully') diff --git a/yaksh/management/commands/create_moderator.py b/yaksh/management/commands/create_moderator.py new file mode 100644 index 0000000..3bbe462 --- /dev/null +++ b/yaksh/management/commands/create_moderator.py @@ -0,0 +1,48 @@ +''' + This command creates a moderator group and adds users to the moderator group with permissions to add, change and delete + the objects in the exam app. +''' + +# django imports +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User, Group, Permission +from django.contrib.contenttypes.models import ContentType +from django.db.utils import IntegrityError + +# Yaksh imports +from yaksh.models import Profile + +class Command(BaseCommand): + help = 'Adds users to the moderator group' + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument('usernames', nargs='*', type=str) + + def handle(self, *args, **options): + app_label = 'yaksh' + + try: + group = Group.objects.get(name='moderator') + except Group.DoesNotExist: + group = Group(name='moderator') + group.save() + # Get the models for the given app + content_types = ContentType.objects.filter(app_label=app_label) + # Get list of permissions for the models + permission_list = Permission.objects.filter(content_type__in=content_types) + group.permissions.add(*permission_list) + group.save() + self.stdout.write('Moderator group added successfully') + + if options['usernames']: + for uname in options['usernames']: + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + raise CommandError('User "{0}" does not exist'.format(uname)) + if user in group.user_set.all(): + self.stdout.write('User "{0}" is already a Moderator'.format(uname)) + else: + group.user_set.add(user) + self.stdout.write('Successfully added User "{0}" to Moderator group'.format(uname)) diff --git a/yaksh/management/commands/dump_user_data.py b/yaksh/management/commands/dump_user_data.py deleted file mode 100644 index 7deee03..0000000 --- a/yaksh/management/commands/dump_user_data.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys - -# Django imports. -from django.core.management.base import BaseCommand -from django.template import Template, Context - -# Local imports. -from yaksh.views import get_user_data -from yaksh.models import User - -data_template = Template('''\ -=============================================================================== -Data for {{ data.user.get_full_name.title }} ({{ data.user.username }}) - -Name: {{ data.user.get_full_name.title }} -Username: {{ data.user.username }} -{% if data.profile %}\ -Roll number: {{ data.profile.roll_number }} -Position: {{ data.profile.position }} -Department: {{ data.profile.department }} -Institute: {{ data.profile.institute }} -{% endif %}\ -Email: {{ data.user.email }} -Date joined: {{ data.user.date_joined }} -Last login: {{ data.user.last_login }} -{% for paper in data.papers %} -Paper: {{ paper.quiz.description }} ---------------------------------------- -Marks obtained: {{ paper.get_total_marks }} -Questions correctly answered: {{ paper.get_answered_str }} -Total attempts at questions: {{ paper.answers.count }} -Start time: {{ paper.start_time }} -User IP address: {{ paper.user_ip }} -{% if paper.answers.count %} -Answers -------- -{% for question, answers in paper.get_question_answers.items %} -Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }}) -{% if question.type == "mcq" %}\ -############################################################################### -Choices: {% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} -Student answer: {{ answers.0|safe }} -{% else %}{# non-mcq questions #}\ -{% for answer in answers %}\ -############################################################################### -{{ answer.answer.strip|safe }} -# Autocheck: {{ answer.error|safe }} -{% endfor %}{# for answer in answers #}\ -{% endif %}\ -{% with answers|last as answer %}\ -Marks: {{answer.marks}} -{% endwith %}\ -{% endfor %}{# for question, answers ... #}\ - -Teacher comments ------------------ -{{ paper.comments|default:"None" }} -{% endif %}{# if paper.answers.count #}\ -{% endfor %}{# for paper in data.papers #} -''') - - -def dump_user_data(unames, stdout): - '''Dump user data given usernames (a sequence) if none is given dump all - their data. The data is dumped to stdout. - ''' - if not unames: - try: - users = User.objects.all() - except User.DoesNotExist: - pass - else: - users = [] - for uname in unames: - try: - user = User.objects.get(username__exact = uname) - except User.DoesNotExist: - stdout.write('User %s does not exist'%uname) - else: - users.append(user) - - for user in users: - data = get_user_data(user.username) - context = Context({'data': data}) - result = data_template.render(context) - stdout.write(result.encode('ascii', 'xmlcharrefreplace')) - -class Command(BaseCommand): - args = '<username1> ... <usernamen>' - help = '''Dumps all user data to stdout, optional usernames can be - specified. If none is specified all user data is dumped. - ''' - - def handle(self, *args, **options): - """Handle the command.""" - # Dump data. - dump_user_data(args, self.stdout) - diff --git a/yaksh/management/commands/load_exam.py b/yaksh/management/commands/load_exam.py deleted file mode 100644 index b354fbd..0000000 --- a/yaksh/management/commands/load_exam.py +++ /dev/null @@ -1,57 +0,0 @@ -# System library imports. -from os.path import basename - -# Django imports. -from django.core.management.base import BaseCommand - -# Local imports. -from yaksh.models import Question, Quiz - -def clear_exam(): - """Deactivate all questions from the database.""" - for question in Question.objects.all(): - question.active = False - question.save() - - # Deactivate old quizzes. - for quiz in Quiz.objects.all(): - quiz.active = False - quiz.save() - -def load_exam(filename): - """Load questions and quiz from the given Python file. The Python file - should declare a list of name "questions" which define all the questions - in pure Python. It can optionally load a Quiz from an optional 'quiz' - object. - """ - # Simply exec the given file and we are done. - exec(open(filename).read()) - - if 'questions' not in locals(): - msg = 'No variable named "questions" with the Questions in file.' - raise NameError(msg) - - for question in questions: - question[0].save() - for tag in question[1]: - question[0].tags.add(tag) - - if 'quiz' in locals(): - quiz.save() - -class Command(BaseCommand): - args = '<q_file1.py q_file2.py>' - help = '''loads the questions from given Python files which declare the - questions in a list called "questions".''' - - def handle(self, *args, **options): - """Handle the command.""" - # Delete existing stuff. - clear_exam() - - # Load from files. - for fname in args: - self.stdout.write('Importing from {0} ... '.format(basename(fname))) - load_exam(fname) - self.stdout.write('Done\n') - diff --git a/yaksh/management/commands/load_questions_xml.py b/yaksh/management/commands/load_questions_xml.py deleted file mode 100644 index 02714ea..0000000 --- a/yaksh/management/commands/load_questions_xml.py +++ /dev/null @@ -1,73 +0,0 @@ -# System library imports. -from os.path import basename -from xml.dom.minidom import parse -from htmlentitydefs import name2codepoint -import re - -# Django imports. -from django.core.management.base import BaseCommand - -# Local imports. -from yaksh.models import Question - -def decode_html(html_str): - """Un-escape or decode HTML strings to more usable Python strings. - From here: http://wiki.python.org/moin/EscapingHtml - """ - return re.sub('&(%s);' % '|'.join(name2codepoint), - lambda m: unichr(name2codepoint[m.group(1)]), html_str) - -def clear_questions(): - """Deactivate all questions from the database.""" - for question in Question.objects.all(): - question.active = False - question.save() - -def load_questions_xml(filename): - """Load questions from the given XML file.""" - q_bank = parse(filename).getElementsByTagName("question") - - for question in q_bank: - - summary_node = question.getElementsByTagName("summary")[0] - summary = (summary_node.childNodes[0].data).strip() - - desc_node = question.getElementsByTagName("description")[0] - description = (desc_node.childNodes[0].data).strip() - - type_node = question.getElementsByTagName("type")[0] - type = (type_node.childNodes[0].data).strip() - - points_node = question.getElementsByTagName("points")[0] - points = float((points_node.childNodes[0].data).strip()) \ - if points_node else 1.0 - - test_node = question.getElementsByTagName("test")[0] - test = decode_html((test_node.childNodes[0].data).strip()) - - opt_node = question.getElementsByTagName("options")[0] - opt = decode_html((opt_node.childNodes[0].data).strip()) - - new_question = Question(summary=summary, - description=description, - points=points, - options=opt, - type=type, - test=test) - new_question.save() - -class Command(BaseCommand): - args = '<q_file1.xml q_file2.xml>' - help = 'loads the questions from given XML files' - - def handle(self, *args, **options): - """Handle the command.""" - # Delete existing stuff. - clear_questions() - - # Load from files. - for fname in args: - self.stdout.write('Importing from {0} ... '.format(basename(fname))) - load_questions_xml(fname) - self.stdout.write('Done\n') - diff --git a/yaksh/management/commands/results2csv.py b/yaksh/management/commands/results2csv.py deleted file mode 100644 index 2644354..0000000 --- a/yaksh/management/commands/results2csv.py +++ /dev/null @@ -1,69 +0,0 @@ -# System library imports. -import sys -from os.path import basename - -# Django imports. -from django.core.management.base import BaseCommand -from django.template import Template, Context - -# Local imports. -from yaksh.models import Quiz, QuestionPaper - -result_template = Template('''\ -"name","username","rollno","email","answered","total","attempts","position",\ -"department","institute" -{% for paper in papers %}\ -"{{ paper.user.get_full_name.title }}",\ -"{{ paper.user.username }}",\ -"{{ paper.profile.roll_number }}",\ -"{{ paper.user.email }}",\ -"{{ paper.get_answered_str }}",\ -{{ paper.get_total_marks }},\ -{{ paper.answers.count }},\ -"{{ paper.profile.position }}",\ -"{{ paper.profile.department }}",\ -"{{ paper.profile.institute }}" -{% endfor %}\ -''') - -def results2csv(filename, stdout): - """Write exam data to a CSV file. It prompts the user to choose the - appropriate quiz. - """ - qs = Quiz.objects.all() - - if len(qs) > 1: - print "Select quiz to save:" - for q in qs: - stdout.write('%d. %s\n'%(q.id, q.description)) - quiz_id = int(raw_input("Please select quiz: ")) - try: - quiz = Quiz.objects.get(id=quiz_id) - except Quiz.DoesNotExist: - stdout.write("Sorry, quiz %d does not exist!\n"%quiz_id) - sys.exit(1) - else: - quiz = qs[0] - - papers = QuestionPaper.objects.filter(quiz=quiz, - user__profile__isnull=False) - stdout.write("Saving results of %s to %s ... "%(quiz.description, - basename(filename))) - # Render the data and write it out. - f = open(filename, 'w') - context = Context({'papers': papers}) - f.write(result_template.render(context)) - f.close() - - stdout.write('Done\n') - -class Command(BaseCommand): - args = '<results.csv>' - help = '''Writes out the results of a quiz to a CSV file. Prompt user - to select appropriate quiz if there are multiple. - ''' - - def handle(self, *args, **options): - """Handle the command.""" - # Save to file. - results2csv(args[0], self.stdout) diff --git a/yaksh/models.py b/yaksh/models.py index e3d2f61..826c0e8 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -166,6 +166,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): @@ -369,6 +386,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, @@ -419,6 +447,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): @@ -514,6 +553,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 @@ -565,14 +616,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) @@ -725,6 +775,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 @@ -1165,8 +1223,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 ) 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..dabf8eb 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> @@ -358,6 +368,9 @@ {% if quiz.questionpaper_set.get %} <a href="{{URL_ROOT}}/exam/manage/designquestionpaper/{{ quiz.id }}/{{quiz.questionpaper_set.get.id}}/"> Question Paper for {{ quiz.description }}</a> + <a href="{{URL_ROOT}}/exam/manage/preview_questionpaper/{{quiz.questionpaper_set.get.id}}" class="btn btn-primary active btn-xs" target="_blank"> + View + </a> <br> {% else %} <p>No Question Paper 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/preview_questionpaper.html b/yaksh/templates/yaksh/preview_questionpaper.html new file mode 100644 index 0000000..123218f --- /dev/null +++ b/yaksh/templates/yaksh/preview_questionpaper.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block pagetitle %} Quiz: {{ paper.quiz.description }} {% endblock pagetitle %} + +{% block content %} +<div class="well"> + <div class="col-md-12"> + <div class="col-md-6">Maximum Mark(s): {{ paper.total_marks }}</div> + <div class="col-md-6"><span class="pull-right">Total Time: {{ paper.quiz.duration }} minutes</span></div> + </div> +</div> +<div class="panel panel-default"> + <div class="panel-heading">Instructions</div> + <div class="panel-body" id="instructions"> + {{ paper.quiz.instructions|safe }} + </div> +</div> +{% for question in questions %} + <div class="panel panel-info"> + <div class="panel-heading"> + <strong> {{forloop.counter}}. {{ question.summary }} + <span class="marks pull-right"> Mark(s): {{ question.points }} </span> + </strong> + </div> + <div class="panel-body"> + <h5><u>Question:</u></h5> <strong>{{ question.description|safe }}</strong> + <br/><b>Answer:</b><br/> + {% if question.type == "code" %} + <div class="well">{{ question.snippet }}<br/></div> + {% endif %} + {% if question.type == "mcq" or question.type == "mcc" %} + <h5> <u>Choices:</u></h5> + {% for testcase in question.get_test_cases %} + <br/><strong> + {{ forloop.counter }}. {{ testcase.options|safe }}</strong> + {% endfor %} + {% endif %} + + </div> + </div> +{% endfor %} +{% endblock %} 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/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/test_models.py b/yaksh/test_models.py index 39a90d1..e7a6fe5 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -1684,6 +1684,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..fd4f040 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): @@ -4040,6 +4100,24 @@ class TestQuestionPaper(TestCase): timezone='UTC' ) + self.user2_plaintext_pass = 'demo2' + self.user2 = User.objects.create_user( + username='demo_user2', + password=self.user_plaintext_pass, + first_name='first_name2', + last_name='last_name2', + email='demo2@test.com' + ) + + Profile.objects.create( + user=self.user2, + roll_number=11, + institute='IIT', + department='Chemical', + position='Student', + timezone='UTC' + ) + self.teacher_plaintext_pass = 'demo_teacher' self.teacher = User.objects.create_user( username='demo_teacher', @@ -4194,6 +4272,54 @@ class TestQuestionPaper(TestCase): self.learning_module.delete() self.learning_unit.delete() + def test_preview_questionpaper_correct(self): + self.client.login( + username=self.user.username, + password=self.user_plaintext_pass + ) + + # Should successfully preview question paper + response = self.client.get( + reverse('yaksh:preview_questionpaper', + kwargs={"questionpaper_id": self.question_paper.id} + ) + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/preview_questionpaper.html') + self.assertEqual( + response.context['questions'], + self.questions_list + ) + self.assertEqual(response.context['paper'], self.question_paper) + + def test_preview_questionpaper_without_moderator(self): + self.client.login( + username=self.user2.username, + password=self.user_plaintext_pass + ) + + # Should raise an HTTP 404 response + response = self.client.get( + reverse('yaksh:preview_questionpaper', + kwargs={"questionpaper_id": self.question_paper.id} + ) + ) + self.assertEqual(response.status_code, 404) + + def test_preview_qustionpaper_without_quiz_owner(self): + self.client.login( + username=self.teacher.username, + password=self.teacher_plaintext_pass + ) + + # Should raise an HTTP 404 response + response = self.client.get( + reverse('yaksh:preview_questionpaper', + kwargs={"questionpaper_id": self.question_paper.id} + ) + ) + self.assertEqual(response.status_code, 404) + def test_mcq_attempt_right_after_wrong(self): """ Case:- Check if answerpaper and answer marks are updated after attempting same mcq question with wrong answer and then right diff --git a/yaksh/urls.py b/yaksh/urls.py index 08c2091..dd450ba 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'), @@ -172,4 +172,6 @@ urlpatterns = [ views.design_course, name="design_course"), url(r'^manage/course_status/(?P<course_id>\d+)/$', views.course_status, name="course_status"), + url(r'^manage/preview_questionpaper/(?P<questionpaper_id>\d+)/$', + views.preview_questionpaper, name="preview_questionpaper"), ] diff --git a/yaksh/views.py b/yaksh/views.py index b97ae93..083fd67 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from django.contrib.auth import login, logout, authenticate from django.shortcuts import render_to_response, get_object_or_404, redirect from django.template import RequestContext, Context, Template +from django.template.loader import get_template, render_to_string from django.http import Http404 from django.db.models import Sum, Max, Q, F from django.views.decorators.csrf import csrf_exempt @@ -390,8 +391,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 +411,7 @@ def prof_manage(request, msg=None): qpaper.quiz.delete() else: answerpaper.delete() + context = {'user': user, 'courses': courses, 'trial_paper': trial_paper, 'msg': msg } @@ -1888,7 +1890,7 @@ def create_demo_course(request): user = request.user ci = RequestContext(request) if not is_moderator(user): - raise("You are not allowed to view this page") + raise Http404("You are not allowed to view this page") demo_course = Course() success = demo_course.create_demo(user) if success: @@ -2258,10 +2260,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/') @@ -2750,3 +2757,22 @@ def _update_unit_status(course_id, user, unit): # make next available unit as current unit course_status.current_unit = unit course_status.save() + + +@login_required +@email_verified +def preview_questionpaper(request, questionpaper_id): + user = request.user + if not is_moderator(user): + raise Http404('You are not allowed to view this page!') + paper = QuestionPaper.objects.get(id=questionpaper_id) + if not paper.quiz.creator == user: + raise Http404('This questionpaper does not belong to you') + context = { + 'questions': paper._get_questions_for_answerpaper(), + 'paper': paper, + } + + return my_render_to_response( + 'yaksh/preview_questionpaper.html', context + ) |