Spaces:
Running on Zero
Running on Zero
Commit ·
b473ad4
1
Parent(s): d925e38
analyzer + other-stream courses
Browse files- app/services/classifier.py +54 -31
- tests/test_logic.py +223 -0
app/services/classifier.py
CHANGED
|
@@ -22,7 +22,7 @@ def norm_title_key(title: str) -> str:
|
|
| 22 |
|
| 23 |
# Short, hand-written synonyms (collapsed course name -> aliases). Cheap to extend.
|
| 24 |
ALIASES: dict[str, list[str]] = {
|
| 25 |
-
"mathematicsfornaturalsciences": ["calculus", "natural math", "math natural"],
|
| 26 |
"appliedmathematicsi": ["applied math", "applied maths", "applied mathematics"],
|
| 27 |
"mathematicsforsocialsciences": ["social math", "math social", "maths social"],
|
| 28 |
"generalphysics": ["physics", "phys", "mechanics", "ፊዚክስ"],
|
|
@@ -31,18 +31,18 @@ ALIASES: dict[str, list[str]] = {
|
|
| 31 |
"communicativeenglishlanguageskillsi": ["english", "eng", "flen", "comm english", "እንግሊዝኛ"],
|
| 32 |
"communicativeenglishlanguageskillsii": ["english", "eng", "flen", "comm english", "እንግሊዝኛ"],
|
| 33 |
"generalpsychology": ["psychology", "psych", "psys", "ሳይኮሎጂ"],
|
| 34 |
-
"criticalthinking": ["logic", "loct", "logic and critical thinking"],
|
| 35 |
"geographyofethiopiaandthehorn": ["geography", "geo", "gees", "ጂኦግራፊ"],
|
| 36 |
"historyofethiopiaandthehorn": ["history", "hist", "ታሪክ"],
|
| 37 |
-
"socialanthropology": ["anthropology", "anth"],
|
| 38 |
-
"entrepreneurship": ["entrepreneur", "entre", "mgmt"],
|
| 39 |
"introductiontoemergingtechnologies": ["emerging", "emte", "emerging technology"],
|
| 40 |
"computerprogramming": ["programming", "coding", "eceg", "python", "c++"],
|
| 41 |
-
"moralandciviceducation": ["civic", "civics", "moral", "mcie", "ethics"],
|
| 42 |
-
"physicalfitness": ["fitness", "sports", "spsc"],
|
| 43 |
-
"introductiontoeconomics": ["economics", "econ"],
|
| 44 |
"globalaffairs": ["global", "global trends"],
|
| 45 |
-
"introductiontolawandtheethiopianlegalsystem": ["law", "legal"],
|
| 46 |
"freshmanguideandgeneralresources": [
|
| 47 |
"cut point", "cutt point", "cutpoint", "cutoff", "cut off", "placement", "department selection",
|
| 48 |
"quantitative reasoning", "verbal reasoning", "logical reasoning", "aptitude", "reasoning", "verbal",
|
|
@@ -66,20 +66,24 @@ def _ratio(a: str, b: str) -> float:
|
|
| 66 |
return difflib.SequenceMatcher(None, a, b).ratio()
|
| 67 |
|
| 68 |
|
| 69 |
-
def _alias_hit(alias: str, ws: Sequence[str]) ->
|
|
|
|
| 70 |
parts = words(alias)
|
| 71 |
if not parts:
|
| 72 |
-
return
|
| 73 |
if len(parts) > 1:
|
| 74 |
-
return all(any(w == p for w in ws) for p in parts)
|
| 75 |
p = parts[0]
|
|
|
|
| 76 |
for w in ws:
|
| 77 |
base = re.sub(r"\d+$", "", w)
|
| 78 |
-
if w == p or base == p
|
| 79 |
-
return
|
| 80 |
-
if len(p) >=
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
def _level(ws: Sequence[str]) -> Optional[int]:
|
|
@@ -113,8 +117,11 @@ def rank_courses(courses: Sequence, sources: Sequence[tuple[str, str]]) -> list[
|
|
| 113 |
score += 8 * weight
|
| 114 |
best_alias = 0
|
| 115 |
for alias in ALIASES.get(cname_key, ()):
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
score += best_alias * weight
|
| 119 |
hits = sum(1 for nw in sig_words if any(_hit(w, nw) for w in ws))
|
| 120 |
score += min(hits * 2.0 * weight, 4.0 if weight < 1 else 8.0)
|
|
@@ -142,27 +149,43 @@ def decide(ranked: Sequence[tuple[float, object]]) -> tuple[Optional[object], bo
|
|
| 142 |
|
| 143 |
|
| 144 |
# ---------------------------------------------------------------- kind
|
| 145 |
-
_EXAM_STRONG =
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
| 150 |
_EXAM_TEXT = ("time allowed", "answer all questions", "choose the best answer", "instruction:", "id no", "name:")
|
| 151 |
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
def detect_kind(name: str, caption: str, text: str, pages: Optional[int], size: Optional[int], ext: str) -> tuple[str, str]:
|
| 154 |
-
label = f"
|
|
|
|
|
|
|
| 155 |
if ext in (".ppt", ".pptx"):
|
| 156 |
return "slide", "presentation file"
|
| 157 |
-
if any(
|
| 158 |
return "exam", "name says exam"
|
| 159 |
-
if
|
| 160 |
-
return "slide", "name says slides/chapter"
|
| 161 |
-
if any(k in label for k in _BOOK):
|
| 162 |
return "book", "name says book"
|
| 163 |
-
if
|
| 164 |
-
return "note", "name says module/notes" #
|
| 165 |
-
if
|
|
|
|
|
|
|
| 166 |
return "exam", "name hints at an exam"
|
| 167 |
if sum(1 for k in _EXAM_TEXT if k in (text or "").lower()) >= 2:
|
| 168 |
return "exam", "content looks like an exam"
|
|
|
|
| 22 |
|
| 23 |
# Short, hand-written synonyms (collapsed course name -> aliases). Cheap to extend.
|
| 24 |
ALIASES: dict[str, list[str]] = {
|
| 25 |
+
"mathematicsfornaturalsciences": ["ሒሳብ", "ሂሳብ", "calculus", "natural math", "math natural"],
|
| 26 |
"appliedmathematicsi": ["applied math", "applied maths", "applied mathematics"],
|
| 27 |
"mathematicsforsocialsciences": ["social math", "math social", "maths social"],
|
| 28 |
"generalphysics": ["physics", "phys", "mechanics", "ፊዚክስ"],
|
|
|
|
| 31 |
"communicativeenglishlanguageskillsi": ["english", "eng", "flen", "comm english", "እንግሊዝኛ"],
|
| 32 |
"communicativeenglishlanguageskillsii": ["english", "eng", "flen", "comm english", "እንግሊዝኛ"],
|
| 33 |
"generalpsychology": ["psychology", "psych", "psys", "ሳይኮሎጂ"],
|
| 34 |
+
"criticalthinking": ["አመክንዮ", "logic", "loct", "logic and critical thinking"],
|
| 35 |
"geographyofethiopiaandthehorn": ["geography", "geo", "gees", "ጂኦግራፊ"],
|
| 36 |
"historyofethiopiaandthehorn": ["history", "hist", "ታሪክ"],
|
| 37 |
+
"socialanthropology": ["አንትሮፖሎጂ", "anthropology", "anth"],
|
| 38 |
+
"entrepreneurship": ["ስራ ፈጠራ", "ሥራ ፈጠራ", "entrepreneur", "entre", "mgmt"],
|
| 39 |
"introductiontoemergingtechnologies": ["emerging", "emte", "emerging technology"],
|
| 40 |
"computerprogramming": ["programming", "coding", "eceg", "python", "c++"],
|
| 41 |
+
"moralandciviceducation": ["ሲቪክስ", "ሥነ ምግባር", "civic", "civics", "moral", "mcie", "ethics"],
|
| 42 |
+
"physicalfitness": ["ስፖርት", "fitness", "sports", "spsc"],
|
| 43 |
+
"introductiontoeconomics": ["ኢኮኖሚክስ", "ምጣኔ ሀብት", "economics", "econ"],
|
| 44 |
"globalaffairs": ["global", "global trends"],
|
| 45 |
+
"introductiontolawandtheethiopianlegalsystem": ["ህግ", "ሕግ", "law", "legal"],
|
| 46 |
"freshmanguideandgeneralresources": [
|
| 47 |
"cut point", "cutt point", "cutpoint", "cutoff", "cut off", "placement", "department selection",
|
| 48 |
"quantitative reasoning", "verbal reasoning", "logical reasoning", "aptitude", "reasoning", "verbal",
|
|
|
|
| 66 |
return difflib.SequenceMatcher(None, a, b).ratio()
|
| 67 |
|
| 68 |
|
| 69 |
+
def _alias_hit(alias: str, ws: Sequence[str], fuzzy_ok: bool = True) -> int:
|
| 70 |
+
"""0 = no match, 1 = prefix match, 2 = exact match, 3 = close typo of a whole word."""
|
| 71 |
parts = words(alias)
|
| 72 |
if not parts:
|
| 73 |
+
return 0
|
| 74 |
if len(parts) > 1:
|
| 75 |
+
return 2 if all(any(w == p for w in ws) for p in parts) else 0
|
| 76 |
p = parts[0]
|
| 77 |
+
best = 0
|
| 78 |
for w in ws:
|
| 79 |
base = re.sub(r"\d+$", "", w)
|
| 80 |
+
if w == p or base == p:
|
| 81 |
+
return 2
|
| 82 |
+
if len(p) >= 4 and w.startswith(p):
|
| 83 |
+
best = max(best, 1)
|
| 84 |
+
elif fuzzy_ok and len(p) >= 6 and len(w) >= 6 and w[0] == p[0] and _ratio(w, p) >= 0.7: # "psyscho" -> psychology
|
| 85 |
+
best = max(best, 3)
|
| 86 |
+
return best
|
| 87 |
|
| 88 |
|
| 89 |
def _level(ws: Sequence[str]) -> Optional[int]:
|
|
|
|
| 117 |
score += 8 * weight
|
| 118 |
best_alias = 0
|
| 119 |
for alias in ALIASES.get(cname_key, ()):
|
| 120 |
+
# the catch-all bucket only matches exact words ('reading' must never look like 'reasoning')
|
| 121 |
+
strength = _alias_hit(alias, ws, fuzzy_ok=cname_key != "freshmanguideandgeneralresources")
|
| 122 |
+
if strength:
|
| 123 |
+
multi = len(alias.split()) > 1
|
| 124 |
+
best_alias = max(best_alias, 8 if multi else (5 if strength == 1 else 6))
|
| 125 |
score += best_alias * weight
|
| 126 |
hits = sum(1 for nw in sig_words if any(_hit(w, nw) for w in ws))
|
| 127 |
score += min(hits * 2.0 * weight, 4.0 if weight < 1 else 8.0)
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
# ---------------------------------------------------------------- kind
|
| 152 |
+
_EXAM_STRONG = {"exam", "exams", "examination", "midterm", "midexam", "quiz", "quizzes", "ፈተና", "ፈተናዎች", "ፈተናው"}
|
| 153 |
+
_EXAM_PHRASES = ("past paper", "model exam", "question paper", "mid exam", "final exam")
|
| 154 |
+
_EXAM_WEAK = {"final", "test", "tests", "question", "questions", "previous", "mid", "ጥያቄ", "ጥያቄዎች"} # "Module Final" is NOT an exam
|
| 155 |
+
_SLIDE = {"slide", "slides", "ppt", "pptx", "presentation", "lecture", "lectures", "ስላይድ", "ስላይዶች", "ፕሬዘንቴሽን"}
|
| 156 |
+
_NOTE = {"note", "notes", "handout", "handouts", "summary", "summaries", "worksheet", "assignment", "tutorial", "module",
|
| 157 |
+
"modules", "lab", "ማስታወሻ", "ሞጁል", "ሞዱል"}
|
| 158 |
+
_BOOK = {"textbook", "book", "books", "edition", "isbn", "volume", "መጽሐፍ", "መፅሐፍ", "መጽሀፍ", "መፅሀፍ"}
|
| 159 |
_EXAM_TEXT = ("time allowed", "answer all questions", "choose the best answer", "instruction:", "id no", "name:")
|
| 160 |
|
| 161 |
|
| 162 |
+
def _kind_hit(tokens: Sequence[str], vocab: set) -> bool:
|
| 163 |
+
"""A whole word of the file name is a keyword, or a close typo of a longer one ('exm', 'quize', 'slid')."""
|
| 164 |
+
for t in tokens:
|
| 165 |
+
if t in vocab:
|
| 166 |
+
return True
|
| 167 |
+
if len(t) >= 3 and t.isascii():
|
| 168 |
+
for k in vocab:
|
| 169 |
+
if k.isascii() and len(k) >= 4 and t[0] == k[0] and abs(len(t) - len(k)) <= 2 and _ratio(t, k) >= 0.8:
|
| 170 |
+
return True
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
|
| 174 |
def detect_kind(name: str, caption: str, text: str, pages: Optional[int], size: Optional[int], ext: str) -> tuple[str, str]:
|
| 175 |
+
label = f"{name or ''} {caption or ''}".lower()
|
| 176 |
+
tokens = words(label)
|
| 177 |
+
spaced = " " + " ".join(tokens) + " "
|
| 178 |
if ext in (".ppt", ".pptx"):
|
| 179 |
return "slide", "presentation file"
|
| 180 |
+
if _kind_hit(tokens, _EXAM_STRONG) or any(f" {p} " in spaced for p in _EXAM_PHRASES):
|
| 181 |
return "exam", "name says exam"
|
| 182 |
+
if _kind_hit(tokens, _BOOK):
|
|
|
|
|
|
|
| 183 |
return "book", "name says book"
|
| 184 |
+
if _kind_hit(tokens, _NOTE):
|
| 185 |
+
return "note", "name says module/notes" # beats the ambiguous words "final" and "chapter"
|
| 186 |
+
if _kind_hit(tokens, _SLIDE):
|
| 187 |
+
return "slide", "name says slides/lecture"
|
| 188 |
+
if _kind_hit(tokens, _EXAM_WEAK):
|
| 189 |
return "exam", "name hints at an exam"
|
| 190 |
if sum(1 for k in _EXAM_TEXT if k in (text or "").lower()) >= 2:
|
| 191 |
return "exam", "content looks like an exam"
|
tests/test_logic.py
CHANGED
|
@@ -634,6 +634,229 @@ def test_analytics_insights_and_screens():
|
|
| 634 |
assert render(empty)
|
| 635 |
|
| 636 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
if __name__ == "__main__":
|
| 638 |
for name, fn in list(globals().items()):
|
| 639 |
if name.startswith("test_"):
|
|
|
|
| 634 |
assert render(empty)
|
| 635 |
|
| 636 |
|
| 637 |
+
REAL_NAMES = [
|
| 638 |
+
[
|
| 639 |
+
"psyscho 2014 mid exam collection.pdf",
|
| 640 |
+
"General Psychology",
|
| 641 |
+
"exam"
|
| 642 |
+
],
|
| 643 |
+
[
|
| 644 |
+
"Physics mid exam 2014.pdf",
|
| 645 |
+
"General Physics",
|
| 646 |
+
"exam"
|
| 647 |
+
],
|
| 648 |
+
[
|
| 649 |
+
"General Physics Final Exam.pdf",
|
| 650 |
+
"General Physics",
|
| 651 |
+
"exam"
|
| 652 |
+
],
|
| 653 |
+
[
|
| 654 |
+
"Math 1003 mid.pdf",
|
| 655 |
+
"Mathematics for Natural Sciences",
|
| 656 |
+
"exam"
|
| 657 |
+
],
|
| 658 |
+
[
|
| 659 |
+
"Chem Exam 2013 EC.pdf",
|
| 660 |
+
"General Chemistry",
|
| 661 |
+
"exam"
|
| 662 |
+
],
|
| 663 |
+
[
|
| 664 |
+
"Anthropology Exam Questions.pdf",
|
| 665 |
+
"Social Anthropology",
|
| 666 |
+
"exam"
|
| 667 |
+
],
|
| 668 |
+
[
|
| 669 |
+
"Logic and critical thinking questions.pdf",
|
| 670 |
+
"Critical Thinking",
|
| 671 |
+
"exam"
|
| 672 |
+
],
|
| 673 |
+
[
|
| 674 |
+
"ኢኮኖሚክስ ጥያቄ.pdf",
|
| 675 |
+
"Introduction to Economics",
|
| 676 |
+
"exam"
|
| 677 |
+
],
|
| 678 |
+
[
|
| 679 |
+
"ፊዚክስ ፈተና 2014.pdf",
|
| 680 |
+
"General Physics",
|
| 681 |
+
"exam"
|
| 682 |
+
],
|
| 683 |
+
[
|
| 684 |
+
"Civics model exam.pdf",
|
| 685 |
+
"Moral and Civic Education",
|
| 686 |
+
"exam"
|
| 687 |
+
],
|
| 688 |
+
[
|
| 689 |
+
"Past paper English 2013.pdf",
|
| 690 |
+
null,
|
| 691 |
+
"exam"
|
| 692 |
+
],
|
| 693 |
+
[
|
| 694 |
+
"geography quiz 1.pdf",
|
| 695 |
+
"Geography of Ethiopia and the Horn",
|
| 696 |
+
"exam"
|
| 697 |
+
],
|
| 698 |
+
[
|
| 699 |
+
"Psychology test 1 answer key.pdf",
|
| 700 |
+
"General Psychology",
|
| 701 |
+
"exam"
|
| 702 |
+
],
|
| 703 |
+
[
|
| 704 |
+
"Mid-term exam for Entrepreneurship.pdf",
|
| 705 |
+
"Entrepreneurship",
|
| 706 |
+
"exam"
|
| 707 |
+
],
|
| 708 |
+
[
|
| 709 |
+
"Psychology note.pdf",
|
| 710 |
+
"General Psychology",
|
| 711 |
+
"note"
|
| 712 |
+
],
|
| 713 |
+
[
|
| 714 |
+
"Chapter 1-3 summary psychology.pdf",
|
| 715 |
+
"General Psychology",
|
| 716 |
+
"note"
|
| 717 |
+
],
|
| 718 |
+
[
|
| 719 |
+
"ፊዚክስ ማስታወሻ.pdf",
|
| 720 |
+
"General Physics",
|
| 721 |
+
"note"
|
| 722 |
+
],
|
| 723 |
+
[
|
| 724 |
+
"Economics handout.pdf",
|
| 725 |
+
"Introduction to Economics",
|
| 726 |
+
"note"
|
| 727 |
+
],
|
| 728 |
+
[
|
| 729 |
+
"Entrepreneurship chapter 2 notes.pdf",
|
| 730 |
+
"Entrepreneurship",
|
| 731 |
+
"note"
|
| 732 |
+
],
|
| 733 |
+
[
|
| 734 |
+
"Anthropology module.pdf",
|
| 735 |
+
"Social Anthropology",
|
| 736 |
+
"note"
|
| 737 |
+
],
|
| 738 |
+
[
|
| 739 |
+
"Physics short note for mid.pdf",
|
| 740 |
+
"General Physics",
|
| 741 |
+
"note"
|
| 742 |
+
],
|
| 743 |
+
[
|
| 744 |
+
"Lecture 3 physics.pdf",
|
| 745 |
+
"General Physics",
|
| 746 |
+
"slide"
|
| 747 |
+
],
|
| 748 |
+
[
|
| 749 |
+
"Chapter 1 Entrepreneurship.pptx",
|
| 750 |
+
"Entrepreneurship",
|
| 751 |
+
"slide"
|
| 752 |
+
],
|
| 753 |
+
[
|
| 754 |
+
"ስላይድ ኢኮኖሚክስ.pdf",
|
| 755 |
+
"Introduction to Economics",
|
| 756 |
+
"slide"
|
| 757 |
+
],
|
| 758 |
+
[
|
| 759 |
+
"General Chemistry textbook.pdf",
|
| 760 |
+
"General Chemistry",
|
| 761 |
+
"book"
|
| 762 |
+
],
|
| 763 |
+
[
|
| 764 |
+
"ባዮሎጂ መጽሐፍ.pdf",
|
| 765 |
+
"General Biology",
|
| 766 |
+
"book"
|
| 767 |
+
],
|
| 768 |
+
[
|
| 769 |
+
"document.pdf",
|
| 770 |
+
null,
|
| 771 |
+
"note"
|
| 772 |
+
],
|
| 773 |
+
[
|
| 774 |
+
"IMG_2034.pdf",
|
| 775 |
+
null,
|
| 776 |
+
"note"
|
| 777 |
+
],
|
| 778 |
+
[
|
| 779 |
+
"1698765432.pdf",
|
| 780 |
+
null,
|
| 781 |
+
"note"
|
| 782 |
+
],
|
| 783 |
+
[
|
| 784 |
+
"PSYCH_1007_Module(final).pdf",
|
| 785 |
+
"General Psychology",
|
| 786 |
+
"note"
|
| 787 |
+
],
|
| 788 |
+
[
|
| 789 |
+
"Psys1007 worksheet 2.pdf",
|
| 790 |
+
"General Psychology",
|
| 791 |
+
"note"
|
| 792 |
+
],
|
| 793 |
+
[
|
| 794 |
+
"physics lab report.pdf",
|
| 795 |
+
"General Physics",
|
| 796 |
+
"note"
|
| 797 |
+
],
|
| 798 |
+
[
|
| 799 |
+
"economics assignment 1.pdf",
|
| 800 |
+
"Introduction to Economics",
|
| 801 |
+
"note"
|
| 802 |
+
],
|
| 803 |
+
[
|
| 804 |
+
"Exm 2014 geography.pdf",
|
| 805 |
+
"Geography of Ethiopia and the Horn",
|
| 806 |
+
"exam"
|
| 807 |
+
],
|
| 808 |
+
[
|
| 809 |
+
"Psychology Exam collection (2013-2015).pdf",
|
| 810 |
+
"General Psychology",
|
| 811 |
+
"exam"
|
| 812 |
+
],
|
| 813 |
+
[
|
| 814 |
+
"philosophy notes.pdf",
|
| 815 |
+
null,
|
| 816 |
+
"note"
|
| 817 |
+
],
|
| 818 |
+
[
|
| 819 |
+
"geometry worked examples.pdf",
|
| 820 |
+
null,
|
| 821 |
+
"note"
|
| 822 |
+
],
|
| 823 |
+
[
|
| 824 |
+
"notebook.pdf",
|
| 825 |
+
null,
|
| 826 |
+
"note"
|
| 827 |
+
],
|
| 828 |
+
[
|
| 829 |
+
"finalize registration.pdf",
|
| 830 |
+
null,
|
| 831 |
+
"note"
|
| 832 |
+
],
|
| 833 |
+
[
|
| 834 |
+
"midnight reading.pdf",
|
| 835 |
+
null,
|
| 836 |
+
"note"
|
| 837 |
+
],
|
| 838 |
+
[
|
| 839 |
+
"latest news.pdf",
|
| 840 |
+
null,
|
| 841 |
+
"note"
|
| 842 |
+
]
|
| 843 |
+
]
|
| 844 |
+
|
| 845 |
+
|
| 846 |
+
def test_realistic_student_file_names():
|
| 847 |
+
"""Amharic names, typos, mid/final/collection, chapter..., plus words that must NOT be misread."""
|
| 848 |
+
import os
|
| 849 |
+
|
| 850 |
+
wrong = []
|
| 851 |
+
for name, want_course, want_kind in REAL_NAMES:
|
| 852 |
+
best, confident, _ = C.decide(C.rank_courses(COURSES, [(name, "name")]))
|
| 853 |
+
course = best.name if (best and confident) else None
|
| 854 |
+
kind, _why = C.detect_kind(name, "", "", None, None, os.path.splitext(name)[1].lower())
|
| 855 |
+
if (course, kind) != (want_course, want_kind):
|
| 856 |
+
wrong.append((name, course, kind))
|
| 857 |
+
assert not wrong, wrong
|
| 858 |
+
|
| 859 |
+
|
| 860 |
if __name__ == "__main__":
|
| 861 |
for name, fn in list(globals().items()):
|
| 862 |
if name.startswith("test_"):
|