diff --git a/cms/cache_registry.py b/cms/cache_registry.py
new file mode 100644
index 000000000..5d11a3e75
--- /dev/null
+++ b/cms/cache_registry.py
@@ -0,0 +1,17 @@
+from django.core.cache import cache
+
+REGISTRY_KEY = "_cache_key_registry"
+
+def register_cache_key(key):
+ keys = cache.get(REGISTRY_KEY) or set()
+ keys.add(key)
+ cache.set(REGISTRY_KEY, keys, None)
+
+def unregister_cache_key(key):
+ keys = cache.get(REGISTRY_KEY) or set()
+ if key in keys:
+ keys.remove(key)
+ cache.set(REGISTRY_KEY, keys, None)
+
+def list_cache_keys():
+ return sorted(cache.get(REGISTRY_KEY) or [])
diff --git a/cms/views.py b/cms/views.py
index 3ae09ebb7..0ceee6768 100644
--- a/cms/views.py
+++ b/cms/views.py
@@ -4,6 +4,11 @@
import random
import string
+from django.contrib.auth.models import User
+
+from cms.models import Profile
+from cms.forms import ChangePasswordForm
+
# Third Party Stuff
from django.conf import settings
from django.contrib import messages, auth
@@ -26,7 +31,7 @@
from mdldjango.models import MdlUser
from mdldjango.urls import *
from django.template.context_processors import csrf
-
+from cms.cache_registry import unregister_cache_key,list_cache_keys
from donate.models import Payee
from django.core.cache import cache
@@ -270,11 +275,13 @@ def account_logout(request):
@login_required
def account_profile(request, username):
user = request.user
- try:
- profile = Profile.objects.get(user_id=user.id)
- except:
- profile = create_profile(user)
+
+ profile = Profile.objects.filter(user_id=user.id).order_by('id').first()
+ if not profile:
+ profile = create_profile(user)
+
old_file_path = settings.MEDIA_ROOT + str(profile.picture)
+
new_file_path = None
if request.method == 'POST':
form = ProfileForm(user, request.POST, request.FILES, instance = profile)
@@ -319,7 +326,8 @@ def account_profile(request, username):
else:
context = {}
context.update(csrf(request))
- instance = Profile.objects.get(user_id=user.id)
+ # instance = Profile.objects.get(user_id=user.id)
+ instance = Profile.objects.filter(user_id=user.id).first()
context['form'] = ProfileForm(user, instance = instance)
return render(request, 'cms/templates/profile.html', context)
@@ -329,13 +337,10 @@ def account_view_profile(request, username):
raise PermissionDenied('You are not allowed to view this page!')
user = User.objects.get(username = username)
- profile = None
- try:
- profile = Profile.objects.get(user = user)
- except:
- profile = create_profile(user)
+ profile = Profile.objects.filter(user=user).first()
+ if not profile:
+ profile = create_profile(user)
-
context = {
'profile' : profile,
'media_url' : settings.MEDIA_URL,
@@ -425,45 +430,62 @@ def password_reset(request):
return render(request, 'cms/templates/password_reset.html', context)
-#@login_required
def change_password(request):
- # chacking uselogin
- pcode = request.GET.get('auto', False)
- username = request.GET.get('username', False)
- nextUrl = request.GET.get('next', False)
+ pcode = request.GET.get('auto')
+ username = request.GET.get('username')
+ nextUrl = request.GET.get('next')
- # check pcode in profile page
- if pcode and username and nextUrl:
- user = User.objects.get(username=username)
- profile = Profile.objects.get(user=user)
- if profile.confirmation_code == pcode:
- user.backend='django.contrib.auth.backends.ModelBackend'
- login(request,user)
+ profile = None
+
+ if pcode and username:
+ user = User.objects.get(username=username).first()
+ if user:
+ profile = Profile.objects.filter(user=user,confirmation_code=pcode).first()
+
+ if profile:
+ user.backend = 'django.contrib.auth.backends.ModelBackend'
+ login(request, user)
if request.user.is_anonymous():
- return HttpResponseRedirect('/accounts/login/?next=/accounts/change-password')
+ return HttpResponseRedirect('/accounts/login/?next=/accounts/change-password/')
+
+ if not profile:
+ profile = (Profile.objects.filter(user=request.user).order_by('id').first())
+
+ if not profile:
+ messages.error(request, "Profile not found.")
+ return HttpResponseRedirect('/accounts/login/')
- context = {}
form = ChangePasswordForm()
+
if request.method == "POST":
form = ChangePasswordForm(request.POST)
if form.is_valid():
- profile = Profile.objects.get(user_id = form.cleaned_data['userid'], confirmation_code = form.cleaned_data['code'])
+ profile = Profile.objects.get(user_id=form.cleaned_data['userid'],confirmation_code=form.cleaned_data['code'])
+
user = profile.user
user.set_password(form.cleaned_data['new_password'])
user.save()
- # change if any mdl user pass too
+ # change if any mdl user pass too
from mdldjango.views import changeMdlUserPass
changeMdlUserPass(user.email, form.cleaned_data['new_password'])
if nextUrl:
return HttpResponseRedirect(nextUrl.split("?", 1)[0])
- messages.success(request, "Your account password has been updated successfully!")
+ messages.success(request,"Your account password has been updated successfully!")
return HttpResponseRedirect("/accounts/view-profile/" + user.username)
- context['form'] = form
+ if profile is None:
+ profile = request.user.profile_set.first()
+ context = {
+ 'form': form,
+ 'profile': profile,
+ 'nextUrl': nextUrl,
+ }
context.update(csrf(request))
- return render(request, 'cms/templates/change_password.html', context)
+
+ return render(request,'cms/templates/change_password.html',context)
+
def confirm_student(request, token):
@@ -511,7 +533,7 @@ def verify_email(request):
@login_required
def manage_cache(request):
- if not request.user.groups.filter(name='Technical-Team').exists():
+ if not request.user.groups.filter(name__in=['Technical-Team', 'Administrator']).exists():
raise PermissionDenied('You are not allowed to view this page!')
context = {}
@@ -535,6 +557,39 @@ def manage_cache(request):
messages.success(request, f"memcache cleared successfully")
except Exception as e:
messages.error(request, f"An error occurred while clearing cache: {e}")
- print(f"cache error -- {e}")
+
+ elif deletion_type == 'homepage':
+ try:
+ all_keys = list_cache_keys()
+ homepage_keys = [
+ key for key in all_keys
+ if key.startswith((
+ 'tutorial_search_foss:',
+ 'tutorial_search_lang:',
+ ))
+ ]
+
+ if homepage_keys:
+ for key in homepage_keys:
+ cache.delete(key)
+ unregister_cache_key(key)
+ messages.success(
+ request,
+ "Homepage cache cleared successfully.
"
+ "Deleted keys:
"
+ + "
".join(homepage_keys)
+ )
+
+ else:
+ messages.warning(
+ request,
+ "Homepage cache keys were not found or already expired."
+ )
+
+ except Exception as e:
+ messages.error(
+ request,
+ "An error occurred while clearing homepage cache: {}".format(e)
+ )
return render(request, status_template, context=context) # return to payment page site
\ No newline at end of file
diff --git a/creation/management/commands/download_course_data.py b/creation/management/commands/download_course_data.py
new file mode 100644
index 000000000..c5cc6ed46
--- /dev/null
+++ b/creation/management/commands/download_course_data.py
@@ -0,0 +1,121 @@
+from django.core.management.base import BaseCommand, CommandError
+
+from pathlib import Path
+import re
+import json
+import requests
+from creation.models import *
+
+
+class Command(BaseCommand):
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--ids",
+ type=str,
+ required=True,
+ help="Comma-separated integers, e.g. 1,2,3",
+ )
+ parser.add_argument(
+ "--out",
+ type=str,
+ default="output.json",
+ help="Output JSON file path (default: output.json)",
+ )
+
+ def handle(self, *args, **options):
+ # python manage.py my_command --ids 1,2,3,10
+ raw = options["ids"]
+ out_path = Path(options["out"])
+ try:
+ ids = [int(x.strip()) for x in raw.split(",") if x.strip() != ""]
+ for course_id in ids:
+
+ try:
+ foss = FossCategory.objects.using('stats').get(id=course_id, show_on_homepage=1)
+ foss_format = foss.foss.strip().replace(' ','+')
+ qs = TutorialResource.objects.using('stats').filter(tutorial_detail__foss_id=foss.id, status = 1)
+ languages = list(qs.distinct().order_by('language__name').values_list('language__name', flat=True))
+ categories = list(foss.category.all().values_list('name', flat=True))
+ course_data = {
+ "course_id": foss.id,
+ "course": foss.foss,
+ "course_url": f"https://spoken-tutorial.org/tutorial-search/?search_foss={foss_format}&search_language=",
+ "categories": categories,
+ "languages": languages
+ }
+ course_data['tutorials'] = []
+ tutorial_resources = qs.select_related('tutorial_detail', 'language', 'tutorial_detail__foss', 'common_content')
+ for tr in tutorial_resources:
+
+ title_formatted = tr.tutorial_detail.tutorial.strip().replace(' ', '+')
+ title_formatted_srt = tr.tutorial_detail.tutorial.strip().replace(' ', '-')
+ language = tr.language.name
+ try:
+ duration = TutorialDuration.objects.using('stats').get(tresource=tr).duration
+ except:
+ duration = None
+ tutorial_script = self.extract_text_one_paragraph(foss.id, tr.tutorial_detail.id, title_formatted_srt, language)
+ tutorial_data = {
+ "tutorial_resource_id": tr.id,
+ "tutorial_detail_id": tr.tutorial_detail.id,
+ "title": tr.tutorial_detail.tutorial.strip(),
+ "url": f"https://spoken-tutorial.org/watch/{foss_format}/{title_formatted}/{language}/",
+ "keywords": tr.common_content.keyword,
+ "outline": tr.outline,
+ "language": language,
+ "script url": f"https://script.spoken-tutorial.org/index.php/{tr.script}",
+ "tutorial_script": tutorial_script,
+ "duration": duration,
+ "level": tr.tutorial_detail.level.level
+
+ }
+ course_data['tutorials'].append(tutorial_data)
+ self.stdout.write(self.style.SUCCESS(f"Added to tutorials: {tr.tutorial_detail.tutorial} - {language}"))
+ except FossCategory.DoesNotExist:
+ self.stderr.write(self.style.ERROR(f"No foss found with id: {course_id}. show_on_homepage = 1"))
+
+ # Ensure parent folder exists
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Write JSON
+ with out_path.open("w", encoding="utf-8") as f:
+ if course_data:
+ json.dump(course_data, f, ensure_ascii=False, indent=2)
+ else:
+ self.stderr.write(self.style.ERROR(f"No data found"))
+ self.stdout.write(self.style.SUCCESS(f"Wrote JSON to: {out_path.resolve()}"))
+ except ValueError:
+ raise CommandError(f"--ids must be comma-separated integers. Got: {raw}")
+
+ if not ids:
+ raise CommandError("--ids is empty")
+
+ self.stdout.write(f"IDs: {ids}")
+
+
+ def extract_text_one_paragraph(self, course_id, tr_id,title,lang):
+ srt_path = "/Users/ankita/workspace/projects/spoken/project/spoken-website/media/videos/48/478/Introduction-to-BASH-Shell-Scripting-English.srt"
+ url = f"https://spoken-tutorial.org/media/videos/{course_id}/{tr_id}/{title}-{lang}.srt"
+ try:
+ res = requests.get(url, timeout=30)
+ res.raise_for_status() # raises error for 4xx/5xx
+ srt_text = res.text
+ except:
+ self.stderr.write(self.style.ERROR(f"Error for srt file: {url}"))
+ srt_text = ""
+ TAG_RE = re.compile(r"<[^>]+>")
+ TIME_RE = re.compile(r"^\d{2}:\d{2}:\d{2}\s*-->\s*\d{2}:\d{2}:\d{2}")
+ # lines = Path(srt_path).read_text(encoding="utf-8", errors="ignore").splitlines()
+ lines = srt_text.splitlines()
+
+ out = []
+ for line in lines:
+ s = line.strip()
+ if not s or s.isdigit() or TIME_RE.match(s):
+ continue
+ s = TAG_RE.sub("", s)
+ s = re.sub(r"\s+", " ", s).strip()
+ if s:
+ out.append(s)
+ data = " ".join(out)
+ return data
\ No newline at end of file
diff --git a/creation/management/commands/get_metadata.py b/creation/management/commands/get_metadata.py
new file mode 100644
index 000000000..6633cda7e
--- /dev/null
+++ b/creation/management/commands/get_metadata.py
@@ -0,0 +1,133 @@
+from django.core.management.base import BaseCommand, CommandError
+from django.conf import settings
+from creation.models import *
+
+from pathlib import Path
+import re
+import json
+import requests
+import os
+
+
+
+
+class Command(BaseCommand):
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--ids",
+ type=str,
+ required=False,
+ help="Comma-separated integers, e.g. 1,2,3",
+ )
+ parser.add_argument(
+ "--out",
+ type=str,
+ default="output.json",
+ help="Output JSON file path (default: output.json)",
+ )
+
+ def handle(self, *args, **options):
+ # python manage.py my_command --ids 1,2,3,10
+ raw = options["ids"]
+ out_path = Path(options["out"])
+ try:
+ if raw:
+ ids = [int(x.strip()) for x in raw.split(",") if x.strip() != ""]
+ else:
+ ids = [x.id for x in FossCategory.objects.using('stats').filter(show_on_homepage=1).only('id')]
+ courses = FossCategory.objects.using('stats').filter(id__in=ids).order_by('foss')
+ # define final data schema
+ data = {}
+ course_details = []
+ total_size = 0.0
+ total_video_size = 0.0
+ total_srt_size = 0.0
+ total_courses = 0
+ total_tutorials = 0
+
+ for course in courses:
+ qs = TutorialResource.objects.using('stats').filter(status=1, tutorial_detail__foss_id=course.id)
+ languages = qs.order_by('language__name').values_list('language__name', flat=True).distinct()
+
+ # define individual course schema
+ course_data = {
+ 'course_id': course.id,
+ 'course': course.foss,
+ 'total_tutorials': qs.count(),
+ 'total_languages': languages.count(),
+ 'languages': list(languages)
+ }
+ course_total_size = 0.0
+ course_video_size = 0.0
+ course_srt_size = 0.0
+ lang_based_details = []
+
+ for lang in languages:
+ tr_recs = qs.filter(language__name=lang)
+ fsize = 0.0
+ vsize = 0.0
+ ssize = 0.0
+ for rec in tr_recs:
+ # calculate video size
+ filepath = 'videos/{}/{}/{}'.format(course.id, rec.tutorial_detail_id, rec.video)
+ if os.path.isfile(settings.MEDIA_ROOT + filepath):
+ #language based
+ fsize += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ vsize += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ #course based
+ course_video_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ course_total_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ #total
+ total_video_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ total_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+
+
+
+ # calculate str file size
+ ptr = filepath.rfind(".")
+ filepath = filepath[:ptr] + '.srt'
+ if os.path.isfile(settings.MEDIA_ROOT + filepath):
+ #language based
+ fsize += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ ssize += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ #course based
+ course_srt_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ course_total_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ #total
+ total_srt_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+ total_size += os.path.getsize(settings.MEDIA_ROOT + filepath)
+
+ # define individual lang-course schema
+ lang_data = {
+ "lang": lang,
+ 'total_tutorials': tr_recs.count(),
+ 'size': f"{fsize / (1024*1024):.2f} MiB",
+ 'videos_size': f"{vsize / (1024*1024):.2f} MiB",
+ 'srt_size': f"{ssize / (1024*1024):.2f} MiB"
+ }
+ lang_based_details.append(lang_data)
+
+ course_data['course_total_size'] = f"{course_total_size / (1024*1024):.2f} MiB"
+ course_data['course_video_size'] = f"{course_video_size / (1024*1024):.2f} MiB"
+ course_data['course_srt_size'] = f"{course_srt_size / (1024*1024):.2f} MiB"
+ course_data['details'] = lang_based_details
+ course_details.append(course_data)
+
+
+ data['total_size'] = f"{total_size / (1024*1024*1024):.2f} GB"
+ data['total_video_size'] = f"{total_video_size / (1024*1024*1024):.2f} GB"
+ data['total_srt_size'] = f"{total_srt_size / (1024*1024*1024):.2f} GB"
+ data['courses'] = course_details
+
+
+ # Ensure parent folder exists
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Write JSON
+ with out_path.open("w", encoding="utf-8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+ self.stdout.write(self.style.SUCCESS(f"Wrote JSON to: {out_path.resolve()}"))
+ except ValueError:
+ raise CommandError(f"--ids must be comma-separated integers. Got: {raw}")
+
+ self.stdout.write(f"IDs: {ids}")
\ No newline at end of file
diff --git a/cron/tasks.py b/cron/tasks.py
index c0a0b96f6..8ccaeb22a 100644
--- a/cron/tasks.py
+++ b/cron/tasks.py
@@ -30,6 +30,16 @@
from events.models import FossMdlCourses, TestAttendance, State, City, InstituteType
from creation.models import FossCategory
+from django.db import transaction
+from training.models import TrainingAttend
+from events.models import Test, TestAttendance
+from mdldjango.helper import get_moodle_user
+from events.helpers import get_fossmdlcourse
+from django.db import close_old_connections
+# from events.views import update_events_log, update_events_notification
+
+
+
def bulk_email(taskid, *args, **kwargs):
task = AsyncCronMail.objects.get(pk=taskid)
if task.log_file.name == "":
@@ -166,4 +176,235 @@ def filter_student_grades(key=None):
def async_filter_student_grades(key):
- TOPPER_QUEUE.enqueue(filter_student_grades, key, job_id=key, job_timeout='72h')
\ No newline at end of file
+ TOPPER_QUEUE.enqueue(filter_student_grades, key, job_id=key, job_timeout='72h')
+
+
+
+def process_test_attendance(test_id, user_id, message, academic_id):
+ from events.views import (
+ update_events_log,
+ update_events_notification
+ )
+ """
+ Background task:
+ - Create TestAttendance
+ - Sync Moodle users
+ """
+ close_old_connections()
+ job = get_current_job()
+
+ print(f"\033[92m job ****** {job} \033[0m")
+ def meta_update(**updates):
+ """Update RQ job meta safely"""
+ if not job:
+ return
+ job.meta.update(updates)
+ job.save_meta()
+
+ meta_update(
+ test_id=test_id,
+ status="starting",
+ started_at=int(time.time()),
+ progress_total=0,
+ progress_processed=0,
+ progress_pct=0,
+ stats_created_attendance=0,
+ stats_skipped_existing=0,
+ stats_missing_moodle=0,
+ message="Starting attendance processing.."
+ )
+
+ try:
+ test = Test.objects.select_related('training', 'foss').get(pk=test_id)
+ except Test.DoesNotExist:
+ meta_update(status="done", message="Test not found. Exiting")
+ return
+
+ if not test.training_id:
+ meta_update(status="done", message="No training attached to test. Exiting.")
+ return
+
+ meta_update(status="running", message="Loading training attendees...")
+
+ tras = TrainingAttend.objects.select_related(
+ 'student__user',
+ 'training__training_planner'
+ ).filter(training=test.training)
+
+ fossmdlcourse = get_fossmdlcourse(
+ test.foss_id,
+ fossmdlmap_id=test.training.fossmdlmap_id
+ )
+
+ # total count for progress (1 extra query; useful for dashboard)
+ total = tras.count()
+ meta_update(progress_total=total, progress_processed=0, progress_pct=0)
+
+ existing = set(
+ TestAttendance.objects.filter(test=test)
+ .values_list("student_id", "mdluser_id")
+ )
+
+ mdluser_cache = {}
+ new_rows = []
+
+ processed = 0
+ skipped_existing = 0
+ missing_moodle = 0
+
+ # Update job meta every N rows to avoid excessive Redis writes
+ UPDATE_EVERY = 10
+ meta_update(message=f"Processing {total} attendees...")
+
+ for tra in tras.iterator():
+ user = tra.student.user
+
+ key = (
+ tra.training.training_planner.academic_id,
+ user.first_name,
+ user.last_name,
+ tra.student.gender,
+ user.email
+ )
+
+ if key not in mdluser_cache:
+ mdluser_cache[key] = get_moodle_user(*key)
+
+ mdluser = mdluser_cache[key]
+ if not mdluser:
+ missing_moodle += 1
+ processed += 1
+ # progress update
+ if (processed % UPDATE_EVERY == 0) or (processed == total):
+ pct = int((processed * 100) / total) if total else 100
+ meta_update(
+ progress_processed=processed,
+ progress_pct=pct,
+ stats_created_attendance=len(new_rows),
+ stats_skipped_existing=skipped_existing,
+ stats_missing_moodle=missing_moodle,
+ message=f"Processing... ({processed}/{total})",
+ )
+ continue
+
+ pair = (tra.student.id, mdluser.id)
+ if pair in existing:
+ skipped_existing += 1
+ processed += 1
+ if (processed % UPDATE_EVERY == 0) or (processed == total):
+ pct = int((processed * 100) / total) if total else 100
+ meta_update(
+ progress_processed=processed,
+ progress_pct=pct,
+ stats_created_attendance=len(new_rows),
+ stats_skipped_existing=skipped_existing,
+ stats_missing_moodle=missing_moodle,
+ message=f"Processing... ({processed}/{total})",
+ )
+ continue
+
+ new_rows.append(
+ TestAttendance(
+ student_id=tra.student.id,
+ test=test,
+ mdluser_id=mdluser.id,
+ mdlcourse_id=fossmdlcourse.mdlcourse_id,
+ mdlquiz_id=fossmdlcourse.mdlquiz_id,
+ mdlattempt_id=0,
+ status=0
+ )
+ )
+ processed += 1
+
+ if (processed % UPDATE_EVERY == 0) or (processed == total):
+ pct = int((processed * 100) / total) if total else 100
+ meta_update(
+ progress_processed=processed,
+ progress_pct=pct,
+ stats_created_attendance=len(new_rows),
+ stats_skipped_existing=skipped_existing,
+ stats_missing_moodle=missing_moodle,
+ message=f"Processing... ({processed}/{total})",
+ )
+
+ # --- write phase ---
+ meta_update(status="writing", message=f"Writing {len(new_rows)} new TestAttendance rows...")
+
+ if new_rows:
+ close_old_connections()
+ with transaction.atomic():
+ TestAttendance.objects.bulk_create(new_rows)
+
+ update_events_log(user_id=user_id, role=0, category=1, category_id=test_id, academic=academic_id,status=0)
+
+ update_events_notification(user_id=user_id, role=0, category=1, category_id=test_id, academic=academic_id, status=0,message=message)
+ meta_update(
+ status="done",
+ finished_at=int(time.time()),
+ stats_created_attendance=len(new_rows),
+ stats_skipped_existing=skipped_existing,
+ stats_missing_moodle=missing_moodle,
+ progress_processed=total,
+ progress_pct=100,
+ message="Done.",
+ )
+
+
+def process_test_post_save(test_id, user_id, message,academic_id):
+ """
+ Background task:
+ - Event log
+ - Notifications
+ """
+ close_old_connections()
+ from events.views import (
+ update_events_log,
+ update_events_notification
+ )
+
+ update_events_log(
+ user_id=user_id,
+ role=0,
+ category=1,
+ category_id=test_id,
+ academic=academic_id,
+ status=0
+ )
+
+ update_events_notification(
+ user_id=user_id,
+ role=0,
+ category=1,
+ category_id=test_id,
+ academic=academic_id,
+ status=0,
+ message=message
+ )
+
+
+def async_process_test_attendance(test, user, message):
+ print(f"\033[92m Adding task to process_test_attendance \033[0m")
+ print(f"\033[93m test.pk : {test.pk} \033[0m")
+ DEFAULT_QUEUE.enqueue(
+ process_test_attendance,
+ test.pk,
+ user.pk,
+ message,
+ test.academic_id,
+ job_id="test_attendance_%s" % test.pk,
+ job_timeout='72h'
+ )
+ print(f"\033[92m Added test attendance job successfully \033[0m")
+
+
+def async_test_post_save(test, user, message):
+ DEFAULT_QUEUE.enqueue(
+ process_test_post_save,
+ test.pk,
+ user.pk,
+ message,
+ test.academic_id,
+ job_id="test_post_save_%s" % test.pk,
+ job_timeout='24h'
+ )
+ print(f"\033[92m Added async_test_post_save job successfully \033[0m")
diff --git a/events/helpers.py b/events/helpers.py
index 66212ad0c..ea1764e97 100644
--- a/events/helpers.py
+++ b/events/helpers.py
@@ -9,6 +9,7 @@
from django.conf import settings
from .models import FossMdlCourses
+
def get_academic_years(default=settings.ACADEMIC_DURATION):
current_year = dt.datetime.now().year
year_choice = [('', '-----')]
@@ -70,9 +71,14 @@ def send_bulk_student_reset_mail(ac, batches, count, new_password, user):
Admin Team
"""
- from_email = settings.ADMINISTRATOR_EMAIL
+ admin_email = settings.ADMINISTRATOR_EMAIL
+ if isinstance(admin_email, (list, tuple)):
+ from_email = admin_email[0]
+ else:
+ from_email = admin_email
recipient_list = [user.email, settings.DEVELOPER_EMAIL]
send_mail(subject, message, from_email, recipient_list, fail_silently=False)
+
def get_fossmdlcourse(foss_id, fossmdlmap_id=None):
diff --git a/events/models.py b/events/models.py
index 8a18f1a8e..a0c700ade 100755
--- a/events/models.py
+++ b/events/models.py
@@ -592,7 +592,10 @@ class StudentBatch(models.Model):
batch_name = models.CharField(max_length=200, null=True)
def __str__(self):
- return '%s, %s Batch' % (self.department.name, self.year)
+ if self.batch_name:
+ return f"{self.batch_name} - [student count: {self.stcount}]"
+ else:
+ return f"{self.department.name}, {self.year} Batch. [student count: {self.stcount}]"
def get_batch_info(self):
return '%s, %s, %s Batch' % (self.academic, self.department.name, self.year)
diff --git a/events/views.py b/events/views.py
index c509dbd04..87f466027 100644
--- a/events/views.py
+++ b/events/views.py
@@ -2,6 +2,10 @@
from django.http import HttpResponseForbidden, HttpResponseBadRequest
from .models import StudentBatch
from django.urls import reverse
+from redis import Redis
+from rq.job import Job
+from rq.exceptions import NoSuchJobError
+from cron import REDIS_CLIENT
def get_batches(request):
school_id = request.GET.get('school_id')
@@ -12,6 +16,10 @@ def get_batches(request):
from django.core.exceptions import PermissionDenied
+from django.core.exceptions import PermissionDenied
+from django.views.decorators.csrf import csrf_protect
+from django.db import connection
+
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Group
from django.contrib import messages
@@ -20,9 +28,8 @@ def get_batches(request):
from django.template import RequestContext
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
-from django.http import HttpResponse, HttpResponseBadRequest
+from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
-
from django.http import Http404
from django.db.models import Q
from django.db import IntegrityError
@@ -71,6 +78,12 @@ def get_batches(request):
from PyPDF2 import PdfFileWriter, PdfFileReader
from django.template.context_processors import csrf
+from cron.tasks import (
+ async_process_test_attendance,
+ async_test_post_save
+)
+
+
from io import StringIO, BytesIO
@@ -1858,41 +1871,49 @@ def training_participant_ceritificate(request, wid, participant_id):
return response
+
@login_required
def test_request(request, role, rid = None):
''' Test request by organiser '''
+
user = request.user
if not (user.is_authenticated() and ( is_organiser(user) or is_resource_person(user) or is_event_manager(user))):
raise PermissionDenied()
context = {}
form = TestForm(user = user)
+
if rid:
t = Test.objects.get(pk = rid)
user = t.organiser.user
form = TestForm(user = user, instance = t)
context['instance'] = t
+
if request.method == 'POST':
form = TestForm(request.POST, user = user)
+
if form.is_valid():
dateTime = request.POST['tdate'].split(' ')
- t = Test()
+ error = 0
+
if rid:
t = Test.objects.get(pk = rid)
else:
- print("New Test.............")
+ t = Test()
t.organiser_id = user.organiser.id
t.academic = user.organiser.academic
+
t.test_category_id = request.POST['test_category']
- """if int(request.POST['test_category']) == 1:
- t.training_id = request.POST['workshop']"""
if int(request.POST['test_category']) == 2:
t.training_id = request.POST['training']
- if int(request.POST['test_category']) == 3:
+ else:
t.training_id = None
- test_trainings = request.POST['training']
- test_training_dept = t.training.department_id
- if request.POST['id_foss']:
+
+ test_training_dept = None
+ if t.training_id:
+ test_training_dept = t.training.department_id
+
+ if request.POST.get('id_foss'):
test_foss = request.POST['id_foss']
else:
test_foss = t.training.course.foss_id
@@ -1901,74 +1922,73 @@ def test_request(request, role, rid = None):
t.foss_id = test_foss
t.tdate = dateTime[0]
t.ttime = dateTime[1]
- error = 0
- errmsg = ""
+
try:
t.save()
- except IntegrityError as e:
- print(f"Test creation failed for {user.email}: {e}")
+ except IntegrityError:
error = 1
- errmsg = "Test already created"
- prev_test = Test.objects.filter(organiser = t.organiser_id, academic = t.academic, foss = t.foss_id, tdate = t.tdate, ttime = t.ttime)
- if prev_test:
- messages.error(request, "You have already scheduled "+ t.foss.foss + " Test on "+t.tdate + " "+ t.ttime + ". Please select some other time.")
-
- if not error and t.id and t.training_id:
- tras = TrainingAttend.objects.filter(training=t.training)
- fossmdlcourse = get_fossmdlcourse(t.foss_id, fossmdlmap_id=t.training.fossmdlmap_id)
- # try:
- # fossmdlcourse = FossMdlCourses.objects.get(foss_id = t.foss_id)
- # except FossMdlCourses.MultipleObjectsReturned:
- # fossmdlcourse = FossMdlCourses.objects.get(id = t.training.fossmdlmap_id)
- for tra in tras:
- user = tra.student.user
- mdluser = get_moodle_user(tra.training.training_planner.academic_id, user.first_name, user.last_name, tra.student.gender, tra.student.user.email)# if it create user rest password for django user too
-
- if mdluser:
- print("mdluser present", mdluser.id)
- try:
- instance = TestAttendance.objects.get(test_id=t.id, mdluser_id=mdluser.id)
- except Exception as e:
- print(e)
- instance = TestAttendance()
- instance.student_id = tra.student.id
- instance.test_id = t.id
- instance.mdluser_id = mdluser.id
- instance.mdlcourse_id = fossmdlcourse.mdlcourse_id
- instance.mdlquiz_id = fossmdlcourse.mdlquiz_id
- instance.mdlattempt_id = 0
- instance.status = 0
- instance.save()
-
- print("test_attendance created for ",tra.student.id)
- else:
- print("mdluser not found for", user.email)
- error = 1
+ messages.error(request, "You have already scheduled "+ t.foss.foss + " Test on "+t.tdate + " "+ t.ttime + ". Please select some other time.")
+
if not error:
- t.department.clear()
- t.department.add(test_training_dept)
- #update logs
- message = t.academic.institution_name+" has made a test request for "+t.foss.foss+" on "+t.tdate
+ message = (
+ t.academic.institution_name +
+ " has made a test request for " +
+ t.foss.foss + " on " + t.tdate
+ )
if rid:
- message = t.academic.institution_name+" has updated test for "+t.foss.foss+" on dated "+t.tdate
- update_events_log(user_id = user.id, role = 0, category = 1, category_id = t.id, academic = t.academic_id, status = 0)
- update_events_notification(user_id = user.id, role = 0, category = 1, category_id = t.id, academic = t.academic_id, status = 0, message = message)
+ message = (
+ t.academic.institution_name +
+ " has updated test for " +
+ t.foss.foss + " dated " + t.tdate
+ )
+ # async attendance
+ if t.training_id:
+ async_process_test_attendance(t,user, message)
+
+ # faster M2M update
+ if test_training_dept:
+ t.department.set([test_training_dept])
+ else:
+ t.department.clear()
+
+ # message = (
+ # t.academic.institution_name +
+ # " has made a test request for " +
+ # t.foss.foss + " on " + t.tdate
+ # )
+ # if rid:
+ # message = (
+ # t.academic.institution_name +
+ # " has updated test for " +
+ # t.foss.foss + " dated " + t.tdate
+ # )
+
+ # async logs & notifications
+ # async_test_post_save(t, user, message)
+ messages.success(request, "Test request submitted successfully. Moodle records are currently being processed for students. Please check back shortly. The process may take up to 30 minutes.")
+ return HttpResponseRedirect(
+ "/software-training/test/{}/pending/".format(role)
+ )
- return HttpResponseRedirect("/software-training/test/"+role+"/pending/")
messages.info(request, """
Note: Clearing all memcache might slow down the site initially
-