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, """ """) + context['role'] = role context['status'] = 'request' context.update(csrf(request)) context['form'] = form return render(request, 'events/templates/test/form.html', context) + + + + @login_required def test_list(request, role, status): """ Organiser test index page """ @@ -2283,6 +2303,17 @@ def test_attendance(request, tid): messages.info(request, "Instruct the students to Register and Login on the Online Test link of Spoken Tutorial. Click on the checkbox so that usernames of all the students who are present for the test are marked, then click the submit button. Students can now proceed for the Test.") return render(request, 'events/templates/test/attendance.html', context) +def is_worker_queued(test_id): + job_id = f"test_attendance_{test_id}" + try: + job = Job.fetch(job_id, connection=REDIS_CLIENT) + return True + except NoSuchJobError as e: + print("********** NoSuchJobError **********") + return False + + + @login_required def test_participant(request, tid=None): user = request.user @@ -2297,6 +2328,13 @@ def test_participant(request, tid=None): test_mdlusers = TestAttendance.objects.filter(test_id=tid, status__gte=2) else: test_mdlusers = TestAttendance.objects.filter(test_id=tid) + + context = {'collection' : test_mdlusers, 'test' : t, 'can_download_certificate':can_download_certificate} + # if training has participants and testattendance has no participants, check if it is in queue + if test_mdlusers.count() == 0 and TrainingAttend.objects.filter(training=t.training).count() != 0: + if is_worker_queued(t.id): + print(f"********** Job is in queue: {t.id} **********") + context["message"] = "Moodle records are currently being processed for students. Please check back shortly. The process may take up to 30 minutes." #ids = [] #print test_mdlusers #for tp in test_mdlusers: @@ -2305,7 +2343,7 @@ def test_participant(request, tid=None): #tp = MdlUser.objects.using('moodle').filter(id__in=ids) #if t.status == 4 and (user == t.organiser or user == t.invigilator): # can_download_certificate = 1 - context = {'collection' : test_mdlusers, 'test' : t, 'can_download_certificate':can_download_certificate} + return render(request, 'events/templates/test/test_participant.html', context) def test_participant_ceritificate(request, wid, participant_id): diff --git a/events/viewsv2.py b/events/viewsv2.py index 00d3777f8..6d57854b1 100755 --- a/events/viewsv2.py +++ b/events/viewsv2.py @@ -50,6 +50,8 @@ from django.contrib.auth.mixins import UserPassesTestMixin from events.formsv2 import StudentGradeFilterForm, AcademicPaymentStatusForm from django.views.generic import FormView +from django.shortcuts import get_object_or_404 + #pdf generate from reportlab.pdfgen import canvas @@ -705,6 +707,7 @@ def form_valid(self, form, **kwargs): self.training.department = selectedDept self.training.batch = selectedBatch self.training.course_type = form.cleaned_data['course_type'] + self.training.fossmdlmap = form.cleaned_data.get('fossmdlmap') if self.training.batch.is_foss_batch_acceptable(selectedCourse): self.training.sem_start_date = form.cleaned_data['sem_start_date'] @@ -787,6 +790,7 @@ def post(self, request, *args, **kwargs): self.training_request.update_participants_count() return HttpResponseRedirect('/software-training/training-planner') +@method_decorator(login_required(login_url="/accounts/login/"), name="dispatch") class TrainingCertificateListView(ListView): queryset = StudentMaster.objects.none() paginate_by = 500 @@ -794,7 +798,7 @@ class TrainingCertificateListView(ListView): training_request = None def dispatch(self, *args, **kwargs): - self.training_request = TrainingRequest.objects.get(pk=kwargs['tid']) + self.training_request = get_object_or_404(TrainingRequest, pk=kwargs['tid']) if self.training_request.status: # if status = 1, display students who attended the training self.queryset = self.training_request.attendances.all().select_related('student', 'student__user') else: # if training not completed show batch students diff --git a/python-scripts/yt-flag-script.py b/python-scripts/yt-flag-script.py new file mode 100644 index 000000000..2e91eb040 --- /dev/null +++ b/python-scripts/yt-flag-script.py @@ -0,0 +1,213 @@ +import re + +import pandas as pd + +ST_FILE = "st_homepage_tutorials.csv" +YT_FILE = "spoken_tutorial.csv" +OUTPUT_FILE = "st_homepage_with_youtube_flag.csv" +TOKEN_MATCH_THRESHOLD = 0.5 +STOP_WORDS = { + "spoken", + "tutorial", + "tutorials", +} +STOP_PHRASES = ( + "spoken tutorial", +) +# Minimum FOSS name length to require substring matching (avoid false positives with "C", "R", etc.) +MIN_FOSS_LENGTH_FOR_SUBSTRING = 3 + +# Enable more lenient matching for edge cases +ENABLE_FALLBACK_MATCHING = True + + +def normalize(text): + if pd.isna(text): + return "" + return str(text).lower().strip() + + +def remove_punctuation(text): + return re.sub(r"[^\w\s]", " ", text) + + +def normalize_text_field(text): + base = normalize(text) + for phrase in STOP_PHRASES: + base = base.replace(phrase, " ") + no_punct = remove_punctuation(base) + return " ".join(no_punct.split()) + + +def tokenize_for_match(text): + cleaned = normalize_text_field(text) + tokens = [tok for tok in cleaned.split() if tok not in STOP_WORDS] + if tokens: + return tokens + return cleaned.split() + + +def tokens_match(source_tokens, target_tokens): + if not source_tokens or not target_tokens: + return False + overlap = set(source_tokens) & set(target_tokens) + ratio = len(overlap) / len(set(source_tokens)) + return ratio >= TOKEN_MATCH_THRESHOLD + + +def build_language_patterns(language_value): + tokens = [tok for tok in language_value.split() if tok] + return [re.compile(rf"\b{re.escape(token)}\b") for token in tokens] + + +def extract_language_from_playlist(playlist_name): + """Extract language from playlist name (e.g., 'Advance C - English' -> 'english')""" + if pd.isna(playlist_name): + return "" + + match = re.search(r'-\s*([a-zA-Z]+)\s*$', str(playlist_name)) + if match: + return match.group(1).lower().strip() + return "" + + +def extract_language_from_video_title(video_name): + """Extract language from video title (e.g., 'Tutorial Name - Hindi' -> 'hindi')""" + if pd.isna(video_name): + return "" + + match = re.search(r'-\s*([a-zA-Z]+)\s*$', str(video_name)) + if match: + return match.group(1).lower().strip() + return "" + + +def main(): + st_df = pd.read_csv(ST_FILE) + yt_df = pd.read_csv(YT_FILE) + + print(f"Loaded {len(st_df)} ST homepage tutorials") + print(f"Loaded {len(yt_df)} YouTube videos") + + # Normalize YouTube data + yt_df["playlist_norm"] = yt_df["playlist_name"].apply(normalize_text_field) + yt_df["title_tokens"] = yt_df["video_name"].apply(tokenize_for_match) + yt_df["title_lang_text"] = yt_df["video_name"].apply(normalize_text_field) + yt_df["playlist_language"] = yt_df["playlist_name"].apply(extract_language_from_playlist) + yt_df["video_language"] = yt_df["video_name"].apply(extract_language_from_video_title) + if "description" in yt_df.columns: + yt_df["description_lang_text"] = yt_df["description"].apply(normalize_text_field) + else: + yt_df["description_lang_text"] = "" + + # Normalize ST homepage data + st_df["foss_norm"] = st_df["foss_name"].apply(normalize_text_field) + st_df["tutorial_tokens"] = st_df["tutorial"].apply(tokenize_for_match) + st_df["language_norm"] = st_df["language"].apply(normalize_text_field) + st_df["language_patterns"] = st_df["language_norm"].apply(build_language_patterns) + + print("Processing tutorials...") + + def is_available(row): + foss = row["foss_norm"] + tutorial_tokens = row["tutorial_tokens"] + language_patterns = row["language_patterns"] + language_norm = row["language_norm"] + + if not foss or not tutorial_tokens or not language_patterns: + return "No" + + # Filter YouTube videos by FOSS name + if len(foss) < MIN_FOSS_LENGTH_FOR_SUBSTRING: + # Use word boundary matching for short names + pattern = rf"\b{re.escape(foss)}\b" + candidates = yt_df[ + yt_df["playlist_norm"].str.contains(pattern, na=False, regex=True) + ] + else: + candidates = yt_df[ + yt_df["playlist_norm"].str.contains(foss, na=False, regex=False) + ] + + if candidates.empty: + return "No" + + for _, video_row in candidates.iterrows(): + # Step 1: Check if tutorial tokens match video title tokens + if not tokens_match(tutorial_tokens, video_row["title_tokens"]): + continue + + # Step 2: Check language match - improved logic + # Method 1: Check if language appears in title or description + title_text = video_row["title_lang_text"] + description_text = video_row["description_lang_text"] + + language_in_content = any( + pattern.search(title_text) or pattern.search(description_text) + for pattern in language_patterns + ) + + # Method 2: Check extracted language from playlist/video name + playlist_lang = video_row["playlist_language"] + video_lang = video_row["video_language"] + + # Match if: + # a) Language found in title/description, OR + # b) Language matches playlist language, OR + # c) Language matches video language + language_matches = ( + language_in_content or + (playlist_lang and playlist_lang == language_norm) or + (video_lang and video_lang == language_norm) + ) + + if language_matches: + return "Yes" + + # Fallback: If no match found with strict language matching, + # check if there's a video with the same tutorial in ANY language + if ENABLE_FALLBACK_MATCHING: + for _, video_row in candidates.iterrows(): + tutorial_set = set(tutorial_tokens) + video_set = set(video_row["title_tokens"]) + + if not tutorial_set or not video_set: + continue + + overlap = tutorial_set & video_set + # Use a higher threshold for fallback to reduce false positives + ratio = len(overlap) / len(tutorial_set) + + if ratio >= 0.7: + video_has_different_lang = ( + (playlist_lang and playlist_lang != language_norm) or + (video_lang and video_lang != language_norm) + ) + pass + + return "No" + + st_df["available_on_youTube"] = st_df.apply(is_available, axis=1) + + st_df.drop( + columns=["foss_norm", "tutorial_tokens", "language_norm", "language_patterns"], + inplace=True, + ) + + st_df.to_csv(OUTPUT_FILE, index=False) + + # Print summary statistics + yes_count = (st_df["available_on_youTube"] == "Yes").sum() + no_count = (st_df["available_on_youTube"] == "No").sum() + + print(f"\n{'='*60}") + print(f"Done. Output written to {OUTPUT_FILE}") + print(f"{'='*60}") + print(f"Total tutorials: {len(st_df)}") + print(f"Available on YouTube: {yes_count} ({yes_count/len(st_df)*100:.1f}%)") + print(f"Not available: {no_count} ({no_count/len(st_df)*100:.1f}%)") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/python-scripts/yt_playlists.py b/python-scripts/yt_playlists.py new file mode 100644 index 000000000..874b0a985 --- /dev/null +++ b/python-scripts/yt_playlists.py @@ -0,0 +1,113 @@ +# YouTube Data API Key Setup Instructions + + +# To run this script, you must generate a YouTube Data API key. +# +# Steps to obtain the key: +# +# 1. Go to Google Cloud Console: +# https://console.cloud.google.com/ +# +# 2. Create a new project (or select an existing one). +# +# 3. Enable the YouTube Data API v3: +# - Navigate to "APIs & Services" → "Enable APIs and Services" +# - Search for "YouTube Data API v3" +# - Click "Enable" +# +# 4. Create an API key: +# - Go to "APIs & Services" → "Credentials" +# - Click "Create Credentials" → "API key" +# +# 5. Put the key in a .env file in the same directory as this script: +# YOUTUBE_API_KEY = "YOUR_API_KEY_HERE" +# +# ============================================================ + + + +import os +import csv + +import requests +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv("YOUTUBE_API_KEY") +if not API_KEY: + raise RuntimeError("YOUTUBE_API_KEY is not set in the environment or .env file") +CHANNEL_ID = "UCcLQJOfR-MCcI5RtIHFl6Ww" +BASE_URL = "https://www.googleapis.com/youtube/v3" + +def get_playlists(): + url = f"{BASE_URL}/playlists" + params = { + "part": "snippet", + "channelId": CHANNEL_ID, + "maxResults": 50, + "key": API_KEY + } + playlists = [] + + while True: + data = requests.get(url, params=params).json() + for item in data.get("items", []): + playlists.append({ + "id": item["id"], + "title": item["snippet"]["title"] + }) + + if "nextPageToken" not in data: + break + params["pageToken"] = data["nextPageToken"] + + return playlists + + +def get_videos(playlist_id): + url = f"{BASE_URL}/playlistItems" + params = { + "part": "snippet", + "playlistId": playlist_id, + "maxResults": 50, + "key": API_KEY + } + videos = [] + + while True: + data = requests.get(url, params=params).json() + for item in data.get("items", []): + videos.append(item["snippet"]["title"]) + + if "nextPageToken" not in data: + break + params["pageToken"] = data["nextPageToken"] + + return videos + + +def main(): + playlists = get_playlists() + rows = [] + + for p in playlists: + print(f"Fetching playlist: {p['title']}") + videos = get_videos(p["id"]) + + for v in videos: + rows.append({ + "playlist_name": p["title"], + "video_name": v + }) + + with open("spoken_tutorial.csv", "w", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=["playlist_name", "video_name"]) + writer.writeheader() + writer.writerows(rows) + + print("CSV created: spoken_tutorial.csv") + + +if __name__ == "__main__": + main() diff --git a/reports/views.py b/reports/views.py index 4840fb8bd..081c2cbcd 100644 --- a/reports/views.py +++ b/reports/views.py @@ -201,18 +201,32 @@ def events_test_csv(request): writer = csv.writer(response) # header - writer.writerow(['State', 'City', 'Institution', 'FOSS', 'Organiser', 'Date', 'Participants']) - + writer.writerow(['State', 'City', 'Institution', 'FOSS', 'Organiser','Department', 'Date', 'Participants']) + + def to_text(value): + if value is None: + return '' + if isinstance(value, bytes): + return value.decode('utf-8', errors='ignore') + return str(value) + + def get_department(record): + training = getattr(record, 'training', None) + if training and getattr(training, 'department', None): + return training.department.name + return '' + # records for record in collection.qs: writer.writerow([ - record.academic.state.name, - record.academic.city.name, - record.academic.institution_name.encode('utf-8'), - record.foss.foss, - record.organiser.user.first_name.encode('utf-8'), - record.tdate, - record.participant_count + to_text(record.academic.state.name), + to_text(record.academic.city.name), + to_text(record.academic.institution_name), + to_text(record.foss.foss), + to_text(record.organiser.user.first_name), + to_text(get_department(record)), + to_text(record.tdate), + to_text(record.participant_count) ]) return response diff --git a/spoken/helpers.py b/spoken/helpers.py index 90f41d08a..074f76e30 100644 --- a/spoken/helpers.py +++ b/spoken/helpers.py @@ -9,7 +9,9 @@ from creation.models import TutorialSummaryCache, TutorialResource, FossCategory from events.models import Testimonials from cms.models import Notification, Event +from cms.cache_registry import register_cache_key from .config import CACHE_RANDOM_TUTORIALS, CACHE_TR_REC, CACHE_TESTIMONIALS, CACHE_NOTIFICATIONS, CACHE_EVENTS, CACHE_TUTORIALS +import hashlib def get_key(identifier, key_val): return f"{identifier}:{key_val.lower().strip().replace(' ','_')}" @@ -122,29 +124,49 @@ def get_tutorials_list(foss, lang): # ---- Foss Choice For Search Bar ---- def get_foss_choice(show_on_homepage=1, lang=None): + if lang and len(lang) > 50: + lang = lang[:50] + if lang: - cache_key = get_key("tutorial_search_foss", f"{show_on_homepage}:{lang}") + raw_key = f"{show_on_homepage}:{lang}" else: - cache_key = f"tutorial_search_foss:{show_on_homepage}:all" + raw_key = f"{show_on_homepage}:all" + + hashed_key = hashlib.md5(raw_key.encode("utf-8")).hexdigest() + cache_key = get_key("tutorial_search_foss", hashed_key) + foss_list_choices = cache.get(cache_key) if foss_list_choices is not None: return foss_list_choices - + foss_list_choices = [('', '-- All Courses --'), ] - foss_qs = TutorialResource.objects.filter(status__in=[1,2], tutorial_detail__foss__show_on_homepage=show_on_homepage) + foss_qs = TutorialResource.objects.filter( + status__in=[1,2], + tutorial_detail__foss__show_on_homepage=show_on_homepage + ) if lang: foss_qs = foss_qs.filter(language__name=lang) - foss_list = foss_qs.values('tutorial_detail__foss__foss').annotate( - Count('id')).order_by('tutorial_detail__foss__foss').values_list('tutorial_detail__foss__foss', 'id__count').distinct() + + foss_list = foss_qs.values( + 'tutorial_detail__foss__foss' + ).annotate( + Count('id') + ).order_by( + 'tutorial_detail__foss__foss' + ).values_list( + 'tutorial_detail__foss__foss', 'id__count' + ).distinct() for foss_row in foss_list: - foss_list_choices.append((str(foss_row[0]), str(foss_row[0]) + ' (' + str(foss_row[1]) + ')')) + foss_list_choices.append( + (str(foss_row[0]), str(foss_row[0]) + ' (' + str(foss_row[1]) + ')') + ) cache.set(cache_key, foss_list_choices, timeout=CACHE_TUTORIALS) + register_cache_key(cache_key) return foss_list_choices - # ---- Language Choice For Search Bar ---- def get_lang_choice(show_on_homepage=1, foss=None): if is_valid_foss(foss): @@ -166,4 +188,5 @@ def get_lang_choice(show_on_homepage=1, foss=None): lang_list_choices.append((str(lang_row[0]), str(lang_row[0]) + ' (' + str(lang_row[1]) + ')')) cache.set(cache_key, lang_list_choices, timeout=CACHE_TUTORIALS) + register_cache_key(cache_key) return lang_list_choices \ No newline at end of file diff --git a/spoken/views.py b/spoken/views.py index 94e2e0561..e9e46f1b6 100644 --- a/spoken/views.py +++ b/spoken/views.py @@ -307,7 +307,7 @@ def watch_tutorial(request, foss, tutorial, lang): # filter questions based on category & tutorial ques = Question.objects.filter(category=td_rec.foss.foss.replace( - ' ', '-'), tutorial=td_rec.tutorial.replace(' ', '-')) + ' ', '-'), tutorial=td_rec.tutorial.replace(' ', '-'), status=1) # annotate each question with its answers count ques = ques.annotate( diff --git a/static/cms/templates/change_password.html b/static/cms/templates/change_password.html index 10964e422..bfaf80a42 100644 --- a/static/cms/templates/change_password.html +++ b/static/cms/templates/change_password.html @@ -16,7 +16,7 @@ {% endif %} - +
{% render_field form.old_password class+="form-control old_password" tabindex="1" %} diff --git a/static/cms/templates/manage_cache.html b/static/cms/templates/manage_cache.html index 0785a5fbc..81f4166b6 100644 --- a/static/cms/templates/manage_cache.html +++ b/static/cms/templates/manage_cache.html @@ -64,9 +64,18 @@
Option 2: Clear all memcache data

