diff --git a/notification/__init__.py b/notification/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notification/admin.py b/notification/admin.py new file mode 100644 index 000000000..9405e58f0 --- /dev/null +++ b/notification/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from .models import Subscription + + +admin.site.register(Subscription) diff --git a/notification/apps.py b/notification/apps.py new file mode 100644 index 000000000..40b3eb9cd --- /dev/null +++ b/notification/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NotificationConfig(AppConfig): + name = 'notification' diff --git a/notification/models.py b/notification/models.py new file mode 100644 index 000000000..5a67ff314 --- /dev/null +++ b/notification/models.py @@ -0,0 +1,26 @@ +from django.db import models +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey + + +class Subscription(models.Model): + user = models.ForeignKey(User, + related_name='subscription', + db_index=True, + on_delete=models.CASCADE) + subscribe = models.BooleanField(default=True) + target_ct = models.ForeignKey(ContentType, + blank=True, + null=True, + related_name='target_obj', + on_delete=models.CASCADE + ) + target_id = models.PositiveIntegerField(null=True, + blank=True, + db_index=True) + target = GenericForeignKey('target_ct', 'target_id') + created = models.DateTimeField(auto_now_add=True, db_index=True) + + class Meta: + ordering = ('-created', ) diff --git a/notification/tasks.py b/notification/tasks.py new file mode 100644 index 000000000..91bbac201 --- /dev/null +++ b/notification/tasks.py @@ -0,0 +1,139 @@ +from textwrap import dedent +from collections import OrderedDict + +from django.utils import timezone +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User +from django.conf import settings + +from celery import task +from celery import shared_task + +from notifications_plugin.models import NotificationMessage, Notification + +from yaksh.models import Course, Quiz, QuestionPaper, AnswerPaper, Post +from .models import Subscription + + +@task(name='course_deadline_task') +def course_deadline_task(): + courses = Course.objects.filter(active=True, is_trial=False) + for course in courses: + if course.is_active_enrollment(): + message = dedent(""" + The deadline for the course {0} is {1}, please complete + the course if not completed before the deadline. + """.format(course.name, course.end_enroll_time) + ) + creator = course.creator + student_ids = course.students.values_list('id', flat=True) + if student_ids: + notification_type = "warning" + nm = NotificationMessage.objects.add_single_message( + creator_id=creator.id, summary='Course Notification', + description=message, msg_type=notification_type + ) + Notification.objects.add_bulk_user_notifications( + receiver_ids=student_ids, msg_id=nm.id + ) + + +@task(name='quiz_deadline_task') +def quiz_deadline_task(): + courses = Course.objects.filter(active=True, is_trial=False) + for course in courses: + student_ids = course.students.values_list('id', flat=True) + creator = course.creator + quizzes = course.get_quizzes() + if student_ids: + for quiz in quizzes: + if not quiz.is_expired(): + message = dedent(""" + The deadline for the quiz {0} of course {1} is + {2}, please complete the quiz if not completed + before the deadline. + """.format( + quiz.description, course.name, quiz.end_date_time + ) + ) + notification_type = 'warning' + nm = NotificationMessage.objects.add_single_message( + creator_id=creator.id, summary='Quiz Notification', + description=message, msg_type=notification_type + ) + Notification.objects.add_bulk_user_notifications( + receiver_ids=student_ids, msg_id=nm.id + ) + + +@task(name='course_quiz_deadline_mail_task') +def course_quiz_deadline_mail_task(): + ct = ContentType.objects.get_for_model(Course) + subs = Subscription.objects.filter(target_ct=ct, subscribe=True) + if subs.exists(): + for sub in subs: + course = sub.target + if course.is_active_enrollment(): + user = sub.user + data = course.get_quizzes_digest(user) + subject = 'Quiz deadline notification for course {0}'.format( + course.name + ) + msg_html = render_to_string('notification/email.html', { + 'data': data + }) + msg_plain = render_to_string('notification/email.txt', { + 'data': data + }) + send_mail( + subject, msg_plain, + settings.SENDER_EMAIL, [user.email], + html_message=msg_html + ) + + +@shared_task +def notify_teachers(data): + teacher_ids = data.get("teacher_ids") + new_post_id = data.get("new_post_id") + course_id = data.get("course_id") + student_id = data.get("student_id") + course = Course.objects.get(id=course_id) + creator = course.creator + post = Post.objects.get(id=new_post_id) + student = User.objects.get(id=student_id) + teacher_ids = User.objects.filter(id__in=teacher_ids).values_list('id', flat=True) + message = '{0} has asked a question in forum on course {1}'.format( + student.get_full_name(), course.name + ) + notification_type = 'success' + nm = NotificationMessage.objects.add_single_message( + creator_id=creator.id, summary='Forum Notification', + description=message, msg_type=notification_type + ) + Notification.objects.add_bulk_user_notifications( + receiver_ids=teacher_ids, msg_id=nm.id + ) + + +@shared_task +def notify_post_creator(data): + course_id = data.get("course_id") + post_id = data.get("post_id") + course = Course.objects.get(id=course_id) + course_creator = course.creator + post = Post.objects.get(id=post_id) + post_creator = post.creator + message = 'You received comment on your post {0} in course {1}'.format( + post.title, course.name + ) + notification_type = 'success' + nm = NotificationMessage.objects.add_single_message( + creator_id=course_creator.id, summary='Forum Notification', + description=message, msg_type=notification_type + ) + Notification.objects.add_single_notification( + receiver_id=post_creator.id, msg_id=nm.id + ) \ No newline at end of file diff --git a/notification/templates/notification/email.html b/notification/templates/notification/email.html new file mode 100644 index 000000000..a37ac908c --- /dev/null +++ b/notification/templates/notification/email.html @@ -0,0 +1,54 @@ + + + + + + + + {% with data.0 as data %} +

