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/cacheurls.py b/cms/cacheurls.py new file mode 100644 index 000000000..e0d633136 --- /dev/null +++ b/cms/cacheurls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url +from cms import views + +urlpatterns = [ + url(r'^cache-tools/$', views.cache_tools, name='cache-tools'), +] diff --git a/cms/management/commands/notify_users.py b/cms/management/commands/notify_users.py new file mode 100644 index 000000000..7e648e63a --- /dev/null +++ b/cms/management/commands/notify_users.py @@ -0,0 +1,133 @@ +from django.core.management.base import BaseCommand +from django.core.mail import EmailMultiAlternatives +from django.conf import settings +from django.contrib.auth.models import User +from django.db import transaction +from datetime import datetime, timedelta +import smtplib +from cms.models import EmailLog + + +class Command(BaseCommand): + help = "Notify users inactive for more than X years or before a given cutoff date." + + def add_arguments(self, parser): + + parser.add_argument( + "--years", + type=int, + default=5, + help="Check inactivity older than this many years (default = 5). Ignored if --cutoff-date provided.", + ) + + parser.add_argument( + "--limit", + type=int, + default=None, + help="Limit number of users to notify", + ) + + parser.add_argument( + "--cutoff-date", + type=str, + default=None, + help="Custom cutoff date in YYYY-MM-DD format (overrides --years)", + ) + + @transaction.atomic + def handle(self, *args, **options): + + years = options.get("years") + limit = options.get("limit") + cutoff_input = options.get("cutoff_date") + + if cutoff_input: + try: + cutoff_date = datetime.strptime(cutoff_input, "%Y-%m-%d") + except ValueError: + self.stdout.write(self.style.ERROR( + "āŒ Invalid cutoff date format! Use YYYY-MM-DD" + )) + return + else: + cutoff_date = datetime.now() - timedelta(days=years * 365) + + cutoff_str = cutoff_date.strftime("%Y-%m-%d") + self.stdout.write(self.style.WARNING( + f"\nšŸ“… Using cutoff date: {cutoff_str} (YYYY-MM-DD)\n" + )) + + users = User.objects.filter( + last_login__lt=cutoff_date, + is_active=True + ).order_by("id") + + if limit: + users = users[:limit] + + total_users = users.count() + self.stdout.write(self.style.NOTICE( + f"šŸ”Ž Users inactive since before {cutoff_str}: {total_users}\n" + )) + + subject = "Reminder: Your account has been inactive for a long time" + + sent_count = 0 + failed_count = 0 + for user in users: + + message = f""" +Dear {user.first_name} {user.last_name}, + +Our system indicates that your account has not been used for a long time. + +Your last login was on: {user.last_login.strftime("%Y-%m-%d")} + +This is a reminder to log in again and continue using our services. + +If you need help, simply reply to this email. + +Regards, +Support Team +""" + + email = EmailMultiAlternatives( + subject, + message, + settings.NO_REPLY_EMAIL, + to=[user.email], + ) + + try: + email.send(fail_silently=False) + + sent_count += 1 + + EmailLog.objects.create( + user=user, + email=user.email, + status=True, + reason=None + ) + + self.stdout.write(self.style.SUCCESS(f"[SENT] {user.email}")) + + except (smtplib.SMTPException, Exception) as e: + + failed_count += 1 + + EmailLog.objects.create( + user=user, + email=user.email, + status=False, + reason=str(e) + ) + + self.stdout.write(self.style.ERROR(f"[FAILED] {user.email} → {e}")) + + self.stdout.write("\n---------------------------------------") + self.stdout.write(self.style.SUCCESS(f"Emails Sent: {sent_count}")) + self.stdout.write(self.style.ERROR(f"Failed: {failed_count}")) + self.stdout.write(self.style.WARNING(f"Total Processed: {total_users}")) + self.stdout.write(self.style.NOTICE(f"Cutoff Date Used: {cutoff_str}")) + self.stdout.write("---------------------------------------\n") diff --git a/cms/models.py b/cms/models.py index 517b6b254..557d40f86 100644 --- a/cms/models.py +++ b/cms/models.py @@ -150,4 +150,16 @@ class UserType(models.Model): ilw = jsonfield.JSONField(null=True) status = models.CharField(choices=STATUS_CHOICES,max_length=25,default=1) created = models.DateTimeField(auto_now_add=True) - updated = models.DateTimeField(auto_now=True) \ No newline at end of file + updated = models.DateTimeField(auto_now=True) + + + +class EmailLog(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + email = models.EmailField() + sent_time = models.DateTimeField(auto_now_add=True) + status = models.BooleanField(default=False) + reason = models.TextField(null=True, blank=True) + + def __str__(self): + return f"{self.email} - {'Success' if self.status else 'Failed'}" diff --git a/cms/templates/cms/cache_tools.html b/cms/templates/cms/cache_tools.html new file mode 100644 index 000000000..40b03d24e --- /dev/null +++ b/cms/templates/cms/cache_tools.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block content %} +
+