Note: Clearing all memcache might slow down the site initially

-
- + +
+ {% csrf_token %} +
+
Option 3: Clear homepage cache
+

This will clear cache entries related to the homepage only.

+ + +
+
+ {% endblock %} {% block jsblock %} diff --git a/static/events/templates/events_dashboard.html b/static/events/templates/events_dashboard.html index 6466a34cc..abb9f986d 100755 --- a/static/events/templates/events_dashboard.html +++ b/static/events/templates/events_dashboard.html @@ -300,7 +300,7 @@
Online Assessment Test
Manage Cache
diff --git a/static/events/templates/test/test_participant.html b/static/events/templates/test/test_participant.html index 8c72cfea0..31f097983 100644 --- a/static/events/templates/test/test_participant.html +++ b/static/events/templates/test/test_participant.html @@ -36,6 +36,11 @@ + {% if message %} +
+
{{message}}
+
+ {% endif %}
{% if collection %} Download all certificates diff --git a/static/statistics/templates/online_test.html b/static/statistics/templates/online_test.html index ac11bb50a..4cdefaf04 100644 --- a/static/statistics/templates/online_test.html +++ b/static/statistics/templates/online_test.html @@ -134,7 +134,12 @@ {{ record.invigilator.user.first_name }} {{ record.tdate|date:"d M Y" }} {{ record.wtime }} {{ record.participant_count }} - {{ record.training.department.name }} + + {% if record.training and record.training.department %} + {{ record.training.department.name }} + {% endif %} + + {%if record.get_test_attendance_count %} diff --git a/training/filters.py b/training/filters.py index 7b712787b..dbb07e530 100644 --- a/training/filters.py +++ b/training/filters.py @@ -1,7 +1,14 @@ import django_filters from .models import Company +from training.models import Participant +from creation.models import FossCategory +from events.models import State, AcademicCenter +from training.models import TrainingEvents, Participant + class CompanyFilter(django_filters.FilterSet): class Meta: model = Company - fields = ['name', 'state', 'company_type'] \ No newline at end of file + fields = ['name', 'state', 'company_type'] + + diff --git a/training/templates/list_event_participants.html b/training/templates/list_event_participants.html index df24ea976..949b6636b 100644 --- a/training/templates/list_event_participants.html +++ b/training/templates/list_event_participants.html @@ -13,12 +13,12 @@ {{message.tags}}
{% csrf_token %} -{% if eventid|is_tr_completed %} +{% if is_tr_completed %}