Course:{{data.course.name}}

+

The deadline for the course is {{data.course.end_enroll_time}}

+

Below is the table of the quizzes for the course {{data.course.name}}. Please + make sure to complete these quizzes before the deadline.

+ {% endwith %} + + + + + + + + + + {% for quiz in data %} + + + + + + {% endfor %} + +
QuizDeadlineCompleted
{{quiz.quiz.description}}{{quiz.quiz.end_date_time|date:'Y-M-D h:i A'}} + {% if quiz.ap.exists %} + Complete + {% else %} + Incomplete + {% endif %} +
+ + \ No newline at end of file diff --git a/notification/templates/notification/email.txt b/notification/templates/notification/email.txt new file mode 100644 index 000000000..a37ac908c --- /dev/null +++ b/notification/templates/notification/email.txt @@ -0,0 +1,54 @@ + + + + + + + + {% with data.0 as data %} +

Course:{{data.course.name}}

+

The deadline for the course is {{data.course.end_enroll_time}}

+

Below is the table of the quizzes for the course {{data.course.name}}. Please + make sure to complete these quizzes before the deadline.

+ {% endwith %} + + + + + + + + + + {% for quiz in data %} + + + + + + {% endfor %} + +
QuizDeadlineCompleted
{{quiz.quiz.description}}{{quiz.quiz.end_date_time|date:'Y-M-D h:i A'}} + {% if quiz.ap.exists %} + Complete + {% else %} + Incomplete + {% endif %} +
+ + \ No newline at end of file diff --git a/notification/tests.py b/notification/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/notification/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/notification/urls.py b/notification/urls.py new file mode 100644 index 000000000..8c4c671ec --- /dev/null +++ b/notification/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url + +from . import views + +app_name = 'notification' + +urlpatterns = [ + url(r'(?P\d+)/$', + views.toggle_subscription_status, name="toggle_subscription_status"), +] diff --git a/notification/views.py b/notification/views.py new file mode 100644 index 000000000..780f7936b --- /dev/null +++ b/notification/views.py @@ -0,0 +1,30 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.contenttypes.models import ContentType +from django.contrib import messages + + +from yaksh.models import Course +from .models import Subscription + + +def toggle_subscription_status(request, course_id): + user = request.user + 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') + + ct = ContentType.objects.get_for_model(course) + course_sub = Subscription.objects.get( + target_ct=ct, user=user, target_id=course.id + ) + + if course_sub.subscribe: + course_sub.subscribe = False + message = 'Unsubscribed from the course mail letter.' + else: + course_sub.subscribe = True + message = 'Subscribed to the course mail letter.' + course_sub.save() + messages.info(request, message) + + return redirect('yaksh:course_modules', course_id) diff --git a/online_test/settings.py b/online_test/settings.py index 3b89c28bf..cf731febb 100644 --- a/online_test/settings.py +++ b/online_test/settings.py @@ -10,6 +10,7 @@ from yaksh.pipeline.settings import AUTH_PIPELINE import os from decouple import config +from celery.schedules import crontab # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -51,7 +52,8 @@ 'rest_framework', 'api', 'corsheaders', - 'rest_framework.authtoken' + 'rest_framework.authtoken', + 'notification' ) MIDDLEWARE = ( @@ -229,6 +231,30 @@ CELERY_BROKER_URL = 'redis://localhost' CELERY_RESULT_BACKEND = 'django-db' +CELERY_BEAT_SCHEDULE = { + 'send-course-deadline-notifications-twice-a-week': { + 'task': 'course_deadline_task', + 'schedule': crontab( + hour='08', minute=30, day_of_week='*/3', day_of_month='*', + month_of_year='*' + ), + }, + 'send-quiz-deadline-notifications-twice-a-week': { + 'task': 'quiz_deadline_task', + 'schedule': crontab( + hour='09', minute=00, day_of_week='*/3', day_of_month='*', + month_of_year='*' + ), + }, + 'send_course_quiz_deadline_mail': { + 'task': 'course_quiz_deadline_mail_task', + 'schedule': crontab( + hour='09', minute=31, day_of_week='*/3', day_of_month='*', + month_of_year='*' + ) + } +} + REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. diff --git a/online_test/urls.py b/online_test/urls.py index bb5a04ab4..7b9802c23 100644 --- a/online_test/urls.py +++ b/online_test/urls.py @@ -17,6 +17,6 @@ url(r'^', include('social_django.urls', namespace='social')), url(r'^grades/', include(('grades.urls', 'grades'))), url(r'^api/', include('api.urls', namespace='api')), - + url(r'^subscribe/', include('notification.urls', namespace='notification')) ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/yaksh/models.py b/yaksh/models.py index 7d4dd9817..3fb67d940 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -11,6 +11,7 @@ from django.db import models from django.contrib.auth.models import User, Group, Permission +from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.contrib.contenttypes.models import ContentType from taggit.managers import TaggableManager @@ -41,6 +42,8 @@ from django.forms.models import model_to_dict from grades.models import GradingSystem +from notification.models import Subscription + languages = ( ("python", "Python"), ("bash", "Bash"), @@ -896,6 +899,7 @@ class Course(models.Model): view_grade = models.BooleanField(default=False) learning_module = models.ManyToManyField(LearningModule, related_name='learning_module') + subscription = GenericRelation(Subscription, related_query_name='courses') # The start date of the course enrollment. start_enroll_time = models.DateTimeField( @@ -1057,6 +1061,19 @@ def get_quizzes(self): quiz_list.extend(module.get_quiz_units()) return quiz_list + def get_quizzes_digest(self, user): + quizzes = self.get_quizzes() + data = [] + for quiz in quizzes: + quiz_data = {} + qp = quiz.questionpaper_set.get() + ap = qp.answerpaper_set.filter(course__id=self.id, user=user) + quiz_data['quiz'] = quiz + quiz_data['course'] = self + quiz_data['user'] = user + data.append(quiz_data) + return data + def get_quiz_details(self): return [(quiz, quiz.get_total_students(self), quiz.get_passed_students(self), diff --git a/yaksh/templates/user.html b/yaksh/templates/user.html index 4e3974b02..31d624f61 100644 --- a/yaksh/templates/user.html +++ b/yaksh/templates/user.html @@ -15,6 +15,16 @@