diff --git a/dist/coursebox-0.0.1-py3-none-any.whl b/dist/coursebox-0.0.1-py3-none-any.whl
deleted file mode 100644
index df9e2089096cf6ca1f2aad32e45273460a2bb899..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.0.1-py3-none-any.whl and /dev/null differ
diff --git a/dist/coursebox-0.0.1.tar.gz b/dist/coursebox-0.0.1.tar.gz
deleted file mode 100644
index 2c018231141ae11aac06c6a010e6ef364541a0f1..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.0.1.tar.gz and /dev/null differ
diff --git a/dist/coursebox-0.0.2-py3-none-any.whl b/dist/coursebox-0.0.2-py3-none-any.whl
deleted file mode 100644
index 73492399eb7b283cb88d5d5c1c8781d0607f2e65..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.0.2-py3-none-any.whl and /dev/null differ
diff --git a/dist/coursebox-0.0.2.tar.gz b/dist/coursebox-0.0.2.tar.gz
deleted file mode 100644
index 8dbe79a08ee6b4ce82d6c9a21cfbd5b36bcb0f43..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.0.2.tar.gz and /dev/null differ
diff --git a/dist/coursebox-0.1.0-py3-none-any.whl b/dist/coursebox-0.1.0-py3-none-any.whl
deleted file mode 100644
index c7d235d2d3f861f5e3d65d255e7334c6862a2443..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.1.0-py3-none-any.whl and /dev/null differ
diff --git a/dist/coursebox-0.1.0.tar.gz b/dist/coursebox-0.1.0.tar.gz
deleted file mode 100644
index 7466bf0b7a234245dca79cec66204d7936caf2b7..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.1.0.tar.gz and /dev/null differ
diff --git a/dist/coursebox-0.1.1-py3-none-any.whl b/dist/coursebox-0.1.1-py3-none-any.whl
deleted file mode 100644
index c1c51a932f4e6547b3bd219ac400208debf80fb3..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.1.1-py3-none-any.whl and /dev/null differ
diff --git a/dist/coursebox-0.1.1.tar.gz b/dist/coursebox-0.1.1.tar.gz
deleted file mode 100644
index a2a4823916f9326ba40da26002f67b9cdee37df0..0000000000000000000000000000000000000000
Binary files a/dist/coursebox-0.1.1.tar.gz and /dev/null differ
diff --git a/setup.py b/setup.py
index e1f037a05a2a5d04708b4ae4051344bc11e2afb6..ef0bd7193b74d48b24dc179a256649a75077a388 100644
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
 # beamer-slider
 setuptools.setup(
     name="coursebox",
-    version="0.1.17.11",
+    version="0.1.18.0",
     author="Tue Herlau",
     author_email="tuhe@dtu.dk",
     description="A course management system currently used at DTU",
diff --git a/src/coursebox.egg-info/PKG-INFO b/src/coursebox.egg-info/PKG-INFO
index 1918ac3784c110b740384e5b40514db1c0690b3b..9a8c93d4ce89a230ecf22b5a8e35d95000ef8566 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.17.11
+Version: 0.1.17.12
 Summary: A course management system currently used at DTU
 Home-page: https://lab.compute.dtu.dk/tuhe/coursebox
 Author: Tue Herlau
diff --git a/src/coursebox/core/info.py b/src/coursebox/core/info.py
index 31cf5cda811131e1c9df23b303118305967f1deb..5addb236bfa10febd2c67144b963ed9ba2943d3b 100644
--- a/src/coursebox/core/info.py
+++ b/src/coursebox/core/info.py
@@ -1,22 +1,20 @@
+import glob
+import os
+import copy
+import re
+import pickle
 from datetime import timedelta
 from datetime import datetime
 import coursebox
-# import thtools
 from coursebox.thtools_base import list_dict2dict_list
-# import jinjafy
 import openpyxl
 from coursebox.core.projects_info import populate_student_report_results
 from coursebox.core.info_paths import get_paths, semester_id, semester, year, today
 from coursebox.core.info_paths import core_conf
-# import six
-# 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):
+def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=False, columns=None):
     # print("Loading...", xlsx_file, sheet, as_dict_list)
     t0 = time.time()
     wb = openpyxl.load_workbook(xlsx_file, data_only=True, read_only=True)
@@ -28,46 +26,19 @@ def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=False):
             return None
         else:
             ws = ws.pop()
-    # 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))
 
+    if columns is not None:
+        I = [a in columns for a in A[0,:] ]
 
-    # 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, :]
+        a = A[:, I]
+        a = a[(a != None).all(axis=1),:]
+        A = a
+    else:
+        A = A[:, A[0] != None]
+        A = A[(A != None).sum(axis=1) > 0, :]
 
     dd2 = []
     for i in range(1, A.shape[0]):
