import datetime from dateutil import parser as dtparser import common class DictConverter: def __init__(self, factories): self.factories = factories def convert(self, item, lang, key = None): if isinstance(item, dict): res = {} for k in item.keys(): res[k] = self.convert(item[k], lang, k) return res elif isinstance(item, list): res = [] for i in item: res.append(self.convert(i, lang)) return res # Replace specific string keywords with their referenced objects elif isinstance(item, str) and key == "link": return self.factories['link'].get_by_raw(item, lang) elif isinstance(item, str) and key == "tag": return self.factories['tag'].get(item, lang) elif isinstance(item, str) and key == "file": return self.factories['file'].get(item) elif isinstance(item, str) and key == "page": return self.factories['page'].get(item, lang) #elif isinstance(item, str) and key == "slug": # Doesn't work because 'slug' is used to override the slug of a page in its metadata. # return self.factories['page'].get(item, lang) else: return item class LocalizedConfigFactory(DictConverter): def __init__(self, config, factories): super().__init__(factories) self.config = config self.store = {} def get(self, lang): if lang not in self.store: self.store[lang] = self.convert(self.config, lang, None) return self.store[lang] class FileFactory: def __init__(self, config, factories): self.config = config self.factories = factories self.store = {} def get(self, filename): if filename not in self.store: self.store[filename] = File(filename, self.config, self.factories) return self.store[filename]; def all(self): return self.store class File: def __init__(self, filename, config, factories): self.name = filename self._config = config self._factories = factories self.initialized = False def init(self, rawcontents, subpathlist): if self.initialized: raise Exception("Tried initializing file twice. Perhaps two files with same name exist? ", self.name) self.initialized = True self.rawcontents = rawcontents self.subpathlist = subpathlist self.link = self._factories['link'].get_by_type("file", self.name) class PageFactory: def __init__(self, config, factories): self.config = config self.factories = factories self.store = {} for lang in self.config['lang']['supported']: self.store[lang] = {} def has(self, slug, lang): return (slug in self.store[lang]) def get(self, slug, lang): if not self.has(slug, lang): self.store[lang][slug] = Page(slug, lang, self.factories) return self.store[lang][slug]; def all(self): return self.store class Page: def __init__(self, slug, lang, factories): self.slug = slug self.lang = lang self._config = None self._factories = factories self.initialized = False self.status = "uninitialized" self.title = "N/A" self._metadata = {} self.content = [] def init(self, filename, subpath, raw, metadata, content, title, category, date_created, date_modified, authors, last_modification_author, status): self.initialized = True self.filename = filename self.subpath = subpath self.raw = raw dictconverter = DictConverter(self._factories) self._metadata = dictconverter.convert(metadata, self.lang) self.content = content self.title = title self.category = self._factories['tag'].get(category, self.lang) self.date_created = Date(date_created, self.config) self.date_modified = Date(date_modified, self.config) self.status = status # authors self.authors = set() for local_part, domain, name in authors: self.authors.add(self._factories['author'].get(local_part + '@' + domain, name)) if last_modification_author: self.last_modification_author = self._factories['author'].get(last_modification_author[0] + '@' + last_modification_author[1], last_modification_author[2]) else: self.last_modification_author = None #print(authors) #print(last_modification_author) # category self.category.reg_page(self, True) # url #self.url = self.lang + '/' + self.category.name + '/' + self.slug + ".html" self.link = self._factories['link'].get_by_type("slug", self.slug, self.lang) gitlabsettings = self._config["gitlab"] raw_edit_url = gitlabsettings["edit_url"]["prefix"] raw_edit_url += '/'.join(self.subpath) if len(self.subpath): raw_edit_url += '/' raw_edit_url += self.filename raw_edit_url += gitlabsettings["edit_url"]["suffix"] self.edit_url = self._factories['link'].get_by_raw(raw_edit_url, self.lang) raw_view_url = gitlabsettings["view_url"]["prefix"] raw_view_url += '/'.join(self.subpath) if len(self.subpath): raw_view_url += '/' raw_view_url += self.filename raw_view_url += gitlabsettings["view_url"]["suffix"] self.view_url = self._factories['link'].get_by_raw(raw_view_url, self.lang) raw_history_url = gitlabsettings["history_url"]["prefix"] raw_history_url += '/'.join(self.subpath) if len(self.subpath): raw_history_url += '/' raw_history_url += self.filename raw_history_url += gitlabsettings["history_url"]["suffix"] self.history_url = self._factories['link'].get_by_raw(raw_history_url, self.lang) def get_config(self): if not self._config: self._config = self._factories['config'].get(self.lang) return self._config config = property(get_config, None, None) def get_metadata(self): deflang = self.config['lang']['default'] if self.lang == deflang: return self._metadata defpage = self._factories['page'].get(self.slug, deflang) res = common.combine(defpage.metadata, self._metadata) return res metadata = property(get_metadata, None, None) def get_tags(self): if not self.initialized: raise Exception("Page not initialized.") res = [] if 'tags' in self.metadata: rawtags = self.tags_str_to_list(self.metadata['tags']) for t in rawtags: if (t != self.category.name): tag = self._factories['tag'].get(t, self.lang) tag.reg_page(self) res.append(tag) res.append(self.category) # the category is also a default tag return res tags = property(get_tags, None, None) def get_relevance(self): res = {} res['is_relevant'] = False res['was_relevant'] = False res['will_be_relevant'] = False if 'relevant' in self.metadata: lrel = self.metadata['relevant'] if not isinstance(lrel, dict): prio = lrel lrel = {} lrel['prio'] = prio if 'prio' in lrel: res['prio']= lrel['prio'] else: res['prio']= 0 start = Date("min", self.config) if 'start' in lrel: start = Date(lrel['start'], self.config) res['start'] = start end = Date("max", self.config) if 'end' in lrel: end = Date(lrel['end'], self.config) res['end'] = end build_date = Date("now", self.config) if (build_date > end): res['was_relevant'] = True elif (build_date < start): res['will_be_relevant'] = True else: res['is_relevant'] = True return res relevance = property(get_relevance, None, None) def get_template(self): if 'template' in self.metadata: return self.metadata['template'] else: return self.config['theme']['default_template'] template = property(get_template, None, None) def tags_str_to_list(self, s): if not self.initialized: raise Exception("Page not initialized.") res = [] l = s.split(',') for t in l: t = t.lower() t = t.lstrip() t = t.rstrip() if len(t) > 0: res.append(t) return res def get_prio(self): if 'prio' in self.metadata: return self.metadata['prio'] else: return 0 prio = property(get_prio, None, None) def __lt__(self, other): # for sorting by date modified (default) if not self.initialized: raise Exception("Page not initialized.") return self.date_modified < other.date_modified class TagFactory: def __init__(self, config, factories): self.config = config self.factories = factories self.store = {} for lang in self.config['lang']['supported']: self.store[lang] = {} def get(self, name, lang): if not isinstance(name, str): raise Exception("name is not a string", name, type(name)) if lang == None: raise Exception("cannot get None lang for tag", name) if name not in self.store[lang]: self.store[lang][name] = Tag(name, lang, self.factories) return self.store[lang][name]; def all(self): return self.store class Tag: def __init__(self, name, lang, factories): self.name = name self.lang = lang self._config = None self.pages_tuple = [] self.is_category = False self._link = None self._factories = factories self._pages_cache = [] def get_config(self): if not self._config: self._config = self._factories['config'].get(self.lang) return self._config config = property(get_config, None, None) def get_link(self): if not self._link: self._link = self._factories['link'].get_by_type("tag", self.name, self.lang) return self._link link = property(get_link, None, None) def get_page(self): if self._factories['page'].has('tag-' + self.name, self.lang): return self._factories['page'].get('tag-' + self.name, self.lang) else: return None page = property(get_page, None, None) def get_color(self): if self.page and 'color' in self.page.metadata: return self.page.metadata['color'] else: return self.config['theme']['default_tag_color'] color = property(get_color, None, None) def get_title(self): if self.page: return self.page.title else: return self.name title = property(get_title, None, None) def reg_page(self, page, is_category = False): if page == None: return if page.status != "published": return if page.lang != self.lang: raise Exception("Page has different lang than Tag.", page.lang, tag.lang) if page not in self.pages: self.pages_tuple.append((page.prio, page)) self.pages_tuple.sort(key=lambda x: x[0]) self.pages_tuple.reverse() if is_category: self.is_category = is_category def get_pages(self): if len(self._pages_cache) != len(self.pages_tuple): res = [] for prio, page in self.pages_tuple: res.append(page) self._pages_cache = res return self._pages_cache pages = property(get_pages, None, None) class Date: def __init__(self, dt, config): if isinstance(dt, str): if dt == "min": dt = dtparser.parse("1970-01-01") elif dt == "max": dt = dtparser.parse("9999-01-01") elif dt == "now": dt = datetime.datetime.now() else: dt = dtparser.parse(dt) if not isinstance(dt, datetime.datetime): dt = datetime.datetime(dt.year, dt.month, dt.day) self.dt = dt #self.config = config # TODO various formats def isoformat(self): return self.dt.isoformat() def strftime(self, formatstr): # https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes return self.dt.strftime(formatstr) def __lt__(self, other): return self.dt.timestamp() < other.dt.timestamp() class AuthorFactory: def __init__(self, config, factories): self.config = config self.factories = factories self.store = {} def get(self, email, name = None): if email not in self.store: self.store[email] = Author(email) author = self.store[email] author.reg_name(name) return author; class Author: def __init__(self, email): self.email = email self.names = set() split = email.split('@') # TODO proper email validation if len(split) != 2: raise Exception("Invalid email address: ", email) self.email_local_part = split[0] self.email_domain = split[1] # TODO md5 hash etc. def reg_name(self, name): if name == None: return self.names.add(name) class LinkFactory: def __init__(self, config, factories): self.config = config self.factories = factories self.store = {} def get_by_raw(self, rawurl, deflang): link = Link(rawurl, self.factories, deflang) # this is dirty lang = link.lang if lang == None: raise Exception("Link lang is None", lang, deflang, link.raw) if lang not in self.store: self.store[lang] = {} if rawurl not in self.store[lang]: self.store[lang][rawurl] = link return self.store[lang][rawurl]; def get_by_type(self, reftype, refid, reflang = None): rawurl = reftype + ':' + refid if reflang: rawurl += ':' + reflang else: reflang = self.config['lang']['default'] return self.get_by_raw(rawurl=rawurl, deflang=reflang) class Link: def __init__(self, rawurl, factories, deflang): self.raw = rawurl self._factories = factories self.is_external = False self.lang = deflang self.type = None self.alias = None urlasplit = rawurl.split('#') self.anchor = None if len(urlasplit) == 2: self.anchor = urlasplit[1] urlwoa = urlasplit[0] # url without anchor if len(urlwoa) == 0: # rawurl: '#some-section' self._url = "" self.type = "local" else: components = urlwoa.split(':') if len(components) == 1: # rawurl: 'lageplan.uni-goettingen.de' self._url = "https://" + urlwoa self.is_external = True else: reftype = components[0] refid = components[1] reflang = deflang if len(components) > 2: reflang = components[2] if reftype == "slug": # rawurl: 'slug:some-page:de' self._page = None self._url = None self.type = reftype self.lang = reflang self.refid = refid elif reftype == "tag": # rawurl: 'tag:some-tag:de' self._tag = None self._url = None self.type = reftype self.lang = reflang self.refid = refid elif reftype == "file": # rawurl: 'file:logo.png' self._file = None self._url = None self.type = reftype self.lang = reflang self.refid = refid else: # rawurl: 'https://example.com' self._url = urlwoa self.is_external = True def get_page(self): if not self._page: self._page = self._factories['page'].get(self.refid, self.lang) # handle link/alias pages if 'link' in self._page.metadata: self.alias = self._page.metadata['link'] if self.alias.type == "slug": self.alias.get_page() # make sure that alias knows if it is an alias return self._page page = property(get_page, None, None) def get_tag(self): if not self._tag: self._tag = self._factories['tag'].get(self.refid, self.lang) return self._tag tag = property(get_tag, None, None) def get_file(self): if not self._file: self._file = self._factories['file'].get(self.refid) return self._file file = property(get_file, None, None) def get_category(self): if self.type == "slug": return self.page.category elif self.type == "tag": return self.tag #if self.tag.is_category: # return self.tag #else: # raise Exception("category can only be called on category tags") else: raise Exception("category is not allowed to be called here") category = property(get_category, None, None) def get_path(self): if self.type == "slug": return '/'.join([self.page.lang, self.page.category.name, self.page.slug + '.html']) elif self.type == "tag": return '/'.join([self.tag.lang, 'tag', self.tag.name + '.html']) elif self.type == "file": return '/'.join(['file', self.file.name]) else: raise Exception("path is not allowed to be called here", self.type) path = property(get_path, None, None) def get_url(self): if self._url == None: self._url = self.path return self._url url = property(get_url, None, None) def get_urlwithanchor(self): if self.anchor: return self.url + "#" + self.anchor else: return self.url urlwithanchor = property(get_urlwithanchor, None, None) def get_rel(self): """ Get the relation in the context of the rel html attribute for this link or None if no relation is known. https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel """ if self.type != "slug": return None if "rel" not in self.page.metadata: return None return self.page.metadata["rel"] rel = property(get_rel, None, None)