From 81667305df2bc148e4ca6247ad3b2d12b30e660c Mon Sep 17 00:00:00 2001 From: adityacp Date: Mon, 9 Mar 2020 17:10:10 +0530 Subject: Download course progress --- yaksh/models.py | 3 +++ yaksh/templates/yaksh/course_progress.html | 13 +++++++++++-- yaksh/urls.py | 2 ++ yaksh/views.py | 25 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/yaksh/models.py b/yaksh/models.py index e161543..072c1b5 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -648,6 +648,9 @@ class LearningUnit(models.Model): order=self.order, type="lesson", lesson=new_lesson) return new_unit + def __str__(self): + return self.lesson.name if self.lesson else self.quiz.description + ############################################################################### class LearningModule(models.Model): diff --git a/yaksh/templates/yaksh/course_progress.html b/yaksh/templates/yaksh/course_progress.html index a833c92..826f84b 100644 --- a/yaksh/templates/yaksh/course_progress.html +++ b/yaksh/templates/yaksh/course_progress.html @@ -4,10 +4,19 @@ {% if student_details %}

Course Progress

-
- Number Of Students: {{ students_no }} +
+
+
+ Number Of Students: {{ students_no }} +
+
+
{% include "yaksh/paginator.html" %} diff --git a/yaksh/urls.py b/yaksh/urls.py index 49c3d4f..b53d335 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -218,4 +218,6 @@ urlpatterns = [ views.get_course_modules, name="get_course_modules"), url(r'^manage/course/teachers/(?P\d+)', views.course_teachers, name="course_teachers"), + url(r'^manage/download/course/progress/(?P\d+)', + views.download_course_progress, name="download_course_progress"), ] diff --git a/yaksh/views.py b/yaksh/views.py index 5d2d560..58fe6e2 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -3155,3 +3155,28 @@ def get_course_modules(request, course_id): modules = course.get_learning_modules() context = {"modules": modules, "is_modules": True, "course": course} return my_render_to_response(request, 'yaksh/course_detail.html', context) + + +@login_required +@email_verified +def download_course_progress(request, course_id): + user = request.user + if not is_moderator(user): + raise Http404('You are not allowed to view this page!') + course = get_object_or_404(Course, pk=course_id) + if not course.is_creator(user) and not course.is_teacher(user): + raise Http404('This course does not belong to you') + students = course.students.order_by("-id") + stud_details = [(student.get_full_name(), course.get_grade(student), + course.get_completion_percent(student), + course.get_current_unit(student)) + for student in students] + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="{0}.csv"'.format( + (course.name).lower().replace(' ', '_')) + header = ['Name', 'Grade', 'Completion Percent', 'Current Unit'] + writer = csv.writer(response) + writer.writerow(header) + for student in stud_details: + writer.writerow(student) + return response -- cgit From 9dacf7cf2a26fb0f954f6f06cfffb65e23809ab7 Mon Sep 17 00:00:00 2001 From: adityacp Date: Mon, 9 Mar 2020 18:47:06 +0530 Subject: Add tests to check download course progress --- yaksh/test_views.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/yaksh/test_views.py b/yaksh/test_views.py index 58c6633..1c7c150 100644 --- a/yaksh/test_views.py +++ b/yaksh/test_views.py @@ -4211,6 +4211,24 @@ class TestDownloadCsv(TestCase): self.assertEqual(response.get('Content-Disposition'), 'attachment; filename="{0}"'.format(file_name)) + def test_download_course_progress_csv(self): + """ + Check for csv result of a course progress + """ + self.client.login( + username=self.user.username, + password=self.user_plaintext_pass + ) + response = self.client.get( + reverse('yaksh:download_course_progress', + kwargs={'course_id': self.course.id}), + follow=True + ) + file_name = "{0}.csv".format(self.course.name.lower().replace(" ", "_")) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.get('Content-Disposition'), + 'attachment; filename="{0}"'.format(file_name)) + def test_download_quiz_csv(self): """ Check for csv result of a quiz -- cgit From 077b15c67238578a7d877a53748ce46065905ddf Mon Sep 17 00:00:00 2001 From: ankitjavalkar Date: Wed, 11 Mar 2020 15:19:17 +0530 Subject: Release 0.13.0 related changes --- CHANGELOG.txt | 8 ++++++++ online_test/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4145e07..18063e5 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,11 @@ +=== 0.13.0 (11-03-2020) === + +* Revamped the UI and UX for the interface and standardised with Bootstrap 4 (Lumen theme from https://bootswatch.com/) +* Fixed a bug that caused failure when pre-existing tags for Questions have first capital letter +* Registered CourseStatus model so that it is available on the Django admin interface +* Added filters in the Django admin for filtering courses +* Modified admin.py to display only specific fields of AnswerPaper + === 0.12.0 (04-02-2020) === * Added syntax highlighting to the code answers in answerpaper diff --git a/online_test/__init__.py b/online_test/__init__.py index 2c7bffb..2d7893e 100644 --- a/online_test/__init__.py +++ b/online_test/__init__.py @@ -1 +1 @@ -__version__ = '0.12.0' +__version__ = '0.13.0' -- cgit From 050ced8942abf802cf09e6a6f9d23d85a0be0d3f Mon Sep 17 00:00:00 2001 From: adityacp Date: Thu, 12 Mar 2020 11:08:15 +0530 Subject: Avoid conditional check on foreign key --- yaksh/models.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/yaksh/models.py b/yaksh/models.py index 072c1b5..065d9cd 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -598,7 +598,12 @@ class LearningUnit(models.Model): check_prerequisite = models.BooleanField(default=True) def get_lesson_or_quiz(self): - return self.lesson if self.lesson else self.quiz + unit = None + if self.type == 'lesson': + unit = self.lesson + else: + unit = self.quiz + return unit def toggle_check_prerequisite(self): if self.check_prerequisite: @@ -649,7 +654,12 @@ class LearningUnit(models.Model): return new_unit def __str__(self): - return self.lesson.name if self.lesson else self.quiz.description + name = None + if self.type == 'lesson': + name = self.lesson.name + else: + name = self.quiz.description + return name ############################################################################### -- cgit From a2186d263aa8927a47fdb9f3a23ad39841b64b33 Mon Sep 17 00:00:00 2001 From: adityacp Date: Thu, 12 Mar 2020 11:30:51 +0530 Subject: Fix test to get lesson or quiz for a unit --- yaksh/test_models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/yaksh/test_models.py b/yaksh/test_models.py index 6e5a26d..a60a1d6 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -322,8 +322,12 @@ class LearningUnitTestCases(unittest.TestCase): def test_learning_unit(self): self.assertEqual(self.learning_unit_one.type, 'lesson') self.assertEqual(self.learning_unit_two.type, 'quiz') - self.assertEqual(self.learning_unit_one.lesson, self.lesson) - self.assertEqual(self.learning_unit_two.quiz, self.quiz) + self.assertEqual( + self.learning_unit_one.get_lesson_or_quiz(), self.lesson + ) + self.assertEqual( + self.learning_unit_two.get_lesson_or_quiz(), self.quiz + ) self.assertIsNone(self.learning_unit_one.quiz) self.assertIsNone(self.learning_unit_two.lesson) self.assertTrue(self.learning_unit_one.check_prerequisite) -- cgit From 8835afbc95c1a398cf62da7880596e7a302ae148 Mon Sep 17 00:00:00 2001 From: manojvaghela Date: Wed, 18 Mar 2020 16:36:52 +0530 Subject: Search bar added --- yaksh/forms.py | 19 +++++++++ yaksh/templates/yaksh/courses.html | 28 +++++++++++- yaksh/templates/yaksh/lessons.html | 30 ++++++++++++- yaksh/templates/yaksh/modules.html | 30 ++++++++++++- yaksh/templates/yaksh/quizzes.html | 30 ++++++++++++- yaksh/views.py | 87 +++++++++++++++++++++++++++++++++++--- 6 files changed, 212 insertions(+), 12 deletions(-) diff --git a/yaksh/forms.py b/yaksh/forms.py index 7d5362b..c0f40ea 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -49,6 +49,12 @@ test_case_types = ( ("floattestcase", "Float Testcase"), ) +status_types = ( + ('select','Select Status'), + ('active', 'Active'), + ('closed', 'Inactive'), + ) + UNAME_CHARS = letters + "._" + digits PWD_CHARS = letters + punctuation + digits @@ -377,6 +383,19 @@ class QuestionFilterForm(forms.Form): ) +class SearchFilterForm(forms.Form): + search_tags = forms.CharField( + label='Search Tags', + widget=forms.TextInput(attrs={'placeholder': 'Search', + 'class': form_input_class,}), + required=False + ) + search_status = forms.CharField(max_length=16, widget=forms.Select( + choices=status_types, + attrs={'class': 'custom-select'}), + ) + + class CourseForm(forms.ModelForm): """ course form for moderators """ class Meta: diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html index 0eb9a38..cc4c69f 100644 --- a/yaksh/templates/yaksh/courses.html +++ b/yaksh/templates/yaksh/courses.html @@ -61,6 +61,32 @@ {% else %}
+
+ {% csrf_token %} +
+
+