Memcache Tools

+ + {% for message in messages %} +
{{ message }}
+ {% endfor %} + + +

Existing Cache Keys (via Memcached slabs)

+ + +
+ + +
+ {% csrf_token %} + + + +
+ + {% if value_output %} +
+ Key Value:
+ {{ value_output|safe }} +
+ {% endif %} + +
+ + +
+ {% csrf_token %} + + + +
+ +
+ + +
+ {% csrf_token %} + +
+ +
+{% endblock %} diff --git a/cms/urls.py b/cms/urls.py index 3cea0526a..da3c73563 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -7,6 +7,7 @@ from spoken.sitemaps import SpokenStaticViewSitemap from donate.views import * + app_name = 'cms' spoken_sitemaps = { @@ -31,6 +32,6 @@ url(r'^sitemap\.xml/$', sitemap, {'sitemaps' : spoken_sitemaps } , name='spoken_sitemap'), url(r'^(?P.+)/$', dispatcher, name="dispatcher"), - + ] \ No newline at end of file diff --git a/cms/views.py b/cms/views.py index bd0ef45a0..c1bbb5946 100644 --- a/cms/views.py +++ b/cms/views.py @@ -29,6 +29,17 @@ from donate.models import Payee + +from django.contrib.admin.views.decorators import staff_member_required +from django.core.cache import caches +from django.shortcuts import render, redirect + +from cms.cache_registry import list_cache_keys, unregister_cache_key +from django.utils.safestring import mark_safe + + +cache = caches['default'] + def dispatcher(request, permalink=''): if permalink == '': return HttpResponseRedirect('/') @@ -504,3 +515,39 @@ def verify_email(request): else: messages.error(request, 'Invalid Email ID', extra_tags='error') return render(request, "cms/templates/verify_email.html", context) + + +@staff_member_required +def cache_tools(request): + + keys = list_cache_keys() + value_output = None + + if request.method == "POST": + + if "view_key" in request.POST: + key = request.POST.get("cache_key").strip() + val = cache.get(key) + print("dfghjkdfghjk",val) + + if val is None: + value_output = f"No value stored for key: '{key}'" + else: + value_output = mark_safe(f"
{val}
") + + elif "clear_key" in request.POST: + key = request.POST.get("cache_key").strip() + cache.delete(key) + unregister_cache_key(key) + messages.success(request, f"Key '{key}' deleted.") + return redirect("cache-tools") + + elif "clear_all" in request.POST: + cache.clear() + messages.success(request, "All cache cleared.") + return redirect("cache-tools") + + return render(request, "cms/cache_tools.html", { + "keys": keys, + "value_output": value_output + }) diff --git a/events/formsv2.py b/events/formsv2.py index 27f15466d..9f798a280 100644 --- a/events/formsv2.py +++ b/events/formsv2.py @@ -7,6 +7,7 @@ from events.models import * from events.helpers import get_academic_years from cms.validators import validate_csv_file +from spoken.config import SUBSCRIPTION_CHOICES class StudentBatchForm(forms.ModelForm): year = forms.ChoiceField(choices = get_academic_years()) @@ -668,4 +669,5 @@ class Meta(object): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['college_type'].widget.attrs.update({'class': 'form-control', 'readonly': 'readonly'}) - self.fields['payment_status'].widget.attrs.update({'class': 'form-control', 'readonly': 'readonly'}) \ No newline at end of file + self.fields['payment_status'].widget.attrs.update({'class': 'form-control', 'readonly': 'readonly'}) + self.fields['subscription'].choices = SUBSCRIPTION_CHOICES \ No newline at end of file diff --git a/events/models.py b/events/models.py index a7e59a6d6..6423db28a 100755 --- a/events/models.py +++ b/events/models.py @@ -24,6 +24,7 @@ #creation app models from creation.models import FossAvailableForWorkshop, FossAvailableForTest +from spoken.config import SUBSCRIPTION_CHOICES PAYMENT_STATUS_CHOICES =( @@ -32,9 +33,7 @@ COLLEGE_TYPE_CHOICES =( ('', '-----'), ('Engg', 'Engg'), ('ASC', 'ASC'), ('Polytechnic', 'Polytechnic'), ('University', 'University'), ('School', 'School') ) -SUBSCRIPTION_CHOICES = ( - ('', '-----'), ('365', 'One_Year'), ('182', 'Six_Months') - ) + # Create your models here. diff --git a/events/templates/academic_payment_details_form.html b/events/templates/academic_payment_details_form.html index e36108a0d..bd18b5567 100644 --- a/events/templates/academic_payment_details_form.html +++ b/events/templates/academic_payment_details_form.html @@ -269,9 +269,10 @@ if(type == 365){ $('#id_amount').val(29500) }else if(type == 182){ - $('#id_amount').val(14750) - - } + $('#id_amount').val(14750) + }else if(type == 455){ + $('#id_amount').val(29500) + } }) diff --git a/events/views.py b/events/views.py index 98561d668..ef76ea22c 100644 --- a/events/views.py +++ b/events/views.py @@ -1,4 +1,5 @@ from django.http import JsonResponse +from django.http import HttpResponseForbidden from .models import StudentBatch from django.urls import reverse @@ -3262,43 +3263,121 @@ def handover(request): return render(request, 'handover.html', context) -def reset_student_pwd(request): - context = {} - template = 'events/templates/reset_student_password.html' - form = StudentPasswordResetForm() - context['form'] = form +# def reset_student_pwd(request): +# context = {} +# template = 'events/templates/reset_student_password.html' +# form = StudentPasswordResetForm() +# context['form'] = form - if request.method == "POST": - form = StudentPasswordResetForm(request.POST) - context['form'] = form +# if request.method == "POST": +# form = StudentPasswordResetForm(request.POST) +# context['form'] = form - if form.is_valid(): - school = form.cleaned_data['school'] - batches = form.cleaned_data['batches'] - new_password = form.cleaned_data['new_password'] +# if form.is_valid(): +# school = form.cleaned_data['school'] +# batches = form.cleaned_data['batches'] +# new_password = form.cleaned_data['new_password'] - batch_ids = [x.id for x in batches] - batches = StudentBatch.objects.filter(id__in=batch_ids) - student_ids = StudentMaster.objects.filter(batch__in=batches).values_list('student_id', flat=True) - students = Student.objects.filter(id__in=student_ids) - students.update(verified=1, error=0) - user_ids = [stu.user_id for stu in students] - users = User.objects.filter(id__in=user_ids) - new_hashed_pwd = make_password(new_password) - users.update(password=new_hashed_pwd, is_active=1) - emails = [user.email for user in users] - mdlUsers = MdlUser.objects.filter(email__in=emails) - mdlUsers.update(password=encript_password(new_password)) - send_bulk_student_reset_mail(school,batches,users.count(), new_password,request.user) +# batch_ids = [x.id for x in batches] +# batches = StudentBatch.objects.filter(id__in=batch_ids) +# student_ids = StudentMaster.objects.filter(batch__in=batches).values_list('student_id', flat=True) +# students = Student.objects.filter(id__in=student_ids) +# students.update(verified=1, error=0) +# user_ids = [stu.user_id for stu in students] +# users = User.objects.filter(id__in=user_ids) +# new_hashed_pwd = make_password(new_password) +# users.update(password=new_hashed_pwd, is_active=1) +# emails = [user.email for user in users] +# mdlUsers = MdlUser.objects.filter(email__in=emails) +# mdlUsers.update(password=encript_password(new_password)) +# send_bulk_student_reset_mail(school,batches,users.count(), new_password,request.user) - # Add the success message - success_msg = "Password updated for {} students.".format(users.count()) - messages.success(request, success_msg) +# # Add the success message +# success_msg = "Password updated for {} students.".format(users.count()) +# messages.success(request, success_msg) - redirect_url = reverse('events:reset_student_pwd') - return HttpResponseRedirect(redirect_url) - return render(request,template,context) +# redirect_url = reverse('events:reset_student_pwd') +# return HttpResponseRedirect(redirect_url) +# return render(request,template,context) + + + +def reset_student_pwd(request): + + # 1. ROLE VALIDATION ------------------------------- + if not request.user.groups.filter(name="School Training Manager").exists(): + return HttpResponseForbidden("You are not allowed to access this page.") + + + template = 'events/templates/reset_student_password.html' + form = StudentPasswordResetForm(request.POST or None) + + if request.method == "POST" and form.is_valid(): + + school = form.cleaned_data['school'] + batches = form.cleaned_data['batches'] + new_password = form.cleaned_data['new_password'] + + # 2. SECURITY FIX — ensure all batches belong to the selected school + for b in batches: + if b.school_id != school.id: + return HttpResponseForbidden("Invalid batch selected.") + + batch_ids = batches.values_list("id", flat=True) + + # 3. Get student IDs efficiently + student_ids = StudentMaster.objects.filter( + batch_id__in=batch_ids + ).values_list('student_id', flat=True) + + # 4. Update Student table in bulk + Student.objects.filter(id__in=student_ids).update( + verified=1, + error=0 + ) + + # 5. Get User IDs linked to these students + user_ids = Student.objects.filter( + id__in=student_ids + ).values_list("user_id", flat=True) + + # 6. Update Django User passwords in bulk + hashed_pwd = make_password(new_password) + User.objects.filter(id__in=user_ids).update( + password=hashed_pwd, + is_active=1 + ) + + # 7. Fetch emails efficiently + emails = User.objects.filter( + id__in=user_ids + ).values_list("email", flat=True) + + # 8. Update Moodle users password + MdlUser.objects.filter(email__in=emails).update( + password=encript_password(new_password) + ) + + # 9. Send email once (not inside loop) + send_bulk_student_reset_mail( + school=school, + batches=batches, + total_students=len(student_ids), + new_password=new_password, + user=request.user + ) + + messages.success( + request, + f"Password updated for {len(student_ids)} students." + ) + + return redirect("events:reset_student_pwd") + + return render(request, template, {"form": form}) + + def get_schools(request): diff --git a/events/viewsv2.py b/events/viewsv2.py index 167c79be9..e2844c953 100755 --- a/events/viewsv2.py +++ b/events/viewsv2.py @@ -23,7 +23,7 @@ from django.db import IntegrityError from django.utils.decorators import method_decorator from events.decorators import group_required -from events import display +# from events import display from events.forms import StudentBatchForm, TrainingRequestForm, \ TrainingRequestEditForm, CourseMapForm, SingleTrainingForm, \ OrganiserFeedbackForm,STWorkshopFeedbackForm,STWorkshopFeedbackFormPre,STWorkshopFeedbackFormPost,LearnDrupalFeedbackForm, LatexWorkshopFileUploadForm, UserForm, \ @@ -75,6 +75,7 @@ from django.db import connection from donate.utils import send_transaction_email from .certificates import * +from spoken.config import BASIC_LEVEL_INSTITUTIONS from spoken.config import BASIC_LEVEL_INSTITUTIONS diff --git a/spoken/helpers.py b/spoken/helpers.py index 488ee1127..4a2dae398 100644 --- a/spoken/helpers.py +++ b/spoken/helpers.py @@ -10,12 +10,16 @@ from events.models import Testimonials from cms.models import Notification, Event from .config import CACHE_RANDOM_TUTORIALS, CACHE_TR_REC, CACHE_TESTIMONIALS, CACHE_NOTIFICATIONS, CACHE_EVENTS +from cms.cache_registry import register_cache_key + # ---- 1. Random tutorials from TutorialSummaryCache ---- def get_home_random_tutorials(): cache_key = "home_random_tutorials" tutorials = cache.get(cache_key) + print("tuto",tutorials) if tutorials is not None: + print("tuturial exit",list(tutorials)) return tutorials try: ids = list( @@ -31,8 +35,10 @@ def get_home_random_tutorials(): "foss", "first_tutorial", "first_tutorial__tutorial_detail", "first_tutorial__language") ) cache.set(cache_key, tutorials, timeout=CACHE_RANDOM_TUTORIALS) # in sec + # register_cache_key(cache_key) except Exception: tutorials = [] + print("exceptionnnnn",tutorials) return tutorials @@ -51,6 +57,7 @@ def get_home_tr_rec(request=None): else: tr_rec = None cache.set(cache_key, tr_rec, timeout=CACHE_TR_REC) #seconds + register_cache_key(cache_key) except Exception as e: tr_rec = None if request is not None: @@ -66,6 +73,7 @@ def get_home_testimonials(): return testimonials testimonials = Testimonials.objects.all().order_by("?")[:2] cache.set(cache_key, testimonials, timeout=CACHE_TESTIMONIALS) # seconds + register_cache_key(cache_key) return testimonials # ---- 4. Notifications (shorter cache) ---- @@ -77,6 +85,7 @@ def get_home_notifications(): today = dt.datetime.today() notifications = Notification.objects.filter(Q(start_date__lte=today) & Q(expiry_date__gte=today)).order_by("expiry_date") cache.set(cache_key, notifications, timeout=CACHE_NOTIFICATIONS) + register_cache_key(cache_key) return notifications # ---- 5. Upcoming events ---- @@ -88,4 +97,5 @@ def get_home_events(): today = dt.datetime.today() events = Event.objects.filter(event_date__gte=today).order_by("event_date")[:2] cache.set(cache_key, events, timeout=CACHE_EVENTS) + register_cache_key(cache_key) return events diff --git a/spoken/urls.py b/spoken/urls.py index f29dbd6ed..c91cb8910 100644 --- a/spoken/urls.py +++ b/spoken/urls.py @@ -13,10 +13,16 @@ from donate.views import ilw_payment_callback from donate.payment import check_ilw_payment_status +# import debug_toolbar + +from django.conf import settings + + app_name = 'spoken' admin.autodiscover() urlpatterns = [ + # url(r'^__debug__/', include(debug_toolbar.urls)), url(r'^robots\.txt', robots_txt, name='robots-txt'), #url(r'^sitemap\.xml$', TemplateView.as_view(template_name='sitemap.xml', content_type='text/xml')), url(r'^sitemap\.html$', sitemap, name='sitemap'), @@ -142,10 +148,23 @@ #donation url(r'^donate/', include('donate.urls', namespace='donate')), + #Caches + url(r'^system-tools/', include('cms.cacheurls')), + + # cms url(r'^', include('cms.urls', namespace='cms')), - + + #nep book fiar url(r'wbf-book-fair-2023', bookfair,name="bookfair"), ] + static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT) + +if settings.DEBUG: + import debug_toolbar + urlpatterns += [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + +