/', RetrieveUpdateRevisionView.as_view(), name='revision_detail'),
+]
diff --git a/website/interview_exp/api/views.py b/website/interview_exp/api/views.py
new file mode 100644
index 00000000..08edf7cc
--- /dev/null
+++ b/website/interview_exp/api/views.py
@@ -0,0 +1,270 @@
+from django.contrib.auth.models import User
+from django.db.models import Q
+from django.contrib.sites.shortcuts import get_current_site
+from django.template.loader import render_to_string
+from django.core.mail import EmailMessage
+from django.shortcuts import get_object_or_404
+from django.conf import settings
+from django.core.mail import send_mass_mail
+from rest_framework.permissions import IsAdminUser, IsAuthenticated, AllowAny
+from rest_framework.decorators import api_view, throttle_classes
+from rest_framework.throttling import AnonRateThrottle
+from rest_framework.response import Response
+from rest_framework.filters import SearchFilter, OrderingFilter
+from rest_framework.generics import (
+ CreateAPIView,
+ ListAPIView,
+ ListCreateAPIView,
+ RetrieveUpdateAPIView,
+)
+from rest_framework.serializers import ValidationError
+from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
+from rest_framework_simplejwt.views import TokenObtainPairView
+from django_filters import rest_framework as filters
+from decouple import config
+from .filters import ExperiencesFilter
+from interview_exp.models import Experiences, Revisions
+from user_profile.models import Profile
+from user_profile.utils import ThreadedMailing
+from user_profile.utils_permissions import ViewUpdatePermission, IsMemberOrAbove
+
+from .serializers import (
+ IESerializer,
+ RevisionSerializer
+)
+
+# email only for trial purposes
+
+
+class IEListView(ListCreateAPIView):
+ permission_classes = (IsAuthenticated,)
+ serializer_class = IESerializer
+ filter_backends = (filters.DjangoFilterBackend, SearchFilter, OrderingFilter)
+ filterset_class = ExperiencesFilter
+ search_fields = ['company', 'user__username', 'year']
+ ordering_fields = ['updated_at', 'total_Compensation', 'year']
+
+ def get_queryset(self):
+ qs = Experiences.objects.all()
+ user = self.request.user
+ if user.profile.role == '3':
+ return qs.filter(Q(user=user) | Q(verification_Status='Approved'))
+ return qs
+
+ def perform_create(self, serializer):
+ # TODO
+ # send thank you mail for posting
+ user = self.request.user
+ exp = serializer.save(user=user, verification_Status='Review Pending')
+
+ ### mailing part
+
+ # # region old mailing
+ # profiles = Profile.objects.filter(~Q(role = '3'))
+ # messages = ()
+ # f=exp
+ # for profile in profiles:
+ # user = profile.user
+ # current_site = get_current_site(self.request)
+ # subject = 'New Activity in Interview Experiences Section'
+ # message = render_to_string('new_experience_entry_email.html', {
+ # 'user': user,
+ # 'domain': current_site.domain,
+ # 'experience': Experiences.objects.get(pk=f.id),
+ # })
+ # msg = (subject, message, 'webmaster@localhost', ["example@gmail.com",])
+ # if msg not in messages:
+ # messages += (msg,)
+ # result = send_mass_mail(messages, fail_silently=False)
+ # # endregion
+
+ # region new mailing
+ member_users = User.objects.filter(~Q(profile__role='3'))
+ domain = get_current_site(self.request).domain
+ subject = 'New Activity in Interview Experiences Section'
+ messages = [
+ EmailMessage(
+ subject,
+ render_to_string(
+ 'new_experience_entry_email.html',
+ {
+ 'user': user,
+ 'domain': domain,
+ 'experience': exp,
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
+ }
+ ),
+ # to=[user.email, ],
+ to=[user.email, ],
+ )
+ for user in member_users
+ ]
+ # uncomment below to mail
+ threaded_mail = ThreadedMailing(messages, fail_silently=False, verbose=1)
+ threaded_mail.start()
+ # endregion
+
+
+class RetrieveUpdateIEView(RetrieveUpdateAPIView):
+ queryset = Profile.objects.all()
+ serializer_class = IESerializer
+ permission_classes = (ViewUpdatePermission,)
+ lookup_field = 'id'
+ lookup_url_kwarg = 'slug'
+
+ def get_queryset(self):
+ user = self.request.user
+ qs = Experiences.objects.all()
+ if user.profile.role == '3':
+ return qs.filter(Q(user=user) | Q(verification_Status='Approved'))
+ return qs
+
+ def perform_update(self, serializer):
+ exp = serializer.save()
+ curr_status = exp.verification_Status
+ emails = []
+ if curr_status == 'Approved':
+ exp.verification_Status = 'Review Pending'
+ elif curr_status == 'Changes Requested':
+ revision = Revisions.objects.filter(experience=exp)
+ if revision.exists():
+ revision = revision.first()
+ reviewer = revision.reviewer
+ else: # send the mail to a random member/superuser(exceptional case that revision is not found)
+ reviewer = Profile.objects.filter(~Q(role='3')).first()
+ ## mailing part
+ domain = get_current_site(self.request).domain
+ subject = 'New Activity in Interview Experiences Section'
+ messages = [
+ EmailMessage(
+ subject,
+ render_to_string(
+ 'update_experience_email.html',
+ {
+ 'user': reviewer,
+ 'domain': domain,
+ 'experience': exp,
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
+ }
+ ),
+ to=[reviewer.email, ], # to=[reviewer.email, ]
+ )
+ ]
+ # uncomment below to mail
+ threaded_mail = ThreadedMailing(messages, fail_silently=True, verbose=1)
+ threaded_mail.start()
+ else:
+ member_users = User.objects.filter(~Q(profile__role='3'))
+ domain = get_current_site(self.request).domain
+ subject = 'New Activity in Interview Experiences Section'
+ messages = [
+ EmailMessage(
+ subject,
+ render_to_string(
+ 'update_Experience_to_all_email.html',
+ {
+ 'user': user,
+ 'domain': domain,
+ 'experience': exp,
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
+ }
+ ),
+ to=[user.email, ], # to=[reviewer.email, ]
+ )
+ for user in member_users
+ ]
+ # uncomment below to mail
+ threaded_mail = ThreadedMailing(messages, fail_silently=True, verbose=1)
+ threaded_mail.start()
+ exp.save()
+
+
+class RevisionsListView(ListAPIView):
+ serializer_class = RevisionSerializer
+
+ permission_classes = (IsMemberOrAbove,)
+
+ def get_queryset(self):
+ qs = Revisions.objects.prefetch_related('experience').prefetch_related('reviewer').all()
+ return qs
+
+
+class RetrieveUpdateRevisionView(RetrieveUpdateAPIView):
+ serializer_class = RevisionSerializer
+ permission_classes = (IsMemberOrAbove,)
+ lookup_field = 'experience_id'
+ lookup_url_kwarg = 'id'
+
+ def get_queryset(self):
+ qs = Revisions.objects.prefetch_related('experience').prefetch_related('reviewer').all()
+ if (qs.first().experience.verification_Status=="Approved"):
+ Revisions.objects.filter(experience=qs.first().experience).update(message="No Changes Required")
+ return qs
+
+
+class CreateRevision(CreateAPIView):
+ serializer_class = RevisionSerializer
+ permission_classes = (IsMemberOrAbove,)
+
+ def perform_create(self, serializer):
+ """
+ the review codes are:
+ `acc` -> Accepted
+ `rev` -> Review Pending
+ `chg` -> Changes Requested
+ """
+ req = self.request
+ curr_user = req.user
+ exp_id = self.kwargs.get('exp_id')
+ exp = get_object_or_404(Experiences, pk=exp_id)
+ review_code = self.kwargs.get('review_code', '')
+ print(self.kwargs)
+ if review_code == 'acc': # Accepted
+ exp.verification_Status = 'Approved'
+ exp.verifier = curr_user
+ # TODO
+ # Send mail to the author about publication
+ exp.save()
+ revisions = Revisions.objects.filter(experience=exp)
+ revisions.delete()
+ else:
+ msg = serializer.validated_data.get('message')
+
+ if not msg:
+ raise ValidationError('Must provide message if not being accepted.')
+ domain = get_current_site(self.request).domain
+ exp_author = exp.user
+ subject = 'New Activity in Interview Experiences Section'
+ messages = [
+ EmailMessage(
+ subject,
+ render_to_string(
+ 'changes_requested_email.html',
+ {
+ 'user': exp.user,
+ 'domain': domain,
+ 'experience': exp,
+ 'message': msg,
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
+ }
+ ),
+ to=[exp.user.email], # exp.user.email
+
+ )
+ ]
+
+ if review_code == 'rev':
+ # doesn't make sense at all
+ exp.verification_Status = 'Review Pending'
+ elif review_code == 'chg':
+ exp.verification_Status = 'Changes Requested'
+ exp.save()
+ revision, rev_created = Revisions.objects.get_or_create(experience=exp,
+ defaults={'reviewer': curr_user,
+ 'message': msg})
+ else:
+ raise ValidationError("Invalid code for verification status.")
+
+ # uncomment below to mail
+ threaded_mail = ThreadedMailing(messages, verbose=1)
+ threaded_mail.start()
diff --git a/website/interview_exp/models.py b/website/interview_exp/models.py
index 6879ee91..0e9f9f8d 100644
--- a/website/interview_exp/models.py
+++ b/website/interview_exp/models.py
@@ -4,16 +4,21 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from markdownx.models import MarkdownxField
from markdownx.utils import markdownify
+from django.urls import reverse
+from django_prometheus.models import ExportModelOperationsMixin
+
# Create your models here.
def current_year():
return datetime.date.today().year
+
def max_value_current_year(value):
return MaxValueValidator(current_year())(value)
-class Experiences(models.Model):
+
+class Experiences(ExportModelOperationsMixin('experience'), models.Model):
company = models.CharField(max_length=100)
year = models.PositiveIntegerField(
default=current_year(), validators=[MinValueValidator(1984), max_value_current_year])
@@ -46,7 +51,7 @@ class Experiences(models.Model):
@property
def formatted_markdown(self):
return markdownify(self.interview_Questions)
-
+
def __str__(self):
return str(self.company) + " " + str(self.user)
@@ -54,6 +59,9 @@ def get_cname(self):
class_name = "Experiences"
return class_name
+ def get_absolute_url(self):
+ return reverse('experiences_api:ie_detail', kwargs={'slug': self.id})
+
class Meta:
managed = True
ordering = ['-year', '-created_at']
@@ -61,7 +69,7 @@ class Meta:
verbose_name_plural = 'Experiences'
-class Revisions(models.Model):
+class Revisions(ExportModelOperationsMixin('revision'), models.Model):
experience = models.OneToOneField(Experiences, on_delete=models.CASCADE)
reviewer = models.ForeignKey(User, on_delete=models.CASCADE)
message = models.TextField()
@@ -77,6 +85,9 @@ def get_cname(self):
class_name = "Revisions"
return class_name
+ def get_absolute_url(self):
+ return reverse('experiences_api:revision_detail', kwargs={'id': self.id})
+
class Meta:
managed = True
ordering = ['-created_at']
diff --git a/website/interview_exp/templates/changes_requested_email.html b/website/interview_exp/templates/changes_requested_email.html
index 2d2e5a36..7f06a1a1 100644
--- a/website/interview_exp/templates/changes_requested_email.html
+++ b/website/interview_exp/templates/changes_requested_email.html
@@ -2,9 +2,11 @@
Hi {{ user.username }},
Changes were requested in your Interview Experience by some Member/Admin.
+They Said:
+"{{ message }}"
Kindly, make them by clicking on the link below:
-http://{{ domain }}{% url 'interview_exp:detail_experiences' experience.id %}
+{{ FRONTEND_BASE_URL }}{% url 'interview_exp:detail_experiences' experience.id %}
Sincerely,
RECursion Team
diff --git a/website/interview_exp/templates/exp_detail.html b/website/interview_exp/templates/exp_detail.html
index f109dee2..60a6aebe 100644
--- a/website/interview_exp/templates/exp_detail.html
+++ b/website/interview_exp/templates/exp_detail.html
@@ -52,7 +52,11 @@
{{ experience.formatted_markdown | safe }}
-
+ {% if profile and profile.image %}
+
+ {% else %}
+
+ {% endif %}
 