Search/Filter Courses

+
+
+
+
+ {{ form.search_tags }} +
+
+ {{ form.search_status }} +
+
+
+ + +
+
+ +
+

{{ courses_found }} Course(s) Available

+ {% if messages %} {% for message in messages %}
@@ -198,7 +224,7 @@
{% for course in courses %}
-
+
{{course.name}} diff --git a/yaksh/templates/yaksh/lessons.html b/yaksh/templates/yaksh/lessons.html index 0146a6b..c806d26 100644 --- a/yaksh/templates/yaksh/lessons.html +++ b/yaksh/templates/yaksh/lessons.html @@ -45,10 +45,36 @@ {% if not lessons %}

-

No new Lessons added

+

No lessons found

{% else %} -

Lessons

+
+
+ {% csrf_token %} +
+
+

Search/Filter Lessons

+
+
+
+
+ {{ form.search_tags }} +
+
+ {{ form.search_status }} +
+
+
+ + +
+
+ +
+

{{ lessons_found }} Lesson(s) Available

+
diff --git a/yaksh/templates/yaksh/modules.html b/yaksh/templates/yaksh/modules.html index 4fafbf1..610960c 100644 --- a/yaksh/templates/yaksh/modules.html +++ b/yaksh/templates/yaksh/modules.html @@ -46,10 +46,36 @@ {% if not modules %}

