151 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			151 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import Colle from '#models/colle'
 | |
| import Examiner from '#models/examiner'
 | |
| import Room from '#models/room'
 | |
| import Subject from '#models/subject'
 | |
| import User from '#models/user'
 | |
| import redis from '@adonisjs/redis/services/main'
 | |
| 
 | |
| export class ColleService {
 | |
|   async getHealthyUntil(className: string) {
 | |
|     const healtyUntil = await redis.get(`healthy_until_${className}`)
 | |
|     return new Date(healtyUntil || '')
 | |
|   }
 | |
| 
 | |
|   async getLastSync(className: string) {
 | |
|     const lastSync = await redis.get(`last_sync_${className}`)
 | |
|     return new Date(lastSync || '')
 | |
|   }
 | |
| 
 | |
|   async getStudent(studentName: string, className: string) {
 | |
|     // Find or create a student by name
 | |
|     const { firstName, lastName } = this.splitNames(studentName)
 | |
|     const student = await User.query()
 | |
|       .where('first_name', firstName)
 | |
|       .where('last_name', lastName)
 | |
|       .where('class_name', className)
 | |
|       .first()
 | |
|     if (!student) {
 | |
|       return User.create({ firstName, lastName, className })
 | |
|     }
 | |
|     return student
 | |
|   }
 | |
| 
 | |
|   splitNames(fullName: string): { firstName: string; lastName: string } {
 | |
|     const words = fullName.trim().split(/\s+/)
 | |
| 
 | |
|     let lastNameParts: string[] = []
 | |
|     let i = 0
 | |
| 
 | |
|     // Collect all uppercase words at the start
 | |
|     while (i < words.length && words[i] === words[i].toUpperCase()) {
 | |
|       lastNameParts.push(words[i])
 | |
|       i++
 | |
|     }
 | |
| 
 | |
|     const lastName = lastNameParts.join(' ')
 | |
|     const firstName = words.slice(i).join(' ')
 | |
| 
 | |
|     return { firstName, lastName }
 | |
|   }
 | |
| 
 | |
|   async getExaminer(examinerName: string) {
 | |
|     // Find or create an examiner by name
 | |
|     const examiner = await Examiner.query().where('name', examinerName).first()
 | |
|     if (!examiner) return Examiner.create({ name: examinerName })
 | |
|     return examiner
 | |
|   }
 | |
| 
 | |
|   async getSubject(subjectName: string) {
 | |
|     // Find or create a subject by name
 | |
|     const subject = await Subject.query().where('name', subjectName).first()
 | |
|     if (!subject) return Subject.create({ name: subjectName })
 | |
|     return subject
 | |
|   }
 | |
| 
 | |
|   async getRoom(roomName: string) {
 | |
|     // Find or create a room by name
 | |
|     const room = await Room.query().where('name', roomName).first()
 | |
|     if (!room) return Room.create({ name: roomName })
 | |
|     return room
 | |
|   }
 | |
| 
 | |
|   async getColles(student: User, startDate: string, endDate: string) {
 | |
|     const classColles = await Colle.query()
 | |
|       .preload('student')
 | |
|       .preload('examiner')
 | |
|       .preload('subject')
 | |
|       .preload('room')
 | |
|       .preload('attachments')
 | |
|       .where('date', '>=', startDate)
 | |
|       .where('date', '<=', endDate)
 | |
|       .whereHas('student', (query) => {
 | |
|         query.where('className', student.className)
 | |
|       })
 | |
|       // Filter only colles that have been graded
 | |
|       .whereNotNull('grade')
 | |
|       .orderBy('date', 'asc')
 | |
| 
 | |
|     const studentColles = await Colle.query()
 | |
|       .preload('student')
 | |
|       .preload('examiner')
 | |
|       .preload('subject')
 | |
|       .preload('room')
 | |
|       .preload('attachments')
 | |
|       .where('date', '>=', startDate)
 | |
|       .where('date', '<=', endDate)
 | |
|       .where('studentId', student.id)
 | |
|       .orderBy('date', 'asc')
 | |
| 
 | |
|     // TODO: Favorite colles
 | |
|     const favoriteColles = [] as Colle[]
 | |
| 
 | |
|     return {
 | |
|       classColles,
 | |
|       studentColles,
 | |
|       favoriteColles,
 | |
|       healthyUntil: await this.getHealthyUntil(student.className),
 | |
|       lastSync: await this.getLastSync(student.className),
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Pre-process HTML content to render LaTeX
 | |
|   private removeTrailingLines(htmlString: string) {
 | |
|     return htmlString.replace(/(<br\s*\/?>\s*)+$/gi, '').trim()
 | |
|   }
 | |
| 
 | |
|   private extractLatexImages(html: string) {
 | |
|     const imgRegex = /<img[^>]+src="(https:\/\/latex\.codecogs\.com\/gif\.latex\?(=?.*?))"[^>]*>/g
 | |
|     let parts = []
 | |
|     let latexMatches: string[] = []
 | |
|     let lastIndex = 0
 | |
| 
 | |
|     html.replace(imgRegex, (match, _, latex, index) => {
 | |
|       parts.push(html.slice(lastIndex, index)) // Add HTML before image
 | |
|       latexMatches.push(decodeURIComponent(latex)) // Extract and decode LaTeX
 | |
|       lastIndex = index + match.length
 | |
|       return ''
 | |
|     })
 | |
| 
 | |
|     parts.push(html.slice(lastIndex)) // Add remaining HTML after last image
 | |
| 
 | |
|     return { parts, latexMatches }
 | |
|   }
 | |
| 
 | |
|   prepareHtmlForRendering(html: string) {
 | |
|     const strippedHtml = this.removeTrailingLines(html)
 | |
|     const { parts, latexMatches } = this.extractLatexImages(strippedHtml)
 | |
| 
 | |
|     const outputHtml = parts
 | |
|       .map((part, i) => {
 | |
|         if (!latexMatches[i]) {
 | |
|           return part
 | |
|         }
 | |
|         return `${part}$$${latexMatches[i]}$$`
 | |
|       })
 | |
|       .join('')
 | |
| 
 | |
|     // Remove all "\," from string
 | |
|     const regex = /\\,/g
 | |
|     return outputHtml.replace(regex, ' ')
 | |
|   }
 | |
| }
 | 
