diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..bccec67 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "MD003": false, + "MD013": false, + "MD014": false +} \ No newline at end of file diff --git a/README.md b/README.md index e478431..b17193e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # grnx -A short (unfinished) static blog generator. \ No newline at end of file +A short (unfinished) static blog generator. + +Ideas: + +* Fetch the author through Git repository: + * by using [Dulwich](https://github.com/dulwich/dulwich), + * or [GitPython](https://github.com/gitpython-developers/GitPython). diff --git a/grnx/__init__.py b/grnx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/grnx/editor.py b/grnx/editor.py new file mode 100644 index 0000000..970aa5e --- /dev/null +++ b/grnx/editor.py @@ -0,0 +1,16 @@ +import os +from os.path import join + +from bottle import Bottle, run + + + +app = Bottle() + + +run(app, host='localhost', port=5000) + +if __name__ == "__main__": + for root, dirs, files in os.walk('articles'): + for file in files: + print(os.path.join(root)) diff --git a/grnx/grnx.py b/grnx/grnx.py new file mode 100644 index 0000000..1d4da06 --- /dev/null +++ b/grnx/grnx.py @@ -0,0 +1,17 @@ +# coding: utf-8 + +from grnx.models import Site + +from jinja2 import Environment, PackageLoader, select_autoescape +env = Environment( + loader=PackageLoader('grnx', 'templates'), + autoescape=select_autoescape(['html']) +) + +if __name__ == "__main__": + root = Site('content') + root.serialize() + + template = env.get_template('single.html') + for article in root.articles: + print(template.render(article.__dict__)) diff --git a/grnx/models.py b/grnx/models.py new file mode 100644 index 0000000..6e26f00 --- /dev/null +++ b/grnx/models.py @@ -0,0 +1,178 @@ +# coding: utf-8 + +import datetime +import json +import re +import os +import logging + +from slugify import slugify + +logger = logging.getLogger(__name__) + + +RE_HEADER = re.compile('---((.)?(\n)?)*---') + +date_handler = lambda obj: ( + obj.isoformat() + if isinstance(obj, datetime.datetime) + or isinstance(obj, datetime.date) + else None +) + +def json_handler(obj): + """Handles the JSON object serializer. + + Returns: + The iso format if the object is a date. + The __dict__ attribute for any other JSON serializable object. + + Excepts: + TypeError when object is not JSON serialisable. + """ + + if hasattr(obj, 'isoformat'): + return obj.isoformat() + elif obj.__dict__: + return obj.__dict__ + else: + raise TypeError('Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj))) + + +def split(file_path): + return os.path.normpath(file_path).split(os.sep) + + +class Article(object): + + def __init__(self, path, content, publication_date=None): + self.path = path + self.content = content + + split_path = split(path) + + self.filename = split_path[-1] # the last element + self.filename_without_extension = os.path.splitext(self.filename)[0] + self.publication_date = get_date_from_article_filename(self.filename) + + self.slug = self.filename_without_extension + + if self.publication_date: + self.slug = self.slug.replace( + self.publication_date.strftime('%Y-%m-%d'), '' + ) + + if self.slug and self.slug.startswith('-'): + self.slug = self.slug[1:] + + self.slug = slugify(self.slug) + + try: + self.category = split_path[1] # + self.keywords = split_path[2:-1] + except IndexError: + self.category = '' + self.keywords = [] + + try: + self.title = content.splitlines()[0] + except IndexError: + self.title = self.filename + + def __str__(self): + """Returns the title and the publication date of the article.""" + return '{} ({})'.format(self.title, self.publication_date) + + +def get_date_from_article_filename(article_filename): + """Get the date from a file name. + + Args: + article_filename (str): the file name of the article. + + Example: + 2018-02-01-firewatch.md -> 1st of February 2018 + 2017-02-03-divinity-origin-sin.md -> None + lynis.md -> None + """ + values = article_filename[0:10] + + date = None + try: + date = datetime.datetime.strptime(values, '%Y-%m-%d').date() + except (ValueError, TypeError): + logger.warn('No publication date found %s', article_filename) + + return date + + +class Site(object): + """Represents a Site object. + + Args: + root_path (path): The path where articles are stored. + articles (array): contain all articles. + """ + + def __init__(self, root_path='articles'): + self.articles = [] + self.categories = {} + self.keywords = {} + + for root, *_, files in os.walk(root_path): + for file in [file for file in files if file.endswith(".md")]: + try: + self.build_article(root, file) + except UnicodeDecodeError: + continue + + def build_article(self, filepath, filename): + """Build a new article from an existing file. + + The newly built article is added to the property `self.articles`. + + Args: + root_path (str): the path where the file is stored. + filename (str): the filename of the file. + + Returns: + None if no publication date can be built. + An `grnx.models.Article` instance instead. + """ + + article_file_path = os.path.join(filepath, filename) + + article = None + with open(article_file_path, encoding="utf8") as f: + content = f.read() + + article = Article(article_file_path, content) + + if article: + logger.warn('article found in %s: %s', article.category, article) + + self.articles.append(article) + + if article.category not in self.categories: + self.categories[article.category] = [] + + self.categories[article.category].append(article) + + return article + + def to_json(self): + """Serialize the content of the current structure to JSON format.""" + + json_dumps = json.dumps( + self, + default=json_handler, + sort_keys=True, + indent=4 + ) + return json_dumps + + def serialize(self): + """Serialize the current files structure to index.json""" + + with open('index.json', 'w') as json_serialized_file: + json_serialized_file.write(self.to_json()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5eb38fc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +bottle +pylint==2.1.1 +clize==4.0.3 +pytest==3.7.2 +pytest-cov==2.5.1 +jinja==2.10 +python-slugify==1.2.5 +markdown==2.6.11 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7ea244f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[flake8] +max-line-length=100 + +[tool:pytest] +addopts = -ra -q \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..2ac8f21 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,9 @@ + + + + + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..5d754d5 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,30 @@ + + + + + + Cryptocurrency Pricing Application + + + +
+

