schoolloop.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """
  2. Schoolloop backend
  3. This file is part of polyglot, etc.
  4. """
  5. import datetime
  6. import requests
  7. import re
  8. import hashlib
  9. from lxml.html import parse
  10. from io import StringIO
  11. from backends.support import Backend, Course, AssignmentGrade, CategoryGrade, AuthTypes, Task
  12. class SchoolloopCourse(Course):
  13. def __init__(self, backend, row):
  14. self.row = row
  15. self.backend = backend
  16. self.period = row.cssselect(".period")[0].text_content().strip()
  17. self.title = row.cssselect(".course")[0].text_content().strip()
  18. self.teacher = row.cssselect(".teacher")[0].text_content().strip()
  19. if len(row.cssselect(".percent")):
  20. self.grade_summary = row.cssselect(".percent")[0].text_content().strip()
  21. self.id = hashlib.md5((self.period + self.title + self.teacher).encode('utf-8')).hexdigest()
  22. @property
  23. def grades(self):
  24. href = self.row.cssselect(".pr_link > a")[0].attrib["href"]
  25. report = self.backend.session.get(self.backend.base_url + href)
  26. score_extractor = re.compile("([0-9\.]+) / ([0-9\.]+)")
  27. pr_root = parse(StringIO(report.text)).getroot()
  28. categories = {}
  29. for row in pr_root.cssselect('.module:contains("Scores per Category")')[0].cssselect("tr")[1:]:
  30. r = row.getchildren()
  31. category = r[0].text_content().strip()
  32. weight = r[1].text_content().strip()
  33. categories[category] = CategoryGrade(category, weight, [])
  34. for row in pr_root.cssselect(".general_body")[0].getchildren():
  35. r = row.getchildren()
  36. category = r[0].getchildren()[0].text.strip()
  37. assignment = r[0].cssselect("a")[0].text_content().strip()
  38. due = r[2].text_content().strip()
  39. (score_numerator, score_denominator) = score_extractor.search(r[3].text_content().strip()).groups()
  40. comment = r[4].text_content().strip()
  41. if category in categories:
  42. categories[category].grades += [AssignmentGrade(assignment, float(score_numerator) / float(score_denominator))]
  43. return list(categories.values())
  44. @property
  45. def tasks(self):
  46. try:
  47. return self.backend.tasks_by_course[self.title]
  48. except KeyError:
  49. return []
  50. class SchoolloopBackend(Backend):
  51. Config = ["Username", "Password", "School"]
  52. def login(self, config):
  53. # First, fetch the login portal itself
  54. self.base_url = "https://" + config["School"] + ".schoolloop.com"
  55. self.session = requests.Session()
  56. form = self.session.get(self.base_url + "/portal/login")
  57. # Extract data id
  58. data_extractor = re.compile('<input type="hidden" name="form_data_id" id="form_data_id" value="(\\d+)">')
  59. data_id = data_extractor.search(form.text).group(1)
  60. credentials = {
  61. "login_name": config["Username"],
  62. "password": config["Password"],
  63. "redirect": "",
  64. "forward": "",
  65. "login_form_reverse": "",
  66. "form_data_id": str(data_id),
  67. "sort": "",
  68. "reverse": "",
  69. "login_form_sort": "",
  70. "event_override": "login",
  71. "login_form_filter": "",
  72. "login_form_letter": "",
  73. "return_url": "",
  74. "login_form_page_index": "",
  75. "login_form_page_item_count": ""
  76. }
  77. portal = self.session.post(self.base_url + "/portal/login?etarget=login_form", data=credentials)
  78. # Save session
  79. self.portal = portal.text
  80. self.root_element = parse(StringIO(self.portal)).getroot()
  81. return True
  82. @property
  83. def courses(self):
  84. self.fetch_tasks()
  85. rows = self.root_element.cssselect(".student_row")
  86. ret = []
  87. for row in rows:
  88. ret += [SchoolloopCourse(self, row)]
  89. return ret
  90. def fetch_tasks(self):
  91. task_list = self.root_element.cssselect('[data-track-container="Active Assignments"]')[0].cssselect('tr')
  92. self.tasks_by_course = {}
  93. for rows in task_list:
  94. r = rows.getchildren()
  95. title = r[3].text_content().strip()
  96. course = r[4].text_content().strip()
  97. due = r[5].text_content().replace("Due:", "").strip() # Help! Internationalisation >_<
  98. if course not in self.tasks_by_course:
  99. self.tasks_by_course[course] = []
  100. self.tasks_by_course[course] += [Task(title, due)]