From e19bb48a4e53e9ac52849f3f8d6cbfdeb7785fba Mon Sep 17 00:00:00 2001 From: Tue Herlau <tuhe@dtu.dk> Date: Sat, 19 Mar 2022 17:20:11 +0100 Subject: [PATCH] Various updates for 02465 during semester --- setup.py | 4 +- src/coursebox.egg-info/PKG-INFO | 32 ++- src/coursebox.egg-info/requires.txt | 12 +- .../core/__pycache__/info.cpython-38.pyc | Bin 9428 -> 9540 bytes .../__pycache__/projects_info.cpython-38.pyc | Bin 4426 -> 5235 bytes src/coursebox/core/info.py | 94 +++++-- src/coursebox/core/projects.py | 109 ++++++-- src/coursebox/core/projects_info.py | 121 +++++++-- ...homepage_lectures_exercises.cpython-38.pyc | Bin 13393 -> 14370 bytes .../material/homepage_lectures_exercises.py | 245 +++++++++++++++--- 10 files changed, 501 insertions(+), 116 deletions(-) diff --git a/setup.py b/setup.py index 5d1e12e..e5eb159 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open("README.md", "r", encoding="utf-8") as fh: # beamer-slider setuptools.setup( name="coursebox", - version="0.1.2", + version="0.1.4", author="Tue Herlau", author_email="tuhe@dtu.dk", description="A course management system currently used at DTU", @@ -30,5 +30,5 @@ setuptools.setup( package_dir={"": "src"}, packages=setuptools.find_packages(where="src"), python_requires=">=3.8", - install_requires=['numpy','pycode_similar','tika','openpyxl', 'xlwings','matplotlib','langdetect','jinjafy','beamer-slider','tinydb'], + install_requires=['numpy','pycode_similar','tika','openpyxl', 'xlwings','matplotlib','langdetect','beamer-slider','tinydb'], ) diff --git a/src/coursebox.egg-info/PKG-INFO b/src/coursebox.egg-info/PKG-INFO index f983e78..a813cf8 100644 --- a/src/coursebox.egg-info/PKG-INFO +++ b/src/coursebox.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: coursebox -Version: 0.1.1 +Version: 0.1.4 Summary: A course management system currently used at DTU Home-page: https://lab.compute.dtu.dk/tuhe/coursebox Author: Tue Herlau @@ -16,6 +16,34 @@ Description-Content-Type: text/markdown License-File: LICENSE # Coursebox DTU +DTU course management software. -DTU course management software. +## Installation +```terminal +pip install coursebox +``` +## What it can do + - Single semester-dependent configuration file + - Integrates with DTU Inside/DTU Learn + - Distribution/evalauation of project reports in Learn-compatible format + - Quiz-generation in DTU Learn/Beamer friendly format + - Automatic website/syllabus generation + - Automatic generation of lectures handouts/exercises (you don't have to track dynamic content like dates/lecture titles; it is all in the configuration) + - Easy compilation to 2/5 day formats (Continuous education) + +## Usage +Coursebox requires a specific directory structure. It is easier to start with an existing course and adapt to your needs. Please contact me at tuhe@dtu.dk for more information. + +## Citing +```bibtex +@online{coursebox, + title={Coursebox (0.1.1): \texttt{pip install coursebox}}, + url={https://lab.compute.dtu.dk/tuhe/coursebox}, + urldate = {2021-09-07}, + month={9}, + publisher={Technical University of Denmark (DTU)}, + author={Tue Herlau}, + year={2021}, +} +``` diff --git a/src/coursebox.egg-info/requires.txt b/src/coursebox.egg-info/requires.txt index 6391557..0a85667 100644 --- a/src/coursebox.egg-info/requires.txt +++ b/src/coursebox.egg-info/requires.txt @@ -1,13 +1,9 @@ -openpyxl +numpy +pycode_similar tika +openpyxl xlwings -pybtex -langdetect -wexpect -pexpect matplotlib -numpy -pycode_similar -jinjafy +langdetect beamer-slider tinydb diff --git a/src/coursebox/core/__pycache__/info.cpython-38.pyc b/src/coursebox/core/__pycache__/info.cpython-38.pyc index 1d680dd6b8885df10e21e68905c1c7757f176b18..e0ab976224268ffd731b58532967876fc91bff4b 100644 GIT binary patch delta 4435 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 delta 4401 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 K<4>w-b^bqwy5MX8 diff --git a/src/coursebox/core/__pycache__/projects_info.cpython-38.pyc b/src/coursebox/core/__pycache__/projects_info.cpython-38.pyc index 4924025fb95a5691dc63e256c55f5a0b44df0bbd..3c221ed674c47518426218c8f64be90b275198d7 100644 GIT binary patch literal 5235 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 B;E@0T delta 2425 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 DT>K1* diff --git a/src/coursebox/core/info.py b/src/coursebox/core/info.py index ba913e1..a80d792 100644 --- a/src/coursebox/core/info.py +++ b/src/coursebox/core/info.py @@ -12,10 +12,14 @@ from coursebox.core.info_paths import core_conf # import pybtex.database.input.bibtex # import pybtex.plugin # import io +from line_profiler_pycharm import profile +import time - +@profile def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=False): - wb = openpyxl.load_workbook(xlsx_file, data_only=True) + # print("Loading...", xlsx_file, sheet, as_dict_list) + t0 = time.time() + wb = openpyxl.load_workbook(xlsx_file, data_only=True, read_only=True) if not sheet: ws = wb.worksheets[0] else: @@ -24,26 +28,65 @@ def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=False): return None else: ws = ws.pop() - dd = [] - key_cols = [j for j in range(ws.max_column) if ws.cell(row=1, column=j + 1).value is not None] - for i in range(1, ws.max_row): - rdict = {} - if not any( [ws.cell(row=i+1, column=j+1).value is not None for j in key_cols] ): - continue - for j in key_cols: - key = ws.cell(row=1, column=j+1).value - if key is not None: - key = key.strip() if isinstance(key,str) else key - value = ws.cell(row=i + 1, column=j + 1).value - value = value.strip() if isinstance(value,str) else value - if isinstance(value, str): - if value == 'True': - value = True - if value == 'False': - value = False - rdict[key] = value - dd.append(rdict) - + # print(time.time()-t0) + # dd = [] + # key_cols = [j for j in range(ws.max_column) if ws.cell(row=1, column=j + 1).value is not None] + # print(time.time()-t0, ws.max_row) + # np.array([[i.value for i in j[1:5]] for j in ws.rows]) + + import numpy as np + A = np.array([[i.value for i in j] for j in ws.rows]) + # print(time.time() - t0, ws.max_row, len(key_cols)) + + + # for j in range(A.shape[1]): + + + + + a = 234 + + # for i in range(1, ws.max_row): + # rdict = {} + # if not any( [ws.cell(row=i+1, column=j+1).value is not None for j in key_cols] ): + # continue + # for j in key_cols: + # key = ws.cell(row=1, column=j+1).value + # if key is not None: + # key = key.strip() if isinstance(key,str) else key + # value = ws.cell(row=i + 1, column=j + 1).value + # value = value.strip() if isinstance(value,str) else value + # if isinstance(value, str): + # if value == 'True': + # value = True + # if value == 'False': + # value = False + # rdict[key] = value + # dd.append(rdict) + + # print(time.time()-t0) + + A = A[:, A[0] != None] + A = A[(A != None).sum(axis=1) > 0, :] + + dd2 = [] + for i in range(1, A.shape[0]): + A[A == 'True'] = True + A[A == 'False'] = False + + d = dict(zip(A[0, :].tolist(), [a.strip() if isinstance(a,str) else a for a in A[i, :].tolist() ])) + dd2.append(d) + + # print(time.time() - t0) + dd = dd2 + # if dd != dd2: + # for k in range(len(dd)): + # if dd[k] != dd2[k]: + # print(k) + # print(dd) + # print(dd2) + # assert False + # print("BAd!") if as_dict_list: dl = list_dict2dict_list(dd) for k in dl.keys(): @@ -51,6 +94,8 @@ def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=False): if len(x) == 1: x = x.pop() dl[k] = x dd = dl + wb.close() + # print("xlsx2dicts", time.time()-t0) return dd def get_enrolled_students(): @@ -200,6 +245,7 @@ def get_forum(paths): d2.append({k: v[i] for k, v in dd.items()}) return d2 +@profile def class_information(): course_number = core_conf['course_number'] piazza = 'https://piazza.com/dtu.dk/%s%s/%s' % (semester().lower(), year(), course_number) @@ -214,8 +260,8 @@ def class_information(): 'piazza': piazza, # deprecated. 'course_number': course_number, 'semester': semester(), - 'reports_handout': [1,6], - 'reports_handin': [6,11], + # 'reports_handout': [1,6], # Set in excel conf. + # 'reports_handin': [6, 11], # set in excel conf. 'semester_id': semester_id(), 'today': today(), 'instructors': get_instructors(), diff --git a/src/coursebox/core/projects.py b/src/coursebox/core/projects.py index 3c017f2..ef3cb07 100644 --- a/src/coursebox/core/projects.py +++ b/src/coursebox/core/projects.py @@ -1,3 +1,6 @@ +import re +import tempfile +import tika import os import shutil import openpyxl @@ -5,7 +8,6 @@ import numpy as np import itertools import math import glob -# import zipfile from tika import parser from openpyxl.worksheet.datavalidation import DataValidation from openpyxl.utils import get_column_letter @@ -22,6 +24,8 @@ from jinjafy.plot.plot_helpers import get_colors import time from collections import defaultdict import zipfile +import hashlib +import pandas as pd def get_dirs(zf): @@ -32,13 +36,11 @@ def get_dirs(zf): def fix_handins_fuckup(project_id=2): """ Handle the problem with multiple hand-ins in DTU learn. """ - import zipfile paths = get_paths() from coursebox.core.info import class_information info = class_information() zf = paths['instructor_project_evaluations'] + f"/zip{project_id}.zip" - tas = [i['shortname'] for i in info['instructors'] ] ta_links = {i['shortname']: i for i in info['instructors']} @@ -51,7 +53,6 @@ def fix_handins_fuckup(project_id=2): ta_reports[r] = ta fls = get_dirs(zf) - # fls = [f for f in zip.namelist() if not f.endswith("tml") and f.endswith("/")] d = defaultdict(lambda: []) for l in fls: @@ -123,7 +124,6 @@ def handle_projects(verbose=False, gather_main_xlsx_file=True, plagiarism_check= zip1 = instructor_path + "/zip1.zip" zip2 = instructor_path + "/zip2.zip" zip3 = instructor_path + "/zip3.zip" - zips = [None, zip1, zip2, zip3] for j,zf in enumerate(zips): @@ -138,12 +138,12 @@ def handle_projects(verbose=False, gather_main_xlsx_file=True, plagiarism_check= continue else: # instructor files do not exist if j == 0: - copy_populate_from_template(info, sheet_number=j, zip_file=None) + copy_populate_from_template(paths, info, sheet_number=j, zip_file=None) elif os.path.exists(zf): # make a copy of report template and populate it with groups obtained from previous report evaluation. # all_groups = get_all_reports_from_collected_xlsx_file() - copy_populate_from_template(info, sheet_number=j, zip_file=zf) + copy_populate_from_template(paths, info, sheet_number=j, zip_file=zf) # distribute_zip_content(info, sheet=j, zf_base=zf) else: print("When available, please move downloaded copy of all reports from campusnet to destination:") @@ -228,13 +228,13 @@ def compute_error_files(info, paths): es = err_label + f"> Report score is {g.get('score', 0)}. The report score has to be between 0 and 4; probably due to a too high value of 'Delta' in instructor sheet." ERRORS[ins].append(es) - if repn >= 1 and not g['comments']: + if repn >= 1 and not g['comments'] and info['course_number'] != '02465': es = err_label + "> Incomplete report evaluation (missing comments field)" es += "Please fill out comments field in your excel sheet." ERRORS[ins].append(es) - if repn >= 1 and not g['approver_comments']: + if repn >= 1 and not g['approver_comments'] and info['course_number'] != '02465': es = err_label + "> Incomplete report evaluation (you are missing the approver comments field; can simply be set to 'ok')." ERRORS.get(g['approver'], []).append(es) @@ -300,10 +300,70 @@ def get_instructor_xlsx_files(info, sheet): return xlsx -import hashlib +def get_groups_from_learn_xslx_file(paths, sheet_number): + fname = f"{paths['instructor_project_evaluations']}/groups{sheet_number}.xlsx" + all_groups = [] + if os.path.exists(fname): + # Reading from the groups{number}.xlsx group-id file exported from DTU learn. Note this file contains fuckups. + dg = defaultdict(list) + df = pd.read_excel(fname) + for uname, group_id in zip(df['Username'], df['Project groups']): + id = int(group_id.split(" ")[1]) + if len(uname) == 7 and uname[0] == 's': + dg[id].append(uname) + else: + dg[id].append("DTU-LEARN-FUCKED-THIS-ID-UP-CHECK-ON-REPORT") + + all_groups = [{'group_id': id, 'student_ids': students} for id, students in dg.items()] + return all_groups + +def search_projects(paths, sheet_number, patterns): + zip_files = [paths['instructor_project_evaluations'] + "/zip%d.zip" % sheet_number] + # print(zip_files) + + all_groups = [] + gps = defaultdict(list) + for zip_file in zip_files: + if os.path.exists(zip_file): + tmpdir = tempfile.TemporaryDirectory() + zipfile.ZipFile(zip_file).extractall(path=tmpdir.name) + pdfs = glob.glob(tmpdir.name + "/**/*.pdf", recursive=True) + for pdf in pdfs: + pdf_parsed = tika.parser.from_file(pdf) + id =int(os.path.dirname(pdf).split(" - ")[1].split(" ")[1]) + students = re.findall('s\d\d\d\d\d\d', pdf_parsed['content'], flags=re.IGNORECASE) + gps[id] += students + + for id, students in gps.items(): + all_groups.append({'group_id': id, 'student_ids': list(set(students))}) + return all_groups + + +def unpack_zip_file_recursively(zip_file, destination_dir): + """ + Unpack the zip_file (extension: .zip) to the given directory. + + If the folders in the zip file contains other zip/files, these are unpacked recursively. + """ + # Unpack zip file recursively and flatten it. + zipfile.ZipFile(zip_file).extractall(path=destination_dir) + ls = glob.glob(destination_dir + "/*") + for f in ls: + if os.path.isdir(f): + zipfiles = glob.glob(f + "/*.zip") + for zp in zipfiles: + print("Unpacking student zip file>", zp) + zipfile.ZipFile(zp).extractall(path=os.path.dirname(zp) + "/") + + +def copy_populate_from_template(paths, info, sheet_number,zip_file): + # Try to load group ids from the project pdf's + all_groups = search_projects(paths, sheet_number, r"s\d{6}") + # all_groups = get_groups_from_learn_xslx_file(paths, sheet_number) + if len(all_groups) == 0: + all_groups = projects_info.get_groups_from_report(repn=sheet_number-1) if sheet_number > 0 else [] + # Hopefully this did the trick and we have the groups all grouped up. -def copy_populate_from_template(info, sheet_number,zip_file): - all_groups = projects_info.get_groups_from_report(repn=sheet_number-1) if sheet_number > 0 else [] # set up which TA approve which TA if any( [i['language'] not in ["en", "any"] for i in info['instructors'] ]): print(info['instructors']) @@ -337,10 +397,13 @@ def copy_populate_from_template(info, sheet_number,zip_file): n_groups_per_instructor = 24 + (sheet_number == 0) * 26 if sheet_number > 0: - zfd = zip_file[:-4] - if not os.path.exists(zfd): - os.mkdir(zfd) - zipfile.ZipFile(zip_file).extractall(path=zfd) + # zfd = zip_file[:-4] + # if not os.path.exists(zfd): + # os.mkdir(zfd) + zfd = tempfile.TemporaryDirectory().name + # zipfile.ZipFile(zip_file).extractall(path=tmpdir.name) + + unpack_zip_file_recursively(zip_file, destination_dir=zfd) # get all report handins (i.e. directories) ls = [l for l in glob.glob(zfd + "/*") if l[-3:] not in ["txt", "tml"]] @@ -431,8 +494,8 @@ def copy_populate_from_template(info, sheet_number,zip_file): corrector = all_tas[shortname]['approver'] if sheet_number > 0: # Copy reports to directory (distribute amongst TAs) - b_dir = os.path.dirname(zip_file) - ins_dir = "%s/project_%i_%s/"%(b_dir, sheet_number, shortname) + # b_dir = os.path.dirname(zip_file) + ins_dir = "%s/project_%i_%s/"%(zfd, sheet_number, shortname) if not os.path.exists(ins_dir): os.mkdir(ins_dir) @@ -440,7 +503,7 @@ def copy_populate_from_template(info, sheet_number,zip_file): for handin in all_tas[shortname]['handins']: shutil.move(handin['path'], ins_dir) - shutil.make_archive(ins_dir[:-1], 'zip', ins_dir) + shutil.make_archive(os.path.dirname(zip_file) +"/"+ os.path.basename(ins_dir[:-1]), 'zip', ins_dir) time.sleep(2) print("Removing tree of reports to clear up space...") shutil.rmtree(ins_dir) @@ -471,10 +534,10 @@ def copy_populate_from_template(info, sheet_number,zip_file): sheet.cell(STUDENT_ID_ROW+j, ccol+i).value = s wb.save(ifile) wb.close() - # clean up zip file directories - if sheet_number > 0: - zfd = zip_file[:-4] - shutil.rmtree(zfd) + # clean up zip file directories; since it is a tmp file, we don't have to. + # if sheet_number > 0: + # zfd = zip_file[:-4] + # shutil.rmtree(zfd) def write_dropdown_sumprod_sheet(sheet): ccol = 2 diff --git a/src/coursebox/core/projects_info.py b/src/coursebox/core/projects_info.py index 0b24331..62e1457 100644 --- a/src/coursebox/core/projects_info.py +++ b/src/coursebox/core/projects_info.py @@ -3,6 +3,7 @@ import os import re import openpyxl import numpy as np +from line_profiler_pycharm import profile INSTRUCTOR_ROW = 6 INSTRUCTOR_CHECKER_ROW = 31 @@ -16,19 +17,6 @@ RANGE_MIN_COL = 5 DELTA_ALLOWED_ROW = 111 # The range of possible delta-values. Should be in an empty (new) row at bottom. -def get_all_reports_from_collected_xlsx_file_DEFUNCT(): # when is this used? - out = get_output_file() - wb = openpyxl.load_workbook(out) - all_reports = {} - for repn in range(3, -1, -1): - cls = [] - for i in range(2, wb.worksheets[repn].max_column + 1): - cp = parse_column(wb.worksheets[repn], report_number=repn, column=i) - if not cp['student_ids']: - continue - cls.append(cp) - all_reports[repn] = cls - return all_reports def parse_column_student_ids(v): sn = [] @@ -42,7 +30,82 @@ def parse_column_student_ids(v): sn.append(g) return sn + +def parse_column_numpy(col, report_number, column): + """ Parse a column assuming it is defined as a numpy array. + This is the recommended method as it is much, much faster. + """ + # ws = worksheet # wb.worksheets[sheet] + sn = [] + group_id = col[STUDENT_ID_ROW - 1-1] #).value + + # col = ['' if col[0] is np.NAN else x for x in col] + + for i in range(0, 3): + v = col[i + STUDENT_ID_ROW-1]#, column=column).value + sn += parse_column_student_ids(v) + + + instructor = col[INSTRUCTOR_ROW-1]#, column=column).value + approver = col[INSTRUCTOR_ROW+1-1]# , column=column).value + + if instructor: + instructor = instructor.lower() + if approver: + approver = str(approver).lower() + + content = None + comments = None + appr_comments = None + if report_number > 0 and sn: + N = 38 + rarr = np.ndarray(shape=(N,1),dtype=np.object) + for j in range(N): + + v = col[3 + STUDENT_ID_ROW+j-1]#, column=column).value + rarr[j,0] = v + content = rarr + comments = col[EVALUATION_ROW_END+5-1]# , column=column).value + appr_comments = col[EVALUATION_ROW_END+6-1]# , column=column).value + + cgroup = {'column_j': column, 'student_ids': sn, 'instructor': instructor, "approver": approver, 'content': content, + "comments": comments, "approver_comments": appr_comments, 'missing_fields': [], + 'group_id': group_id} + + # Now, find errors... This involves first finding non-zero columns + if report_number > 0 and sn: + score = cgroup['content'][-3, 0] + cgroup['score'] = score + cgroup['pct'] = score2pct(score) + + # if report_number == 3: # this obviously needs fixing for next semester. + # raise Exception("No report number 3 anymore. ") + # I = [] + # for i in range(42): # max number of evaluation fields (irrelevant) + # v1 = col[WEIGHT_ROW_START+i-1, RANGE_MIN_COL-1]# ).value + # v2 = col[WEIGHT_ROW_START+i-1, RANGE_MIN_COL+1-1]#).value + # if (v1 == -1 and v2 == 1) or (v1 == 0 and v2 == 4): + # I.append(i) + # if v1 == -1 and v2 == 1: + # # print("delta col") + # break + # + # for i in I: + # w1 = worksheet.cell(row=WEIGHT_ROW_START + i, column=1).value + # w3_ = worksheet.cell(row=INSTRUCTOR_ROW + i+2, column=1).value # should agree with label in w1 + # w2 = worksheet.cell(row=INSTRUCTOR_ROW + i+2, column=column).value + # if w2 == None: + # cgroup['missing_fields'].append( (i, w1) ) + # if report_number < 3: + # print("old report nr.") + + return cgroup + + + def parse_column(worksheet, report_number, column): + """ This is the old method. It is very slow. Use the numpy-version above. + """ ws = worksheet # wb.worksheets[sheet] sn = [] group_id = ws.cell(row=STUDENT_ID_ROW - 1, column=column).value @@ -54,7 +117,8 @@ def parse_column(worksheet, report_number, column): instructor = ws.cell(row=INSTRUCTOR_ROW, column=column).value approver = ws.cell(row=INSTRUCTOR_ROW+1, column=column).value - if instructor: instructor = instructor.lower() + if instructor: + instructor = instructor.lower() if approver: approver = str(approver).lower() @@ -135,32 +199,47 @@ def get_groups_from_report(repn): cls.append(cp) return cls + +# @profile def populate_student_report_results(students): # take students (list-of-dicts in the info format) and assign them the results from the reports. out = get_output_file() + import time + t0 = time.time() print("> Loading student report scores from: %s"%out) if not os.path.exists(out): return students, [] for k in students: students[k]['reports'] = {i: None for i in range(4)} + import pandas as pd - wb = openpyxl.load_workbook(out,data_only=True) + wb = openpyxl.load_workbook(out, data_only=True, read_only=True) # Perhaps find non-empty cols (i.e. those with content) - + print("> time elapsed", time.time() - t0) maximal_groups = [] maximal_groups_students = [] for repn in range(3, -1, -1): cls = [] - for i in range(2, wb.worksheets[repn].max_column + 1): - cp = parse_column(wb.worksheets[repn], report_number=repn, column=i) + sheet = pd.read_excel(out, sheet_name=repn, index_col=None, header=None) + sheet = sheet.fillna('') + sheet = sheet.to_numpy() + # to_numpy() + for i in range(1,sheet.shape[1]): + + # for i in range(2, wb.worksheets[repn].max_column + 1): + # print(i, wb.worksheets[repn].max_column) + # s = pd.read_excel(out, sheet_name=1) + cp = parse_column_numpy(sheet[:,i], report_number=repn, column=i) + + + # cp = parse_column(wb.worksheets[repn], report_number=repn, column=i) if not cp['student_ids']: - continue + break cls.append(cp) for g in cls: - for sid in g['student_ids']: student = students.get(sid, None) if student is None: @@ -172,5 +251,5 @@ def populate_student_report_results(students): if sid not in maximal_groups_students: maximal_groups.append(g) maximal_groups_students += g['student_ids'] - + print("> time elapsed", time.time() -t0) return students, maximal_groups \ No newline at end of file diff --git a/src/coursebox/material/__pycache__/homepage_lectures_exercises.cpython-38.pyc b/src/coursebox/material/__pycache__/homepage_lectures_exercises.cpython-38.pyc index fcdc53b8ce3e88021bd3cba3dda0c41923e0c4a1..6ad3c21eb6a79dbef53f5e24270dca2a8f885fc2 100644 GIT binary patch delta 5770 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 delta 4831 zcmZt}TWlOxb?)pu_GNp$ekYE-c{uji$;QsZX`99sZQ{PulDbe0X*cV=<MrCJJL@}R zJ8{?9v`(?3wlv8Vv?UbTg;FB*BQ#JxK!`6sDg;#GqYMurM1@*`kPs?Ws1(k*yRn_% zUCo(0=Qa17dtUQq@q_W&Cu?fL68sj<?oI#lfs3{6WaMJsNI94fn_(hRiK=!aA2p+5 zt=TpCTC-NHby(M#bz&W~WBGcsUaUiQJl|k8h;`U*%r}`$VjZ!Y^DSnJSVwI=-)gps zHL=^wHd;e#&q?McDw&&Uo!MTdF>?#uLmO!0IbwFuCfW?-PTEgfXe(e6bbz+eO|aTZ zH`8{YZKGRg2i&*Qy|j}ifU<*brP~10MfcI|bO&H|(k{9aFx|A9_Q1V|8nhSg2JNG} z;NDC3)7>-)oO&N6Pe{fA{vk>5Ci!>d1plY}dF)SK%|z{TT`A&4OZoy?;r|JAbhln0 zQ!y`wghgqL&=^(E1xxA?X@OMP<oP<Up5LJucaE3pOLe7Ko75te0wo<+cS|7ulozM< zAP3mxh7}2AK{`(3H*x%%%4X8Y-%}2ePx-$TqkG6}qAd%Ap!BDuhUG}9X=U{ZbjO9> zzwmq26HPHV-z%~&C;NRlM;Sh{7{8{DlMddl9Uvw@u9-t-#Pz1BRq#?d>Ix=9W^v&4 zNwzA3f{h580PKRxRRCy`ESdZh?QF8585!yUTp?yOZ8IxPPp76`tNP+q`6Pc$zi$VM zRNrID#Vp&5(n8NV#vA&Vx8P1pnQge0J=_gG)U*2b)qT4gsXNF&3Lfnf%<Z_#K-I`e zpUiAz=VAV2=>Fd7KDU5`A8;*?b*^so*4-ZFzX)}JOFs%_0?kb04~P3ngqOmtgF-fx zai*DTjXI~ys5kC8j_syK)2?OK7Sqhjc{!($vU9HI>s2a$BRm7@8YBJpZ$cLYY#@@H zDbp-VAVSXWWgTKc?aKaz57B*oL)P+AW0pln)0xRsmO0Z!w~@aX84Sfq5dJYa!QYSc zXorBAwO;!yLKM=<uh;cx&4_8?J+XFjCqEiH*;REwB_O;jgx!f8x7t3%-;WIt^V;XJ zmq?F>^ezND5p*Na0r;9bKJDdfU&%Pd(=5WT#`lp!{IBtCWIxv%j+9ko3ZhdA>Ac0z zZ7f<5v~$#AtOhZ=5z{oDE>PP_73o+i<Jh*9K?fR5>>i{lc?Z}ZL+-}`n9+QC(n?KD z=jP@tmmNj?F$4#Y7$0?<$<!Eg`1=i!_(4GV!L)6oYS$;TCjVQ*Q8LJPH@0>NqrZTX z!pSv4Exvo2S?&nWG|rNVYrk)tBBYr=+FT&_@SisKmmfiaNAXpmiCkeKo$}Jz>txUH zh#~u`J8s#wsb_2}U6?L18+i$W9DwWco?fHoc3*x4$(VmEgJ9Ju>^MF?fnXHDNdUga zwpe**#xm-KwQLM2tDa$B<V!7`B+tL!(gnNyRZF7d2r>*J5Z11pGX>oDc(nCCLiyvZ z|0PHGBW>;5A3{zCfFI5kT#rp>Jck)7D<HmNIe)QjTN%yy`12b#Zd_NA;8_SG#28UF zf{Z2tvGb>JwH7?WkTl?YomoXYow1mRl8A`!ob6f66&^4`75nh~%(!Ref!VWCV>t*C zNUMI2%^*b`!_a~pqHyi?6{MTGXPrtp)82ZE`H4+!<kx&=Q)l@s2xc&-Z~ictLvRJ) za!&#%k@FHIRJNr&IZyKPc^Nt!p#eLPSI+BsbzT$Cimm1K^8q3)N{eJsUJNWMi|V4b zs4oV`LR6*Nx!`$iNh?WIZv&k)_%iBIL(q`I{7UD!az=)i-TdJ$a90g*xg!uWOA33y zla?irCoR$nIU+s1e_mc9OHxT*klpxFkR?6XB|Z^pNiWGX0Dt9W*wK<);UdyhysAS* z9rJ-w0QCHOUMVTdWtnO#giTDDR4*x$8ao3>nFaxOX3F#e6Uwq$QsC+5u=!AnBrqCc zTBLbZJn1EMg}6WT0?W`Tv2TI7N=d~H6`tQfN)y{x7G(B!FHnXT=q1>~1>)WdmV`^n zWr;>g+GXht1zVyQS_T|&fI6|YDDtv9mQ(?Q0MP?AB~S>j4nXq+ZB^?lxF9ouqZYM+ znG(3Z?%15Rr#I=&WXp5)8K*E~LAfqu6P}ZRK!zkXl?-!=P}QwdUdnS)#k4ohrr=cO zLYZ`C+)5TFZ8LN;Bwh9(kVMMN$wP@bC9!)b!Jb9-c;Ytoh3uS`piaTs3W;)3Dudj9 z6vl*DOA=qrxirTrNiV9cs1U2wz}I1)dy9pvq5BbcJk2aBIuFBChn_Z*^;OIRSERie zaOhrBcE+xR%&^#hh50!N-p`)LH@|@3D1t@QP|K_gB-*J|MK@bO3TE57Ch-zHegv0$ z7XS&GO`L=QhDj}n!WD#-EXU!Bl19LUNmOnmtzsRQHF$=<Tvo{3wzU@}@3x&x+IEMM zw{2huUIC-P65NE1$Bdv^BTDbs>7+1;fi)Fl{90G^mSolSmEE61vZ$_TtD#qF%a=rL zM4Kzti;4mvkuPGESvz1MEehw0$cw!~RJcFD!i@ru6otg#AC=p8GLyU8UMb_A*)RYR zY!FIoVZua>eHds)P!#obL7<{o7lNG#M7hMi2^AQd8x!R);1y}5q>m%+wz`f5xQ0Ks zy@`zQSGPaX`?Xtn)*8SWAT&draYYq^oWEAH<7WZ270@RCO?P)kGfLugVAfWfVhY=I zuE1si$)D+I=L0=6gC9c|xFH&~5fFWmokGFW2<}6Gok1iBo5L061}pJD_G}qwgXPCq z_zdDJG=16yJ6n*91YbFArJ1khoq{*+2Y?K7OrP;q>{aCW27=cRg!!M1Tp3FzLz7qp zfoMbhxDsQ>noC7O2{&Vt@^#2GF`bCEEBuXZfL%sn5W>+a&@?l{ViPIB`7NZsfpm4c zScEYKzq>*VJDt(a?%^Nw?rS=VJmSQJFYwqH=WTsQ$udv%9o&RoWmyC(2;M~S9Rwb~ z+Se9YcclEcecMSJ|E%wI@;1M;D?fy$_&Sc3SR^n^d}YGP6?`q{V%o188@>xfLp8%L zjH($c1^gyIyt{wbI`u+Xen!-bSquj85eexT6sgSK;ji!h?8L2!;@6Kmd8?Stt|bY> zaGq;@aCKyG6Jrj8`iC>}T?sY`WHYihQMh6#nv)MDO#YALa9Nz0I6^TxtQ{AcAVvg{ zB!0*$Ov5M%qd;X2od+^x5xUz{Gr0o1R1AU2EIOwH(E$p{hh@rCMr}5FTe|pSmK;dU zTBB&t8FT@PNkoHxJGcP{Yy~P8j@h+P#iFE{u&eyB{(}cR@7e2QUTC~Re03&mPg}R? zXBR-%rFB_$0VEIY`2k=nobRr##HonlauIM}Ung@BWCr*r{Soo?!Zs4Q1R}4k6S>4Y z2L=wffSr?1qzuB)QyWUY0))%!BwxAq!oV`Q`Q6aB@2fBwzPoR%?q1ZTCK8FEgrTq* z-o4){??<KL*u(*@9h=Cq)wTtR1YZauID;sG!<G>fC8N?<euUWf5d0W`uT9RTS=L=E zd7=vWq+lvG2C)Y96z?GoI$;XxQI0nr=-PT)Sl5Y80G|`0#ary#gqsH*DE|ast{)Qt zI5ILaJZz9*Lm3`{`CUCW3<!hZ3Z93JU6o`KcAdoSVX0)IK)i)313?CWsaN|^Wsp|^ z<+rVVAwn}^RuZE#RpciJ)j@Qb*<9^Zwv|n1PN#DDqQfAr#kUnhKbg@gN97jcmj}0P z#{4uJHc$;8U>^?lma!jIz9EvhI)vZ^f@cssjbH)+MQ|2@$T(3iMfrRiaql3wiU8x6 zVL&nrF^18?(1q*bd^K?W;N8dJQ*GL|hR{(ihD<B;MMxbv2#NFmf50DBAiZF8I36^@ UHQ|9^C>RJo6V$_v!9eBzKM`S5I{*Lx diff --git a/src/coursebox/material/homepage_lectures_exercises.py b/src/coursebox/material/homepage_lectures_exercises.py index f3f71f2..0a6b395 100644 --- a/src/coursebox/material/homepage_lectures_exercises.py +++ b/src/coursebox/material/homepage_lectures_exercises.py @@ -3,6 +3,9 @@ import shutil, os, glob from datetime import datetime, timedelta import calendar import pickle +import time +from line_profiler_pycharm import profile +from coursebox.thtools_base import partition_list import slider from jinjafy import jinjafy_comment @@ -16,6 +19,7 @@ from coursebox.core.info import class_information from coursebox.material.lecture_questions import lecture_question_compiler from slider import latexmk import coursebox +# from line_profiler_pycharm import profile def get_feedback_groups(): paths = get_paths() @@ -47,12 +51,13 @@ def get_feedback_groups(): reduced_groups = [rg for rg in reduced_groups if len(rg)>0] # groups are now partitioned. if len(remaining_lectures) > 0: - fbgs = coursebox.thtools_base.partition_list(reduced_groups, len(remaining_lectures)) + fbgs = partition_list(reduced_groups, len(remaining_lectures)) for gg in fbgs: for g in gg: already_used = already_used + g - lst = thtools.thtools_base.partition_list([s for s in all_students if s not in already_used], len(remaining_lectures)) + + lst = partition_list([s for s in all_students if s not in already_used], len(remaining_lectures)) for i in range(len(remaining_lectures)): dg = [] for g in fbgs[i]: dg += g # flatten the list @@ -217,7 +222,49 @@ def compile_simple_files(paths, info, template_file_list, verbose=False): jinjafy_template(data=d2, file_in=fname, file_out=tex_out, filters=get_filters(), template_searchpath=paths['instructor']) latexmk(tex_out, pdf_out= paths['pdf_out'] + "/" + os.path.basename(tex_out)[:-4]+".pdf") -def fix_shared(paths, output_dir, pdf2png=False,dosvg=True,verbose=False, compile_templates=True): +# rec_fix_shared(shared_base=paths['shared'], output_dir=output_dir) +import time +# import dirsync +# dirsync.sync(paths['shared'], output_dir, 'diff') + + +# Do smarter fixin' +from pathlib import Path + +from jinjafy.cache.simplecache import hash_file_ + +@profile +def get_hash_from_base(base): + if not os.path.exists(base + "/sharedcache.pkl"): + source = {} + else: + with open(base + "/sharedcache.pkl", 'rb') as f: + source = pickle.load(f) + + actual_files = {} + for f in glob.glob(base + "/**", recursive=True): + if os.path.isdir(f): + continue + if f.endswith("sharedcache.pkl"): + continue + rel = os.path.relpath(f, base) + + # d = dict(mtime=os.path.getmtime(f)) + actual_files[rel] = dict(mtime=os.path.getmtime(f), hash=-1, modified=False) + + if rel not in source or (actual_files[rel]['mtime'] != source[rel].get('mtime', -1)): # It has been modified, update hash + # print(rel, time.ctime(actual_files[rel]['mtime']), time.ctime(source[rel].get('mtime', -1))) + new_hash = hash_file_(f) + # actual_files[rel] = {} + actual_files[rel]['modified'] = new_hash != source.get(rel, {}).get('hash', -1) + actual_files[rel]['hash'] = new_hash + else: + actual_files[rel]['hash'] = source[rel]['hash'] + return actual_files + + +@profile +def fix_shared(paths, output_dir, pdf2png=False,dosvg=True,verbose=False, compile_templates=True,shallow=True): ''' Copy shared files into lecture directories ''' @@ -225,46 +272,171 @@ def fix_shared(paths, output_dir, pdf2png=False,dosvg=True,verbose=False, compil from jinjafy.cache import cache_contains_file, cache_update_file from slider.convert import svg2pdf, pdfcrop from slider import convert + import filecmp - def rec_fix_shared(shared_base, output_dir): - if dosvg: - for svg in glob.glob(shared_base+"/*.svg"): - if not cache_contains_file(cache_base, svg): - if verbose: - print("converting to pdf", svg) - svg2pdf(svg,crop=True, text_to_path=True) - cache_update_file(cache_base, svg) - files = glob.glob(shared_base+"/*") - for f in files: - if f.endswith("cache.pkl"): - continue - # check if template - if "templates" in f and f.endswith("_partial.tex"): - continue + t0 = time.time() + shared_base = paths['shared'] + output_dir = output_dir - if os.path.isdir(f): - od2 = output_dir + "/" + os.path.basename(f) - if not os.path.exists(od2): - os.mkdir(od2) - rec_fix_shared(f, od2) - else: - of = output_dir + "/" + os.path.basename(f) - if not cache_contains_file(cache_base, f) or not os.path.exists(of): - print(f"> {f} -> {of}") - shutil.copy(f, of) - if f.endswith(".pdf") and pdf2png: + import glob + # def get_cache_from_dir(shared_base): + # print("Beginning file cache..") + + + source = get_hash_from_base(shared_base) + target = get_hash_from_base(output_dir) - if verbose: - print(" converting to png", f) - convert.pdf2png(of) - cache_update_file(cache_base, f) + # update_source_cache = False + source_extra = {} + for rel in source: + if rel.endswith(".svg") and source[rel]['modified']: + pdf_file = svg2pdf(shared_base + "/"+rel, crop=True, text_to_path=True, verbose=True) + rel = os.path.relpath(pdf_file, shared_base) + source_extra[rel] = dict(mtime=os.path.getmtime(pdf_file), hash=hash_file_(pdf_file), modified=True) - if verbose: - print(" done!") + for k, v in source_extra.items(): + source[k] = v + # update_source_cache = True + # Perform sync here. + for rel in source: + if rel.endswith("_partial.tex"): + continue + + if rel not in target or target[rel]['hash'] != source[rel]['hash']: + print(" -> ", output_dir + "/" + rel) + shutil.copy(shared_base +"/" + rel, output_dir + "/" + rel) + target[rel] = source[rel].copy() + target[rel]['modified'] = True + target[rel]['mtime'] = os.path.getmtime(output_dir + "/" + rel) + + if pdf2png: + for rel in target: + if rel.endswith(".pdf") and target[rel]['modified']: + # print("pdf2png: ") + png = convert.pdf2png(output_dir + "/" + rel, verbose=True) + target[rel]['modified'] = False + target[rel]['hash'] = hash_file_(output_dir + "/" + rel) + target[rel]['mtime'] = os.path.getmtime(output_dir + "/" + rel) + + # Save the cache. + + with open(shared_base + "/sharedcache.pkl", 'wb') as f: + pickle.dump(source, f) + + with open(output_dir + "/sharedcache.pkl", 'wb') as f: + pickle.dump(target, f) + + print("fix_shared()", time.time() - t0) + + # + # if pdf2png: + # if f.endswith(".pdf") and pdf2png: + # if verbose: + # print("converting to png", f) + # convert.pdf2png(of) + # + # for f in source: + # if f not in target: + # print(f) + # else: + # if source[f]['hash'] != target[f]['hash']: + # print(f, f) + # + # + # + # a = 234 + # # if rel not in source: + # + # # source[rel] = dict(mtime=os.path.getmtime(f), hash=hash_file_(f)) + # # + # + # + # # Everything has a hash/mtime that is up to date. Now look at target dir + # + # get_cache_from_dir(output_dir) + # + # # Get the corresponding output at destination: + # + # + # + # + # + # + # for path in Path(shared_base).rglob('*'): + # print(path) + # a = 234 + # def rec_fix_shared(shared_base, output_dir): + # if dosvg: + # for svg in glob.glob(shared_base+"/*.svg"): + # # if not os.path.exists(shared_base + ) + # if not cache_contains_file(cache_base, svg): + # # if verbose: + # print("converting to pdf", svg) + # svg2pdf(svg,crop=True, text_to_path=True) + # cache_update_file(cache_base, svg) + # assert False + # + # files = glob.glob(shared_base+"/*") + # for f in files: + # if f.endswith("cache.pkl"): + # continue + # + # if "templates" in f and f.endswith("_partial.tex"): + # continue + # + # if os.path.isdir(f): + # od2 = output_dir + "/" + os.path.basename(f) + # if not os.path.exists(od2): + # os.mkdir(od2) + # rec_fix_shared(f, od2) + # else: + # of = output_dir + "/" + os.path.basename(f) + # if not os.path.exists(of) or not filecmp.cmp(f, of,shallow=shallow): + # print(f"> fix_shared() -> {of}") + # shutil.copy(f, of) + # if f.endswith(".pdf") and pdf2png: + # if verbose: + # print("converting to png", f) + # convert.pdf2png(of) + # # cache_update_file(cache_base, f) + # + # if verbose: + # print(" done!") + + # if pdf2png: + # assert False + + + + # get diff. + + # directory_cmp = filecmp.dircmp(a=paths['shared'], b=output_dir) + # from filecmp import dircmp + # from filecmp import dircmp + # def print_diff_files(dcmp): + # for name in dcmp.diff_files: + # print("diff_file %s found in %s and %s" % (name, dcmp.left, dcmp.right)) + # print("") + # for sub_dcmp in dcmp.subdirs.values(): + # print_diff_files(sub_dcmp) + # + + # t0 = time.time() + # dcmp = dircmp(paths['shared'], output_dir) + # print_diff_files(dcmp) + # print("dircmp", time.time() - t0) + # directory_cmp.report() + # import time + # t0 = time.time() + # rec_fix_shared(shared_base=paths['shared'], output_dir=output_dir) + # import time + # # import dirsync + # # dirsync.sync(paths['shared'], output_dir, 'diff') + # print("mine", time.time() - t0) + a = 234 - rec_fix_shared(shared_base=paths['shared'], output_dir=output_dir) def jinjafy_shared_templates_dir(paths, info): tpd = paths['shared'] + "/templates" @@ -379,6 +551,7 @@ def mvfiles(source_dir, dest_dir): if (os.path.isfile(full_file_name)): shutil.copy(full_file_name, os.path.dirname(dest_dir)) +@profile def make_webpage(dosvg=True): cinfo = class_information() paths = get_paths() -- GitLab