Cryptocurrency Pricing

+
+
+
+

{{ index }}

+
+
+

$ {{ result.USD }}

+
+
+

€ {{ result.EUR }}

+
+
+
+
+ + + + + diff --git a/templates/list.html b/templates/list.html new file mode 100644 index 0000000..fd96fc6 --- /dev/null +++ b/templates/list.html @@ -0,0 +1,6 @@ +{% extends "base.html" } + +{% block content %} + I'm a list. + +{% endblock %} \ No newline at end of file diff --git a/templates/single.html b/templates/single.html new file mode 100644 index 0000000..76f7c3c --- /dev/null +++ b/templates/single.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} + I'm an article. I'm called {{ title }}. I was published at {{ published_date }} + +{% endblock %} \ No newline at end of file diff --git a/templates/vueApp.js b/templates/vueApp.js new file mode 100644 index 0000000..6753a8d --- /dev/null +++ b/templates/vueApp.js @@ -0,0 +1,16 @@ +const vm = new Vue({ + el: '#app', + data: { + results: { + "BTC": { + "USD":3759.91, + "EUR":3166.21 + }, + "ETH": { + "USD":281.7, + "EUR":236.25 + }, + "NEW Currency":{"USD":5.60,"EUR":4.70} + } + } + }); \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration.py b/tests/integration.py new file mode 100644 index 0000000..f4bf0b3 --- /dev/null +++ b/tests/integration.py @@ -0,0 +1,4 @@ +from grnx.models import Site + +if __name__ == "__main__": + site = Site() \ No newline at end of file diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..dcd10dd --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,63 @@ +"""Checks structure associated with articles.""" + +from datetime import date + +from grnx.models import get_date_from_article_filename, split +from grnx.models import Article + + +def test_split_path(): + assert split('articles/sys/2018-01-01 Scaleway Review.md') == ['articles', 'sys', '2018-01-01 Scaleway Review.md'] + + +def test_filename(): + article = Article("2019-01-01-blabla.md", "") + assert "2019-01-01-blabla" == article.filename_without_extension + assert "2019-01-01-blabla.md" == article.filename + +def test_article_match_date(): + """Checks that a date can be extracted from a filename.""" + + assert get_date_from_article_filename('2018-09-01-test.md') == date(2018, 9, 1) + assert get_date_from_article_filename('2017-02-30-divinity-origin-sin.md') == None + assert get_date_from_article_filename('lynis.md') == None + assert get_date_from_article_filename('2018-01-01 scaleway-review.md') == date(2018, 1, 1) + + +def test_article_slug(): + """Check the article slug is well built.""" + + assert "scaleway-review" == Article('articles/sys/2018-01-01 Scaleway Review.md', '').slug + + +def test_article_category(): + """Asserts that the category of an article is found, based on the filepath.""" + + assert "home" == Article('articles/home/2019-01-01-blabla.md', "").category + + assert "dev" == Article('articles/dev/python/django/2019-01-01-blabla.md', "").category + + assert '' == Article('articles', "").category + + +def test_article_keywords(): + """Asserts that the keywords of an article are found, based on the filepath.""" + + article = Article('articles/dev/python/django/2019-01-01-blabla.md', "") + + assert "python" in article.keywords + assert "django" in article.keywords + assert "dev" not in article.keywords + + article = Article('articles/dev/2019-01-01-blabla.md', "") + + assert len(article.keywords) == 0 + + +def test_article_properties(): + article = Article('articles/dev/python/django/2019-01-01-blabla.md', "") + + assert article.filename == '2019-01-01-blabla.md' + assert article.filename_without_extension == '2019-01-01-blabla' + assert article.slug == 'blabla' + assert article.publication_date == date(2019, 1, 1) \ No newline at end of file