diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4808ac2 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 238e0ab..445be84 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,4 @@ dmypy.json /CDC_Backend/Storage/ .idea *.pyc +dev.env diff --git a/CDC_Backend.zip b/CDC_Backend.zip new file mode 100644 index 0000000..c170b6a Binary files /dev/null and b/CDC_Backend.zip differ diff --git a/CDC_Backend/.DS_Store b/CDC_Backend/.DS_Store new file mode 100644 index 0000000..e1c3358 Binary files /dev/null and b/CDC_Backend/.DS_Store differ diff --git a/CDC_Backend/APIs/admin.py b/CDC_Backend/APIs/admin.py index e2a87c3..08ac51b 100644 --- a/CDC_Backend/APIs/admin.py +++ b/CDC_Backend/APIs/admin.py @@ -4,10 +4,50 @@ from django.shortcuts import resolve_url from django.utils.html import format_html from django.utils.safestring import SafeText +from simple_history.admin import SimpleHistoryAdmin +from import_export.admin import ImportExportMixin, ExportMixin +from import_export import resources + from .models import * -admin.site.register(User) -admin.site.register(Admin) + +class ArrayFieldListFilter(admin.SimpleListFilter): + """This is a list filter based on the values + from a model's `keywords` ArrayField. """ + + title = 'Roles' + parameter_name = 'user_type' + + def lookups(self, request, model_admin): + # Very similar to our code above, but this method must return a + # list of tuples: (lookup_value, human-readable value). These + # appear in the admin's right sidebar + + keywords = User.objects.values_list("user_type", flat=True) + keywords = [(kw, kw) for sublist in keywords for kw in sublist if kw] + keywords = sorted(set(keywords)) + return keywords + + def queryset(self, request, queryset): + # when a user clicks on a filter, this method gets called. The + # provided queryset with be a queryset of Items, so we need to + # filter that based on the clicked keyword. + + lookup_value = self.value() # The clicked keyword. It can be None! + if lookup_value: + # the __contains lookup expects a list, so... + queryset = queryset.filter(user_type__contains=[lookup_value]) + return queryset + + +class UserAdmin(ImportExportMixin, SimpleHistoryAdmin): + list_display = ('email', 'user_type', 'last_login_time') + list_filter = (ArrayFieldListFilter, 'last_login_time') + search_fields = ('email', 'user_type') + ordering = ('email', 'user_type') + + +admin.site.register(User, UserAdmin) admin.site.site_header = "CDC Recruitment Portal" @@ -17,8 +57,12 @@ def model_admin_url(obj, name=None) -> str: return format_html('{}', url, name or str(obj)) +class StudentAdmin(ImportExportMixin, SimpleHistoryAdmin): + pass + + @admin.register(Student) -class Student(admin.ModelAdmin): +class Student(StudentAdmin): list_display = ("roll_no", "name", "batch", "branch", "phone_number", 'can_apply') search_fields = ("roll_no", "name", "phone_number") ordering = ("roll_no", "name", "batch", "branch", "phone_number") @@ -35,17 +79,57 @@ class Student(admin.ModelAdmin): queryset.update(can_apply=True) self.message_user(request, "Registered the users") +class PlacementResources(resources.ModelResource): + class Meta: + model = Placement + exclude = ('id','changed_by', 'is_company_details_pdf', 'is_description_pdf', + 'is_compensation_details_pdf', 'is_selection_procedure_details_pdf') +class AdminAdmin(ExportMixin, SimpleHistoryAdmin): + resource_class = PlacementResources + + +class PlacementResources(resources.ModelResource): + class Meta: + model = Placement + exclude = ('id', 'changed_by', 'is_company_details_pdf', 'is_description_pdf', + 'is_compensation_details_pdf', 'is_selection_procedure_details_pdf') + + +class AdminAdmin(ExportMixin, SimpleHistoryAdmin): + resource_class = PlacementResources + + +class PlacementResources(resources.ModelResource): + class Meta: + model = Placement + exclude = ('id', 'changed_by', 'is_company_details_pdf', 'is_description_pdf', + 'is_compensation_details_pdf', 'is_selection_procedure_details_pdf') + + +class AdminAdmin(ExportMixin, SimpleHistoryAdmin): + resource_class = PlacementResources + @admin.register(Placement) -class Placement(admin.ModelAdmin): +class Placement(AdminAdmin): list_display = (COMPANY_NAME, CONTACT_PERSON_NAME, PHONE_NUMBER, 'tier', 'compensation_CTC') search_fields = (COMPANY_NAME, CONTACT_PERSON_NAME) ordering = (COMPANY_NAME, CONTACT_PERSON_NAME, 'tier', 'compensation_CTC') list_filter = ('tier',) +class PlacementApplicationResources(resources.ModelResource): + class Meta: + model = PlacementApplication + exclude = ('id', 'changed_by') + + +class PlacementAdmin(ExportMixin, SimpleHistoryAdmin): + resource_class = PlacementApplicationResources + + @admin.register(PlacementApplication) -class PlacementApplication(admin.ModelAdmin): +class PlacementApplication(PlacementAdmin): list_display = ('id', 'Placement', 'Student', 'selected') search_fields = ('id',) ordering = ('id',) @@ -58,8 +142,18 @@ class PlacementApplication(admin.ModelAdmin): return model_admin_url(obj.student) +class PrePlacementResources(resources.ModelResource): + class Meta: + model = PrePlacementOffer + exclude = ('id', 'changed_by') + + +class PrePlacementOfferAdmin(ExportMixin, SimpleHistoryAdmin): + resource_class = PrePlacementResources + + @admin.register(PrePlacementOffer) -class PrePlacementOffer(admin.ModelAdmin): +class PrePlacementOffer(PrePlacementOfferAdmin): list_display = ('company', 'Student', 'accepted') search_fields = ('company',) ordering = ('company',) diff --git a/CDC_Backend/APIs/adminUrls.py b/CDC_Backend/APIs/adminUrls.py index 90d9c71..da8acd6 100644 --- a/CDC_Backend/APIs/adminUrls.py +++ b/CDC_Backend/APIs/adminUrls.py @@ -8,10 +8,12 @@ urlpatterns = [ path('updateDeadline/', adminViews.updateDeadline, name="Update Deadline"), path('updateOfferAccepted/', adminViews.updateOfferAccepted, name="Update Offer Accepted"), path('updateEmailVerified', adminViews.updateEmailVerified, name="Update Email Verified"), - path('updateAdditionalInfo/', adminViews.updateAdditionalInfo, name="Update Additional Info"), + path('deleteAdditionalInfo/', adminViews.deleteAdditionalInfo, name="Delete Additional Info"), + path('addAdditionalInfo/', adminViews.addAdditionalInfo, name="Add Additional Info"), path('getApplications/', adminViews.getApplications, name="Get Applications"), path("submitApplication/", adminViews.submitApplication, name="Submit Application"), path('generateCSV/', adminViews.generateCSV, name="Generate CSV"), path('addPPO/', adminViews.addPPO, name="Add PPO"), path('getStudentApplication/', adminViews.getStudentApplication, name="Get student application"), + path('getStats/', adminViews.getStats, name="Get Stats"), ] diff --git a/CDC_Backend/APIs/adminViews.py b/CDC_Backend/APIs/adminViews.py index 4acf47c..e935e8d 100644 --- a/CDC_Backend/APIs/adminViews.py +++ b/CDC_Backend/APIs/adminViews.py @@ -39,6 +39,7 @@ def markStatus(request, id, email, user_type): sendEmail(email, subject, data, STUDENT_APPLICATION_STATUS_SELECTED_TEMPLATE) else: sendEmail(email, subject, data, STUDENT_APPLICATION_STATUS_NOT_SELECTED_TEMPLATE) + application.chaged_by = get_object_or_404(User, id=id) application.save() else: raise ValueError("Student - " + i[STUDENT_ID] + " didn't apply for this opening") @@ -89,6 +90,7 @@ def updateDeadline(request, id, email, user_type): opening = get_object_or_404(Placement, pk=data[OPENING_ID]) # Updating deadline date with correct format in datetime field opening.deadline_datetime = datetime.datetime.strptime(data[DEADLINE_DATETIME], '%Y-%m-%d %H:%M:%S %z') + opening.changed_by = get_object_or_404(User, id=id) opening.save() return Response({'action': "Update Deadline", 'message': "Deadline Updated"}, status=status.HTTP_200_OK) @@ -102,21 +104,30 @@ def updateDeadline(request, id, email, user_type): @api_view(['POST']) -@isAuthorized([ADMIN]) +@isAuthorized([SUPER_ADMIN]) @precheck([OPENING_ID, OFFER_ACCEPTED]) def updateOfferAccepted(request, id, email, user_type): try: data = request.data - print(data) + offer_accepted = data[OFFER_ACCEPTED] opening = get_object_or_404(Placement, pk=data[OPENING_ID]) - opening.offer_accepted = True if data[OFFER_ACCEPTED] == True else False - print(opening.offer_accepted) - opening.save() + if opening.offer_accepted is None: + opening.offer_accepted = offer_accepted == "true" + opening.changed_by = get_object_or_404(User, id=id) + opening.save() + if opening.offer_accepted: + send_opening_notifications(opening.id) + else: + raise ValueError("Offer Status already updated") + return Response({'action': "Update Offer Accepted", 'message': "Offer Accepted Updated"}, status=status.HTTP_200_OK) except Http404: return Response({'action': "Update Offer Accepted", 'message': 'Opening Not Found'}, status=status.HTTP_404_NOT_FOUND) + except ValueError as e: + return Response({'action': "Update Offer Accepted", 'message': str(e)}, + status=status.HTTP_400_BAD_REQUEST) except: logger.warning("Update Offer Accepted: " + str(sys.exc_info())) return Response({'action': "Update Offer Accepted", 'message': "Something went wrong"}, @@ -131,6 +142,7 @@ def updateEmailVerified(request, id, email, user_type): data = request.data opening = get_object_or_404(Placement, pk=data[OPENING_ID]) opening.email_verified = True if data[EMAIL_VERIFIED] == "true" else False + opening.changed_by = get_object_or_404(User, id=id) opening.save() return Response({'action': "Update Email Verified", 'message': "Email Verified Updated"}, status=status.HTTP_200_OK) @@ -145,29 +157,55 @@ def updateEmailVerified(request, id, email, user_type): @api_view(['POST']) @isAuthorized([ADMIN]) -@precheck([OPENING_ID, ADDITIONAL_INFO]) -def updateAdditionalInfo(request, id, email, user_type): +@precheck([OPENING_ID, FIELD]) +def deleteAdditionalInfo(request, id, email, user_type): try: data = request.data opening = get_object_or_404(Placement, pk=data[OPENING_ID]) - if data[ADDITIONAL_INFO] == "": - opening.additional_info = [] - elif isinstance(data[ADDITIONAL_INFO], list): - opening.additional_info = data[ADDITIONAL_INFO] + if data[FIELD] in opening.additional_info: + opening.additional_info.remove(data[FIELD]) + opening.changed_by = get_object_or_404(User, id=id) + opening.save() + return Response({'action': "Delete Additional Info", 'message': "Additional Info Deleted"}, + status=status.HTTP_200_OK) else: - raise ValueError("Additional Info must be a list") - opening.save() - return Response({'action': "Update Additional Info", 'message': "Additional Info Updated"}, - status=status.HTTP_200_OK) + raise ValueError("Additional Info Not Found") except Http404: - return Response({'action': "Update Additional Info", 'message': 'Opening Not Found'}, + return Response({'action': "Delete Additional Info", 'message': 'Opening Not Found'}, status=status.HTTP_404_NOT_FOUND) except ValueError: - return Response({'action': "Update Additional Info", 'message': "Additional Info must be a list"}, + return Response({'action': "Delete Additional Info", 'message': "Additional Info not found"}, + status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.warning("Delete Additional Info: " + str(e)) + return Response({'action': "Delete Additional Info", 'message': "Something went wrong"}, + status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@isAuthorized([ADMIN]) +@precheck([OPENING_ID, FIELD]) +def addAdditionalInfo(request, id, email, user_type): + try: + data = request.data + opening = get_object_or_404(Placement, pk=data[OPENING_ID]) + if data[FIELD] not in opening.additional_info: + opening.additional_info.append(data[FIELD]) + opening.save() + return Response({'action': "Add Additional Info", 'message': "Additional Info Added"}, + status=status.HTTP_200_OK) + else: + raise ValueError("Additional Info Found") + + except Http404: + return Response({'action': "Add Additional Info", 'message': 'Opening Not Found'}, + status=status.HTTP_404_NOT_FOUND) + except ValueError: + return Response({'action': "Add Additional Info", 'message': "Additional Info already found"}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: - logger.warning("Update Additional Info: " + str(e)) - return Response({'action': "Update Additional Info", 'message': "Something went wrong"}, + logger.warning("Add Additional Info: " + str(e)) + return Response({'action': "Add Additional Info", 'message': "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST) @@ -199,7 +237,7 @@ def submitApplication(request, id, email, user_type): data = request.data student = get_object_or_404(Student, pk=data[STUDENT_ID]) opening = get_object_or_404(Placement, pk=data[OPENING_ID]) - + student_user = get_object_or_404(User, id=student.id) if data[APPLICATION_ID] == "": application = PlacementApplication() application.id = generateRandomString() @@ -216,7 +254,16 @@ def submitApplication(request, id, email, user_type): else: additional_info[i] = data[ADDITIONAL_INFO][i] application.additional_info = json.dumps(additional_info) + data = { + "name": student.name, + "company_name": opening.company_name, + "application_type": "Placement", + "additional_info": dict(json.loads(application.additional_info)), + } + subject = STUDENT_APPLICATION_SUBMITTED_TEMPLATE_SUBJECT.format(company_name=opening.company_name) + application.changed_by = get_object_or_404(User, id=id) application.save() + sendEmail(student_user.email, subject, data, STUDENT_APPLICATION_SUBMITTED_TEMPLATE) return Response({'action': "Add Student Application", 'message': "Application added"}, status=status.HTTP_200_OK) else: @@ -235,7 +282,17 @@ def submitApplication(request, id, email, user_type): additional_info[i] = data[ADDITIONAL_INFO][i] application.additional_info = json.dumps(additional_info) + data = { + "name": student.name, + "company_name": opening.company_name, + "application_type": "Placement", + "resume": application.resume[16:], + "additional_info_items": dict(json.loads(application.additional_info)), + } + subject = STUDENT_APPLICATION_UPDATED_TEMPLATE_SUBJECT.format(company_name=opening.company_name) + application.changed_by = get_object_or_404(User, id=id) application.save() + sendEmail(student_user.email, subject, data, STUDENT_APPLICATION_UPDATED_TEMPLATE) return Response({'action': "Add Student Application", 'message': "Application updated"}, status=status.HTTP_200_OK) else: @@ -300,7 +357,6 @@ def generateCSV(request, id, email, user_type): status=status.HTTP_200_OK) except: logger.warning("Create csv: " + str(sys.exc_info())) - print(sys.exc_info()) return Response({'action': "Create csv", 'message': "Something Went Wrong"}, status=status.HTTP_400_BAD_REQUEST) @@ -313,7 +369,7 @@ def addPPO(request, id, email, user_type): data = request.data PPO = PrePlacementOffer() PPO.company = data[COMPANY_NAME] - PPO.compensation = data[COMPENSATION_GROSS] + PPO.compensation = int(data[COMPENSATION_GROSS]) if data[OFFER_ACCEPTED] == "true": PPO.accepted = True elif data[OFFER_ACCEPTED] == "false": @@ -322,15 +378,15 @@ def addPPO(request, id, email, user_type): PPO.accepted = None PPO.student = get_object_or_404(Student, id=data[STUDENT_ID]) PPO.designation = data[DESIGNATION] - PPO.tier = data[TIER] + PPO.tier = int(data[TIER]) if COMPENSATION_DETAILS in data: PPO.compensation_details = data[COMPENSATION_DETAILS] + PPO.changed_by = get_object_or_404(User, id=id) PPO.save() return Response({'action': "Add PPO", 'message': "PPO added"}, status=status.HTTP_200_OK) except: logger.warning("Add PPO: " + str(sys.exc_info())) - print(sys.exc_info()) return Response({'action': "Add PPO", 'message': "Something Went Wrong"}, status=status.HTTP_400_BAD_REQUEST) @@ -373,3 +429,205 @@ def getStudentApplication(request, id, email, user_type): logger.warning("Get Student Application: " + str(sys.exc_info())) return Response({'action': "Get Student Application", 'message': "Something Went Wrong"}, status.HTTP_400_BAD_REQUEST) + +@api_view(['GET']) +@isAuthorized(allowed_users=[ADMIN]) +def getStats(request, id, email, user_type): + try: + stats = [] + placement_ids = {} + + tier_count = { + "CSE": { + "1":0, + "2":0, + "3":0, + "4":0, + "5":0, + "6":0, + "7":0, + "psu":0, + }, + "EE": { + "1":0, + "2":0, + "3":0, + "4":0, + "5":0, + "6":0, + "7":0, + "psu":0, + }, + "MMAE": { + "1":0, + "2":0, + "3":0, + "4":0, + "5":0, + "6":0, + "7":0, + "psu":0, + + }, + "Total": { + "1":0, + "2":0, + "3":0, + "4":0, + "5":0, + "6":0, + "7":0, + "psu":0, + }, + } + number_of_students_placed = { + "CSE": 0, + "EE": 0, + "MMAE": 0, + "Total": 0, + } + number_of_students_with_multiple_offers = 0 + number_of_students_with_no_offers = { + "CSE": 0, + "EE": 0, + "MMAE": 0, + "Total": 0, + } + max_CTC = { + "CSE": 0, + "EE": 0, + "MMAE": 0 + } + average_CTC = { + "CSE": 0, + "EE": 0, + "MMAE": 0 + } + count = { + "CSE": 0, + "EE": 0, + "MMAE": 0 + } + + + + students = Student.objects.all().order_by("roll_no") + for student in students.iterator(): + + applications = PlacementApplication.objects.filter(student=student, selected=True) + ppos = PrePlacementOffer.objects.filter(student=student, accepted=True) + + first_offer_data = None + + second_offer_data = None + + # get the first and second offer + offers = [] + offers.extend(applications) + offers.extend(ppos) + + if len(offers) == 0: + number_of_students_with_no_offers[student.branch] += 1 + number_of_students_with_no_offers["Total"] += 1 + else: + number_of_students_placed[student.branch] += 1 + number_of_students_placed["Total"] += 1 + if len(offers) > 1: + number_of_students_with_multiple_offers += 1 + + + + for offer in offers: + if type(offer) == PrePlacementOffer: + if first_offer_data is None: + first_offer_data = { + "id": offer.id, + "company": offer.company, + "compensation": offer.compensation, + "tier": offer.tier, + "type": "PPO", + } + elif second_offer_data is None: + second_offer_data = { + "id": offer.id, + "company": offer.company, + "compensation": offer.compensation, + "tier": offer.tier, + "type": "PPO", + } + elif type(offer) == PlacementApplication: + if first_offer_data is None: + first_offer_data = { + "id": offer.placement.id, + "company": offer.placement.company_name, + "compensation": offer.placement.compensation_CTC, + "tier": offer.placement.tier, + "type": "Placement", + } + elif second_offer_data is None: + second_offer_data = { + "id": offer.placement.id, + "company": offer.placement.company_name, + "compensation": offer.placement.compensation_CTC, + "tier": offer.placement.tier, + "type": "Placement", + } + + data = { + "id": student.id, + "name": student.name, + "roll_no": student.roll_no, + "batch": student.batch, + "branch": student.branch, + "cpi": student.cpi, + "first_offer": first_offer_data['company'] if first_offer_data is not None else None, + "first_offer_tier": first_offer_data['tier'] if first_offer_data is not None else None, + "first_offer_compensation": first_offer_data['compensation'] if first_offer_data is not None else None, + + "second_offer": second_offer_data['company'] if second_offer_data is not None else None, + "second_offer_tier": second_offer_data['tier'] if second_offer_data is not None else None, + "second_offer_compensation": second_offer_data['compensation'] if second_offer_data is not None else None, + } + if first_offer_data is not None: + tier_count[student.branch][first_offer_data['tier']] += 1 + tier_count['Total'][first_offer_data['tier']] += 1 + max_CTC[student.branch] = max(max_CTC[student.branch], first_offer_data['compensation']) + average_CTC[student.branch] += first_offer_data['compensation'] + count[student.branch] += 1 + + if first_offer_data['type'] == "Placement": + placement_ids[first_offer_data['company']] = first_offer_data['id'] + + if second_offer_data is not None: + tier_count[student.branch][second_offer_data['tier']] += 1 + tier_count['Total'][second_offer_data['tier']] += 1 + max_CTC[student.branch] = max(max_CTC[student.branch], second_offer_data['compensation']) + average_CTC[student.branch] += second_offer_data['compensation'] + count[student.branch] += 1 + + if second_offer_data['type'] == "Placement": + placement_ids[second_offer_data['company']] = second_offer_data['id'] + + stats.append(data) + + for branch in average_CTC: + if count[branch] > 0: + average_CTC[branch] /= count[branch] + # round off to 2 decimal places + average_CTC[branch] = round(average_CTC[branch], 2) + else: + average_CTC[branch] = 0 + return Response({'action': "Get Stats", 'message': "Stats fetched", 'stats': stats, 'placement_ids': placement_ids, + "tier_count": {br: tier_count[br].values() for br in tier_count}, + "number_of_students_placed": number_of_students_placed, + "number_of_students_with_multiple_offers": number_of_students_with_multiple_offers, + "number_of_students_with_no_offers": number_of_students_with_no_offers, + "max_CTC": max_CTC, + "average_CTC": average_CTC, + }, + status=status.HTTP_200_OK) + except: + logger.warning("Get Stats: " + str(sys.exc_info())) + print(sys.exc_info()) + return Response({'action': "Get Stats", 'message': "Something Went Wrong"}, + status=status.HTTP_400_BAD_REQUEST) diff --git a/CDC_Backend/APIs/companyViews.py b/CDC_Backend/APIs/companyViews.py index 2593513..77d228e 100644 --- a/CDC_Backend/APIs/companyViews.py +++ b/CDC_Backend/APIs/companyViews.py @@ -194,7 +194,7 @@ def addPlacement(request): '%d-%m-%Y').date() # Only Allowing Fourth Year for Placement - opening.allowed_batch = [FOURTH_YEAR, ] + opening.allowed_batch = [2017, 2018, 2019, 2020, 2021] # Check if allowed_branch are valid if data[ALLOWED_BRANCH] is None: raise ValueError('Allowed Branch cannot be empty') @@ -230,9 +230,14 @@ def addPlacement(request): status=status.HTTP_200_OK) except ValueError as e: + store_all_files(request) + exception_email(data) + logger.info("ValueError in addPlacement: " + str(e)) return Response({'action': "Add Placement", 'message': str(e)}, status=status.HTTP_400_BAD_REQUEST) except: + store_all_files(request) + exception_email(data) logger.warning("Add New Placement: " + str(sys.exc_info())) return Response({'action': "Add Placement", 'message': "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST) @@ -276,11 +281,9 @@ def verifyEmail(request): data = { "designation": opening.designation, "opening_type": PLACEMENT, - "opening_link": PLACEMENT_OPENING_URL.format(id=opening.id), # Some Changes here too "company_name": opening.company_name, } - json_data = json.dumps(data, default=str) - sendEmail(opening.email, COMPANY_OPENING_SUBMITTED_TEMPLATE_SUBJECT.format(id=opening.id), json_data, + sendEmail([opening.email, CDC_MAIl_ADDRESS], COMPANY_OPENING_SUBMITTED_TEMPLATE_SUBJECT.format(id=opening.id), data, COMPANY_OPENING_SUBMITTED_TEMPLATE, attachment_jnf_respone) return Response({'action': "Verify Email", 'message': "Email Verified Successfully"}, diff --git a/CDC_Backend/APIs/constants.py b/CDC_Backend/APIs/constants.py index 5981cba..5696feb 100644 --- a/CDC_Backend/APIs/constants.py +++ b/CDC_Backend/APIs/constants.py @@ -1,20 +1,24 @@ +import os + BRANCH_CHOICES = [ ["CSE", "CSE"], ["EE", "EE"], ["ME", "ME"], + ['MMAE', 'MMAE'], ['EP', 'EP'], ] BRANCHES = [ "CSE", "EE", - "ME", + "MMAE", "EP" ] BATCH_CHOICES = [ ["2021", "2021"], ["2020", "2020"], ["2019", "2019"], - ["2018", "2018"] + ["2018", "2018"], + ["2017", "2017"], ] OFFER_CITY_TYPE = [ @@ -29,31 +33,42 @@ TIERS = [ ['3', 'Tier 3'], ['4', 'Tier 4'], ['5', 'Tier 5'], - ['6', 'Tier 6'] + ['6', 'Tier 6'], + ['7', 'Tier 7'], +] + +DEGREE_CHOICES = [ + ['bTech', 'B.Tech'], + ['ms/phd', 'MS/ PhD'], ] TOTAL_BRANCHES = 4 # Total No of Branches -TOTAL_BATCHES = 4 # Total No of Batches +TOTAL_BATCHES = 5 # Total No of Batches + +CDC_MAIl_ADDRESS = 'cdc@iitdh.ac.in' # To be Configured Properly -CLIENT_ID = "956830229554-290mirc16pdhd5j7ph7v7ukibo4t1qcp.apps.googleusercontent.com" # Google Login Client ID +CLIENT_ID = os.environ.get('GOOGLE_OAUTH_CLIENT_ID') # Google Login Client ID # To be Configured Properly -PLACEMENT_OPENING_URL = "https://www.googleapis.com/auth/adwords/{id}" # On frontend, this is the URL to be opened -LINK_TO_STORAGE_COMPANY_ATTACHMENT = "http://localhost/storage/Company_Attachments/" -LINK_TO_STORAGE_RESUME = "http://localhost/storage/Resumes/" -LINK_TO_APPLICATIONS_CSV = "http://localhost/storage/Application_CSV/" -LINK_TO_EMAIl_VERIFICATION_API = "http://localhost:3000/company/verifyEmail?token={token}" +PLACEMENT_OPENING_URL = "https://cdc.iitdh.ac.in/portal/student/dashboard/placements/{id}" # On frontend, this is the URL to be opened +LINK_TO_STORAGE_COMPANY_ATTACHMENT = "https://cdc.iitdh.ac.in/storage/Company_Attachments/" +LINK_TO_STORAGE_RESUME = "https://cdc.iitdh.ac.in/storage/Resumes/" +LINK_TO_APPLICATIONS_CSV = "https://cdc.iitdh.ac.in/storage/Application_CSV/" +LINK_TO_EMAIl_VERIFICATION_API = "https://cdc.iitdh.ac.in/portal/company/verifyEmail?token={token}" +PDF_FILES_SERVING_ENDPOINT = 'https://cdc.iitdh.ac.in/storage/Company_Attachments/' # TODO: Change this to actual URL EMAIL = "email" STUDENT = 'student' ADMIN = 'admin' -COMPANY = '' +SUPER_ADMIN = 's_admin' +COMPANY = 'company' TIER = 'tier' # To be Configured Properly FOURTH_YEAR = '2019' MAX_OFFERS_PER_STUDENT = 2 +MAX_RESUMES_PER_STUDENT = 3 EMAIL_VERIFICATION_TOKEN_TTL = 48 # in hours JNF_TEXT_MAX_CHARACTER_COUNT = 100 JNF_TEXTMEDIUM_MAX_CHARACTER_COUNT = 200 @@ -70,6 +85,7 @@ RESUME_FILE_NAME = 'resume_file_name' APPLICATION_ID = "application_id" OPENING_ID = "opening_id" ADDITIONAL_INFO = "additional_info" +FIELD = "field" STATUS_ACCEPTING_APPLICATIONS = "Accepting Applications" @@ -133,21 +149,25 @@ EXCLUDE_IN_PDF = ['id', 'is_company_details_pdf', 'offer_accepted', 'is_descript 'email_verified', 'created_at'] SPECIAL_FORMAT_IN_PDF = ['website', 'company_details_pdf_names', 'description_pdf_names', 'compensation_details_pdf_names', - 'selection_procedure_pdf_names'] + 'selection_procedure_details_pdf_names'] +COMPANY_OPENING_ERROR_TEMPLATE = "Alert! Error submitting opening for {company_name}." COMPANY_OPENING_SUBMITTED_TEMPLATE_SUBJECT = "Notification Submitted - {id} - Career Development Cell, IIT Dharwad" -STUDENT_APPLICATION_STATUS_TEMPLATE_SUBJECT = 'Application Status : {company_name} - {id}' +STUDENT_APPLICATION_STATUS_TEMPLATE_SUBJECT = 'Application Status - {company_name} - {id}' STUDENT_APPLICATION_SUBMITTED_TEMPLATE_SUBJECT = 'CDC - Application Submitted - {company_name}' +STUDENT_APPLICATION_UPDATED_TEMPLATE_SUBJECT = 'CDC - Application Updated - {company_name}' COMPANY_EMAIl_VERIFICATION_TEMPLATE_SUBJECT = 'Email Verification - Career Development Cell, IIT Dharwad' +NOTIFY_STUDENTS_OPENING_TEMPLATE_SUBJECT = 'Placement Opportunity at {company_name}' STUDENT_APPLICATION_SUBMITTED_TEMPLATE = 'student_application_submitted.html' COMPANY_OPENING_SUBMITTED_TEMPLATE = 'company_opening_submitted.html' STUDENT_APPLICATION_STATUS_SELECTED_TEMPLATE = 'student_application_status_selected.html' STUDENT_APPLICATION_STATUS_NOT_SELECTED_TEMPLATE = 'student_application_status_not_selected.html' +STUDENT_APPLICATION_UPDATED_TEMPLATE = 'student_application_updated.html' COMPANY_EMAIL_VERIFICATION_TEMPLATE = 'company_email_verification.html' COMPANY_JNF_RESPONSE_TEMPLATE = 'company_jnf_response.html' +NOTIFY_STUDENTS_OPENING_TEMPLATE = 'notify_students_new_opening.html' APPLICATION_CSV_COL_NAMES = ['Applied At', 'Roll No.', 'Name', 'Email', 'Phone Number', 'Branch', 'Batch', 'CPI', 'Resume', 'Selected', ] -PDF_FILES_SERVING_ENDPOINT = 'http://localhost/storage/Company_Attachments/' # TODO: Change this to actual URL diff --git a/CDC_Backend/APIs/models.py b/CDC_Backend/APIs/models.py index b986089..77d8c6a 100644 --- a/CDC_Backend/APIs/models.py +++ b/CDC_Backend/APIs/models.py @@ -1,6 +1,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils import timezone +from simple_history.models import HistoricalRecords from .constants import * @@ -10,9 +11,10 @@ from .constants import * class User(models.Model): email = models.EmailField(primary_key=True, blank=False, max_length=JNF_TEXT_MAX_CHARACTER_COUNT) - id = models.CharField(blank=False, max_length=25) + id = models.CharField(blank=False, max_length=25, db_index=True) user_type = ArrayField(models.CharField(blank=False, max_length=10), size=4, default=list, blank=False) last_login_time = models.DateTimeField(default=timezone.now) + history = HistoricalRecords() class Meta: verbose_name_plural = "User" @@ -30,18 +32,46 @@ class Student(models.Model): default=list, blank=True) cpi = models.DecimalField(decimal_places=2, max_digits=4) can_apply = models.BooleanField(default=True, verbose_name='Registered') + changed_by = models.ForeignKey(User, blank=True, on_delete=models.RESTRICT, default=None, null=True) + degree = models.CharField(choices=DEGREE_CHOICES, blank=False, max_length=10, default=DEGREE_CHOICES[0][0]) + history = HistoricalRecords(user_model=User) def __str__(self): return str(self.roll_no) + @property + def _history_user(self): + return self.changed_by + + @_history_user.setter + def _history_user(self, value): + if isinstance(value, User): + self.changed_by = value + else: + self.changed_by = None + class Admin(models.Model): id = models.CharField(blank=False, max_length=15, primary_key=True) name = models.CharField(blank=False, max_length=JNF_TEXT_MAX_CHARACTER_COUNT) + changed_by = models.ForeignKey(User, blank=True, on_delete=models.RESTRICT, default=None, null=True) + history = HistoricalRecords(user_model=User) + + @property + def _history_user(self): + return self.changed_by + + @_history_user.setter + def _history_user(self, value): + if isinstance(value, User): + self.changed_by = value + else: + self.changed_by = None def two_day_after_today(): - return timezone.now() + timezone.timedelta(days=2) + # round off to nearest day + return timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timezone.timedelta(days=2) class Placement(models.Model): @@ -53,7 +83,7 @@ class Placement(models.Model): nature_of_business = models.CharField(blank=False, max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, default="") type_of_organisation = models.CharField(max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, default="", blank=False) website = models.CharField(blank=True, max_length=JNF_TEXT_MAX_CHARACTER_COUNT) - company_details = models.CharField(max_length=JNF_TEXTAREA_MAX_CHARACTER_COUNT, default=None, null=True) + company_details = models.CharField(max_length=JNF_TEXTAREA_MAX_CHARACTER_COUNT, default=None, null=True, blank=True) company_details_pdf_names = ArrayField( models.CharField(null=True, default=None, max_length=JNF_TEXT_MAX_CHARACTER_COUNT), size=5, default=list, blank=True) @@ -114,6 +144,8 @@ class Placement(models.Model): deadline_datetime = models.DateTimeField(blank=False, verbose_name="Deadline Date", default=two_day_after_today) created_at = models.DateTimeField(blank=False, default=None, null=True) updated_at = models.DateTimeField(blank=False, default=None, null=True) + changed_by = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True) + history = HistoricalRecords(user_model=User) def format(self): if self.company_name is not None: @@ -156,7 +188,19 @@ class Placement(models.Model): if self.other_requirements is not None: self.other_requirements = self.other_requirements.strip()[:JNF_TEXTAREA_MAX_CHARACTER_COUNT] if self.additional_info is not None: - self.additional_info = [info.strip()[:JNF_TEXTMEDIUM_MAX_CHARACTER_COUNT] for info in list(self.additional_info)] + self.additional_info = [info.strip()[:JNF_TEXTMEDIUM_MAX_CHARACTER_COUNT] for info in + list(self.additional_info)] + + @property + def _history_user(self): + return self.changed_by + + @_history_user.setter + def _history_user(self, value): + if isinstance(value, User): + self.changed_by = value + else: + self.changed_by = None def save(self, *args, **kwargs): ''' On save, add timestamps ''' @@ -179,6 +223,8 @@ class PlacementApplication(models.Model): selected = models.BooleanField(null=True, default=None, blank=True) applied_at = models.DateTimeField(blank=False, default=None, null=True) updated_at = models.DateTimeField(blank=False, default=None, null=True) + changed_by = models.ForeignKey(User, blank=False, on_delete=models.RESTRICT, default=None, null=True) + history = HistoricalRecords(user_model=User) def save(self, *args, **kwargs): ''' On save, add timestamps ''' @@ -188,6 +234,17 @@ class PlacementApplication(models.Model): return super(PlacementApplication, self).save(*args, **kwargs) + @property + def _history_user(self): + return self.changed_by + + @_history_user.setter + def _history_user(self, value): + if isinstance(value, User): + self.changed_by = value + else: + self.changed_by = None + class Meta: verbose_name_plural = "Placement Applications" unique_together = ('placement_id', 'student_id') @@ -204,5 +261,31 @@ class PrePlacementOffer(models.Model): compensation = models.IntegerField(blank=False) # Job - Per Year compensation_details = models.CharField(blank=True, max_length=200) tier = models.CharField(blank=False, choices=TIERS, max_length=10) - designation = models.CharField(blank=False, max_length=25, default=None, null=True) + designation = models.CharField(blank=False, max_length=100, default=None, null=True) accepted = models.BooleanField(default=None, null=True) + changed_by = models.ForeignKey(User, blank=False, on_delete=models.RESTRICT, default=None, null=True) + history = HistoricalRecords(user_model=User) + + @property + def _history_user(self): + return self.changed_by + + @_history_user.setter + def _history_user(self, value): + if isinstance(value, User): + self.changed_by = value + else: + self.changed_by = None + + +class Contributor(models.Model): + id = models.AutoField(primary_key=True) + name = models.CharField(max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, blank=False, default="") + email = models.EmailField(max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, blank=False, default="", unique=True) + github_id = models.CharField(max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, blank=False, default="", unique=True) + linkedin = models.CharField(max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, unique=True, null=True) + commits = models.IntegerField(blank=False, default=0) + image = models.CharField(max_length=JNF_SMALLTEXT_MAX_CHARACTER_COUNT, blank=False, default="", null=True) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/CDC_Backend/APIs/serializers.py b/CDC_Backend/APIs/serializers.py index f7cc868..bc8cbaa 100644 --- a/CDC_Backend/APIs/serializers.py +++ b/CDC_Backend/APIs/serializers.py @@ -162,7 +162,8 @@ class PlacementApplicationSerializer(serializers.ModelSerializer): return data def get_resume_link(self, obj): - ele = {'link': LINK_TO_STORAGE_RESUME + urllib.parse.quote(obj.id + "/" + obj.resume), 'name': obj.resume} + ele = {'link': LINK_TO_STORAGE_RESUME + urllib.parse.quote(str(obj.student.roll_no) + "/" + obj.resume), + 'name': obj.resume} return ele class Meta: @@ -185,3 +186,9 @@ class PlacementApplicationSerializerForAdmin(serializers.ModelSerializer): class Meta: model = PlacementApplication exclude = ['placement', 'resume'] + + +class ContributorSerializer(serializers.ModelSerializer): + class Meta: + model = Contributor + fields = '__all__' diff --git a/CDC_Backend/APIs/studentUrls.py b/CDC_Backend/APIs/studentUrls.py index f532849..8a1acce 100644 --- a/CDC_Backend/APIs/studentUrls.py +++ b/CDC_Backend/APIs/studentUrls.py @@ -9,4 +9,6 @@ urlpatterns = [ path("addResume/", studentViews.addResume, name="Upload Resume"), path("deleteResume/", studentViews.deleteResume, name="Upload Resume"), path("submitApplication/", studentViews.submitApplication, name="Submit Application"), + path("deleteApplication/", studentViews.deleteApplication, name="Delete Application"), + path("getContributorStats/", studentViews.getContributorStats, name="Get Contributor Stats"), ] diff --git a/CDC_Backend/APIs/studentViews.py b/CDC_Backend/APIs/studentViews.py index d3c3fe9..c68d846 100644 --- a/CDC_Backend/APIs/studentViews.py +++ b/CDC_Backend/APIs/studentViews.py @@ -40,17 +40,23 @@ def addResume(request, id, email, user_type): student = get_object_or_404(Student, id=id) files = request.FILES + if len(student.resumes) >= MAX_RESUMES_PER_STUDENT: + raise PermissionError('Max Number of Resumes limit reached') + file = files['file'] destination_path = STORAGE_DESTINATION_RESUMES + str(student.roll_no) + "/" file_name = saveFile(file, destination_path) student.resumes.append(file_name) - + student.changed_by = get_object_or_404(User, id=id) student.save() return Response({'action': "Upload Resume", 'message': "Resume Added"}, status=status.HTTP_200_OK) except Http404: return Response({'action': "Upload Resume", 'message': 'Student Not Found'}, status=status.HTTP_404_NOT_FOUND) + except PermissionError: + return Response({'action': "Upload Resume", 'message': 'Max Number of Resumes limit reached'}, + status=status.HTTP_400_BAD_REQUEST) except: if path.exists(destination_path): logger.error("Upload Resume: Error in Saving Resume") @@ -71,7 +77,10 @@ def getDashboard(request, id, email, user_type): allowed_branch__contains=[studentDetails.branch], deadline_datetime__gte=datetime.datetime.now(), offer_accepted=True, email_verified=True).order_by('deadline_datetime') - placementsdata = PlacementSerializerForStudent(placements, many=True).data + filtered_placements = placement_eligibility_filters(studentDetails, placements) + + placementsdata = PlacementSerializerForStudent(filtered_placements, many=True).data + placementApplications = PlacementApplication.objects.filter(student_id=id) placementApplications = PlacementApplicationSerializer(placementApplications, many=True).data return Response( @@ -83,7 +92,6 @@ def getDashboard(request, id, email, user_type): status=status.HTTP_404_NOT_FOUND) except: logger.warning("Get Dashboard -Student: " + str(sys.exc_info())) - print(sys.exc_info()) return Response({'action': "Get Dashboard - Student", 'message': "Something Went Wrong"}, status=status.HTTP_400_BAD_REQUEST) @@ -104,6 +112,7 @@ def deleteResume(request, id, email, user_type): if path.exists(destination_path): # remove(destination_path) student.resumes.remove(file_name) + student.changed_by = get_object_or_404(User, id=id) student.save() return Response({'action': "Delete Resume", 'message': "Resume Deleted"}, status=status.HTTP_200_OK) @@ -177,7 +186,7 @@ def submitApplication(request, id, email, user_type): } subject = STUDENT_APPLICATION_SUBMITTED_TEMPLATE_SUBJECT.format(company_name=opening.company_name) sendEmail(email, subject, data, STUDENT_APPLICATION_SUBMITTED_TEMPLATE) - + application.changed_by = get_object_or_404(User, id=id) application.save() return Response({'action': "Submit Application", 'message': "Application Submitted"}, status=status.HTTP_200_OK) @@ -192,8 +201,49 @@ def submitApplication(request, id, email, user_type): status=status.HTTP_404_NOT_FOUND) except: logger.warning("Submit Application: " + str(sys.exc_info())) - print(traceback.format_exc()) - print(sys.exc_info()) return Response({'action': "Submit Application", 'message': "Something Went Wrong"}, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +@isAuthorized(allowed_users=[STUDENT]) +@precheck(required_data=[APPLICATION_ID]) +def deleteApplication(request, id, email, user_type): + try: + data = request.data + application = get_object_or_404(PlacementApplication, id=data[APPLICATION_ID], + student_id=id) + if application.placement.deadline_datetime < timezone.now(): + raise PermissionError("Deadline Passed") + + application.delete() + return Response({'action': "Delete Application", 'message': "Application Deleted"}, + status=status.HTTP_200_OK) + except Http404 as e: + return Response({'action': "Delete Application", 'message': str(e)}, + status=status.HTTP_404_NOT_FOUND) + except PermissionError as e: + return Response({'action': "Delete Application", 'message': str(e)}, + status=status.HTTP_403_FORBIDDEN) + except: + logger.warning("Delete Application: " + str(sys.exc_info())) + + return Response({'action': "Delete Application", 'message': "Something Went Wrong"}, + status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET']) +@isAuthorized(allowed_users='*') +def getContributorStats(request, id, email, user_type): + try: + contributors = Contributor.objects.all() + serialized_data = ContributorSerializer(contributors, many=True).data + return Response({'action': "Get Contributor Stats", 'message': "Contributor Stats Fetched", + 'data': serialized_data}, + status=status.HTTP_200_OK) + except: + logger.warning("Get Contributor Stats: " + str(sys.exc_info())) + + return Response({'action': "Get Contributor Stats", 'message': "Something Went Wrong"}, + status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/CDC_Backend/APIs/utils.py b/CDC_Backend/APIs/utils.py index 83c2a93..be57175 100644 --- a/CDC_Backend/APIs/utils.py +++ b/CDC_Backend/APIs/utils.py @@ -12,6 +12,7 @@ from os import path, remove import background_task import jwt import pdfkit +import pytz import requests as rq from django.conf import settings from django.core.mail import EmailMultiAlternatives @@ -27,7 +28,7 @@ from rest_framework import status from rest_framework.response import Response from .constants import * -from .models import User, PrePlacementOffer, PlacementApplication, Placement +from .models import User, PrePlacementOffer, PlacementApplication, Placement, Student logger = logging.getLogger('db') @@ -57,6 +58,7 @@ def precheck(required_data=None): return view_func(request, *args, **kwargs) except: + logger.warning("Pre check: " + str(sys.exc_info())) return Response({'action': "Pre check", 'message': "Something went wrong"}, status=status.HTTP_400_BAD_REQUEST) @@ -77,7 +79,6 @@ def isAuthorized(allowed_users=None): token_id = headers['HTTP_AUTHORIZATION'][7:] idinfo = id_token.verify_oauth2_token(token_id, requests.Request(), CLIENT_ID) email = idinfo[EMAIL] - print(email) user = get_object_or_404(User, email=email) if user: user.last_login_time = timezone.now() @@ -138,7 +139,7 @@ def saveFile(file, location): return file_name -@background_task.background(schedule=5) +@background_task.background(schedule=2) def sendEmail(email_to, subject, data, template, attachment_jnf_response=None): try: if not isinstance(data, dict): @@ -147,12 +148,15 @@ def sendEmail(email_to, subject, data, template, attachment_jnf_response=None): text_content = strip_tags(html_content) email_from = settings.EMAIL_HOST_USER - recipient_list = [str(email_to), ] + if type(email_to) is list: + recipient_list = [str(email) for email in email_to] + else: + recipient_list = [str(email_to), ] msg = EmailMultiAlternatives(subject, text_content, email_from, recipient_list) msg.attach_alternative(html_content, "text/html") if attachment_jnf_response: - logger.info(attachment_jnf_response) + # logger.info(attachment_jnf_response) pdf = pdfkit.from_string(attachment_jnf_response['html'], False, options={"--enable-local-file-access": "", '--dpi': '96'}) msg.attach(attachment_jnf_response['name'], pdf, 'application/pdf') @@ -160,7 +164,6 @@ def sendEmail(email_to, subject, data, template, attachment_jnf_response=None): return True except: logger.error("Send Email: " + str(sys.exc_info())) - print(str(sys.exc_info()[1])) return False @@ -169,31 +172,36 @@ def PlacementApplicationConditions(student, placement): selected_companies = PlacementApplication.objects.filter(student=student, selected=True) selected_companies_PSU = [i for i in selected_companies if i.placement.tier == 'psu'] PPO = PrePlacementOffer.objects.filter(student=student, accepted=True) - # find lenght of PPO - print(PPO) - print(len(PPO), "ere") + PPO_PSU = [i for i in PPO if i.tier == 'psu'] + # find length of PPO if len(selected_companies) + len(PPO) >= MAX_OFFERS_PER_STUDENT: raise PermissionError("Max Applications Reached for the Season") if len(selected_companies_PSU) > 0: raise PermissionError('Selected for PSU Can\'t apply anymore') + if len(PPO_PSU) > 0: + raise PermissionError('Selected for PSU Can\'t apply anymore') + if placement.tier == 'psu': return True, "Conditions Satisfied" for i in selected_companies: - print(int(i.placement.tier) < int(placement.tier), int(i.placement.tier), int(placement.tier)) if int(i.placement.tier) < int(placement.tier): return False, "Can't apply for this tier" + for i in PPO: + if int(i.tier) < int(placement.tier): + return False, "Can't apply for this tier" + + if student.degree != 'bTech' and not placement.rs_eligible: + raise PermissionError("Can't apply for this placement") + return True, "Conditions Satisfied" except PermissionError as e: return False, e except: - print(sys.exc_info()) - print(traceback.format_exc()) - logger.warning("Utils - PlacementApplicationConditions: " + str(sys.exc_info())) return False, "_" @@ -231,7 +239,6 @@ def getTier(compensation_gross, is_psu=False): logger.warning("Utils - getTier: " + str(sys.exc_info())) return False, e except: - print(sys.exc_info()) logger.warning("Utils - getTier: " + str(sys.exc_info())) return False, "_" @@ -248,36 +255,35 @@ def generateOneTimeVerificationLink(email, opening_id, opening_type): link = LINK_TO_EMAIl_VERIFICATION_API.format(token=token) return True, link except: - print(sys.exc_info()) logger.warning("Utils - generateOneTimeVerificationLink: " + str(sys.exc_info())) return False, "_" def verify_recaptcha(request): try: - print(settings.RECAPTCHA_SECRET_KEY) data = { 'secret': settings.RECAPTCHA_SECRET_KEY, 'response': request } - print(data) r = rq.post('https://www.google.com/recaptcha/api/siteverify', data=data) result = r.json() - # logger.info("Recaptcha Response: " + str(result)+"request: "+str(data)) - - print(result, "Result") + if not result['success']: + logger.warning("Utils - verify_recaptcha: " + str(result)) return result['success'] except: # get exception line number - print(sys.exc_info()) - print(traceback.format_exc()) logger.warning("Utils - verify_recaptcha: " + str(sys.exc_info())) return False, "_" def opening_description_table_html(opening): - details = model_to_dict(opening, fields=[field.name for field in Placement._meta.fields], - exclude=EXCLUDE_IN_PDF) + # check typing of opening + if isinstance(opening, Placement): + details = model_to_dict(opening, fields=[field.name for field in Placement._meta.fields], + exclude=EXCLUDE_IN_PDF) + # check typing of opening is query dict + else: # if isinstance(opening, QueryDict): + details = opening keys = list(details.keys()) newdetails = {} for key in keys: @@ -287,7 +293,7 @@ def opening_description_table_html(opening): if key == 'website': details[key] = {"details": details[key], "type": ["link"]} else: - details[key] = {"details": details[key]["details"], "type": ["list", "link"], + details[key] = {"details": [item for item in details[key]["details"]], "type": ["list", "link"], "link": PDF_FILES_SERVING_ENDPOINT + opening.id + "/"} new_key = key.replace('_', ' ') if new_key.endswith(' names'): @@ -295,9 +301,95 @@ def opening_description_table_html(opening): new_key = new_key.capitalize() newdetails[new_key] = details[key] imagepath = os.path.abspath('./templates/image.png') - print(imagepath) data = { "data": newdetails, "imgpath": imagepath } return render_to_string(COMPANY_JNF_RESPONSE_TEMPLATE, data) + + +def placement_eligibility_filters(student, placements): + try: + filtered_placements = [] + for placement in placements.iterator(): + + if PlacementApplicationConditions(student, placement)[0]: + filtered_placements.append(placement) + + return filtered_placements + except: + logger.warning("Utils - placement_eligibility_filters: " + str(sys.exc_info())) + return placements + + +@background_task.background(schedule=2) +def send_opening_notifications(placement_id): + try: + placement = get_object_or_404(Placement, id=placement_id) + students = Student.objects.all() + for student in students.iterator(): + if student.branch in placement.allowed_branch: + if student.degree == 'bTech' or placement.rs_eligible is True: + if PlacementApplicationConditions(student, placement)[0]: + try: + student_user = get_object_or_404(User, id=student.id) + subject = NOTIFY_STUDENTS_OPENING_TEMPLATE_SUBJECT.format( + company_name=placement.company_name) + deadline_datetime = placement.deadline_datetime.astimezone(pytz.timezone('Asia/Kolkata')) + data = { + "company_name": placement.company_name, + "opening_type": 'Placement', + "designation": placement.designation, + "deadline": deadline_datetime.strftime("%A, %-d %B %Y, %-I:%M %p"), + "link": PLACEMENT_OPENING_URL.format(id=placement.id) + } + sendEmail(student_user.email, subject, data, NOTIFY_STUDENTS_OPENING_TEMPLATE) + except Http404: + logger.warning('Utils - send_opening_notifications: user not found : ' + student.id) + except Exception as e: + logger.warning('Utils - send_opening_notifications: For Loop' + str(e)) + + except: + logger.warning('Utils - send_opening_notifications: ' + str(sys.exc_info())) + return False + + +def exception_email(opening): + opening = opening.dict() + data = { + "designation": opening["designation"], + "opening_type": PLACEMENT, + "company_name": opening["company_name"], + } + pdfhtml = opening_description_table_html(opening) + name = opening["company_name"] + '_jnf_response.pdf' + attachment_jnf_respone = { + "name": name, + "html": pdfhtml, + } + + sendEmail(CDC_MAIl_ADDRESS, COMPANY_OPENING_ERROR_TEMPLATE.format(company_name=opening["company_name"]), data, + COMPANY_OPENING_SUBMITTED_TEMPLATE, attachment_jnf_respone) + + +def store_all_files(request): + files = request.FILES + data = request.data + # save all the files + if files: + # company details pdf + for file in files.getlist(COMPANY_DETAILS_PDF): + file_location = STORAGE_DESTINATION_COMPANY_ATTACHMENTS + "temp" + '/' + saveFile(file, file_location) + # compensation details pdf + for file in files.getlist(COMPENSATION_DETAILS_PDF): + file_location = STORAGE_DESTINATION_COMPANY_ATTACHMENTS + "temp" + '/' + saveFile(file, file_location) + # selection procedure details pdf + for file in files.getlist(SELECTION_PROCEDURE_DETAILS_PDF): + file_location = STORAGE_DESTINATION_COMPANY_ATTACHMENTS + "temp" + '/' + saveFile(file, file_location) + # description pdf + for file in files.getlist(DESCRIPTION_PDF): + file_location = STORAGE_DESTINATION_COMPANY_ATTACHMENTS + "temp" + '/' + saveFile(file, file_location) diff --git a/CDC_Backend/CDC_Backend/settings.py b/CDC_Backend/CDC_Backend/settings.py index 4c24f27..2447ee0 100644 --- a/CDC_Backend/CDC_Backend/settings.py +++ b/CDC_Backend/CDC_Backend/settings.py @@ -15,7 +15,6 @@ import os from dotenv import load_dotenv load_dotenv("../dev.env") -# import django_heroku # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -24,13 +23,14 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'e_i2g3z!y4+p3dwm%k9k=zmsot@aya-0$mmetgxz4mp#8_oy#*' +SECRET_KEY = os.environ.get("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.environ.get('DEBUG') == "True" -ALLOWED_HOSTS = ['cdc-iitdh.herokuapp.com/', 'localhost', '192.168.29.199'] +ALLOWED_HOSTS = ['cdc.iitdh.ac.in', 'localhost'] +ADMINS = [('Gowtham Sai', '190010036@iitdh.ac.in'), ('Karthik Mv', '200010030@iitdh.ac.in')] # Application definition INSTALLED_APPS = [ @@ -44,7 +44,10 @@ INSTALLED_APPS = [ 'rest_framework', 'corsheaders', 'django_db_logger', - 'background_task' + 'background_task', + 'simple_history', + 'import_export', + "django_extensions" ] MIDDLEWARE = [ @@ -54,10 +57,11 @@ MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', + 'corsheaders.middleware.CorsPostCsrfMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - + 'simple_history.middleware.HistoryRequestMiddleware', ] ROOT_URLCONF = 'CDC_Backend.urls' @@ -144,13 +148,14 @@ STATICFILES_DIR = ( os.path.join(BASE_DIR, 'static'), ) -CORS_ORIGIN_ALLOW_ALL = True +CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = [ + "https://cdc.iitdh.ac.in", "http://localhost:3000", - "http://127.0.0.1:3000", - "http://localhost:8000", - "http://127.0.0.1:8000" + "https://localhost:3000" ] +CORS_REPLACE_HTTPS_REFERER = True +CSRF_TRUSTED_ORIGINS = [ "https://cdc.iitdh.ac.in", "http://cdc.iitdh.ac.in"] # EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' EMAIL_FILE_PATH = './emails' @@ -181,14 +186,13 @@ LOGGING = { 'class': 'django_db_logger.db_log_handler.DatabaseLogHandler' }, 'mail_admins': { - 'level': 'ERROR', + 'level': 'WARNING', 'class': 'django.utils.log.AdminEmailHandler', - 'include_html': True, } }, 'loggers': { 'db': { - 'handlers': ['db_log'], + 'handlers': ['db_log', 'mail_admins'], 'level': 'DEBUG' } } diff --git a/CDC_Backend/CDC_Backend/wsgi.py b/CDC_Backend/CDC_Backend/wsgi.py index 057007a..0537202 100644 --- a/CDC_Backend/CDC_Backend/wsgi.py +++ b/CDC_Backend/CDC_Backend/wsgi.py @@ -6,7 +6,6 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ """ - import os from django.core.wsgi import get_wsgi_application diff --git a/CDC_Backend/run_prod.sh b/CDC_Backend/run_prod.sh new file mode 100644 index 0000000..3ad9b30 --- /dev/null +++ b/CDC_Backend/run_prod.sh @@ -0,0 +1 @@ +gunicorn --certfile=/home/cdc/Desktop/1f9476e3959ebe60.crt --keyfile=/home/cdc/Desktop/star_iitdh_key.key --bind localhost:8000 CDC_Backend.wsgi --access-logfile access.log --error-logfile error.log & diff --git a/CDC_Backend/scripts/__init__.py b/CDC_Backend/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/CDC_Backend/scripts/get_contributor_stats.py b/CDC_Backend/scripts/get_contributor_stats.py new file mode 100644 index 0000000..ef98c88 --- /dev/null +++ b/CDC_Backend/scripts/get_contributor_stats.py @@ -0,0 +1,70 @@ +from APIs.models import Contributor +from django.shortcuts import get_object_or_404 +import time +from dotenv import load_dotenv +import requests +import os +load_dotenv("../dev.env") + +owner = 'CDC-IITDH' +access_token = os.environ.get("GITHUB_ACCESS_TOKEN") +headers = {'Authorization': "Token " + access_token} +maxRetires = 10 +REPEAT_AFTER = 60 * 15 # 15 minutes + +def getStats(): + try: + stats = {} + repos = ['cdc-placement-website-backend', 'cdc-placement-website-frontend'] + for i in repos: + try: + repo_name = i + print(repo_name) + url = f"https://api.github.com/repos/{owner}/{repo_name}/stats/contributors" + retry = 0 + contributors = [] + while True: + if retry > maxRetires: + break + req = requests.get(url, headers=headers) + contributors = req.json() + if req.status_code != 200: + print("ERROR:", req.json()) + retry += 1 + elif len(contributors): + break + retry += 1 + + time.sleep(1) + + for contributor in contributors: + if contributor['author']['login'] not in stats: + stats[contributor['author']['login']] = 0 + stats[contributor['author']['login']] += contributor['total'] + except Exception as e: + print(e) + + for i in stats: + try: + contributor = get_object_or_404(Contributor, github_id=i) + contributor.commits = stats[i] + contributor.save() + except: + pass + + stats = sorted(stats.items(), key=lambda x: x[1], reverse=True) + for i in stats: + print(i) + except Exception as e: + print(e) + return stats + + +def run(): + while True: + getStats() + print("Sleeping for", REPEAT_AFTER, "seconds") + time.sleep(REPEAT_AFTER) + print("Running send_reminder_mails()") + +run() \ No newline at end of file diff --git a/CDC_Backend/templates/company_jnf_response.html b/CDC_Backend/templates/company_jnf_response.html index 6e7191c..33484c1 100644 --- a/CDC_Backend/templates/company_jnf_response.html +++ b/CDC_Backend/templates/company_jnf_response.html @@ -44,7 +44,7 @@ {% for item in value.details %}
  • {% if 'link' in value.type and value.link %} - {{ item }} + {{ item|slice:"16:" }} {% elif 'link' in value.type %} {{ item }} {% else %} diff --git a/CDC_Backend/templates/company_opening_submitted.html b/CDC_Backend/templates/company_opening_submitted.html index aab48dc..c19374a 100644 --- a/CDC_Backend/templates/company_opening_submitted.html +++ b/CDC_Backend/templates/company_opening_submitted.html @@ -65,8 +65,7 @@

    We have received your {{ opening_type }} notification for a {{ designation }} offer at - {{ company_name }}. Click here to view your - notification. + {{ company_name }}.

    We will keep you informed with the updates. If you have any queries, please diff --git a/CDC_Backend/templates/notify_students_new_opening.html b/CDC_Backend/templates/notify_students_new_opening.html index 9193f0d..2b5e98b 100644 --- a/CDC_Backend/templates/notify_students_new_opening.html +++ b/CDC_Backend/templates/notify_students_new_opening.html @@ -50,12 +50,12 @@

    CDC is excited to announce that {{ company_name }} is interested in recruiting {{ designation }} from IIT Dharwad. - More details can be found in the CDC-webportal. + More details can be found in the CDC Web Portal.

    Interested students can apply before {{ deadline }} in the CDC-webportal. + href="{{ link }}">CDC-Web Portal.

    diff --git a/CDC_Backend/templates/student_application_updated.html b/CDC_Backend/templates/student_application_updated.html new file mode 100644 index 0000000..2cb0246 --- /dev/null +++ b/CDC_Backend/templates/student_application_updated.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + +
    + +
    + + + + + + + +
    +

    Hello, {{ name }}

    +

    + We have received some update in your application for a {{ application_type }} offer at + + {{ company_name }}. + + + + + +
    resume:{{ resume }}
    + + {% if additional_info_items %} + We received these additional details +
    + +

    + {% for i,j in additional_info_items.items %} + +

    + + + + {% endfor %} +
    {{ i }}:{{ j }}
    + {% endif %} + +

    +

    +

    + We will keep you informed with the updates. If you have any queries, please + feel to + write to + cdc.support@iitdh.ac.in +

    +
    + + + + +
    + + +
    +
    +
    + + + + + + + +
    +

    + ® CDC,IIT Dharwad,2021
    +

    +
    +
    +
    + + \ No newline at end of file diff --git a/Email_service_README.md b/Email_service_README.md new file mode 100644 index 0000000..26f134e --- /dev/null +++ b/Email_service_README.md @@ -0,0 +1,13 @@ +we have defined a service for running the `start_email_service.sh` file continusouly on the server. Use the following commands for doing anything to the email + service. + +## Enable the Service +`sudo systemctl enable cdc-email-sender.service` + +## Start the Service +`sudo systemctl start cdc-email-sender.service` + +## Check Status of the Service +`sudo systemctl status cdc-email-sender.service` + +Any Doubts contact Gowtham Sai - 190010036 diff --git a/README.md b/README.md index 5c37318..1c41ba8 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ python# CDC - Backend 3. Create a Virtual Environment in the [CDC_Backend](./) folder with this command below
    `python -m venv venv` 3. Activate the environment with this command
    - `.\venv\Scripts\activate` + `.\venv\Scripts\activate` (for WINDOWS)
    + `source ./venv/bin/activate` (for LINUX) 4. Install the dependencies
    `pip install -r requirements.txt ` 5. Ensure that you have the PostgreSQL installed on your machine and is running on PORT **5432**
    -6. Make sure to give the correct database credentials in [settings.py](./CDC_Backend/CDC_Backend/settings.py) +6. Make sure to give the correct database credentials in [settings.py](./CDC_Backend/CDC_Backend/settings.py)(https://www.youtube.com/watch?v=bE9h6aAky4s&t=193s) 7. Run these following commands below. (The same are there in setup.sh for linux users and setup.bat for windows users) ```cd CDC_Backend python manage.py flush --no-input @@ -25,10 +26,12 @@ python manage.py makemigrations APIs ``` + ### Running the Application 1. Activate the environment with this command.
    - `.\venv\Scripts\activate` + `.\venv\Scripts\activate` (for WINDOWS)
    + `source ./venv/bin/activate` (for LINUX) 2. Start the application by running this command (_Run the command where [manage.py](./CDC_Backend/manage.py) is located_)
    ` python manage.py runserver` @@ -45,6 +48,12 @@ python manage.py makemigrations APIs And then recognize it 8.Check the client link in dev.env in backend and .env in frontend is the same + + # Error + 1.make sure that your machine time and google time are same ,if not go to setting of date and time and sync this + 2.make sure u have used same id for both student and Admin that is your iitfh roll_no + 3. same client link in .env of frontend or constants.py of bakcend + ### Deploying 1. Add the hosted domain name in `ALLOWED_HOSTS` in [settings.py](./CDC_Backend/CDC_Backend/settings.py) diff --git a/dev.env b/dev.env deleted file mode 100644 index fff40b9..0000000 --- a/dev.env +++ /dev/null @@ -1,12 +0,0 @@ -HOSTING_URL=http://localhost:8000/ -DEBUG=True -EMAIL=saisurya3127@gmail.com -EMAIL_PASSWORD=lvryxwieedpervtv -SECRET_KEY=%2e!&f6(ib^690y48z=)&w6fczhwukzzp@3y*^*7u+7%4s-mie -EMAIL_VERIFICATION_SECRET_KEY=b'<\xa3\xaf&(*|\x0e\xbces\x07P\xf7\xd6\xa9sf\x19$\x96\xb7\x90\x8b\x88\x84\x0e\x191\xde,M\x90\x17(\xf7\nG\x13"\x8d$\x9f&\xb0\xcd\xa4\xaf\xa9\x1b\x15\x02B\x8a\xaf\xff\x0c\x1e\xd5\xb3\x06\xb8\xa6\x9bQ\xa0TR\xe8\x98\x9ae\xe0n}\xcc/[\xdaFz\x18\xfeX\xaf\xbd\xd0\x88\xeal\xe3\xd2\xe3\xb8\x8c\x199{\xf3<\xb0\xc5\xd0\xe7*Rv\xda\xbb \x1d\x85~\xff%>\x1e\xb8\xa7\xbf\xbc\xb2\x06\x86X\xc3\x9f\x13<\x9fd\xea\xb5"\\5&\x01\xa4\x7f=\xa0\x1b\x8bO\x01h\xe8\xfd\x1f\xfe\xba\xbeg\\\xc2\xcb\xc3\xd1~\xff\xd5/9d\xa8\xa7x{\x16\xdb\\\xbb\x08\rI\xcd\x9e7\x8c~\x0f\x1d\x81rXZD\xf0\xf7\x87K\x8f\xfb,\xf4\xf0\xa5\x9e\xde^\xca\xae\x80|9b\x9b\xaaE"\xba\xfb\xdf\x80\xb1\x99\x83e[\xf8\xce&Rq\x99\xdb}\xeeO\xd5\x18\x8d\x0bv\xe7\xab\xf9\xb9{\xb5u\xce\xcf\x90\xa6HE\xc5\x92p\x00\x158\xdf\x1d' -DB_NAME=cdc -DB_USER=postgres -DB_PASSWORD=postgres -DB_HOST=localhost -DB_PORT=5432 -RECAPTCHA_SECRET_KEY=6Lcv-mEfAAAAAOxM3pzPc-9W96yPlkWnn6v41fLl diff --git a/doc/setup/postgres.md b/doc/setup/postgres.md new file mode 100644 index 0000000..d3dba5e --- /dev/null +++ b/doc/setup/postgres.md @@ -0,0 +1,16 @@ +typical conf file for pg_hba.conf for dev work. + + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all md5 +# IPv4 local connections: +host all all 127.0.0.1/32 md5 +# IPv6 local connections: +host all all ::1/128 md5 +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all peer +host replication all 127.0.0.1/32 ident +host replication all ::1/128 ident \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index 544da1f..5afd9b3 100755 --- a/nginx.conf +++ b/nginx.conf @@ -27,6 +27,8 @@ http { sendfile on; #tcp_nopush on; + client_max_body_size 50M; + #keepalive_timeout 0; keepalive_timeout 65; @@ -34,21 +36,27 @@ http { server { listen 80; - server_name localhost; + server_name cdc.iitdh.ac.in; - # listen 443 ssl; + listen 443 ssl; # server_name localhost; - # ssl_certificate cert.pem; - # ssl_certificate_key cert.key; + ssl_certificate /home/cdc/Desktop/1f9476e3959ebe60.pem; + ssl_certificate_key /home/cdc/Desktop/star_iitdh_key.key; #charset koi8-r; #access_log logs/host.access.log main; - + location / { root /usr/share/nginx/html; - index index.html index.htm; + try_files $uri $uri/ /portal; + } + + location /portal { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /portal/index.html; } # Server static files /storage @@ -65,14 +73,14 @@ http { } location /api/ { - proxy_pass http://localhost:8000; + proxy_pass https://localhost:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; } location /admin/ { - proxy_pass http://localhost:8000; + proxy_pass https://localhost:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; diff --git a/requirements.txt b/requirements.txt index 6ca6f8a..d6465e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,13 +5,19 @@ certifi==2021.10.8 chardet==4.0.0 charset-normalizer==2.0.12 colorama==0.4.4 +defusedxml==0.7.1 +diff-match-patch==20200713 +dill==0.3.5.1 dj-database-url==0.5.0 Django==3.2.13 django-background-tasks==1.2.5 django-compat==1.0.15 django-cors-headers==3.11.0 django-db-logger==0.1.12 +django-import-export==2.8.0 +django-simple-history==3.1.1 djangorestframework==3.13.1 +et-xmlfile==1.1.0 google-auth==2.6.6 gunicorn==20.1.0 idna==3.3 @@ -20,23 +26,31 @@ isort==5.10.1 jsonfield==3.1.0 lazy-object-proxy==1.7.1 Markdown==3.3.6 +MarkupPy==1.14 mccabe==0.7.0 +odfpy==1.4.1 +openpyxl==3.0.10 pdfkit==1.0.0 platformdirs==2.5.1 psycopg2-binary==2.9.3 pyasn1==0.4.8 pyasn1-modules==0.2.8 +PyJWT==2.4.0 pylint==2.13.5 python-dotenv==0.20.0 pytz==2022.1 -PyJWT==2.4.0 +PyYAML==6.0 requests==2.27.1 rsa==4.8 six==1.16.0 sqlparse==0.4.2 +tablib==3.2.1 toml==0.10.2 +tomli==2.0.1 typing_extensions==4.1.1 urllib3==1.26.9 whitenoise==6.0.0 +wrapt==1.14.0 +xlrd==2.0.1 +xlwt==1.3.0 zipp==3.8.0 -wrapt==1.14.0 \ No newline at end of file diff --git a/start_email_service.sh b/start_email_service.sh new file mode 100755 index 0000000..048b305 --- /dev/null +++ b/start_email_service.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +source /home/cdc/Desktop/CDC_Web_Portal_Backend/cdc-placement-website-backend/venv/bin/activate + +cd /home/cdc/Desktop/CDC_Web_Portal_Backend/cdc-placement-website-backend/CDC_Backend + +python manage.py process_tasks