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 %} +
{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)),
+ ]
+
+