@@ -77,16 +48,7 @@ def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=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():
@@ -95,7 +57,6 @@ def xlsx_to_dicts(xlsx_file,sheet=None, as_dict_list=False):
             dl[k] = x
         dd = dl
     wb.close()
-    # print("xlsx2dicts", time.time()-t0)
     return dd
 
 def get_enrolled_students():
@@ -306,10 +267,25 @@ def get_forum(paths):
     return d2
 
 # @profile
-def class_information(verbose=False):
+def class_information(verbose=False,
+                      update_with_core_conf=False, # Whether to include (module) level core-conf items. Nearly always yes, but core is likely to include classes that are not easily pickled. so when 0xxxxprivate is excluded, this should be False.
+                      ):
+    paths = get_paths()
+    if not os.path.isfile(paths['information.xlsx']):
+        print("Tried loading", paths['information.xlsx'])
+        cf = _info_cache_file()
+        print("Information configuration file not found. Loading from cache:", cf)
+        if not os.path.isfile(cf):
+            raise Exception("No configuration found. Please set up configuration file at: " + paths['information.xlsx'])
+        else:
+            with open(cf, 'rb') as f:
+                print("Loaded cached configuration from", cf)
+                info = pickle.load(f)
+                info = _update_with_core_conf(info)
+            return info
+
     course_number = core_conf['course_number']
     piazza = 'https://piazza.com/dtu.dk/%s%s/%s' % (semester().lower(), year(), course_number)
-    paths = get_paths()
     teachers = xlsx_to_dicts(paths['information.xlsx'], sheet='teachers')
     students, all_groups = populate_student_report_results( get_enrolled_students(), verbose=verbose)
     continuing_education_mode = core_conf['continuing_education_mode']