{{experience.user}}
diff --git a/website/interview_exp/templates/exp_list.html b/website/interview_exp/templates/exp_list.html
index 976bac44..f0024f48 100644
--- a/website/interview_exp/templates/exp_list.html
+++ b/website/interview_exp/templates/exp_list.html
@@ -1,32 +1,51 @@
+{% load static %}
+
{% for experience in experiences %}
-
+
{% for p in profile %}
- {% if p.user == experience.user %}
-

- {% endif %}
+ {% if p.user == experience.user %}
+ {% if p.image %}
+

+ {% else %}
+

+ {% endif %}
+ {% endif %}
{% endfor %}
+
-
- {% endfor %}
+
+{% endfor %}
diff --git a/website/interview_exp/templates/new_experience_entry_email.html b/website/interview_exp/templates/new_experience_entry_email.html
index cbca3274..9e224339 100644
--- a/website/interview_exp/templates/new_experience_entry_email.html
+++ b/website/interview_exp/templates/new_experience_entry_email.html
@@ -4,7 +4,7 @@
A new Interview Experience has been Added in Interview Experiences Section.
Please, verify it by clicking on the link below:
-http://{{ domain }}{% url 'interview_exp:detail_experiences' experience.id %}
+{{ FRONTEND_BASE_URL }}{% url 'interview_exp:detail_experiences' experience.id %}
Sincerely,
RECursion Team
diff --git a/website/interview_exp/templates/update_Experience_to_all_email.html b/website/interview_exp/templates/update_Experience_to_all_email.html
index 60e8081b..eeccfcdf 100644
--- a/website/interview_exp/templates/update_Experience_to_all_email.html
+++ b/website/interview_exp/templates/update_Experience_to_all_email.html
@@ -4,7 +4,7 @@
An Interview Experience has been updated in Interview Experiences Section.
Please, verify it by clicking on the link below:
-http://{{ domain }}{% url 'interview_exp:detail_experiences' experience.id %}
+{{ FRONTEND_BASE_URL }}{% url 'interview_exp:detail_experiences' experience.id %}
Sincerely,
RECursion Team
diff --git a/website/interview_exp/templates/update_experience_email.html b/website/interview_exp/templates/update_experience_email.html
index c6cee9af..2b67b5c7 100644
--- a/website/interview_exp/templates/update_experience_email.html
+++ b/website/interview_exp/templates/update_experience_email.html
@@ -4,7 +4,7 @@
An Interview Experience you reviewed has been updated in Interview Experience Section.
Please, verify it by clicking on the link below:
-http://{{ domain }}{% url 'interview_exp:detail_experiences' experience.id %}
+{{ FRONTEND_BASE_URL }}{% url 'interview_exp:detail_experiences' experience.id %}
Sincerely,
RECursion Team
diff --git a/website/interview_exp/views.py b/website/interview_exp/views.py
index a5f2f5b6..60145078 100644
--- a/website/interview_exp/views.py
+++ b/website/interview_exp/views.py
@@ -1,4 +1,5 @@
from django.shortcuts import render, redirect,get_object_or_404, get_list_or_404
+from utils.content_filter import check_content_safety
from .models import *
from .forms import *
from django.http import HttpResponse, HttpResponseRedirect
@@ -11,12 +12,23 @@
from django.contrib.sites.shortcuts import get_current_site
from django.template.loader import render_to_string
from django.core.mail import send_mass_mail
+from django.conf import settings
+from django_ratelimit.decorators import ratelimit
+
@login_required
+@ratelimit(key='user', rate='1/10m', method='POST', block=True)
def add_experience(request):
form = ExperienceForm(request.POST or None)
if form.is_valid():
+ interview_Questions = form.cleaned_data.get('interview_Questions')
+ company = form.cleaned_data.get('company')
+ job_profile = form.cleaned_data.get('job_Profile')
+ title = f"{company} - {job_profile}"
+ if not check_content_safety(interview_Questions, title):
+ form.add_error('interview_Questions', "Your post contains irrelevant/inappropriate content and cannot be published.")
+ return render(request, 'experience-form.html', {'form': form})
f = form.save(commit=False)
f.user = request.user
f.save()
@@ -31,8 +43,9 @@ def add_experience(request):
'user': user,
'domain': current_site.domain,
'experience': Experiences.objects.get(pk=f.id),
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
})
- msg = (subject, message, 'webmaster@localhost', [user.email])
+ msg = (subject, message, settings.SERVER_EMAIL, [user.email])
if msg not in messages:
messages += (msg,)
result = send_mass_mail(messages, fail_silently=False)
@@ -76,8 +89,9 @@ def update_experience(request, id):
'user': user,
'domain': current_site.domain,
'experience': Experiences.objects.get(pk=experience.id),
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
})
- msg = (subject, message, 'webmaster@localhost', [user.email])
+ msg = (subject, message, settings.SERVER_EMAIL, [user.email])
if msg not in messages:
messages += (msg,)
result = send_mass_mail(messages, fail_silently=False)
@@ -93,8 +107,9 @@ def update_experience(request, id):
'user': user,
'domain': current_site.domain,
'experience': Experiences.objects.get(pk=experience.id),
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
})
- msg = (subject, message, 'webmaster@localhost', [user.email])
+ msg = (subject, message, settings.SERVER_EMAIL, [user.email])
if msg not in messages:
messages += (msg,)
result = send_mass_mail(messages, fail_silently=False)
@@ -290,8 +305,9 @@ def revise_experience(request, id, action):
'user': user,
'domain': current_site.domain,
'experience': Experiences.objects.get(pk=experience.id),
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
})
- msg = (subject, message, 'webmaster@localhost', [user.email])
+ msg = (subject, message, settings.SERVER_EMAIL, [user.email])
if msg not in messages:
messages += (msg,)
result = send_mass_mail(messages, fail_silently=False)
@@ -317,8 +333,9 @@ def revise_experience(request, id, action):
'user': user,
'domain': current_site.domain,
'experience': Experiences.objects.get(pk=experience.id),
+ 'FRONTEND_BASE_URL': settings.FRONTEND_BASE_URL,
})
- msg = (subject, message, 'webmaster@localhost', [user.email])
+ msg = (subject, message, settings.SERVER_EMAIL, [user.email])
if msg not in messages:
messages += (msg,)
result = send_mass_mail(messages, fail_silently=False)
diff --git a/website/package-lock.json b/website/package-lock.json
new file mode 100644
index 00000000..4ff32e61
--- /dev/null
+++ b/website/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "website",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
diff --git a/website/team/api/__init__.py b/website/team/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/website/team/api/filters.py b/website/team/api/filters.py
new file mode 100644
index 00000000..6e5af6af
--- /dev/null
+++ b/website/team/api/filters.py
@@ -0,0 +1,10 @@
+from django_filters import rest_framework as filters
+from team.models import Members
+
+
+class MembersFilter(filters.FilterSet):
+ batch_year = filters.NumberFilter(field_name='batch_year', label='batch')
+
+ class Meta:
+ model = Members
+ fields = ['batch_year', ]
diff --git a/website/team/api/permissions.py b/website/team/api/permissions.py
new file mode 100644
index 00000000..06d8e7ff
--- /dev/null
+++ b/website/team/api/permissions.py
@@ -0,0 +1,11 @@
+from rest_framework.permissions import BasePermission, SAFE_METHODS
+
+
+class MembersListCreatePermission(BasePermission):
+ def has_permission(self, request, view):
+ if request.method in SAFE_METHODS:
+ return True
+ if not request.user.is_authenticated:
+ return False
+ role = request.user.profile.role
+ return role in ('1', '2')
diff --git a/website/team/api/serilazers.py b/website/team/api/serilazers.py
new file mode 100644
index 00000000..bfd24a6e
--- /dev/null
+++ b/website/team/api/serilazers.py
@@ -0,0 +1,9 @@
+from rest_framework import serializers
+
+from team.models import Members
+
+
+class MemberSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Members
+ exclude = ('id',)
diff --git a/website/team/api/urls.py b/website/team/api/urls.py
new file mode 100644
index 00000000..ff69082f
--- /dev/null
+++ b/website/team/api/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path
+
+from .views import (
+ ListCreateMembersView,
+ AlumniYearWiseView
+)
+
+app_name = 'team_api'
+
+urlpatterns = [
+ path('', ListCreateMembersView.as_view(), name='members_list'),
+ path('alumni/', AlumniYearWiseView.as_view(), name='alumni_year_wise'),
+]
diff --git a/website/team/api/views.py b/website/team/api/views.py
new file mode 100644
index 00000000..5f1acd43
--- /dev/null
+++ b/website/team/api/views.py
@@ -0,0 +1,40 @@
+from django_filters import rest_framework as filters
+from rest_framework.generics import ListCreateAPIView
+from rest_framework.filters import SearchFilter, OrderingFilter
+
+from team.models import Members
+from .serilazers import MemberSerializer
+from .permissions import MembersListCreatePermission
+from .filters import MembersFilter
+
+from rest_framework import generics
+from team.models import Members
+from rest_framework.permissions import IsAuthenticatedOrReadOnly
+from django_filters import rest_framework as filters
+# SCOPE FOR CACHING #
+
+class ListCreateMembersView(ListCreateAPIView):
+ serializer_class = MemberSerializer
+ permission_classes = (MembersListCreatePermission,)
+ pagination_class = None
+ filter_backends = (SearchFilter, OrderingFilter, filters.DjangoFilterBackend)
+ filterset_class = MembersFilter
+ search_fields = ['name', 'branch', 'designation']
+ ordering_fields = ['batch_year', 'name', 'designation', 'branch']
+
+ def get_queryset(self):
+ return Members.objects.all()
+
+
+
+
+class AlumniYearWiseView(generics.ListAPIView):
+ serializer_class = MemberSerializer
+ filter_backends = (filters.DjangoFilterBackend,)
+ filterset_class = MembersFilter
+
+ def get_queryset(self):
+ year = self.request.query_params.get('year', None)
+ if year:
+ return Members.objects.filter(batch_year=year).order_by('-batch_year', 'name')
+ return Members.objects.all().order_by('-batch_year', 'name') # Default to all alumni if no year is specified
diff --git a/website/team/templates/team/team.html b/website/team/templates/team/team.html
index a19a6e0c..39bdfd3d 100644
--- a/website/team/templates/team/team.html
+++ b/website/team/templates/team/team.html
@@ -9,149 +9,170 @@
-
Meet The Team
-
-
-
- {% for p in presi %}
-
-
-
-

