Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add announcements and profile picture to Profile #9

Merged
merged 3 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pesuacademy/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .announcement import Announcement
from .course import Course, Attendance
from .profile import (
Profile,
Expand Down
27 changes: 27 additions & 0 deletions pesuacademy/models/announcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import datetime
from typing import Optional


class AnnouncementFile:
def __init__(self, name: str, content: bytes):
self.name = name
self.content = content


class Announcement:
def __init__(
self,
title: str,
date: datetime.date,
content: str,
img: str = None,
files: Optional[list[AnnouncementFile]] = None,
):
self.title = title
self.date = date
self.content = content
self.img = img
self.files = files

def __str__(self):
return f"{self.__dict__}"
2 changes: 2 additions & 0 deletions pesuacademy/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(
branch: str,
semester: str,
section: str,
img: str,
program: Optional[str] = None,
email: Optional[str] = None,
mobile: Optional[str] = None,
Expand All @@ -53,6 +54,7 @@ def __init__(
self.section = section
self.email = email
self.mobile = mobile
self.img = img
self.aadhar = aadhar
self.name_as_in_aadhar = name_as_in_aadhar

Expand Down
1 change: 1 addition & 0 deletions pesuacademy/pages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .announcements import AnnouncementPageHandler
from .attendance import AttendancePageHandler
from .courses import CoursesPageHandler
from .profile import ProfilePageHandler
124 changes: 124 additions & 0 deletions pesuacademy/pages/announcements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import datetime
import re
from typing import Optional

import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import requests_html
from bs4 import BeautifulSoup

from pesuacademy.models.announcement import Announcement, AnnouncementFile


class AnnouncementPageHandler:
@staticmethod
def get_announcement_by_id(
session: requests_html.HTMLSession, csrf_token: str, announcement_id: str
) -> Announcement:
url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin"
data = {
"controllerMode": "6411",
"actionType": "4",
"AnnouncementId": announcement_id,
"menuId": "667",
}
headers = {
"accept": "*/*",
"accept-language": "en-IN,en-US;q=0.9,en-GB;q=0.8,en;q=0.7",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://www.pesuacademy.com",
"priority": "u=1, i",
"referer": "https://www.pesuacademy.com/Academy/s/studentProfilePESU",
"sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"x-csrf-token": csrf_token,
"x-requested-with": "XMLHttpRequest",
}

response = session.post(url, data=data, headers=headers)
if response.status_code != 200:
data["actionType"] = "6"
response = session.post(url, data=data, headers=headers)

soup = BeautifulSoup(response.text, "lxml")

title = soup.find("h4", class_="text-info").text.strip()
date = soup.find("span", class_="text-muted text-date pull-right").text.strip()
date = datetime.datetime.strptime(date, "%d-%B-%Y").date()

content_tag = soup.find("div", class_="col-md-12")
if content_tag is None:
content_tag = soup.find("div", class_="col-md-8")
paragraph_or_list_tags = content_tag.find_all(["p", "li"])
content = "\n".join([tag.text.strip() for tag in paragraph_or_list_tags])

img_tag = soup.find("img", class_="img-responsive")
img = img_tag.attrs["src"] if img_tag else None

attachment_tags = [
tag
for tag in content_tag.find_all("a")
if tag.text.strip().endswith(".pdf")
]
attachments = list()
for attachment_tag in attachment_tags:
attachment_name = attachment_tag.text.strip()
pattern = re.compile(r"handleDownloadAnoncemntdoc\('(\d+)'\)")
attachment_id = re.findall(pattern, attachment_tag.attrs["href"])[0]
response = session.get(
f"https://pesuacademy.com/Academy/s/studentProfilePESUAdmin/downloadAnoncemntdoc/{attachment_id}",
headers={"x-csrf-token": csrf_token},
verify=False,
)
attachment_bytes = response.content
attachments.append(
AnnouncementFile(name=attachment_name, content=attachment_bytes)
)

return Announcement(
title=title, date=date, content=content, img=img, files=attachments
)

def get_page(
self,
session: requests_html.HTMLSession,
csrf_token: str,
start_date: Optional[datetime.date] = None,
end_date: Optional[datetime.date] = None,
) -> list[Announcement]:
url = "https://www.pesuacademy.com/Academy/s/studentProfilePESUAdmin"
query = {
"menuId": "667",
"controllerMode": "6411",
"actionType": "5",
"_": str(int(datetime.datetime.now().timestamp() * 1000)),
}
response = session.get(url, allow_redirects=False, params=query)
if response.status_code != 200:
raise ConnectionError("Unable to fetch announcement data.")
soup = BeautifulSoup(response.text, "lxml")

announcement_ids = soup.find_all("a", class_="pull-right readmorelink")
pattern = re.compile(r"handleShowMoreAnnouncement\(\d+, \d+,(\d+)\)")
announcement_ids = [
pattern.match(ann.attrs.get("onclick")).group(1) for ann in announcement_ids
]

announcements = list()
for announcement_id in announcement_ids:
announcement = self.get_announcement_by_id(
session, csrf_token, announcement_id
)
if start_date and announcement.date < start_date:
continue
if end_date and announcement.date > end_date:
continue
announcements.append(announcement)
announcements.sort(key=lambda x: x.date, reverse=True)
return announcements
19 changes: 18 additions & 1 deletion pesuacademy/pesuacademy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from pesuacademy import util
from pesuacademy.util.page import PageHandler
from .exceptions import CSRFTokenError, AuthenticationError
from .models import Profile, ClassAndSectionInfo, Course
from .models import Profile, ClassAndSectionInfo, Course, Announcement


class PESUAcademy:
Expand Down Expand Up @@ -155,3 +155,20 @@ def attendance(self, semester: Optional[int] = None) -> dict[int, list[Course]]:
raise AuthenticationError("You need to authenticate first.")
attendance_info = self.page_handler.get_attendance(semester)
return attendance_info

def announcements(
self, start_date: Optional[str] = None, end_date: Optional[str] = None
) -> list[Announcement]:
"""
Get the announcements from the PESU Academy website.

:param start_date: The start date of the announcements to fetch in "yyyy-mm-dd" format. If not provided, all
announcements from the beginning are fetched.
:param end_date: The end date of the announcements to fetch in "yyyy-mm-dd" format. If not provided, all
announcements till the end are fetched.
:return: The list of announcements.
"""
announcements = self.page_handler.get_announcements(
self._csrf_token, start_date, end_date
)
return announcements
15 changes: 15 additions & 0 deletions pesuacademy/util/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def __init__(self, session: requests_html.HTMLSession):
self.course_page_handler = pages.CoursesPageHandler()
self.attendance_page_handler = pages.AttendancePageHandler()
self.profile_page_handler = pages.ProfilePageHandler()
self.announcement_handler = pages.AnnouncementPageHandler()

def set_semester_id_to_number_mapping(self, csrf_token: str):
try:
Expand Down Expand Up @@ -80,3 +81,17 @@ def get_courses(self, semester: Optional[int] = None):
def get_attendance(self, semester: Optional[int] = None):
semester_ids = self.get_semester_ids_from_semester_number(semester)
return self.attendance_page_handler.get_page(self.__session, semester_ids)

def get_announcements(
self,
csrf_token: str,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
):
if start_date is not None:
start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
if end_date is not None:
end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
return self.announcement_handler.get_page(
self.__session, csrf_token, start_date, end_date
)
4 changes: 4 additions & 0 deletions pesuacademy/util/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def create_personal_details_object_from_profile_page(
:param soup: The BeautifulSoup object of the page.
:return: The PersonalDetails object.
"""

personal_details = dict()
personal_details_section = soup.find(
"div", attrs={"class": "elem-info-wrapper box-shadow clearfix"}
Expand Down Expand Up @@ -111,6 +112,8 @@ def create_personal_details_object_from_profile_page(
value = None if value == "NA" else value
personal_details[key] = value

image_b64_encoded = soup.find("img", attrs={"class": "media-object"})["src"]

return PersonalDetails(
name=personal_details["name"],
prn=personal_details["pesu_id"],
Expand All @@ -119,6 +122,7 @@ def create_personal_details_object_from_profile_page(
branch=personal_details["branch"],
semester=personal_details["semester"],
section=personal_details["section"],
img=image_b64_encoded,
email=personal_details["email_id"],
mobile=personal_details["contact_no"],
aadhar=personal_details["aadhar_no"],
Expand Down
Loading