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 %}
+
+
+
+ Quiz
+ Deadline
+ Completed
+
+
+
+ {% for quiz in data %}
+
+ {{quiz.quiz.description}}
+ {{quiz.quiz.end_date_time|date:'Y-M-D h:i A'}}
+
+ {% if quiz.ap.exists %}
+ Complete
+ {% else %}
+ Incomplete
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+
\ 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 %}
+
+
+
+ Quiz
+ Deadline
+ Completed
+
+
+
+ {% for quiz in data %}
+
+ {{quiz.quiz.description}}
+ {{quiz.quiz.end_date_time|date:'Y-M-D h:i A'}}
+
+ {% if quiz.ap.exists %}
+ Complete
+ {% else %}
+ Incomplete
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+
\ 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 @@
Home
+
+
+ Notifications
+ {% if request.custom_notifications > 0 %}
+
+ {{request.custom_notifications}}
+
+ {% endif %}
+
+
{{user.get_full_name|title}}
diff --git a/yaksh/templates/yaksh/course_modules.html b/yaksh/templates/yaksh/course_modules.html
index b80856224..52f20804f 100644
--- a/yaksh/templates/yaksh/course_modules.html
+++ b/yaksh/templates/yaksh/course_modules.html
@@ -4,10 +4,38 @@
{% block main %}
+
+ {% if messages %}
+ {% for message in messages %}
+
+
+
+
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+
{% if course.view_grade %}
diff --git a/yaksh/views.py b/yaksh/views.py
index e4a9038e2..7f0ccd241 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -10,6 +10,7 @@
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Group
+from django.contrib.contenttypes.models import ContentType
from django.forms.models import inlineformset_factory
from django.forms import fields
from django.utils import timezone
@@ -46,6 +47,8 @@
LessonFileForm, LearningModuleForm, ExerciseForm, TestcaseForm,
SearchFilterForm, PostForm, CommentForm
)
+from notification.models import Subscription
+from notification.tasks import notify_teachers, notify_post_creator
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
from .settings import URL_ROOT
from .file_utils import extract_files, is_csv
@@ -3057,6 +3060,11 @@ def course_modules(request, course_id, msg=None):
if not course_status.grade:
course_status.set_grade()
context['grade'] = course_status.get_grade()
+ ct = ContentType.objects.get_for_model(course)
+ course_sub = Subscription.objects.get(
+ target_ct=ct, user=user, target_id=course.id
+ )
+ context['course_sub'] = course_sub
return my_render_to_response(request, 'yaksh/course_modules.html', context)
@@ -3331,7 +3339,7 @@ def course_forum(request, course_id):
else:
posts = course.post.get_queryset().filter(
active=True).order_by('-modified_at')
- paginator = Paginator(posts, 1)
+ paginator = Paginator(posts, 10)
page = request.GET.get('page')
posts = paginator.get_page(page)
if request.method == "POST":
@@ -3341,6 +3349,18 @@ def course_forum(request, course_id):
new_post.creator = user
new_post.course = course
new_post.save()
+ teacher_ids = list(course.get_teachers().values_list(
+ 'id', flat=True
+ )
+ )
+ teacher_ids.append(course.creator.id) # append course creator id
+ data = {
+ "teacher_ids": teacher_ids,
+ "new_post_id": new_post.id,
+ "course_id": course.id,
+ "student_id": user.id
+ }
+ notify_teachers.delay(data)
return redirect('yaksh:post_comments',
course_id=course.id, uuid=new_post.uid)
else:
@@ -3364,6 +3384,7 @@ def post_comments(request, course_id, uuid):
if is_moderator(user):
base_template = 'manage.html'
post = get_object_or_404(Post, uid=uuid)
+ post_creator = post.creator
comments = post.comment.filter(active=True)
course = get_object_or_404(Course, id=course_id)
if (not course.is_creator(user) and not course.is_teacher(user)
@@ -3374,9 +3395,14 @@ def post_comments(request, course_id, uuid):
form = CommentForm(request.POST, request.FILES)
if form.is_valid():
new_comment = form.save(commit=False)
- new_comment.creator = request.user
+ new_comment.creator = user
new_comment.post_field = post
new_comment.save()
+ data = {
+ "course_id": course.id,
+ "post_id": post.id
+ }
+ notify_post_creator.delay(data)
return redirect(request.path_info)
return render(request, 'yaksh/post_comments.html', {
'post': post,