-

No new learning modules added

+

No learning modules found

{% else %} -

Learning Modules

+
+ + {% csrf_token %} +
+
+

Search/Filter Learning Modules

+
+
+
+
+ {{ form.search_tags }} +
+
+ {{ form.search_status }} +
+
+
+ + +
+
+ +
+

{{ modules_found }} Learning Module(s) Available

+
Sr.No
diff --git a/yaksh/templates/yaksh/quizzes.html b/yaksh/templates/yaksh/quizzes.html index 7e53d31..7532940 100644 --- a/yaksh/templates/yaksh/quizzes.html +++ b/yaksh/templates/yaksh/quizzes.html @@ -49,10 +49,36 @@ {% if not quizzes %}

-

No new Quiz added

+

No quizzes found

{% else %} -

Quizzes

+
+ + {% csrf_token %} +
+
+

Search/Filter Quizzes

+
+
+
+
+ {{ form.search_tags }} +
+
+ {{ form.search_status }} +
+
+
+ + +
+
+ +
+

{{ quizzes_found }} Quiz(zes) Available

+
Sr.No
diff --git a/yaksh/views.py b/yaksh/views.py index 58fe6e2..cf7f3b4 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -44,7 +44,8 @@ from yaksh.forms import ( UserRegisterForm, UserLoginForm, QuizForm, QuestionForm, QuestionFilterForm, CourseForm, ProfileForm, UploadFileForm, FileForm, QuestionPaperForm, LessonForm, - LessonFileForm, LearningModuleForm, ExerciseForm, TestcaseForm + LessonFileForm, LearningModuleForm, ExerciseForm, TestcaseForm, + SearchFilterForm ) from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME from .settings import URL_ROOT @@ -1046,6 +1047,23 @@ def courses(request): courses = Course.objects.filter( Q(creator=user) | Q(teachers=user), is_trial=False).order_by('-active').distinct() + + form = SearchFilterForm() + + if request.method == 'POST': + course_tags = request.POST.get('search_tags') + course_status = request.POST.get('search_status') + + if course_status == 'select' : + courses = courses.filter( + name__contains=course_tags) + elif course_status == 'active' : + courses = courses.filter( + name__contains=course_tags, active=True) + elif course_status == 'closed': + courses = courses.filter( + name__contains=course_tags, active=False) + paginator = Paginator(courses, 30) page = request.GET.get('page') try: @@ -1056,7 +1074,9 @@ def courses(request): except EmptyPage: # If page is out of range (e.g. 9999), deliver last page of results. courses = paginator.page(paginator.num_pages) - context = {'objects': courses, 'created': True} + courses_found = courses.object_list.count() + context = {'objects': courses, 'created': True, + 'form': form, 'courses_found': courses_found} return my_render_to_response(request, 'yaksh/courses.html', context) @@ -2729,7 +2749,26 @@ def show_all_quizzes(request): if not is_moderator(user): raise Http404('You are not allowed to view this page!') quizzes = Quiz.objects.filter(creator=user, is_trial=False) - context = {"quizzes": quizzes} + + form = SearchFilterForm() + + if request.method == 'POST': + quiz_tags = request.POST.get('search_tags') + quiz_status = request.POST.get('search_status') + + if quiz_status == 'select' : + quizzes = quizzes.filter( + description__contains=quiz_tags) + elif quiz_status == 'active' : + quizzes = quizzes.filter( + description__contains=quiz_tags, active=True) + elif quiz_status == 'closed': + quizzes = quizzes.filter( + description__contains=quiz_tags, active=False) + quizzes_found = quizzes.count() + + context = {"quizzes": quizzes, "form": form, + "quizzes_found": quizzes_found} return my_render_to_response(request, 'yaksh/quizzes.html', context) @@ -2740,7 +2779,26 @@ def show_all_lessons(request): if not is_moderator(user): raise Http404('You are not allowed to view this page!') lessons = Lesson.objects.filter(creator=user) - context = {"lessons": lessons} + + form = SearchFilterForm() + + if request.method == 'POST': + lesson_tags = request.POST.get('search_tags') + lesson_status = request.POST.get('search_status') + + if lesson_status == 'select' : + lessons = lessons.filter( + description__contains=lesson_tags) + elif lesson_status == 'active' : + lessons = lessons.filter( + description__contains=lesson_tags, active=True) + elif lesson_status == 'closed': + lessons = lessons.filter( + description__contains=lesson_tags, active=False) + lessons_found = lessons.count() + + context = {"lessons": lessons, "form": form, + "lessons_found": lessons_found} return my_render_to_response(request, 'yaksh/lessons.html', context) @@ -2752,7 +2810,26 @@ def show_all_modules(request): raise Http404('You are not allowed to view this page!') learning_modules = LearningModule.objects.filter( creator=user, is_trial=False) - context = {"modules": learning_modules} + + form = SearchFilterForm() + + if request.method == 'POST': + module_tags = request.POST.get('search_tags') + module_status = request.POST.get('search_status') + + if module_status == 'select' : + learning_modules = learning_modules.filter( + name__contains=module_tags) + elif module_status == 'active' : + learning_modules = learning_modules.filter( + name__contains=module_tags, active=True) + elif module_status == 'closed': + learning_modules = learning_modules.filter( + name__contains=module_tags, active=False) + learning_modules_found = learning_modules.count() + + context = {"modules": learning_modules, "form": form, + "modules_found": learning_modules_found} return my_render_to_response( request, 'yaksh/modules.html', context ) -- cgit From 87ee03b7dc137f769f1a9dec708bb3194294d4b7 Mon Sep 17 00:00:00 2001 From: manojvaghela Date: Wed, 18 Mar 2020 17:06:16 +0530 Subject: Search bar - related testing views added --- yaksh/test_views.py | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/yaksh/test_views.py b/yaksh/test_views.py index 1c7c150..61ed3eb 100644 --- a/yaksh/test_views.py +++ b/yaksh/test_views.py @@ -2242,6 +2242,154 @@ class TestCourses(TestCase): self.user1_course.learning_module.remove(self.learning_module1) +class TestSearchFilters(TestCase): + def setUp(self): + self.client = Client() + + # Create moderator group + self.mod_group = Group.objects.create(name="moderator") + + #Create user1 with profile + self.user1_plaintext_pass = "demo1" + self.user1 = User.objects.create_user( + username='demo_user1', + password=self.user1_plaintext_pass, + first_name='user1_first_name', + last_name='user1_last_name', + email='demo1@test.com' + ) + Profile.objects.create( + user=self.user1, + roll_number=10, + institute="IIT", + department="Chemical", + position="moderator", + timezone="UTC", + is_moderator=True + ) + + # Add user1 to moderator group + self.mod_group.user_set.add(self.user1) + + # Create courses for user1 + self.user1_course1 = Course.objects.create( + name="Demo Course", + enrollment="Enroll Request", creator=self.user1) + self.user1_course2 = Course.objects.create( + name="Test Course", + enrollment="Enroll Request", creator=self.user1) + + # Create learning modules for user1 + self.learning_module1 = LearningModule.objects.create( + order=0, name="Demo Module", description="Demo Module", + check_prerequisite=False, creator=self.user1) + self.learning_module2 = LearningModule.objects.create( + order=0, name="Test Module", description="Test Module", + check_prerequisite=False, creator=self.user1) + + # Create quizzes for user1 + self.quiz1 = Quiz.objects.create( + time_between_attempts=0, description='Demo Quiz', + creator=self.user1) + self.question_paper1 = QuestionPaper.objects.create( + quiz=self.quiz1, total_marks=1.0) + + self.quiz2 = Quiz.objects.create( + time_between_attempts=0, description='Test Quiz', + creator=self.user1) + self.question_paper2 = QuestionPaper.objects.create( + quiz=self.quiz2, total_marks=1.0) + + # Create lessons for user1 + self.lesson1 = Lesson.objects.create( + name="Demo Lesson", description="Demo Lession", + creator=self.user1) + self.lesson2 = Lesson.objects.create( + name="Test Lesson", description="Test Lesson", + creator=self.user1) + + # Create units for lesson and quiz + self.lesson_unit1 = LearningUnit.objects.create( + order=1, type="lesson", lesson=self.lesson1) + self.lesson_unit2 = LearningUnit.objects.create( + order=1, type="lesson", lesson=self.lesson2) + self.quiz_unit1 = LearningUnit.objects.create( + order=2, type="quiz", quiz=self.quiz1) + self.quiz_unit2 = LearningUnit.objects.create( + order=2, type="quiz", quiz=self.quiz2) + + # Add units to module + self.learning_module1.learning_unit.add(self.lesson_unit1) + self.learning_module1.learning_unit.add(self.quiz_unit1) + self.learning_module2.learning_unit.add(self.lesson_unit2) + self.learning_module2.learning_unit.add(self.quiz_unit2) + + def tearDown(self): + self.client.logout() + self.user1.delete() + self.mod_group.delete() + + def test_courses_search_filter(self): + """ Test to check if courses are obtained with tags and status """ + self.client.login( + username=self.user1.username, + password=self.user1_plaintext_pass + ) + response = self.client.post( + reverse('yaksh:courses'), + data={'course_tags': 'demo', 'course_status': 'active'} + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/courses.html') + self.assertIsNotNone(response.context['form']) + self.assertIn(self.user1_course1, response.context['courses']) + + def test_quizzes_search_filter(self): + """ Test to check if quizzes are obtained with tags and status """ + self.client.login( + username=self.user1.username, + password=self.user1_plaintext_pass + ) + response = self.client.post( + reverse('yaksh:show_all_quizzes'), + data={'quiz_tags': 'demo', 'quiz_status': 'active'} + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/quizzes.html') + self.assertIsNotNone(response.context['form']) + self.assertIn(self.quiz1, response.context['quizzes']) + + def test_lessons_search_filter(self): + """ Test to check if lessons are obtained with tags and status """ + self.client.login( + username=self.user1.username, + password=self.user1_plaintext_pass + ) + response = self.client.post( + reverse('yaksh:show_all_lessons'), + data={'lesson_tags': 'demo', 'lesson_status': 'active'} + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/lessons.html') + self.assertIsNotNone(response.context['form']) + self.assertIn(self.lesson1, response.context['lessons']) + + def test_learning_modules_search_filter(self): + """ Test to check if learning modules are obtained with tags and status """ + self.client.login( + username=self.user1.username, + password=self.user1_plaintext_pass + ) + response = self.client.post( + reverse('yaksh:show_all_modules'), + data={'module_tags': 'demo', 'module_status': 'active'} + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/modules.html') + self.assertIsNotNone(response.context['form']) + self.assertIn(self.learning_module1, response.context['modules']) + + class TestAddCourse(TestCase): def setUp(self): self.client = Client() -- cgit From d642d4906801df836ecefa92544786f07b08dc77 Mon Sep 17 00:00:00 2001 From: manojvaghela Date: Thu, 19 Mar 2020 14:51:02 +0530 Subject: button tag changed to attribute tag --- yaksh/templates/yaksh/courses.html | 4 ++-- yaksh/templates/yaksh/lessons.html | 4 ++-- yaksh/templates/yaksh/modules.html | 4 ++-- yaksh/templates/yaksh/quizzes.html | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html index cc4c69f..c890b86 100644 --- a/yaksh/templates/yaksh/courses.html +++ b/yaksh/templates/yaksh/courses.html @@ -78,9 +78,9 @@
- + diff --git a/yaksh/templates/yaksh/lessons.html b/yaksh/templates/yaksh/lessons.html index c806d26..d75e3e0 100644 --- a/yaksh/templates/yaksh/lessons.html +++ b/yaksh/templates/yaksh/lessons.html @@ -66,9 +66,9 @@
- + diff --git a/yaksh/templates/yaksh/modules.html b/yaksh/templates/yaksh/modules.html index 610960c..f98be9d 100644 --- a/yaksh/templates/yaksh/modules.html +++ b/yaksh/templates/yaksh/modules.html @@ -67,9 +67,9 @@
- + diff --git a/yaksh/templates/yaksh/quizzes.html b/yaksh/templates/yaksh/quizzes.html index 7532940..d374404 100644 --- a/yaksh/templates/yaksh/quizzes.html +++ b/yaksh/templates/yaksh/quizzes.html @@ -70,9 +70,9 @@
- + -- cgit
Sr.No