-
-
-
{{ p.name }}
-
{{ p.designation }}
-
-
- {% endfor %}
-
- {% for c in convener %}
-
-
-
-

-
-
-
{{ c.name }}
-
{{ c.designation }}
-
-
- {% endfor %}
-
- {% for t in treasurer %}
-
-
-
-

-
-
-
{{ t.name }}
-
{{ t.designation }}
-
-
- {% endfor %}
-
- {% for v in vice_presi %}
-
-
-
-

-
-
-
{{ v.name }}
-
{{ v.designation }}
-
-
- {% endfor %}
-
- {% for g in gen_sec %}
-
-
-
-

-
-
-
{{ g.name }}
-
{{ g.designation }}
-
-
- {% endfor %}
-
+
+ Meet The Team
+
+
+
+
+ {% for p in presi %}
+
+
+
+

+
-
+
{{ p.name }}
+
{{ p.designation }}
+
+
+ {% endfor %}
+
+ {% for v in vice_presi %}
+
+
+
+

+
+
+
{{ v.name }}
+
{{ v.designation }}
+
+
+ {% endfor %}
+
+ {% for t in treasurer %}
+
+
+
+

+
+
+
{{ t.name }}
+
{{ t.designation }}
+
+
+ {% endfor %}
+
+
+
+
+ {% for c in convener %}
+
+
+
+

+
+
+
{{ c.name }}
+
{{ c.designation }}
+
+
+ {% endfor %}
+
+ {% for g in gen_sec %}
+
+
+
+

