Skip to content
Snippets Groups Projects
datatypes.py 19 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jake's avatar
    Jake committed
    import datetime
    
    Jake's avatar
    Jake committed
    from dateutil import parser as dtparser
    
    
    Jake's avatar
    Jake committed
    import common
    
    
    Jake's avatar
    Jake committed
    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
    
    Jake's avatar
    Jake committed
            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.
    
    Jake's avatar
    Jake committed
            #    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)
    
    
    Jake's avatar
    Jake committed
    class PageFactory:
    
        def __init__(self, config, factories):
            self.config = config
            self.factories = factories
    
    Jake's avatar
    Jake committed
            self.store = {}
    
            for lang in self.config['lang']['supported']:
                self.store[lang] = {}
    
        def has(self, slug, lang):
            return (slug in self.store[lang])
    
    
    Jake's avatar
    Jake committed
        def get(self, slug, lang):
    
            if not self.has(slug, lang):
    
    Jake's avatar
    Jake committed
                self.store[lang][slug] = Page(slug, lang, self.factories)
    
            return self.store[lang][slug];
    
    Jake's avatar
    Jake committed
    
        def all(self):
            return self.store
    
    Jake's avatar
    Jake committed
    class Page:
    
    Jake's avatar
    Jake committed
        def __init__(self, slug, lang, factories):
    
    Jake's avatar
    Jake committed
            self.slug = slug
            self.lang = lang
    
    Jake's avatar
    Jake committed
            self._config = None
    
            self._factories = factories
    
    Jake's avatar
    Jake committed
            self.initialized = False
    
            self.status = "uninitialized"
            self.title = "N/A"
            self._metadata = {}
            self.content = []
    
    Jake's avatar
    Jake committed
    
    
        def init(self, filename, subpath, raw, metadata, content, title, category, date_created, date_modified, authors, last_modification_author, status):
    
    Jake's avatar
    Jake committed
            self.initialized = True
    
    Jake's avatar
    Jake committed
            self.filename =  filename
            self.subpath =  subpath
            self.raw =  raw
    
    Jake's avatar
    Jake committed
            dictconverter = DictConverter(self._factories)
            self._metadata =  dictconverter.convert(metadata, self.lang)
    
    Jake's avatar
    Jake committed
            self.content =  content
            self.title =  title
    
            self.category = self._factories['tag'].get(category, self.lang)
    
    Jake's avatar
    Jake committed
            self.date_created =  Date(date_created, self.config)
            self.date_modified =  Date(date_modified, self.config)
    
    Jake's avatar
    Jake committed
            self.status =  status
    
            # authors
    
    Jake's avatar
    Jake committed
            self.authors = set()
    
    Jake's avatar
    Jake committed
            for local_part, domain, name in authors:
    
    Jake's avatar
    Jake committed
                self.authors.add(self._factories['author'].get(local_part + '@' + domain, name))
    
    Jake's avatar
    Jake committed
            if last_modification_author:
    
                self.last_modification_author = self._factories['author'].get(last_modification_author[0] + '@' + last_modification_author[1], last_modification_author[2])
    
    Jake's avatar
    Jake committed
            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)
    
    Jake's avatar
    Jake committed
    
    
            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)
    
    
    Jake's avatar
    Jake committed
            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)
    
    
    Jake's avatar
    Jake committed
        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)
    
    
    Jake's avatar
    Jake committed
    
    
        def get_metadata(self):
    
    Jake's avatar
    Jake committed
            deflang = self.config['lang']['default']
    
            if self.lang == deflang:
                return self._metadata
            defpage = self._factories['page'].get(self.slug, deflang)
    
    Jake's avatar
    Jake committed
            res = common.combine(defpage.metadata, self._metadata)
    
            return res
        metadata = property(get_metadata, None, None)
    
    Jake's avatar
    Jake committed
    
    
        def get_tags(self):
    
    Jake's avatar
    Jake committed
            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):
    
    Jake's avatar
    Jake committed
                        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']
    
    Jake's avatar
    Jake committed
                if not isinstance(lrel, dict):
                    prio = lrel
                    lrel = {}
                    lrel['prio'] = prio
    
    Jake's avatar
    Jake committed
    
                if 'prio' in lrel:
    
                    res['prio']= lrel['prio']
    
    Jake's avatar
    Jake committed
                else:
    
                    res['prio']= 0
    
    Jake's avatar
    Jake committed
                start = Date("min", self.config)
    
    Jake's avatar
    Jake committed
                if 'start' in lrel:
    
    Jake's avatar
    Jake committed
                    start = Date(lrel['start'], self.config)
    
                res['start'] = start
    
    Jake's avatar
    Jake committed
                end = Date("max", self.config)
    
    Jake's avatar
    Jake committed
                if 'end' in lrel:
    
    Jake's avatar
    Jake committed
                    end = Date(lrel['end'], self.config)
    
                res['end'] = end
    
    Jake's avatar
    Jake committed
                build_date = Date("now", self.config)
    
    Jake's avatar
    Jake committed
                if (build_date > end):
    
                    res['was_relevant'] = True
    
    Jake's avatar
    Jake committed
                elif (build_date < start):
    
                    res['will_be_relevant'] = True
    
    Jake's avatar
    Jake committed
                else:
    
                    res['is_relevant'] = True
            return res
        relevance = property(get_relevance, None, None)
    
    Jake's avatar
    Jake committed
    
    
        def get_template(self):
            if 'template' in self.metadata:
                return self.metadata['template']
            else:
    
    Jake's avatar
    Jake committed
                return self.config['theme']['default_template']
    
        template = property(get_template, None, None)
    
    Jake's avatar
    Jake committed
    
    
    
    Jake's avatar
    Jake committed
        def tags_str_to_list(self, s):
    
    Jake's avatar
    Jake committed
            if not self.initialized:
                raise Exception("Page not initialized.")
    
    
    Jake's avatar
    Jake committed
            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)
    
    Jake's avatar
    Jake committed
            if not self.initialized:
                raise Exception("Page not initialized.")
    
    Jake's avatar
    Jake committed
            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))
    
    Jake's avatar
    Jake committed
            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
    
    Jake's avatar
    Jake committed
            self._config = None
    
            self.pages_tuple = []
    
            self.is_category = False
            self._link = None
            self._factories = factories
    
            self._pages_cache = []
    
    Jake's avatar
    Jake committed
        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):
    
    Jake's avatar
    Jake committed
            if self.page and 'color' in self.page.metadata:
    
                return self.page.metadata['color']
            else:
    
    Jake's avatar
    Jake committed
                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
    
    Jake's avatar
    Jake committed
    
    
    Jake's avatar
    Jake committed
    
    
        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)
    
    
    Jake's avatar
    Jake committed
    class Date:
        def __init__(self, dt, config):
    
    Jake's avatar
    Jake committed
            if isinstance(dt, str):
                if dt == "min":
    
    Jake's avatar
    Jake committed
                    dt = dtparser.parse("1970-01-01")
    
    Jake's avatar
    Jake committed
                elif dt == "max":
    
    Jake's avatar
    Jake committed
                    dt = dtparser.parse("9999-01-01")
    
    Jake's avatar
    Jake committed
                elif dt == "now":
    
    Jake's avatar
    Jake committed
                    dt = datetime.datetime.now()
    
    Jake's avatar
    Jake committed
                else:
                    dt = dtparser.parse(dt)
    
    Jake's avatar
    Jake committed
            if not isinstance(dt, datetime.datetime):
    
    Jake's avatar
    Jake committed
                    dt = datetime.datetime(dt.year, dt.month, dt.day)
    
    Jake's avatar
    Jake committed
            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)
    
    
    Jake's avatar
    Jake committed
        def __lt__(self, other):
            return self.dt.timestamp() < other.dt.timestamp()
    
    
    Jake's avatar
    Jake committed
    class AuthorFactory:
    
        def __init__(self, config, factories):
            self.config = config
            self.factories = factories
    
    Jake's avatar
    Jake committed
            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;
    
    Jake's avatar
    Jake committed
    class Author:
    
        def __init__(self, email):
    
    Jake's avatar
    Jake committed
            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]
    
    Jake's avatar
    Jake committed
            # TODO md5 hash etc.
    
    Jake's avatar
    Jake committed
    
        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):
    
    Jake's avatar
    Jake committed
            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
    
    Jake's avatar
    Jake committed
            self._factories = factories
    
            self.is_external = False
    
    Jake's avatar
    Jake committed
            self.lang = deflang
    
    Jake's avatar
    Jake committed
            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'
    
    Jake's avatar
    Jake committed
                self._url = ""
    
                self.type = "local"
            else:
                components = urlwoa.split(':')
                if len(components) == 1:
                    # rawurl: 'lageplan.uni-goettingen.de'
    
    Jake's avatar
    Jake committed
                    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'
    
    Jake's avatar
    Jake committed
                        self._page = None
                        self._url = None
    
                        self.type = reftype
    
    Jake's avatar
    Jake committed
                        self.lang = reflang
                        self.refid = refid
    
                    elif reftype == "tag":
                        # rawurl: 'tag:some-tag:de'
    
    Jake's avatar
    Jake committed
                        self._tag = None
                        self._url = None
    
                        self.type = reftype
    
    Jake's avatar
    Jake committed
                        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'
    
    Jake's avatar
    Jake committed
                        self._url = urlwoa
    
                        self.is_external = True
    
    
    Jake's avatar
    Jake committed
        def get_page(self):
            if not self._page:
                self._page = self._factories['page'].get(self.refid, self.lang)
    
    Jake's avatar
    Jake committed
                # 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
    
    Jake's avatar
    Jake committed
            return self._page
        page = property(get_page, None, None)
    
    Jake's avatar
    Jake committed
        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)
    
    
    Jake's avatar
    Jake committed
        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")
    
    Jake's avatar
    Jake committed
            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])
    
    Jake's avatar
    Jake committed
            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)
    
    
    
    Jake's avatar
    Jake committed