diff --git a/.github/workflows/bundle_js.yaml b/.github/workflows/bundle_js.yaml new file mode 100644 index 0000000..d731eb1 --- /dev/null +++ b/.github/workflows/bundle_js.yaml @@ -0,0 +1,41 @@ +name: Bundle JavaScript + +on: + workflow_dispatch: # Trigger on manual run + push: + branches: + - main # Trigger on push to the 'main' branch + pull_request: + branches: + - main # Trigger on pull request to 'main' branch + +jobs: + bundle-js: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' # Specify the Node.js version + + - name: Install Dependencies + run: | + npm install terser + + - name: Concatenate and Minify JavaScript + run: | + # Create a combined JS file from all *.js files in the 'js/src' directory + npx terser js/src/*.js -o js/dist/bundle.min.js --compress --mangle + + - name: Commit Minified File + run: | + # Commit the minified file if it has changed + git config --global user.name "GitHub Actions" + git config --global user.email "github-actions@github.com" + git add js/dist/bundle.min.js + git commit -m "Merged and combined JavaScript files to 'dist/bundle.min.js'" + git push diff --git a/README.md b/README.md index adcc0f2..576bcb2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,81 @@ -# sdk - +
+ +![GitHub Repo stars](https://img.shields.io/github/stars/itu-helper/sdk) +![GitHub issues](https://img.shields.io/github/issues-raw/itu-helper/sdk?label=Issues&style=flat-square) + +# **ITU Helper** + +
+ +
+ ITU Helper Logo +
+
+ +_İTÜ'lüler için İTÜ'lülerden_ + +_ITU Helper_ İstanbul Teknik Üniversitesi öğrencilerine yardım etmek amacıyla ön şart görselleştirme, ders planı oluşturma ve resmi İTÜ sitelerini birleştirme gibi hizmetler sağlayan bir açık kaynaklı websitesidir. + +_ITU Helper_'a [_bu adresten_](https://itu-helper.github.io/home/) ulaşabilirsiniz. + +
+
+
+
+ +# **itu-helper/sdk** + +## **Ne İşe Yarar?** + +[itu-helper/data-updater](https://github.com/itu-helper/data-updater) _repo_'suyla toplanan ve [itu-helper/data](https://github.com/itu-helper/data) _repo_'sunda saklanan verilere, kolayca ulaşılmasına olanak sağlar. + +> [!NOTE] +> Verilerin nasıl isimlendirildiğine ve güncelleme sıklığına ulaşmak için, [itu-helper/data-updater](https://github.com/itu-helper/data-updater) _repo_'sununa bakınız. + +## **Nasıl Kullanılır?** + +### **JavaScript** + +`` _tag_'inin en alt kısmına şu satırı ekleyerek _script_'leri importlamanız lazım. + +```html + +``` + +JavaScript SDK'sinin detaylı kullanımı için [buraya](js/README.md) bakınız. + +### **HTTP Request** + +Programlama dilinden bağımsız olarak, verilere _HTTP request_ göndererek de ulaşabilirsiniz. Ders planları `.txt` formatında, kalan veriler ise `.psv` (Pipe separated values) formatında saklanmakta. Aşağıdaki linklerden, dosyalara ulaşabilir ve okuyabilirsiniz. + +- Dersler (_lessons_): https://raw.githubusercontent.com/itu-helper/data/main/lessons.psv + +- Dersler (_courses_): https://raw.githubusercontent.com/itu-helper/data/main/courses.psv + +- Ders Planları: https://raw.githubusercontent.com/itu-helper/data/main/course_plans.txt + +- Bina Kodları: https://raw.githubusercontent.com/itu-helper/data/main/building_codes.psv + +- Program Kodları: https://raw.githubusercontent.com/itu-helper/data/main/programme_codes.psv + +#### **Python Örneği** + +Aşağıdaki kodda _requests_ modülüyle; CRN kullanarak, dersin [bu sayfadaki](https://obs.itu.edu.tr/public/DersProgram) verilerine erişim gösterilmiştir. + +```python +from requests import get + +URL = "https://raw.githubusercontent.com/itu-helper/data/main/lessons.psv" + +# Dersleri (lessons) oku ve satır satır ayır. +lines = get(URL).text.split("\n") + +# .psv formatından dolayı, her bir satırdaki elementler "|" sembolü ile ayırılıyor. +# İlk eleman, dersin CRN'si. CRN'lerin "key", satırın tamamının da "value" olduğu bir sözlük oluştur. +crn_to_lesson = {line.split("|")[0] : line for line in lines} + +print(crn_to_lesson["13590"]) +# OUTPUT +# '13590|BLG 212E|Fiziksel (Yüz yüze)|Gökhan İnce|BBB|Monday|08:30/11:29 - |Z-18|120|111|BLG_LS, BLGE_LS, CEN_LS' +``` diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..1779cff --- /dev/null +++ b/js/README.md @@ -0,0 +1,33 @@ +
+ +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/itu-helper/data-updater/refresh_lessons.yml?label=Refreshing%20Lesson&logo=docusign&style=flat-square) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/itu-helper/data-updater/refresh_courses_and_plans.yml?label=Refreshing%20Courses%20%26%20Course%20Plans&logo=docusign&style=flat-square) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/itu-helper/data-updater/refresh_misc.yml?label=Refreshing%20Misc&logo=docusign&style=flat-square) +![GitHub repo size](https://img.shields.io/github/repo-size/itu-helper/data-updater?label=Repository%20Size&logo=github&style=flat-square) +![GitHub](https://img.shields.io/github/license/itu-helper/data-updater?label=License&style=flat-square) +![GitHub issues](https://img.shields.io/github/issues-raw/itu-helper/data-updater?label=Issues&style=flat-square) + +# **ITU Helper** + +
+ +
+ ITU Helper Logo +
+
+ +_İTÜ'lüler için İTÜ'lülerden_ + +_ITU Helper_ İstanbul Teknik Üniversitesi öğrencilerine yardım etmek amacıyla ön şart görselleştirme, ders planı oluşturma ve resmi İTÜ sitelerini birleştirme gibi hizmetler sağlayan bir açık kaynaklı websitesidir. + +_ITU Helper_'a [_bu adresten_](https://itu-helper.github.io/home/) ulaşabilirsiniz. + +
+
+
+
+ +# **itu-helper/sdk - JavaScript SDK** + +... diff --git a/js/dist/bundle.min.js b/js/dist/bundle.min.js new file mode 100644 index 0000000..78037de --- /dev/null +++ b/js/dist/bundle.min.js @@ -0,0 +1 @@ +class Course{constructor(e,s,t,r,i,c,n,h){this.courseCode=e,this.courseTitle=s,this.lang=t,this.credits=r,this.ects=i,this.classRestrictions=n,this.description=h,this.majorRestrictions="",this.lessons=[],this._createRequirementNames(c)}static createAutoGeneratedCourse(e){return new Course(e,"Auto Generated Course","",0,0,"","","")}_createRequirementNames(e){if(this._requirementNames=[],!e.includes("Yok")&&!(e.includes("planının")||e.includes("Diğer")||e.includes("Özel")||e.includes("için"))){var s=(e=e.replaceAll("veya","\nveya").replaceAll("ve","\nve").replaceAll("(","").replaceAll(")","")).split("\n");for(let e=0;e{}}get courses(){return this._courses.length<=0&&(this._createCourses(),this._courses.forEach((e=>{this.coursesDict[e.courseCode]=e})),this._createLessons(),this._connectAllCourses()),this._courses}get semesters(){return Object.keys(this._semesters).length<=0&&(this.courses,this._createSemesters()),this._semesters}fetchData(){this._fetchTextFile(this.LESSON_PATH,(e=>{this.lesson_lines=e.split("\n"),this._onTextFetchSuccess()})),this._fetchTextFile(this.COURSE_PATH,(e=>{this.course_lines=e.split("\n"),this._onTextFetchSuccess()})),this._fetchTextFile(this.COURSE_PLAN_PATH,(e=>{this.course_plan_lines=e.split("\n"),this._onTextFetchSuccess()}))}_onTextFetchSuccess(){this.fileFetchStatus++,this.fileFetchStatus>=3&&this.onFetchComplete()}_createCourses(){let e=this.course_lines;this._courses=[],this.coursesDict={};for(let s=0;s{e.connectCourses(this)}))}findCourseByCode(e){let s=this.coursesDict[e];if(null==s){if(""===e)return null;s=Course.createAutoGeneratedCourse(e),s.requirements=[],this._courses.push(s),this.coursesDict[e]=s}return s}_createSemesters(){let e="",s="",t="",r=[];this._semesters=[];let i=this.course_plan_lines;for(let c=0;c{r.push(this.findCourseByCode(e))})),i.push(new CourseGroup(r,e[0]))}else{let e=this.findCourseByCode(s);if(null==e)continue;i.push(e)}}r.push(i),8==r.length&&(this._semesters[e][s][t]=r)}}}_fetchTextFile(e,s){fetch(e).then((e=>e.text())).then(s).catch((e=>console.error(e)))}}class Lesson{constructor(e,s,t,r,i,c,n,h,l){this.crn=e,this.teachingMethod=s?.trim(),this.instructor=t?.trim(),this.building=r?.trim(),this.time=c?.trim(),this.day=i?.trim(),this.room=n?.trim(),this.capacity=h,this.enrolled=l}getStartAndEndTime(){const e=this.time.split("/");return[e[0].slice(0,2)+":"+e[0].slice(2),e[1].slice(0,2)+":"+e[1].slice(2)]}getDayAsNumber(){switch(this.day.toLowerCase()){case"pazartesi":return 0;case"salı":return 1;case"çarşamba":return 2;case"perşember":return 3;case"cuma":return 4;case"cumartesi":return 5;case"pazar":return 6;default:return-1}}} \ No newline at end of file diff --git a/js/src/course.js b/js/src/course.js new file mode 100644 index 0000000..fc3f4bd --- /dev/null +++ b/js/src/course.js @@ -0,0 +1,89 @@ +class Course { + constructor(code, title, lang, credits, ects, requirementsText, classRestrictions, description) { + this.courseCode = code; + this.courseTitle = title; + this.lang = lang; + this.credits = credits; + this.ects = ects; + this.classRestrictions = classRestrictions; + this.description = description; + + this.majorRestrictions = ""; + this.lessons = []; + + this._createRequirementNames(requirementsText); + } + + static createAutoGeneratedCourse(code) { + return new Course(code, "Auto Generated Course", "", 0, 0, "", "", ""); + } + + /** + * parses the `requirementsText` and creates `this._requirementNames` array. + * @param {string} requirementsText the text written in ITU's site for requirements + */ + _createRequirementNames(requirementsText) { + // (MAT 201 MIN DDveya MAT 201E MIN DDveya MAT 210 MIN DDveya MAT 210E MIN DD)ve (EHB 211 MIN DDveya EHB 211E MIN DD) + // FIZ 102 MIN DDveya FIZ 102E MIN DDveya EHB 211 MIN DDveya EHB 211E MIN DD + this._requirementNames = []; + + // If there are no requirements, return an empty list. + if (requirementsText.includes("Yok")) { + return; + } + else if (requirementsText.includes("planının") || requirementsText.includes("Diğer") || requirementsText.includes("Özel") || requirementsText.includes("için")) { + // TODO: Implement this. + return; + } + + requirementsText = requirementsText + .replaceAll("veya", "\nveya") + .replaceAll("ve", "\nve") + .replaceAll("(", "") + .replaceAll(")", ""); + + var lines = requirementsText.split("\n"); + for (let i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + var words = line.split(" "); + + // If this is the first line, then there + // is no "ve" or "veya" in the line. + // ex: '(FIZ 101 MIN DD' ... + if (i == 0) { + this._requirementNames.push([words[0] + " " + words[1]]); + continue + } + + // If the line contains "ve" or "veya". + // ex: 'veya FIZ 101E MIN DD)' + // ex2: 've (STA 201 MIN DD' + let requirementName = words[1] + " " + words[2]; + let logicGate = words[0]; + + // Append to the last array. + if (logicGate == "veya") + this._requirementNames[this._requirementNames.length - 1].push(requirementName); + // Create a new array. + else if (logicGate == "ve") + this._requirementNames.push([requirementName]); + } + } + + /** + * creates `this.requirements` array by replacing the names in `this._requirementNames` + * with the courses of the given `ITUHelper` object. + * @param {ITUHelper} ituHelper + */ + connectCourses(ituHelper) { + this.requirements = []; + for (let i = 0; i < this._requirementNames.length; i++) { + this.requirements.push([]); + for (let j = 0; j < this._requirementNames[i].length; j++) { + let course = ituHelper.findCourseByCode(this._requirementNames[i][j]); + if (course != null) + this.requirements[i].push(course); + } + } + } +} diff --git a/js/src/course_group.js b/js/src/course_group.js new file mode 100644 index 0000000..9a7e9c9 --- /dev/null +++ b/js/src/course_group.js @@ -0,0 +1,6 @@ +class CourseGroup { + constructor (courses, title){ + this.courses = courses; + this.title = title; + } +} diff --git a/js/src/itu_helper.js b/js/src/itu_helper.js new file mode 100644 index 0000000..13eee2e --- /dev/null +++ b/js/src/itu_helper.js @@ -0,0 +1,241 @@ +class ITUHelper { + LESSON_PATH = "https://raw.githubusercontent.com/itu-helper/data/refs/heads/main/lessons.psv"; + COURSE_PATH = "https://raw.githubusercontent.com/itu-helper/data/refs/heads/main/courses.psv"; + COURSE_PLAN_PATH = "https://raw.githubusercontent.com/itu-helper/data/refs/heads/main/course_plans.txt"; + + constructor() { + this._courses = []; + this._semesters = {}; + this.coursesDict = {}; + + this.fileFetchStatus = 0; + this.onFetchComplete = () => { }; + } + + /** + * a list of `Course` objects. + * + * ⚠️NOTE: `fetchData` must be called before accessing this property. + */ + get courses() { + if (this._courses.length <= 0) { + this._createCourses(); + this._courses.forEach(course => { + this.coursesDict[course.courseCode] = course; + }); + this._createLessons(); + this._connectAllCourses(); + } + + return this._courses; + } + + /** + * a dictionary of dictionaries of dictionaries of arrays of `Course` & `CourseGroup` (Mixed). + * Where each array represents a semester. + * + * Structure: + * + * `semesters["faculty name"]["programme name"]["iteration name"]` + * + * Example: + * + * `semesters["Bilgisayar ve Bilişim Fakültesi"]["Yapay Zeka ve Veri Mühendisliği (% 100 İngilizce)"]["2021-2022 Güz Dönemi Sonrası"]` + * + * ⚠️NOTE: `fetchData` must be called before accessing this property. + */ + get semesters() { + if (Object.keys(this._semesters).length <= 0) { + this.courses; + this._createSemesters(); + } + + return this._semesters; + } + + /** + * fetches the data from itu-helper/data repo, calls `onFetchComplete` + * when all files are fetches. + */ + fetchData() { + this._fetchTextFile(this.LESSON_PATH, (txt) => { + this.lesson_lines = txt.split("\n"); + this._onTextFetchSuccess(); + }); + this._fetchTextFile(this.COURSE_PATH, (txt) => { + this.course_lines = txt.split("\n"); + this._onTextFetchSuccess(); + }); + this._fetchTextFile(this.COURSE_PLAN_PATH, (txt) => { + this.course_plan_lines = txt.split("\n"); + this._onTextFetchSuccess(); + }); + } + + _onTextFetchSuccess() { + this.fileFetchStatus++; + if (this.fileFetchStatus >= 3) + this.onFetchComplete(); + } + + /** + * processes `course_lines` to create the `courses` array. + */ + _createCourses() { + let lines = this.course_lines; + this._courses = []; + this.coursesDict = {}; + + for (let i = 0; i < lines.length; i++) { + let line = lines[i].replace("\r", ""); + if (line.length == 0) continue; + + let data = line.split("|"); + if (data.length < 8) continue; + + let course = new Course(data[0], data[1], data[2], parseInt(data[3]), parseInt(data[4]), data[5], data[6], data[7]); + this._courses.push(course); + } + } + + /** + * processes `lesson_lines` to create lessons and add them to + * corresponding courses of the `courses` array. + */ + _createLessons() { + let lines = this.lesson_lines; + for (let i = 0; i < lines.length; i++) { + let line = lines[i].replace("\r", ""); + + let data = line.split("|"); + let courseCode = data[1]; + let majorRest = data[9]; + let currentLesson = new Lesson(data[0], data[2], data[3], data[4], + data[5], data[6], data[7], data[8]); + + let course = this.findCourseByCode(courseCode); + if (!course) continue; + + course.lessons.push(currentLesson); + course.majorRest = majorRest; + } + } + + /** + * calls the `course.connectCourses` method for all courses in the `courses` array. + */ + _connectAllCourses() { + this._courses.forEach(course => { + course.connectCourses(this); + }); + } + + /** + * + * @param {string} courseCode the code of the course, Ex: "MAT 281E" + * @returns the corresponding course in the `courses` array, + * if the `courseCode` argument is empty returns null. If it is not empty + * but a match cannot be found, creates a new course with the given title + * and the name `"Auto Generated Course"` and returns it. + */ + findCourseByCode(courseCode) { + let course = this.coursesDict[courseCode]; + if (course == undefined) { + if (courseCode === "") return null; + course = Course.createAutoGeneratedCourse(courseCode); + course.requirements = []; + this._courses.push(course); + this.coursesDict[courseCode] = course; + + // console.warn("[Course Generation] " + courseCode + " got auto-generated."); + } + + return course; + } + + /** + * processes `course_plan_lines` to create the course plans + * and fills it with the courses in the `courses` array. + */ + _createSemesters() { + let currentFaculty = ""; + let currentProgram = ""; + let currentIteration = ""; + let currentSemesters = []; + this._semesters = []; + + let lines = this.course_plan_lines; + for (let i = 0; i < lines.length; i++) { + const line = lines[i].replace("\r", "").trim(); + if (line.includes('# ')) { + currentSemesters = []; + let hashtagCount = line.split(' ')[0].length; + let title = line.slice(hashtagCount + 1).trim(); + if (hashtagCount == 1) { + currentFaculty = title; + this._semesters[currentFaculty] = {}; + } + if (hashtagCount == 2) { + // Check if the last program had any iterations + // If not delete it. + if (this._semesters[currentFaculty][currentProgram] != undefined) { + if (!Object.keys(this._semesters[currentFaculty][currentProgram]).length) + delete this._semesters[currentFaculty][currentProgram]; + } + + currentProgram = title; + this._semesters[currentFaculty][currentProgram] = {}; + } + if (hashtagCount == 3) + currentIteration = title; + } + else { + let semester = []; + let courses = line.split('='); + for (let j = 0; j < courses.length; j++) { + let course = courses[j]; + // Course Group + if (course[0] === "[") { + course = course.replace("[", "").replace("]", ""); + let courseGroupData = course.split("*"); + courseGroupData[1] = courseGroupData[1].replace("(", "").replace(")", ""); + let selectiveCourseNames = courseGroupData[1].split('|'); + let selectiveCourses = []; + selectiveCourseNames.forEach(selectiveCourseName => { + selectiveCourses.push(this.findCourseByCode(selectiveCourseName)); + }); + semester.push(new CourseGroup(selectiveCourses, courseGroupData[0])); + } + // Course + else { + let courseObject = this.findCourseByCode(course); + if (courseObject == null) continue; + + semester.push(courseObject); + } + } + + currentSemesters.push(semester); + + if (currentSemesters.length == 8) + this._semesters[currentFaculty][currentProgram][currentIteration] = currentSemesters; + } + } + } + + /** + * @param {string} path path of the text file to fetch. + * @param {*} onSuccess the method to call on success. + */ + _fetchTextFile(path, onSuccess) { + // $.ajax({ + // url: path, + // type: 'get', + // success: onSuccess, + // }); + fetch(path) + .then((res) => res.text()) + .then(onSuccess) + .catch((e) => console.error(e)); + } +} diff --git a/js/src/lesson.js b/js/src/lesson.js new file mode 100644 index 0000000..dd74b0b --- /dev/null +++ b/js/src/lesson.js @@ -0,0 +1,51 @@ +class Lesson { + constructor(crn, teachingMethod, instructor, building, day, time, room, capacity, enrolled) { + this.crn = crn; + this.teachingMethod = teachingMethod?.trim(); + this.instructor = instructor?.trim(); + this.building = building?.trim(); + this.time = time?.trim(); + this.day = day?.trim(); + this.room = room?.trim(); + this.capacity = capacity; + this.enrolled = enrolled; + } + + /** + * + * @returns An array with 2 elements, first is the start time, second is the end time. + * Example: if `this.time` = `"0830/1129"`, then this returns `["08:30", "11:29"]` + */ + getStartAndEndTime() { + const times = this.time.split("/"); + return [ + times[0].slice(0, 2) + ":" + times[0].slice(2), + times[1].slice(0, 2) + ":" + times[1].slice(2), + ]; + } + + /** + * Converts the string `this.day` to a number representing which day of the week it is (Week starts from mondays). + * @returns number representing the day of the week. (-1 if cannot be parsed) + */ + getDayAsNumber() { + switch (this.day.toLowerCase()) { + case "pazartesi": + return 0; + case "salı": + return 1; + case "çarşamba": + return 2; + case "perşember": + return 3; + case "cuma": + return 4; + case "cumartesi": + return 5; + case "pazar": + return 6; + default: + return -1; + } + } +}