+
+
+
{{ g.name }}
+
{{ g.designation }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ {% for member in members %}
+ {% if member.designation != "President" and member.designation != "Convenor" and member.designation != "Treasurer" and member.designation != "Vice President" and member.designation != "General Secretary" %}
+
+
+
+

+
+
+
{{ member.name }}
+
{{ member.designation }}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
-
-
-
+
+
+ Meet our Alumni
+
+
+
- {% for member in members %}
- {% if member.designation != "President" and member.designation != "Convener" and member.designation != "Treasurer" and member.designation != "Vice President" and member.designation != "General Secretary" %}
-
-
- 
- {{ member.name }}
- {{ member.designation }}
-
-
- {% endif %}
- {% endfor %}
+ {% for year in year_set %}
-
-
-
-
-
Meet our Alumni
-
-
-
- {% for year in year_set %}
-
-
Batch of {{ year }}
-
- {% for a in alumni %}
- {% if a.batch_year == year %}
-
-
-
-
-
-

-
-
-
-
-
-
- {% endif %}
- {% endfor %}
- {% endfor %}
+
+ Batch of {{ year }}
+
+
+ {% for a in alumni %}
+ {% if a.batch_year == year %}
+
+
+
+
+

+
+
+
-{% endblock %}
+
+ {% endif %}
+ {% endfor %}
+ {% endfor %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/website/team/views.py b/website/team/views.py
index c142869d..bf271e29 100644
--- a/website/team/views.py
+++ b/website/team/views.py
@@ -22,7 +22,7 @@ def team_page(request):
year_set.append(a.batch_year)
presi = Members.objects.filter(batch_year = curr_batch_year, designation = "President")
- convener = Members.objects.filter(batch_year=curr_batch_year, designation="Convener")
+ convener = Members.objects.filter(batch_year=curr_batch_year, designation="Convenor")
treasurer = Members.objects.filter(batch_year=curr_batch_year, designation="Treasurer")
vice_presi = Members.objects.filter(batch_year=curr_batch_year, designation="Vice President")
gen_sec = Members.objects.filter(batch_year=curr_batch_year, designation="General Secretary")
diff --git a/website/user_profile/api/__init__.py b/website/user_profile/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/website/user_profile/api/serializers.py b/website/user_profile/api/serializers.py
new file mode 100644
index 00000000..d15a6656
--- /dev/null
+++ b/website/user_profile/api/serializers.py
@@ -0,0 +1,121 @@
+from rest_framework import serializers
+from rest_framework.validators import UniqueValidator
+from django.contrib.auth.models import User
+
+from user_profile.models import Profile
+from user_profile.utils import LowerEmailField
+
+
+# class RegistrationSerializer(serializers.ModelSerializer):
+# password2 = serializers.CharField(style={'input_type': 'password'}, write_only=True)
+# email = LowerEmailField(
+# required=True,
+# allow_blank=False,
+# label='Email address',
+# max_length=30,
+# validators=[UniqueValidator(queryset=User.objects.all())],
+# )
+
+# class Meta:
+# model = User
+# fields = ['username', 'email', 'password', 'password2']
+# extra_kwargs = {
+# 'password': {'write_only': True}
+# }
+
+# def save(self):
+# password = self.validated_data['password']
+# password2 = self.validated_data['password2']
+# if password != password2:
+# raise serializers.ValidationError({'confirm_password': 'Passwords must match!'})
+# account = User(
+# username=self.validated_data['username'],
+# email=self.validated_data['email'].lower(),
+# is_active=False # TO BE CHANGED TO FALSE
+# )
+# account.set_password(password)
+# account.save()
+# return account
+
+class RegistrationSerializer(serializers.ModelSerializer):
+ password2 = serializers.CharField(write_only=True)
+ dept = serializers.CharField(max_length=70, required=False, allow_blank=True)
+
+ email = LowerEmailField(
+ required=True,
+ allow_blank=False,
+ max_length=30,
+ validators=[UniqueValidator(queryset=User.objects.all())],
+ )
+
+ class Meta:
+ model = User
+ fields = ['username', 'email', 'password', 'password2', 'dept']
+ extra_kwargs = {
+ 'password': {'write_only': True}
+ }
+
+ def validate(self, attrs):
+ if attrs['password'] != attrs['password2']:
+ raise serializers.ValidationError({
+ 'password2': 'Passwords must match!'
+ })
+ return attrs
+
+ def create(self, validated_data):
+ validated_data.pop('password2')
+ dept = validated_data.pop('dept', None)
+
+ user = User.objects.create_user(
+ username=validated_data['username'],
+ email=validated_data['email'].lower(),
+ password=validated_data['password'],
+ is_active=False
+ )
+
+ if dept:
+ user.profile.dept = dept
+ user.profile.save()
+
+ return user
+
+
+
+class UserSerializer(serializers.ModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='user_profile_api:user_detail', lookup_field='username',
+ lookup_url_kwarg='username')
+
+ class Meta:
+ model = User
+ fields = ['username', 'email', 'url']
+
+
+class ProfileSerializer(serializers.ModelSerializer):
+ user = UserSerializer()
+ role = serializers.CharField(source='get_role_display', )
+
+ class Meta:
+ model = Profile
+ exclude = ('id',)
+ read_only_fields = ('user', 'role', 'email_confirmed', 'created_at', 'updated_at',)
+
+
+class PasswordResetRequestSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+
+ def validate_email(self, value):
+ if not User.objects.filter(email=value.lower()).exists():
+ raise serializers.ValidationError("No user with that Email exists.")
+ return value.lower()
+
+
+class PasswordResetConfirmSerializer(serializers.Serializer):
+ uidb64 = serializers.CharField()
+ token = serializers.CharField()
+ password = serializers.CharField(write_only=True)
+ confirm_password = serializers.CharField(write_only=True)
+
+ def validate(self, attrs):
+ if attrs['password'] != attrs['confirm_password']:
+ raise serializers.ValidationError({"confirm_password": "Passwords must match."})
+ return attrs
diff --git a/website/user_profile/api/urls.py b/website/user_profile/api/urls.py
new file mode 100644
index 00000000..1bc0e76e
--- /dev/null
+++ b/website/user_profile/api/urls.py
@@ -0,0 +1,42 @@
+from django.urls import path
+
+from .views import (
+ RegistrationView,
+ # ProfileRegistrationView,
+ ListProfileView,
+ RetrieveUpdateProfileView,
+ UserSearchView,
+ username_existence_check,
+ email_existence_check,
+ username_existence_check,
+ email_existence_check,
+ GetProfileRoleView,
+ RequestPasswordResetAPI,
+ ResetPasswordConfirmAPI,
+)
+from user_profile.views import activate
+
+app_name = 'user_profile_api'
+
+urlpatterns = [
+ # Profile related(except first)
+ path('register/', RegistrationView.as_view(), name='user_registration'),
+ path('', ListProfileView.as_view(), name='all_users_list'),
+ # path('resend-user-activation/', resendVerificationView, name='resend-user-activation'),
+
+ # Password Reset
+ path('password-reset/', RequestPasswordResetAPI.as_view(), name='password_reset_api'),
+ path('password-reset-confirm/', ResetPasswordConfirmAPI.as_view(), name='password_reset_confirm_api'),
+
+ # Specific Checks and Views
+ path('search', UserSearchView.as_view(), name='user_search'),
+ path('username-check', username_existence_check, name='username_exists'),
+ path('email-check', email_existence_check, name='email_exists'),
+ path('roles', GetProfileRoleView.as_view(), name='roles'),
+
+ # Account Activation
+ path('activate/
//', activate, name='activate'),
+
+ # Profile Detail (Catch-all)
+ path('/', RetrieveUpdateProfileView.as_view(), name='user_detail'),
+]
diff --git a/website/user_profile/api/views.py b/website/user_profile/api/views.py
new file mode 100644
index 00000000..4f242e72
--- /dev/null
+++ b/website/user_profile/api/views.py
@@ -0,0 +1,267 @@
+from django.contrib.auth.models import User
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework.permissions import IsAdminUser, IsAuthenticated
+from rest_framework.decorators import api_view, throttle_classes
+from rest_framework.throttling import AnonRateThrottle
+from rest_framework.response import Response
+from rest_framework.generics import (
+ CreateAPIView,
+ ListAPIView,
+ RetrieveUpdateAPIView,
+)
+from rest_framework.serializers import ValidationError
+from django.contrib.sites.shortcuts import get_current_site
+from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
+from rest_framework_simplejwt.views import TokenObtainPairView
+from rest_framework.permissions import IsAdminUser, IsAuthenticated, AllowAny
+
+from user_profile.models import Profile
+from user_profile.utils import send_verification_mail
+from user_profile.utils_permissions import ViewUpdatePermission
+from user_profile.utils import ProfileMatcher
+
+import requests
+from rest_framework_simplejwt.tokens import RefreshToken
+from django.conf import settings
+from django.core.files.base import ContentFile
+
+
+from .serializers import (
+ RegistrationSerializer,
+ UserSerializer,
+ UserSerializer,
+ ProfileSerializer,
+ PasswordResetRequestSerializer,
+ PasswordResetConfirmSerializer,
+)
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
+from user_profile.tokens import password_reset_token
+from django.template.loader import render_to_string
+
+
+
+# TODO
+# 1) Email verification and resend email verification
+
+# For customised tokens. Don't use if not needed
+class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
+ def validate(self, attrs):
+ data = super().validate(attrs)
+ refresh = self.get_token(self.user)
+ data['refresh'] = str(refresh)
+ data['access'] = str(refresh.access_token)
+
+ # data['isStudent'] = self.user.groups.first().name == 'student'
+ # data['user'] = UserSerializer(self.user).data
+ return data
+
+
+# Google Login api handler
+class LoginWithGoogleView(APIView):
+
+ def generate_token(self,user):
+ refresh = RefreshToken.for_user(user)
+
+ refresh['email'] = user.email
+
+ return {
+ 'refresh': str(refresh),
+ 'access': str(refresh.access_token),
+ }
+
+ def post(self, request):
+ access_token = request.data.get('token')
+ GOOGLE_CLIENT_ID = getattr(settings, "GOOGLE_CLIENT_ID", None)
+ if not GOOGLE_CLIENT_ID:
+ GOOGLE_CLIENT_ID = getattr(settings, "SOCIAL_AUTH_GOOGLE_OAUTH2_KEY", None)
+
+ try:
+ TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=" + access_token
+ headers = {
+ "Authorization": f"Bearer {access_token}",
+ "Content-Type": "application/json"
+ }
+ token_info = requests.get(TOKEN_INFO_URL, data={}, headers=headers).json()
+
+ if 'error' in token_info:
+ print(f"Google API Error: {token_info}")
+ return Response(data={'response': 'Invalid token from Google'}, status=400)
+
+ if token_info['issued_to'] == GOOGLE_CLIENT_ID:
+ try:
+ USER_INFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo?access_token=" + access_token
+ user_info = requests.get(USER_INFO_URL, data={}, headers=headers).json()
+ user, created = User.objects.get_or_create(
+ username=user_info['email'].split('@')[0],
+ defaults={
+ 'first_name': user_info.get('name', ''),
+ 'last_name': user_info.get('given_name', ''),
+ 'email': user_info['email']
+ }
+ )
+
+ if created:
+ try:
+ profile = user.profile
+ profile.name = user_info.get('name', '')
+ picture_url = user_info.get('picture')
+ if picture_url:
+ image_response = requests.get(picture_url)
+ if image_response.status_code == 200:
+ profile.image.save(f"{user.username}_google.jpg", ContentFile(image_response.content), save=False)
+ profile.save()
+ except Exception as e:
+ print(f"Error saving user profile data from Google: {e}")
+
+ tokens = self.generate_token(user)
+ return Response(data={'access': tokens['access'], 'refresh': tokens['refresh'], 'response': 'valid', 'is_new_user': created}, status=200)
+ except Exception as e:
+ print(f"User creation/retrieval error: {e}")
+ return Response(data={'response': 'Unauthorized'}, status=401)
+ else:
+ print(f"Client ID mismatch. Expected: {GOOGLE_CLIENT_ID}, Got: {token_info.get('issued_to')}")
+ return Response(data={'response': 'Unauthorized - Client ID mismatch'}, status=401)
+ except Exception as e:
+ print(f"Top level error in LoginWithGoogleView: {e}")
+ return Response(data={'response': 'Invalid token'}, status=400)
+
+
+
+# For Getting The Role of the User
+class GetProfileRoleView(APIView):
+ permission_classes = (IsAuthenticated,)
+
+ def post(self, request):
+ try:
+ profile = Profile.objects.get(user=request.user)
+ return Response(data={'role': profile.role}, status=200)
+ except Exception as e:
+ print(e)
+ return Response(data={'response': 'Unauthorized'}, status=401)
+
+
+# For customised tokens. Don't use if not needed
+class MyTokenObtainPairView(TokenObtainPairView):
+ serializer_class = MyTokenObtainPairSerializer
+
+
+class RegistrationView(CreateAPIView):
+ serializer_class = RegistrationSerializer
+
+ def perform_create(self, serializer):
+ # is_active set to false in save method of the serializer
+ user = serializer.save()
+ domain = get_current_site(self.request).domain
+ _ = send_verification_mail(domain=domain, user=user)
+ # auto creation of profile done in model signal
+
+
+class ListProfileView(ListAPIView):
+ serializer_class = ProfileSerializer
+ queryset = Profile.objects.all()
+
+
+class RetrieveUpdateProfileView(RetrieveUpdateAPIView):
+ queryset = Profile.objects.all()
+ serializer_class = ProfileSerializer
+ permission_classes = (ViewUpdatePermission,)
+ lookup_field = 'user__username'
+ lookup_url_kwarg = 'username'
+
+ def get_queryset(self):
+ return Profile.objects.all()
+
+
+class UserSearchView(ListAPIView):
+ serializer_class = ProfileSerializer
+
+ def get_queryset(self):
+ search_query = self.request.query_params.get('query')
+ qs = Profile.objects.all()
+ if not search_query: # returns all profiles in case of empty query
+ return qs
+ matcher = ProfileMatcher(query=search_query)
+ # using generator to save on space
+ unsorted_matches = ((matcher.matcher(i), i) for i in qs if matcher.matcher(i) >= 0.5)
+ return [i[1] for i in sorted(unsorted_matches, key=lambda x: x[0], reverse=True)]
+
+
+@api_view(['GET', ])
+def username_existence_check(request):
+ search_query = request.query_params.get('username')
+ if (not search_query) or (not User.objects.filter(username=search_query).exists()):
+ return Response(data={'response': 'Username is available!', 'exists': 0})
+ return Response(data={'response': 'Username already exists!', 'exists': 1})
+
+
+@api_view(['GET', ])
+def email_existence_check(request):
+ search_query = request.query_params.get('email')
+ if (not search_query) or (not User.objects.filter(email=search_query.lower()).exists()):
+ return Response(data={'response': 'Email ID is not used yet!', 'exists': 0})
+ return Response(data={'response': 'An account is registered with the email address!', 'exists': 1})
+
+
+class RequestPasswordResetAPI(APIView):
+ permission_classes = [AllowAny]
+ authentication_classes = []
+ def post(self, request):
+ serializer = PasswordResetRequestSerializer(data=request.data)
+ if serializer.is_valid():
+ email = serializer.validated_data['email']
+ user = User.objects.get(email=email)
+
+ # Logic from existing view
+ current_site = get_current_site(request)
+ subject = 'Reset Your RECursion Account Password'
+ uid = urlsafe_base64_encode(force_bytes(user.pk))
+ token = password_reset_token.make_token(user)
+
+ # Using specific frontend URL construction
+ frontend_base = settings.FRONTEND_BASE_URL
+
+ # Send email
+ # We reuse the template but ensure it uses the frontend link
+ # Or we can just send the link directly if the template allows
+ # The existing template likely uses 'uid' and 'token' to build a link.
+ # We should check if the template uses 'frontend_base_url' correctly.
+
+ message = render_to_string('registration/password_reset_email.html', {
+ 'user': user,
+ 'domain': current_site.domain,
+ 'uid': uid,
+ 'token': token,
+ 'frontend_base_url': frontend_base,
+ })
+ user.email_user(subject, message)
+
+ return Response({"success": True, "message": "Password reset email sent."})
+ return Response(serializer.errors, status=400)
+
+
+class ResetPasswordConfirmAPI(APIView):
+ permission_classes = [AllowAny]
+ authentication_classes = []
+ def post(self, request):
+ serializer = PasswordResetConfirmSerializer(data=request.data)
+ if serializer.is_valid():
+ uidb64 = serializer.validated_data['uidb64']
+ token = serializer.validated_data['token']
+ password = serializer.validated_data['password']
+
+ try:
+ uid = urlsafe_base64_decode(uidb64).decode()
+ user = User.objects.get(pk=uid)
+ except (TypeError, ValueError, OverflowError, User.DoesNotExist):
+ return Response({"error": "Invalid UID"}, status=400)
+
+ if user is not None and password_reset_token.check_token(user, token):
+ user.set_password(password)
+ user.save()
+ return Response({"success": True, "message": "Password has been reset."})
+ else:
+ return Response({"error": "Invalid or expired token"}, status=400)
+
+ return Response(serializer.errors, status=400)
diff --git a/website/user_profile/models.py b/website/user_profile/models.py
index 981f2933..0d8fd3d2 100644
--- a/website/user_profile/models.py
+++ b/website/user_profile/models.py
@@ -10,18 +10,22 @@
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
+from django.urls import reverse
import os
from PIL import Image
from io import BytesIO
-from django.core.files.uploadedfile import InMemoryUploadedFile
-import sys
+from django.core.files.base import ContentFile
+from django.core.files.storage import default_storage
+from django_prometheus.models import ExportModelOperationsMixin
-def content_file_name(instance,filename):
- ext="png"
- filename= str(instance.user.username)+"."+str(ext)
- return os.path.join('images/',filename)
-class Profile(models.Model):
+def content_file_name(instance, filename):
+ ext = "png"
+ filename = str(instance.user.username) + "." + str(ext)
+ return os.path.join('images/', filename)
+
+
+class Profile(ExportModelOperationsMixin('profile'), models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
college = models.CharField(max_length=100)
@@ -30,8 +34,8 @@ class Profile(models.Model):
('2', 'Member'),
('3', 'User')
)
- role = models.CharField(max_length=50, choices=role_choices ,default='3')
- dept = models.CharField(max_length=20, blank=True, null=True)
+ role = models.CharField(max_length=50, choices=role_choices, default='3')
+ dept = models.CharField(max_length=70, blank=True, null=True)
url_CodeChef = models.URLField(blank=True, null=True)
url_Codeforces = models.URLField(blank=True, null=True)
url_SPOJ = models.URLField(blank=True, null=True)
@@ -39,28 +43,52 @@ class Profile(models.Model):
image_url = models.URLField(blank=True, null=True)
image = models.ImageField(blank=True, null=True, upload_to=content_file_name)
email_confirmed = models.BooleanField(default=False)
- created_at = models.DateTimeField(auto_now=False, auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=False, auto_now_add=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.user.username
- def save(self, *args, **kwargs):
- if self.image:
- img= Image.open(self.image)
- output = BytesIO()
- img = img.resize((100, 100))
- img.save(output, format='PNG', quality=100)
- output.seek(0)
- self.image = InMemoryUploadedFile(output, 'ImageField', ".png" , 'image/png',
- sys.getsizeof(output), None)
- super(Profile, self).save()
+ def save(self, *args, **kwargs):
+ previous_image_name = None
+ if self.pk:
+ previous = Profile.objects.filter(pk=self.pk).only('image').first()
+ if previous and previous.image:
+ previous_image_name = previous.image.name
+
+ # Only process when a new image file is uploaded.
+ if self.image and not getattr(self.image, '_committed', True):
+ img = Image.open(self.image)
+ img = img.convert('RGB')
+ output = BytesIO()
+ img = img.resize((100, 100))
+ img.save(output, format='PNG', quality=100)
+ output.seek(0)
+
+ canonical_name = content_file_name(self, self.image.name or '')
+ if default_storage.exists(canonical_name):
+ default_storage.delete(canonical_name)
+
+ self.image.save(canonical_name, ContentFile(output.getvalue()), save=False)
+
+ super(Profile, self).save(*args, **kwargs)
+
+ new_image_name = self.image.name if self.image else None
+ if (
+ previous_image_name
+ and previous_image_name != new_image_name
+ and default_storage.exists(previous_image_name)
+ ):
+ default_storage.delete(previous_image_name)
+
+ def get_absolute_url(self):
+ return reverse('user_profile_api:user_detail', kwargs={'username': self.user.username})
class Meta:
managed = True
+
@receiver(post_save, sender=User)
def update_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
- instance.profile.save()
diff --git a/website/user_profile/templates/account_activation_email.html b/website/user_profile/templates/account_activation_email.html
index 389d7a1f..7965b506 100644
--- a/website/user_profile/templates/account_activation_email.html
+++ b/website/user_profile/templates/account_activation_email.html
@@ -3,7 +3,7 @@
Please click on the link below to confirm your registration:
-http://{{ domain }}{% url 'user_profile:activate' uidb64=uid token=token %}
+http://{{ domain }}{% url 'user_profile_api:activate' uidb64=uid token=token %}
Sincerely,
RECursion Team
diff --git a/website/user_profile/templates/profile/profile.html b/website/user_profile/templates/profile/profile.html
index 7836cfb2..96f4c370 100644
--- a/website/user_profile/templates/profile/profile.html
+++ b/website/user_profile/templates/profile/profile.html
@@ -7,9 +7,14 @@
- {% if profile.image %}
-

- {% endif %}
+ {% load static %}
+
+ {% if profile and profile.image %}
+
+ {% else %}
+
+ {% endif %}
+
{{ profile.name }}
@{{ profile.user }}
diff --git a/website/user_profile/templates/registration/password_reset_email.html b/website/user_profile/templates/registration/password_reset_email.html
index 2cdbb383..77863354 100644
--- a/website/user_profile/templates/registration/password_reset_email.html
+++ b/website/user_profile/templates/registration/password_reset_email.html
@@ -2,7 +2,7 @@
To initiate the password reset process for your {{ user.get_username }} RECursion Account,
click the link below:
-{{ protocol }}http://{{ domain }}{% url 'user_profile:password_reset_confirm' uidb64=uid token=token %}
+{{ protocol }}{{ frontend_base_url }}{% url 'user_profile:password_reset_confirm' uidb64=uid token=token %}
If clicking the link above doesn't work, please copy and paste the URL in a new browser
window instead.
diff --git a/website/user_profile/tokens.py b/website/user_profile/tokens.py
index 3fa8b747..9fb533c9 100644
--- a/website/user_profile/tokens.py
+++ b/website/user_profile/tokens.py
@@ -1,5 +1,5 @@
from django.contrib.auth.tokens import PasswordResetTokenGenerator
-from django.utils import six
+import six
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
def _make_hash_value(self, user, timestamp):
@@ -9,4 +9,4 @@ def _make_hash_value(self, user, timestamp):
)
account_activation_token = AccountActivationTokenGenerator()
-password_reset_token = AccountActivationTokenGenerator()
\ No newline at end of file
+password_reset_token = PasswordResetTokenGenerator()
\ No newline at end of file
diff --git a/website/user_profile/urls.py b/website/user_profile/urls.py
index 4a11f503..9c3e38fb 100644
--- a/website/user_profile/urls.py
+++ b/website/user_profile/urls.py
@@ -7,12 +7,12 @@
app_name="profile"
urlpatterns = [
path('account_activation_sent/', account_activation_sent, name='account_activation_sent'),
- path('activate/(
[0-9A-Za-z_\-]+)/([0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/',
+ path('activate///',
activate, name='activate'),
path('change_password/', change_password, name='change_password'),
path('password_reset/', password_reset, name='password_reset'),
path('password_reset/done/', password_reset_done, name='password_reset_done'),
- path('reset/([0-9A-Za-z_\-]+)/([0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/',
+ path('reset///',
password_reset_confirm, name='password_reset_confirm'),
path('reset/done/', password_reset_complete, name='password_reset_complete'),
path('viewprofile//', view_profile, name='view_profile'),
diff --git a/website/user_profile/utils.py b/website/user_profile/utils.py
index 0c6ae2cc..7bc78440 100644
--- a/website/user_profile/utils.py
+++ b/website/user_profile/utils.py
@@ -1,4 +1,15 @@
+import threading
+import six
from difflib import SequenceMatcher
+from typing import List, Tuple, Union
+from django.core.exceptions import ValidationError
+from django.core.mail import EmailMessage
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.template.loader import render_to_string
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
+from django.conf import settings
+from rest_framework import serializers
class ProfileMatcher:
@@ -26,4 +37,77 @@ def matcher(self, obj):
return 0
def __str__(self):
- return 'ProfileMatcher object with query="{}", ratio_threshold={} .'.format(self.query,self.ratio_threshold)
+ return 'ProfileMatcher object with query="{}", ratio_threshold={} .'.format(self.query, self.ratio_threshold)
+
+
+from .tokens import account_activation_token
+
+
+class LowerEmailField(serializers.EmailField):
+ def to_representation(self, value):
+ return str(value)
+
+ def to_internal_value(self, data):
+ if isinstance(data, bool) or not isinstance(data, (str, int, float,)):
+ self.fail('invalid')
+ value = str(data).lower()
+ return value.strip() if self.trim_whitespace else value
+
+
+class ThreadedMailing(threading.Thread):
+ def __init__(self, emails: List[EmailMessage], fail_silently: bool = True, verbose: int = 0):
+ self.email_msgs = emails
+ self.fail_silently = fail_silently
+ self.verbose = verbose
+ threading.Thread.__init__(self)
+
+ def run(self):
+ if self.verbose > 0:
+ print('Sending {} emails.'.format(len(self.email_msgs)))
+ for email_number, email in enumerate(self.email_msgs, start=1):
+ if self.verbose > 1:
+ print('Sending email number:', email_number)
+ print(email)
+ email.send(fail_silently=self.fail_silently)
+
+
+# needs change, UNFIT FOR USE
+def send_verification_mail(domain, user, *args, **kwargs):
+ if domain is None or user is None:
+ raise ValidationError('Domain/User instance not provided')
+ mail_subject = 'Activate your RECursion website account.'
+ message = render_to_string('account_activation_email.html', {
+ 'user': user,
+ 'domain': domain,
+ 'uid': urlsafe_base64_encode(force_bytes(user.id)),
+ 'token': account_activation_token.make_token(user),
+ })
+ to_email = user.email
+ mail = EmailMessage(
+ mail_subject,
+ message,
+ to=[to_email, ]
+ )
+
+ # UNCOMMENT BELOW LINES TO SEND EMAIL
+ threaded_mail = ThreadedMailing([mail, ])
+ threaded_mail.start()
+
+ return message
+
+
+def valid_url_extension(url, allowed=('.jpg', '.jpeg', '.png', '.gif', '.webp')):
+ """
+ Return True if the URL's path ends with an allowed image extension.
+ Keeps a lightweight check so views can safely attempt to download image files.
+ """
+ try:
+ from urllib.parse import urlparse
+ path = urlparse(url).path or ''
+ path = path.lower()
+ for ext in allowed:
+ if path.endswith(ext):
+ return True
+ return False
+ except Exception:
+ return False
diff --git a/website/user_profile/utils_permissions.py b/website/user_profile/utils_permissions.py
new file mode 100644
index 00000000..c5d66e0e
--- /dev/null
+++ b/website/user_profile/utils_permissions.py
@@ -0,0 +1,19 @@
+from rest_framework.permissions import IsAuthenticated, SAFE_METHODS, BasePermission
+from user_profile.models import Profile
+
+
+class ViewUpdatePermission(BasePermission):
+ def has_permission(self, request, view):
+ return (request.method in SAFE_METHODS) or bool(request.user and request.user.is_authenticated)
+
+ def has_object_permission(self, request, view, obj):
+ if request.method in SAFE_METHODS:
+ return True
+ return bool(request.user and request.user.is_authenticated) and request.user == obj.user
+
+
+class IsMemberOrAbove(IsAuthenticated):
+ def has_object_permission(self, request, view, obj):
+ curr_user = request.user
+ curr_profile = Profile.objects.get(user=curr_user)
+ return curr_profile.role != '3'
diff --git a/website/user_profile/views.py b/website/user_profile/views.py
index 9cdd6e90..afda95e6 100644
--- a/website/user_profile/views.py
+++ b/website/user_profile/views.py
@@ -1,42 +1,34 @@
-from django.shortcuts import render, redirect,get_object_or_404, get_list_or_404
+from django.shortcuts import render, redirect, get_object_or_404, get_list_or_404
from .models import *
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django.template import loader, RequestContext
+from django.template.loader import render_to_string
from .forms import *
from django.contrib.auth.models import User
-from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth import authenticate, login, logout, update_session_auth_hash
from django.contrib.auth.decorators import login_required
-from django.contrib.auth.forms import UserCreationForm
+from django.contrib.auth.forms import UserCreationForm, PasswordChangeForm, SetPasswordForm
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.forms import modelformset_factory
-from django.contrib.auth.forms import UserCreationForm
from itertools import chain
from django.core.files.base import ContentFile
from io import BytesIO
import urllib.request
from PIL import Image
from django.contrib.sites.shortcuts import get_current_site
-from django.shortcuts import render, redirect
from django.utils.encoding import force_bytes
-from django.utils.http import urlsafe_base64_encode
-from django.template.loader import render_to_string
-from .tokens import account_activation_token
-from .tokens import password_reset_token
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
-from django.utils.encoding import force_bytes, force_text
+from .tokens import account_activation_token, password_reset_token
from django.contrib import messages
-from django.contrib.auth import update_session_auth_hash
-from django.contrib.auth.forms import PasswordChangeForm
-from django.shortcuts import render, redirect
-from django.contrib.auth.forms import SetPasswordForm
-from django.contrib.auth.models import *
+from .utils import ProfileMatcher, valid_url_extension
+from website.utils import save_local_profile_pic_to_media
import random
from forum.models import *
from blog.models import *
-from .utils import ProfileMatcher
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from rest_framework_simplejwt.tokens import RefreshToken
def view_profile(request, id=None):
@@ -109,10 +101,12 @@ def user_register(request):
user.save()
current_site = get_current_site(request)
subject = 'Activate Your RECursion Account'
+ # ensure uid is a clean string (urlsafe_base64_encode returns str in recent Django)
+ uid = urlsafe_base64_encode(force_bytes(user.pk))
message = render_to_string('account_activation_email.html', {
'user': user,
'domain': current_site.domain,
- 'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
+ 'uid': uid,
'token': account_activation_token.make_token(user),
})
user.email_user(subject, message)
@@ -170,17 +164,22 @@ def activate(request, uidb64, token, backend='django.contrib.auth.backends.Model
user.save()
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
profile = Profile.objects.get(user = user)
- image_url = 'https://recursionnitd.in/'+'static/image/profile_pic/' + str(random.randint(1,15)) + '.png'
- type = valid_url_extension(image_url)
- full_path = 'media/images/' + profile.user.username + '.png'
+ # Copy a local static profile pic into MEDIA and set ImageField
try:
- urllib.request.urlretrieve(image_url, full_path)
- except:
- return HttpResponse("Downloadable Image Not Found!")
- if profile.user == request.user:
- profile.image = '../' + full_path
+ rel_media = save_local_profile_pic_to_media(profile.user.username)
+ except Exception as e:
+ print("activate: save_local_profile_pic_to_media exception:", repr(e))
+ rel_media = False
+
+ if not rel_media:
+ print("Downloadable Image Not Found!")
+ else:
+ profile.image.name = rel_media # set path relative to MEDIA_ROOT
profile.save()
- return redirect('user_profile:edit_profile')
+
+ if profile.user == request.user:
+ return redirect(settings.FRONTEND_BASE_URL + '/profile/edit?activated=true')
+ return redirect(settings.FRONTEND_BASE_URL + '/profile/edit?activated=true')
else:
return render(request, 'account_activation_invalid.html')
@@ -193,17 +192,16 @@ def edit_profile(request):
form = Profileform(request.POST or None, request.FILES or None, instance=profile)
if form.is_valid():
form.save()
- if form.cleaned_data['image'] is None or form.cleaned_data['image'] == False:
- image_url = 'https://recursionnitd.in/'+'static/image/profile_pic/' + str(random.randint(1,15)) + '.png'
- type = valid_url_extension(image_url)
- full_path = 'media/images/' + profile.user.username + '.png'
- try:
- urllib.request.urlretrieve(image_url, full_path)
- except:
- return HttpResponse("Downloadable Image Not Found!")
- if profile.user == request.user:
- profile.image = '../' + full_path
- form.save()
+ # ensure a local default exists if no image supplied
+ if not profile.image:
+ try:
+ rel_media = save_local_profile_pic_to_media(profile.user.username)
+ except Exception as e:
+ print("edit_profile: save_local_profile_pic_to_media exception:", repr(e))
+ rel_media = False
+ if rel_media:
+ profile.image.name = rel_media
+ profile.save()
return HttpResponseRedirect(reverse('user_profile:view_profile', args=(id,)))
return render(request, 'create.html', {'form': form, })
@@ -216,7 +214,7 @@ def change_password(request):
user = form.save()
update_session_auth_hash(request, user) # Important!
messages.success(request, 'Your password was successfully updated!')
- return redirect('profile:edit_profile')
+ return redirect('user_profile:edit_profile')
else:
messages.error(request, 'Please correct the error below.')
else:
@@ -237,11 +235,13 @@ def password_reset(request):
user.save()
current_site = get_current_site(request)
subject = 'Reset Your RECursion Account Password'
+ uid = urlsafe_base64_encode(force_bytes(user.pk))
message = render_to_string('registration/password_reset_email.html', {
'user': user,
'domain': current_site.domain,
- 'uid': urlsafe_base64_encode(force_bytes(user.pk)).decode(),
+ 'uid': uid,
'token': password_reset_token.make_token(user),
+ 'frontend_base_url': settings.FRONTEND_BASE_URL,
})
user.email_user(subject, message)
return HttpResponse("We've emailed you instructions for setting your password, if an account exists with the email you entered! You should receive them shortly."
@@ -262,15 +262,26 @@ def password_reset_confirm(request, uidb64, token, backend='django.contrib.auth.
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None
- if user is not None and account_activation_token.check_token(user, token):
- form = SetPasswordForm(user, request.POST)
- if form.is_valid():
- user = form.save()
- update_session_auth_hash(request, user) # Important!
- user.is_active = True
- user.save()
- return redirect('login')
- return render(request, 'registration/password_reset_confirm.html', {'form': form})
+
+ # Use password_reset_token (was incorrectly using account_activation_token)
+ if user is not None and password_reset_token.check_token(user, token):
+ if request.method == 'POST':
+ data = {
+ 'new_password1': request.POST.get('password'),
+ 'new_password2': request.POST.get('confirmPassword'),
+ }
+ form = SetPasswordForm(user, data=data)
+ if form.is_valid():
+ user = form.save()
+ update_session_auth_hash(request, user) # Important!
+ user.is_active = True
+ user.save()
+ return HttpResponse("Changed.")
+ # if invalid, re-render with errors
+ return HttpResponse(form)
+ else:
+ form = SetPasswordForm(user)
+ return render(request, 'registration/password_reset_confirm.html', {'form': form})
else:
return render(request, 'registration/password_reset_invalid.html')
diff --git a/website/utils/__init__.py b/website/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/website/utils/content_filter.py b/website/utils/content_filter.py
new file mode 100644
index 00000000..2c39b408
--- /dev/null
+++ b/website/utils/content_filter.py
@@ -0,0 +1,100 @@
+import os
+import requests
+import json
+
+def check_content_safety(text, title=None):
+ """
+ Checks if the provided text (and optional title) is safe using the Gemini API.
+ Returns:
+ - True: if content is safe
+ - False: if content is unsafe
+ """
+ api_key = os.environ.get("GEMINI_API_KEY")
+ api_url = os.environ.get("GEMINI_API_URL")
+
+ if not api_key or not api_url:
+ print("Gemini API credentials missing in .env")
+ return True # Fail open if not configured
+
+ headers = {
+ "Content-Type": "application/json"
+ }
+
+ # Append key to URL if not present (standard Gemini API pattern)
+ if "?key=" not in api_url and "&key=" not in api_url:
+ final_url = f"{api_url}?key={api_key}"
+ else:
+ final_url = api_url
+
+ content_to_analyze = f"Title: {title}\n" if title else ""
+ content_to_analyze += f"Body: {text}"
+
+ prompt = f"""
+ You are an AI Content Moderator for a college technical club website. Your goal is to filter user submissions for a Blog, Ask Section, Interview Experience Section. The environment must remain professional, academic, and safe.
+
+ Analyze the input text (Title and Body) based on the following criteria:
+
+ 1. **SPAM & ADVERTISING (Strict Ban):**
+ - Flag any promotion of pharmaceuticals, supplements, "useless medicines," weight loss pills, or male enhancement products.
+ - Flag any commercial advertising, affiliate links, or "get rich quick" schemes.
+ - Flag any text that looks like SEO spam (repetitive keywords without context).
+
+ 2. **SAFETY & TOXICITY (Strict Ban):**
+ - Flag any sexual content, NSFW material, or innuendo.
+ - Flag hate speech, harassment, or bullying.
+ - Flag content encouraging self-harm or violence.
+
+ 3. **RELEVANCE (Allow):**
+ - Allow questions about coding, engineering, science, and technology.
+ - Allow general curiosity questions suitable for an academic environment.
+ - Allow constructive discussions about college life or club activities.
+
+ 4. **TONE:**
+ - The content must be professional or semi-professional.
+ - Reject gibberish or incoherent text.
+
+ Respond ONLY with a JSON object in the following format:
+ {{
+ "is_safe": true/false,
+ "reason": "brief explanation if unsafe"
+ }}
+
+ Text to analyze:
+ {content_to_analyze}
+ """
+
+ data = {
+ "contents": [{
+ "parts": [{"text": prompt}]
+ }]
+ }
+
+ try:
+ response = requests.post(final_url, headers=headers, json=data, timeout=10)
+ response.raise_for_status()
+ result = response.json()
+
+ # Extract text response
+ try:
+ content_text = result['candidates'][0]['content']['parts'][0]['text']
+
+ # Clean potential markdown formatting
+ content_text = content_text.strip()
+ if content_text.startswith("```json"):
+ content_text = content_text[7:]
+ if content_text.startswith("```"):
+ content_text = content_text[3:]
+ if content_text.endswith("```"):
+ content_text = content_text[:-3]
+
+ parsed = json.loads(content_text)
+ return parsed.get("is_safe", True)
+
+ except (KeyError, IndexError, json.JSONDecodeError) as e:
+ print(f"Error parsing Gemini response: {e}")
+ print(f"Response body: {result}")
+ return True # Fail open on parse error
+
+ except Exception as e:
+ print(f"Error checking content safety: {e}")
+ return True # Fail open on network/API error
diff --git a/website/website/middleware.py b/website/website/middleware.py
new file mode 100644
index 00000000..6ee6b4d2
--- /dev/null
+++ b/website/website/middleware.py
@@ -0,0 +1,62 @@
+from django.conf import settings
+from django.http import HttpResponse, HttpResponseForbidden
+from django.shortcuts import render
+
+class APIModeMiddleware:
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ # 1. Check if strict API mode is enabled
+ if not getattr(settings, 'API_ONLY_MODE', False):
+ return self.get_response(request)
+
+ # 2. Check if the path is allowed
+ path = request.path
+
+ # Always allow these prefixes
+ allowed_prefixes = [
+ '/api/',
+ '/admin/',
+ '/static/',
+ '/media/',
+ '/oauth/',
+ ]
+
+ # Add whitelisted paths from settings
+ whitelist = getattr(settings, 'API_MODE_WHITELIST', [])
+
+ # Helper to check permissions
+ is_allowed = False
+
+ # Check standard prefixes
+ for prefix in allowed_prefixes:
+ if path.startswith(prefix):
+ is_allowed = True
+ break
+
+ # Check whitelist (can be exact match or prefix)
+ if not is_allowed:
+ for item in whitelist:
+ # If item ends with slash, treat as prefix?
+ # Ideally, simple startswith check is flexible enough
+ if path.startswith(item):
+ is_allowed = True
+ break
+
+ if is_allowed:
+ return self.get_response(request)
+
+ # 3. Block access
+ # Return a simple unauthorized HTML page
+ content = """
+
+ Unauthorized
+
+
+
APIs are routed through this! (●'◡'●)
+
+
+
+ """
+ return HttpResponse(content, status=200) # Using 200 so it renders nicely in browser, or could use 403
diff --git a/website/website/settings.py b/website/website/settings.py
index c4edb438..f6e86f4f 100644
--- a/website/website/settings.py
+++ b/website/website/settings.py
@@ -8,23 +8,48 @@
"""
import os
-from decouple import config
+from dotenv import load_dotenv
+# Always load .env from the Django project root, regardless of current cwd.
+load_dotenv(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env'))
+from decouple import config, Csv
+from datetime import timedelta
+import dj_database_url
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '9%b%-x(_!zd(ffdc!s=8j(clv&(_92d!+lh@#o9&t8*y40v1+3'
+# GOOGLE_CLIENT_ID
+GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', None)
+
+
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = config('DEBUG', default=True, cast=bool)
+
+ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='127.0.0.1,localhost', cast=Csv())
+
-ALLOWED_HOSTS = ['*']
+API_ONLY_MODE = config('API_ONLY_MODE', default=False, cast=bool)
+API_MODE_WHITELIST = []
+FRONTEND_BASE_URL = config('FRONTEND_BASE_URL', default='http://localhost:3000')
+
+# Add explicit local dev origins by default while keeping env-driven control.
+CORS_ALLOWED_ORIGINS = config(
+ 'CORS_ALLOWED_ORIGINS',
+ default='http://localhost:3000,http://127.0.0.1:3000,https://www.recursionnitd.in',
+ cast=Csv(),
+)
+
+if FRONTEND_BASE_URL and FRONTEND_BASE_URL not in CORS_ALLOWED_ORIGINS:
+ CORS_ALLOWED_ORIGINS.append(FRONTEND_BASE_URL)
+
+CORS_ALLOW_CREDENTIALS = True
# Application definition
@@ -46,23 +71,31 @@
'markdownx',
'blog',
'getting_started',
+ 'rest_framework',
+ 'django_filters',
+ 'django_prometheus',
'events_calendar',
+ 'corsheaders',
+ 'rest_framework_simplejwt.token_blacklist'
]
MIDDLEWARE = [
+ 'django_prometheus.middleware.PrometheusBeforeMiddleware',
+ 'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
- #'django.middleware.csrf.CsrfViewMiddleware',
+ # 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
-
+ 'django_prometheus.middleware.PrometheusAfterMiddleware',
+ 'website.middleware.APIModeMiddleware',
]
AUTHENTICATION_BACKENDS = (
'social_core.backends.open_id.OpenIdAuth', # for Google authentication
- 'social_core.backends.google.GoogleOpenId', # for Google authentication
+ # 'social_core.backends.google.GoogleOpenId', # for Google authentication
'social_core.backends.google.GoogleOAuth2', # for Google authentication
'social_core.backends.facebook.FacebookOAuth2', # for Facebook authentication
@@ -85,24 +118,73 @@
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
+ 'libraries': {
+ 'staticfiles': 'django.templatetags.static',
+ }
},
},
]
WSGI_APPLICATION = 'website.wsgi.application'
+# REST FRAMEWORK settings
+REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
+ 'rest_framework_simplejwt.authentication.JWTAuthentication',
-# Database
-# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
+ ],
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.AllowAny',
+ ],
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+ 'PAGE_SIZE': 10,
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
- }
+ 'DEFAULT_THROTTLE_RATES': {
+ 'anon': '40/day',
+ },
+ 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
+# REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' }
+
+
+# JWT SETTINGS
+SIMPLE_JWT = {
+ 'ACCESS_TOKEN_LIFETIME': timedelta(days=28), # TO BE REDUCED
+ 'REFRESH_TOKEN_LIFETIME': timedelta(days=56),
+ 'ROTATE_REFRESH_TOKENS': False,
+ 'BLACKLIST_AFTER_ROTATION': True,
+ 'UPDATE_LAST_LOGIN': False,
+
+ 'ALGORITHM': 'HS256',
+ 'SIGNING_KEY': SECRET_KEY,
+ 'VERIFYING_KEY': None,
+ 'AUDIENCE': None,
+ 'ISSUER': None,
+
+ 'AUTH_HEADER_TYPES': ('Bearer',),
+ 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
+ 'USER_ID_FIELD': 'id',
+ 'USER_ID_CLAIM': 'user_id',
+ 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
+
+ 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
+ 'TOKEN_TYPE_CLAIM': 'token_type',
+ 'JTI_CLAIM': 'jti',
+
+ 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
+ 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
+ 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
+}
+
+# Database
+# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
+DATABASES = {
+ 'default': dj_database_url.config(
+ default="sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3")
+ )
+}
# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
@@ -121,22 +203,20 @@
},
]
-
-
SOCIAL_AUTH_PIPELINE = (
- 'social.pipeline.social_auth.social_details',
- 'social.pipeline.social_auth.social_uid',
- 'social.pipeline.social_auth.auth_allowed',
- 'social.pipeline.social_auth.social_user',
-
+ 'social_core.pipeline.social_auth.social_details',
+ 'social_core.pipeline.social_auth.social_uid',
+ 'social_core.pipeline.social_auth.auth_allowed',
+ 'social_core.pipeline.social_auth.social_user',
+
'website.utils.associate_by_email',
-
- 'social.pipeline.user.get_username',
- 'social.pipeline.user.create_user',
- 'social.pipeline.social_auth.associate_user',
- 'social.pipeline.social_auth.load_extra_data',
- 'social.pipeline.user.user_details',
-
+
+ 'social_core.pipeline.user.get_username',
+ 'social_core.pipeline.user.create_user',
+ 'social_core.pipeline.social_auth.associate_user',
+ 'social_core.pipeline.social_auth.load_extra_data',
+ 'social_core.pipeline.user.user_details',
+
'website.utils.set_image_for_new_users'
)
@@ -145,11 +225,9 @@
LOGOUT_REDIRECT_URL = 'home'
LOGIN_REDIRECT_URL = 'home'
-
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = config('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY')
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = config('SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET')
-
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
@@ -163,20 +241,39 @@
USE_TZ = True
-
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # to silence the 26 errors
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/'
-MEDIA_URL ='/media/'
+MEDIA_URL = '/media/'
+
+MEDIA_ROOT = os.path.join(BASE_DIR, '../website/media')
+
+# Select backend via env; default stays console for development
+EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
+
+# Sender addresses
+DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL')
+SERVER_EMAIL = config('SERVER_EMAIL', default=DEFAULT_FROM_EMAIL)
+
+# SMTP settings (used when EMAIL_BACKEND is set to SMTP)
+EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com')
+EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
+EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool)
+EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
+EMAIL_HOST_PASSWORD = config('EMAIL_PASSWORD', default='')
-MEDIA_ROOT=os.path.join(BASE_DIR,'../website/media')
+TRIAL_REC_MAIL = 'jiwegaw290@randrai.com'
+STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+# STATICFILES_DIRS = [
+# os.path.join(BASE_DIR, 'website/static'),
+# ]
-#PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
-#STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
-#MEDIA_URL='/media/'
-#MEDIA_ROOT=os.path.join(BASE_DIR,'members/media')
+# PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
+# STATIC_ROOT = os.path.join(PROJECT_DIR, 'static')
+# MEDIA_URL='/media/'
+# MEDIA_ROOT=os.path.join(BASE_DIR,'members/media')
\ No newline at end of file
diff --git a/website/website/urls.py b/website/website/urls.py
index f7d73d38..86ce87d2 100755
--- a/website/website/urls.py
+++ b/website/website/urls.py
@@ -15,28 +15,50 @@
"""
from django.contrib import admin
from django.contrib.auth import views as auth_views
-from django.urls import path,include
+from django.urls import path, include
from django.conf.urls import url, include
from forum import views
from django.conf import settings
from django.conf.urls.static import static
+from rest_framework_simplejwt.views import (
+ TokenRefreshView,
+)
+from user_profile.api.views import MyTokenObtainPairView, LoginWithGoogleView
+from forum.api.views import home_view
urlpatterns = [
- path('admin/', admin.site.urls),
- path('', views.home, name='home'),
- path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True), name='login'),
- path('logout/', auth_views.LogoutView.as_view(), name='logout'),
- path('oauth/', include('social_django.urls', namespace='social')),
- path('forum/',include('forum.urls',namespace='forum')),
- # path('events/',include('events.urls',namespace='events')),
- path('events/',include('events_calendar.urls',namespace='events_calendar')),
- path('profile/',include('user_profile.urls',namespace='user_profile')),
- path('team/',include('team.urls',namespace='team')),
- path('blog/',include('blog.urls',namespace='blog')),
- path('experience/',include('interview_exp.urls',namespace='interview_exp')),
- path('get_started/',include('getting_started.urls',namespace='getting_started')),
- # path('members/',include('members.urls')),
- url(r'^markdownx/', include('markdownx.urls')),
+ path('admin/', admin.site.urls),
+ path('', views.home, name='home'),
+ path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True), name='login'),
+ path('logout/', auth_views.LogoutView.as_view(), name='logout'),
+ path('oauth/', include('social_django.urls', namespace='social')),
+ path('forum/', include('forum.urls', namespace='forum')),
+ # path('events/',include('events.urls',namespace='events')),
+ path('events/', include('events_calendar.urls', namespace='events_calendar')),
+ path('profile/', include('user_profile.urls', namespace='user_profile')),
+ path('team/', include('team.urls', namespace='team')),
+ path('blog/', include('blog.urls', namespace='blog')),
+ path('experience/', include('interview_exp.urls', namespace='interview_exp')),
+ path('get_started/', include('getting_started.urls', namespace='getting_started')),
+ # path('members/',include('members.urls')),
+ url(r'^markdownx/', include('markdownx.urls')),
-]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ # API region
+
+ # API URLs
+ path('api/', home_view, name='api_home'),
+ path('api/users/', include('user_profile.api.urls', namespace='user_profile_api')),
+ path('api/experiences/', include('interview_exp.api.urls', namespace='experiences_api')),
+ path('api/events/', include('events_calendar.api.urls', namespace='events_api')),
+ path('api/team/', include('team.api.urls', namespace='team_api')),
+ path('api/getting_started/', include('getting_started.api.urls', namespace='getting_started_api')),
+
+ # JWT
+ path('api/token/google/', LoginWithGoogleView.as_view(), name='token_for_google'),
+ path('api/token/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
+ path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
+ # Prometheus monitoring
+ path('', include('django_prometheus.urls')),
+
+ ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/website/website/utils.py b/website/website/utils.py
index d5aaf6f6..0beadff5 100644
--- a/website/website/utils.py
+++ b/website/website/utils.py
@@ -1,8 +1,15 @@
from django.contrib.auth.models import User
from user_profile.models import Profile
-import urllib.request
+import os
import random
+import urllib.request
+from urllib.error import URLError, HTTPError
+import mimetypes
+from django.core.files.base import ContentFile
+from django.conf import settings
from django.shortcuts import redirect
+from django.contrib.staticfiles import finders
+import shutil
def associate_by_email(**kwargs):
try:
@@ -16,6 +23,28 @@ def associate_by_email(**kwargs):
pass
return kwargs
+def _save_image_from_url_to_profile(user, image_url):
+ """
+ Download image_url and save into user.profile.image using Django storage.
+ Returns True on success, False on failure. Saves to 'images/.'.
+ """
+ try:
+ # Request with a common User-Agent to avoid blocking by some servers
+ req = urllib.request.Request(image_url, headers={'User-Agent': 'Mozilla/5.0'})
+ with urllib.request.urlopen(req, timeout=10) as resp:
+ data = resp.read()
+ # try to get content-type to derive extension
+ content_type = resp.info().get_content_type()
+ ext = mimetypes.guess_extension(content_type) or '.png'
+ filename = f"images/{user.username}{ext}"
+ # Save using Django File storage so ImageField works as expected
+ user.profile.image.save(filename, ContentFile(data), save=True)
+ return True
+ except (HTTPError, URLError, ValueError, Exception) as e:
+ # print full exception so you can diagnose (network, ssl, 403, etc.)
+ print("profile image download failed:", repr(e))
+ return False
+
def set_image_for_new_users(backend, user, response, *args, **kwargs):
#import pdb;pdb.set_trace();
try:
@@ -32,22 +61,63 @@ def set_image_for_new_users(backend, user, response, *args, **kwargs):
user.save()
if not user.profile.image:
- full_path = 'media/images/' + user.username + '.png'
+ # try provider picture first
try:
- image_url = response['picture']
- urllib.request.urlretrieve(image_url, full_path)
- user.profile.image = '../' + full_path
- user.save()
- except:
- try:
- image_url = 'https://recursionnitd.in/'+'static/image/profile_pic/' + str(random.randint(1,15)) + '.png'
- urllib.request.urlretrieve(image_url, full_path)
- user.profile.image = '../' + full_path
- user.save()
- except:
+ image_url = response.get('picture')
+ if image_url:
+ ok = _save_image_from_url_to_profile(user, image_url)
+ if ok:
+ return
+ # fallback to site default
+ image_url = 'https://recursionnitd.in/static/image/profile_pic/' + str(random.randint(1,15)) + '.png'
+ ok = _save_image_from_url_to_profile(user, image_url)
+ if not ok:
print("Downloadable Image Not Found!")
-
+ except Exception as e:
+ print("set_image_for_new_users error:", repr(e))
+ print("Downloadable Image Not Found!")
except Exception as e:
- print(e)
- print("error")
- pass
\ No newline at end of file
+ print("set_image_for_new_users outer error:", repr(e))
+ pass
+
+def save_local_profile_pic_to_media(username):
+ """
+ Copy a random profile pic from static/image/profile_pic/<1-15>.png
+ into MEDIA_ROOT/images/.png.
+
+ Returns the relative path to MEDIA (e.g. 'images/username.png') on success,
+ or False on failure.
+ Works on Linux and Windows by using os.path.join and Django settings.
+ """
+ try:
+ n = random.randint(1, 15)
+ rel_static_path = os.path.join('image', 'profile_pic', f'{n}.png')
+
+ # 1) Try Django staticfiles finders (dev & when using collectstatic + staticfiles app)
+ src = finders.find(rel_static_path)
+
+ # 2) Fallback to STATIC_ROOT (deployed collected static)
+ if not src and getattr(settings, 'STATIC_ROOT', None):
+ src = os.path.join(settings.STATIC_ROOT, rel_static_path)
+
+ # 3) Fallback to project-level "static" directory (common in development)
+ if not src:
+ src = os.path.join(getattr(settings, 'BASE_DIR', ''), 'static', rel_static_path)
+
+ if not src or not os.path.exists(src):
+ print("local static profile pic not found:", rel_static_path, "resolved to:", src)
+ return False
+
+ media_root = getattr(settings, 'MEDIA_ROOT', None) or os.path.join(getattr(settings, 'BASE_DIR', ''), 'media')
+ dest_dir = os.path.join(media_root, 'images')
+ os.makedirs(dest_dir, exist_ok=True)
+
+ dest_path = os.path.join(dest_dir, f"{username}.png")
+ shutil.copyfile(src, dest_path)
+
+ # return path relative to MEDIA_ROOT (ImageField stores relative paths)
+ relative_media_path = os.path.join('images', f"{username}.png").replace('\\', '/')
+ return relative_media_path
+ except Exception as e:
+ print("save_local_profile_pic_to_media error:", repr(e))
+ return False
\ No newline at end of file