@@ -318,9 +294,8 @@ def class_information(verbose=False):
     d = {'year': year(),
          'piazza': piazza, # deprecated.
          'course_number': course_number,
+         'exam': list_dict2dict_list(xlsx_to_dicts(paths['information.xlsx'], sheet='exam')),
          'semester': semester(),
-         # '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(),
@@ -338,16 +313,17 @@ def class_information(verbose=False):
 
     d['written_exam'] = written_exam
 
+    kv = xlsx_to_dicts(paths['information.xlsx'], sheet='general_information', as_dict_list=False, columns=("key", "value") )
+    kvs = {}
+    for k in kv:
+        kvs[k['key']] = k['value']
+
     gi = xlsx_to_dicts(paths['information.xlsx'], sheet='general_information', as_dict_list=True)
-    for (k, v) in zip(gi['key'], gi['value']):
-        if v == 'True':
-            v = True
-        if v == 'False':
-            v= False
-        gi[k] = v
     del gi['key']
     del gi['value']
 
+    gi = {**gi, **kvs}
+
     from snipper.load_citations import get_bibtex, get_aux
     if "pensum_bib" in gi:
         bibtex = get_bibtex(paths['02450public'] + "/" + gi['pensum_bib'])
@@ -384,9 +360,6 @@ def class_information(verbose=False):
         freeze = freeze[0] if isinstance(freeze, list) else freeze
         gi[k] = freeze
 
-    for k,v in core_conf.items():
-        d[k] = v
-
     d['CE2'] = gi.get("days", 5) == 2 if continuing_education_mode else False
     d['CE5'] = gi.get("days", 5) == 5 if continuing_education_mode else False
 
@@ -399,10 +372,6 @@ def class_information(verbose=False):
     d['teams'] = xlsx_to_dicts(paths['information.xlsx'], sheet='teams')
     fix_instructor_comma(d['teams'], d['instructors'])
 
-    if 'reports_delta' in d:
-        print(234)
-
-
 
     if 'handin_day_delta' in d:
         d['reports_info'] = {}
@@ -413,20 +382,8 @@ def class_information(verbose=False):
             nd = d['lectures'][r-1]['date'] + timedelta(days=int(d['handin_day_delta']))
             ri['date'] = nd
             ri['html'] = f"{nd.day} {nd.strftime('%b')}"
-            # ab = 'st'
-            # if nd.day == 2:
-            #     ab = "nd"
-            # elif nd.day == 3:
-            #     ab = 'rd'
-            # elif nd.day >= 4:
-            #     ab = 'th'
-            # latex_short, latex_long = date2format(nd)
-
-            # ri['latex_long'] = latex_long # f"{nd.strftime('%A')} {nd.day}{ab} {nd.strftime('%B')}, {nd.year}"
-            # ri['latex_short'] = latex_short # f"{nd.strftime('%B')} {nd.day}{ab}, {nd.year}"
-            ri = {**ri, **date2format(nd)}
-
 
+            ri = {**ri, **date2format(nd)}
 
             d['reports_info'][k] = ri
 
@@ -449,15 +406,7 @@ def class_information(verbose=False):
 
         ri['date'] = nd
         ri['html'] = f"{nd.day} {nd.strftime('%b')}"
-        # ab = 'st'
-        # if nd.day == 2:
-        #     ab = "nd"
-        # elif nd.day == 3:
-        #     ab = 'rd'
-        # elif nd.day >= 4:
-        #     ab = 'th'
-        # ri['latex_long'] = f"{nd.strftime('%A')} {nd.day}{ab} {nd.strftime('%B')}, {nd.year}"
-        # ri['latex_short'] = f"{nd.strftime('%B')} {nd.day}{ab}, {nd.year}"
+
         ri = {**ri, **date2format(nd)}
         # d['reports_info'][k] = ri
 
@@ -474,10 +423,69 @@ def class_information(verbose=False):
                 r['handout'] = get_lecture_date(r['handout'], delta_days=0)
                 r['exercises'] = [e.strip() for e in r['exercises'].split(",") if len(e.strip()) > 0]
 
-
     ice = xlsx_to_dicts(paths['information.xlsx'], sheet='ce', as_dict_list=True)
+
+    d['release_rules'] = {}
+
+    for l in d['lectures']:
+        n = l['number']
+        date = l['date']
+
+        dd = timedelta(days=l['show_solutions_after'])
+        d['release_rules'][str(n)] = dict(start=date+dd, end=date+timedelta(days=2000))
+
+    if update_with_core_conf:
+        d = _update_with_core_conf(d)
+
+    return d
+
+def _update_with_core_conf(d):
+    for k,v in core_conf.items():
+        d[k] = v
     return d
 
+
+
+def _info_cache_file():
+    paths = get_paths()
+    f = glob.glob(paths['02450public'] + "/src/*_box").pop()
+    return f + f"/cache/{semester_id()}.pkl"
+
+
+def _save_info_cache():
+    """ Save a cached version of info.
+    """
+    paths = get_paths()
+    if not os.path.isfile(paths['information.xlsx']):
+        print("Tried saving cache file from installation without information.xlsx file. Exiting without saving...")
+        return
+
+    d = class_information(update_with_core_conf=False)
+    cdir = _info_cache_file()
+    if not os.path.isdir(os.path.dirname(cdir)):
+        os.makedirs(os.path.dirname(cdir))
+    known = {}
+
+    def _remove_ids(d):
+        if isinstance(d, str):
+            # v = "asdf s123456 safdasfd"
+            o = re.findall(r'(s\d{6})', d)
+            for id in o:
+                if id not in known:
+                    known[id] = f"s{len(known):6d}".replace(" ", "0")
+                d = d.replace(id, known[id])
+        elif isinstance(d, dict):
+            for k, v in d.items():
+                d[_remove_ids(k)] = _remove_ids(v)
+        elif isinstance(d, list):
+            d = [_remove_ids(k) for k in d]
+        elif isinstance(d, tuple):
+            d = tuple(_remove_ids(k) for k in d)
+        return d
+    dd = _remove_ids(copy.deepcopy(d))
+    with open(cdir, 'wb') as f:
+        pickle.dump(dd, f)
+
 def fix_instructor_comma(dd, instructors):
     for r in dd:
         ri_shortnames = [i.strip().lower() for i in r['instructors'].split(",")]
diff --git a/src/coursebox/core/info_paths.py b/src/coursebox/core/info_paths.py
index 96668314afaca3e7f944f659aab649a932edc1df..dec085b143bec235e4baf439f7353eaf0497ada1 100644
--- a/src/coursebox/core/info_paths.py
+++ b/src/coursebox/core/info_paths.py
@@ -22,13 +22,13 @@ def get_paths():
     root_02450public = root_02450public.replace("\\", "/")
     root_02450private = root_02450private.replace("\\", "/")
 
-    if not os.path.isdir(root_02450private):
-        root_02450private = f'{root_02450public}/{num}private'
-        warn('Private repository not found at the expected location.')
-        warn('Using mock info from resources folder at:')
-        warn(root_02450private)
-        # Tue: always overwrite semester path.
-        # semester_path = root_02450private +"/resources/mock_semesters/" + semester_id()
+    # if not os.path.isdir(root_02450private):
+    #     root_02450private = f'{root_02450public}/{num}private'
+    #     warn('Private repository not found at the expected location.')
+    #     warn('Using mock info from resources folder at:')
+    #     warn(root_02450private)
+    # Tue: always overwrite semester path.
+    # semester_path = root_02450private +"/resources/mock_semesters/" + semester_id()
     # else:
     semester_path = root_02450private + "/semesters/" + semester_id()
 
@@ -36,10 +36,16 @@ def get_paths():
         os.makedirs(semester_path)
 
     main_conf = semester_path + "/" + semester_id() + ".xlsx"
+
     if not os.path.exists(main_conf):
         main_conf = f"{semester_path}/{course_number}_{semester_id()}.xlsx"
     if not os.path.exists(main_conf):
-        raise Exception("Main config file not found " + main_conf)
+        # cf = _info_cache_file()
+        # print("Information configuration file not found. Loading from cache:", cf)
+        # if not os.path.isfile(cf):
+        #     raise Exception("No configuration found. Please set up configuration file at: " + paths['information.xlsx'])
+        pass
+        # raise Exception("Main config file not found " + main_conf)
 
     _files = []
     sCE = "CE" if core_conf['continuing_education_mode'] else ""
@@ -48,27 +54,27 @@ def get_paths():
         # 'docs':
         # 'docs':
         '02450private': root_02450private,
-            '02450public': root_02450public,
-            '02450instructors': root_02450instructors,
-            '02450students': root_02450students,
-            'shared': root_02450public+"/shared",
-            'exams': root_02450private+"/Exam",
-            'course_number': course_number,
-            'semester': semester_path,
-            'information.xlsx': main_conf,
-            'homepage_template': "%s/WEB/index_partial.html"%root_02450public,
-            'homepage_out': "%s/WEB/%sindex.html"%(root_02450public, sCE),
-            'pdf_out': "%s/%spdf_out"%(root_02450public, sCE),
-            'instructor': root_02450public + "/Exercises",
-            'shared_latex_compilation_dir': root_02450public + "/Exercises/LatexCompilationDir/Latex",
-            'book': root_02450public + "/MLBOOK/Latex",
-            'lectures': root_02450public + "/Lectures",
-            'instructor_project_evaluations': "%s/project_evaluations_%s" % (root_02450instructors, semester_id()),
-            'project_evaluations_template.xlsx': root_02450private +"/ReportEvaluation/%s_project_template.xlsx"%num,
-            'collected_project_evaluations.xlsx': semester_path + "/"+course_number+"_project_" + semester_id() + ".xlsx",
-            'electronic_exam_handin_dir': semester_path + "/exam/electronic_handin",
-            'exam_results_template.xlsx': root_02450private +"/Exam/%s_results_TEMPLATE.xlsx"%num,
-            'exam_instructions': root_02450public + "/ExamInstructions",
+        '02450public': root_02450public,
+        '02450instructors': root_02450instructors,
+        '02450students': root_02450students,
+        'shared': root_02450public+"/shared",
+        'exams': root_02450private+"/Exam",
+        'course_number': course_number,
+        'semester': semester_path,
+        'information.xlsx': main_conf,
+        'homepage_template': "%s/WEB/index_partial.html"%root_02450public,
+        'homepage_out': "%s/WEB/%sindex.html"%(root_02450public, sCE),
+        'pdf_out': "%s/%spdf_out"%(root_02450public, sCE),
+        'instructor': root_02450public + "/Exercises",
+        'shared_latex_compilation_dir': root_02450public + "/Exercises/LatexCompilationDir/Latex",
+        'book': root_02450public + "/MLBOOK/Latex",
+        'lectures': root_02450public + "/Lectures",
+        'instructor_project_evaluations': "%s/project_evaluations_%s" % (root_02450instructors, semester_id()),
+        'project_evaluations_template.xlsx': root_02450private +"/ReportEvaluation/%s_project_template.xlsx"%num,
+        'collected_project_evaluations.xlsx': semester_path + "/"+course_number+"_project_" + semester_id() + ".xlsx",
+        'electronic_exam_handin_dir': semester_path + "/exam/electronic_handin",
+        'exam_results_template.xlsx': root_02450private +"/Exam/%s_results_TEMPLATE.xlsx"%num,
+        'exam_instructions': root_02450public + "/ExamInstructions",
     }
     if os.path.exists(os.path.dirname(paths['instructor_project_evaluations'])):
         if not os.path.isdir(paths['instructor_project_evaluations']):
diff --git a/src/coursebox/material/homepage_lectures_exercises.py b/src/coursebox/material/homepage_lectures_exercises.py
index 0b83c37ce3dd39f672f83edc3a38243fa40fed4c..c6e122de68e90e3f79aa0565f2f33c5fa6364782 100644
--- a/src/coursebox/material/homepage_lectures_exercises.py
+++ b/src/coursebox/material/homepage_lectures_exercises.py
@@ -291,10 +291,8 @@ def fix_shared(paths, output_dir, pdf2png=False,dosvg=True,verbose=False, compil
     # 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)
-
     # update_source_cache = False
     source_extra = {}
     for rel in source:
@@ -338,6 +336,7 @@ def fix_shared(paths, output_dir, pdf2png=False,dosvg=True,verbose=False, compil
         pickle.dump(target, f)
 
 
+
 def jinjafy_shared_templates_dir(paths, info):
     tpd = paths['shared'] + "/templates"
     for f in glob.glob(tpd + "/*.*"):