To select all participants

{% endif %} -{% if eventid|is_tr_ongoing%} +{% if is_tr_ongoing%}

To select all participants

@@ -27,7 +27,7 @@ - {% if eventid|is_tr_completed or eventid|is_tr_ongoing%} + {% if is_tr_completed or is_tr_ongoing%} {% endif %} @@ -41,9 +41,9 @@ - {% if eventid|is_tr_ongoing%} + {% if is_tr_ongoing%} - {% if eventid|is_reg_confirmed:record.id %} + {% if record.reg_approval_status == 1 %} {% else %} @@ -51,16 +51,16 @@ {% endif %} - {% if eventid|is_tr_completed %} + {% if is_tr_completed %} - {% if eventid|is_attendance_marked:record.id %} + {% if record.attendance_marked %} {% else %} {% endif %} {% endif %} - {% if eventid|is_event_closed %} + {% if is_event_closed %} @@ -108,12 +108,12 @@ {% endfor %}
Sr. No.CheckName
{{ forloop.counter }}{{ record.participant.name}} {{ record.participant.email}} {{ record.participant.college.institution_name}}
-{% if eventid|is_tr_ongoing %} +{% if is_tr_ongoing %} Submit Registration {% if eventid|event_has_registration %} Approve Registration{% endif %} -{% elif eventid|is_tr_completed %} +{% elif is_tr_completed %} Submit Attendance {% if eventid|event_has_attendance %} Close Event and Generate Certificates{% endif %} diff --git a/training/templates/register_user.html b/training/templates/register_user.html index 794db2f6f..4e52aaa2b 100644 --- a/training/templates/register_user.html +++ b/training/templates/register_user.html @@ -141,7 +141,7 @@

Welcome {{ form.state.errors }}

- {% if event_obj.event_type == "CDP" or event_obj.event_type == "PDP" or event_obj.event_type == "HN" %} + {% if event_obj.event_type == "CDP" or event_obj.event_type == "PDP" %}
@@ -150,7 +150,7 @@

Welcome

{% endif %} - {% if event_obj.event_type == "CDP" or event_obj.event_type == "PDP" or event_obj.event_type == "HN" %} + {% if event_obj.event_type == "CDP" or event_obj.event_type == "PDP" %} @@ -337,22 +337,40 @@

Welcome