Browse Source

Initial commit

master
Dylan Baker 5 years ago
commit
9738091e81

+ 2
- 0
.coveragerc View File

@@ -0,0 +1,2 @@
1
+[run]
2
+omit = yird/[a_][p_][pi]*.py

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+*.egg-info
2
+.pytest_cache

+ 0
- 0
LICENSE View File


+ 17
- 0
Pipfile View File

@@ -0,0 +1,17 @@
1
+[[source]]
2
+name = "pypi"
3
+url = "https://pypi.org/simple"
4
+verify_ssl = true
5
+
6
+[dev-packages]
7
+
8
+[packages]
9
+flask = "*"
10
+wtforms = "*"
11
+gitpython = "*"
12
+markdown = "*"
13
+pytest = "*"
14
+pytest-cov = "*"
15
+
16
+[requires]
17
+python_version = "3.7"

+ 223
- 0
Pipfile.lock View File

@@ -0,0 +1,223 @@
1
+{
2
+    "_meta": {
3
+        "hash": {
4
+            "sha256": "c37bdd943d5c3cbb47764bbad66de712f10e5eddea7273ca31240f906d380677"
5
+        },
6
+        "pipfile-spec": 6,
7
+        "requires": {
8
+            "python_version": "3.7"
9
+        },
10
+        "sources": [
11
+            {
12
+                "name": "pypi",
13
+                "url": "https://pypi.org/simple",
14
+                "verify_ssl": true
15
+            }
16
+        ]
17
+    },
18
+    "default": {
19
+        "atomicwrites": {
20
+            "hashes": [
21
+                "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
22
+                "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
23
+            ],
24
+            "version": "==1.2.1"
25
+        },
26
+        "attrs": {
27
+            "hashes": [
28
+                "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
29
+                "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
30
+            ],
31
+            "version": "==18.2.0"
32
+        },
33
+        "click": {
34
+            "hashes": [
35
+                "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
36
+                "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
37
+            ],
38
+            "version": "==7.0"
39
+        },
40
+        "coverage": {
41
+            "hashes": [
42
+                "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f",
43
+                "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe",
44
+                "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d",
45
+                "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0",
46
+                "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607",
47
+                "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d",
48
+                "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b",
49
+                "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3",
50
+                "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e",
51
+                "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815",
52
+                "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36",
53
+                "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1",
54
+                "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14",
55
+                "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c",
56
+                "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794",
57
+                "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b",
58
+                "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840",
59
+                "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd",
60
+                "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82",
61
+                "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952",
62
+                "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389",
63
+                "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f",
64
+                "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4",
65
+                "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da",
66
+                "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647",
67
+                "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d",
68
+                "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42",
69
+                "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478",
70
+                "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b",
71
+                "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb",
72
+                "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9"
73
+            ],
74
+            "version": "==4.5.2"
75
+        },
76
+        "flask": {
77
+            "hashes": [
78
+                "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
79
+                "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
80
+            ],
81
+            "index": "pypi",
82
+            "version": "==1.0.2"
83
+        },
84
+        "gitdb2": {
85
+            "hashes": [
86
+                "sha256:83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2",
87
+                "sha256:e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"
88
+            ],
89
+            "version": "==2.0.5"
90
+        },
91
+        "gitpython": {
92
+            "hashes": [
93
+                "sha256:563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82",
94
+                "sha256:8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8"
95
+            ],
96
+            "index": "pypi",
97
+            "version": "==2.1.11"
98
+        },
99
+        "itsdangerous": {
100
+            "hashes": [
101
+                "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
102
+                "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
103
+            ],
104
+            "version": "==1.1.0"
105
+        },
106
+        "jinja2": {
107
+            "hashes": [
108
+                "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
109
+                "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
110
+            ],
111
+            "version": "==2.10"
112
+        },
113
+        "markdown": {
114
+            "hashes": [
115
+                "sha256:c00429bd503a47ec88d5e30a751e147dcb4c6889663cd3e2ba0afe858e009baa",
116
+                "sha256:d02e0f9b04c500cde6637c11ad7c72671f359b87b9fe924b2383649d8841db7c"
117
+            ],
118
+            "index": "pypi",
119
+            "version": "==3.0.1"
120
+        },
121
+        "markupsafe": {
122
+            "hashes": [
123
+                "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
124
+                "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
125
+                "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
126
+                "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
127
+                "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
128
+                "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
129
+                "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
130
+                "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
131
+                "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
132
+                "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
133
+                "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
134
+                "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
135
+                "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
136
+                "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
137
+                "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
138
+                "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
139
+                "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
140
+                "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
141
+                "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
142
+                "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
143
+                "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
144
+                "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
145
+                "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
146
+                "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
147
+                "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
148
+                "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
149
+                "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
150
+                "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
151
+            ],
152
+            "version": "==1.1.0"
153
+        },
154
+        "more-itertools": {
155
+            "hashes": [
156
+                "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
157
+                "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
158
+                "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
159
+            ],
160
+            "version": "==5.0.0"
161
+        },
162
+        "pluggy": {
163
+            "hashes": [
164
+                "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
165
+                "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
166
+            ],
167
+            "version": "==0.8.0"
168
+        },
169
+        "py": {
170
+            "hashes": [
171
+                "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
172
+                "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
173
+            ],
174
+            "version": "==1.7.0"
175
+        },
176
+        "pytest": {
177
+            "hashes": [
178
+                "sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9",
179
+                "sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9"
180
+            ],
181
+            "index": "pypi",
182
+            "version": "==4.0.2"
183
+        },
184
+        "pytest-cov": {
185
+            "hashes": [
186
+                "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7",
187
+                "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"
188
+            ],
189
+            "index": "pypi",
190
+            "version": "==2.6.0"
191
+        },
192
+        "six": {
193
+            "hashes": [
194
+                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
195
+                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
196
+            ],
197
+            "version": "==1.12.0"
198
+        },
199
+        "smmap2": {
200
+            "hashes": [
201
+                "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde",
202
+                "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"
203
+            ],
204
+            "version": "==2.0.5"
205
+        },
206
+        "werkzeug": {
207
+            "hashes": [
208
+                "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
209
+                "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
210
+            ],
211
+            "version": "==0.14.1"
212
+        },
213
+        "wtforms": {
214
+            "hashes": [
215
+                "sha256:0cdbac3e7f6878086c334aa25dc5a33869a3954e9d1e015130d65a69309b3b61",
216
+                "sha256:e3ee092c827582c50877cdbd49e9ce6d2c5c1f6561f849b3b068c1b8029626f1"
217
+            ],
218
+            "index": "pypi",
219
+            "version": "==2.2.1"
220
+        }
221
+    },
222
+    "develop": {}
223
+}

+ 3
- 0
README.md View File

@@ -0,0 +1,3 @@
1
+# yird
2
+
3
+Yird is for building websites.

+ 3
- 0
setup.py View File

@@ -0,0 +1,3 @@
1
+from setuptools import find_packages, setup
2
+
3
+setup(name="yird", packages=find_packages())

+ 200
- 0
tests/test_posts.py View File

@@ -0,0 +1,200 @@
1
+import datetime
2
+import json
3
+import os
4
+import tempfile
5
+
6
+from yird.settings import Settings
7
+
8
+
9
+def setup():
10
+    # Tears down automatically when variable goes out of scope
11
+    temp_dir = tempfile.TemporaryDirectory()
12
+
13
+    os.environ["YIRD_PATH"] = temp_dir.name
14
+    os.mkdir(os.path.join(temp_dir.name, 'posts'))
15
+    os.mkdir(os.path.join(temp_dir.name, 'templates'))
16
+    os.mkdir(os.path.join(temp_dir.name, 'public'))
17
+    with open(os.path.join(temp_dir.name, 'settings.json'), "w") as f:
18
+        f.write(json.dumps({
19
+            "PUBLIC_PATH": os.path.join(temp_dir.name, 'public')
20
+        }))
21
+
22
+    # Importing PostsService initializes a Settings object which depends on
23
+    # YIRD_PATH already being set, and needs to be reinitialized between tests,
24
+    # so we're doing all that explicitly in the setup
25
+    from yird.services.posts import PostsService
26
+    PostsService.settings = Settings()
27
+
28
+    return temp_dir, PostsService
29
+
30
+
31
+def test_create_post():
32
+    # Setup
33
+    root, PostsService = setup()
34
+
35
+    # Create a post
36
+    post = PostsService.create_post({
37
+        "title": "Test Post",
38
+        "slug": "test-post",
39
+        "date": "2000-01-01",
40
+        "content": "# Lorem ipsum dolor sit amet"
41
+    })
42
+
43
+    # Read metadata and contents files
44
+    with open(os.path.join(root.name, 'posts', post.id + '.json')) as f:
45
+        metadata = json.loads(f.read())
46
+    with open(os.path.join(root.name, 'posts', post.id + '.md')) as f:
47
+        content = f.read()
48
+
49
+    # Assert metadata
50
+    assert metadata == {
51
+        "id": post.id,
52
+        "title": "Test Post",
53
+        "slug": "test-post",
54
+        "date": "2000-01-01",
55
+        "changed": True
56
+    }
57
+
58
+    # Assert content
59
+    assert content == "# Lorem ipsum dolor sit amet"
60
+
61
+
62
+def test_get_post():
63
+    _root, PostsService = setup()
64
+
65
+    # Create a post
66
+    post = PostsService.create_post({
67
+        "title": "Test Post",
68
+        "slug": "test-post",
69
+        "date": "2000-01-01",
70
+        "content": "# Lorem ipsum dolor sit amet"
71
+    })
72
+
73
+    # Get the post
74
+    post = PostsService.get_post(post.id)
75
+
76
+    # Assert post data
77
+    assert post.title == "Test Post"
78
+    assert post.slug == "test-post"
79
+    assert post.date == datetime.date.fromisoformat("2000-01-01")
80
+    assert post.content == "# Lorem ipsum dolor sit amet"
81
+
82
+
83
+def test_get_posts():
84
+    _root, PostsService = setup()
85
+
86
+    # Create a couple of posts
87
+    post1 = PostsService.create_post({
88
+        "title": "Test Post 1",
89
+        "slug": "test-post-1",
90
+        "date": "2000-01-01",
91
+        "content": "# Lorem ipsum dolor sit amet"
92
+    })
93
+    post2 = PostsService.create_post({
94
+        "title": "Test Post 2",
95
+        "slug": "test-post-2",
96
+        "date": "2000-01-02",
97
+        "content": "# Lorem ipsum dolor sit amet"
98
+    })
99
+
100
+    # Get the post and sort by title
101
+    posts = PostsService.get_posts()
102
+    posts = sorted(posts, key=lambda post: post.title)
103
+
104
+    # Assert the post data
105
+    assert posts[0].id == post1.id
106
+    assert posts[0].title == "Test Post 1"
107
+    assert posts[0].slug == "test-post-1"
108
+    assert posts[0].date == datetime.date.fromisoformat("2000-01-01")
109
+    assert posts[0].content == "# Lorem ipsum dolor sit amet"
110
+    assert posts[1].id == post2.id
111
+    assert posts[1].title == "Test Post 2"
112
+    assert posts[1].slug == "test-post-2"
113
+    assert posts[1].date == datetime.date.fromisoformat("2000-01-02")
114
+    assert posts[1].content == "# Lorem ipsum dolor sit amet"
115
+
116
+
117
+def test_generate_posts():
118
+    root, PostsService = setup()
119
+
120
+    # Create post template
121
+    with open(os.path.join(root.name, 'templates', 'post.html'), "w") as f:
122
+        f.write('''
123
+<div><h1>{{ post.title }}</h1><div>{{ post.content }}</div></div>
124
+    ''')
125
+
126
+    # Create post
127
+    post = PostsService.create_post({
128
+        "title": "Test Post",
129
+        "slug": "test-post",
130
+        "date": "2000-01-01",
131
+        "content": "# Lorem ipsum dolor sit amet"
132
+    })
133
+
134
+    # Generate post
135
+    PostsService.generate_posts()
136
+
137
+    # Assert generated HTML
138
+    with open(os.path.join(root.name, 'public', post.id, 'index.html')) as f:
139
+        contents = f.read()
140
+    assert contents == '''
141
+<div><h1>Test Post</h1><div><h1>Lorem ipsum dolor sit amet</h1></div></div>
142
+    '''
143
+
144
+    # Assert that generating the post marks the post unchanged
145
+    with open(os.path.join(root.name, 'posts', post.id + '.json')) as f:
146
+        metadata = json.loads(f.read())
147
+    assert metadata["changed"] is False
148
+
149
+
150
+def test_update_post():
151
+    _root, PostsService = setup()
152
+
153
+    # Create post
154
+    post = PostsService.create_post({
155
+        "title": "Test Post",
156
+        "slug": "test-post",
157
+        "date": "2000-01-01",
158
+        "content": "# Lorem ipsum dolor sit amet"
159
+    })
160
+
161
+    # Update post
162
+    PostsService.update_post(post.id, {
163
+        "id": post.id,
164
+        "title": "Edited Title",
165
+        "slug": "edited-slug",
166
+        "date": "2000-01-01",
167
+        "content": "# Lorem ipsum dolor sit amet",
168
+    })
169
+
170
+    # Load metadata
171
+    metadata = post.load_metadata()
172
+
173
+    # Assert that the title and slug changed
174
+    assert metadata["title"] == "Edited Title"
175
+    assert metadata["slug"] == "edited-slug"
176
+
177
+
178
+def test_get_post_form():
179
+    _root, PostsService = setup()
180
+    form = PostsService.get_post_form()
181
+    assert form.data == {
182
+        "title": None,
183
+        "slug": None,
184
+        "date": datetime.date.today(),
185
+        "content": None
186
+    }
187
+
188
+    post = PostsService.create_post({
189
+        "title": "Test Post",
190
+        "slug": "test-post",
191
+        "date": "2000-01-01",
192
+        "content": "# Lorem ipsum dolor sit amet"
193
+    })
194
+    form = PostsService.get_post_form(post)
195
+    assert form.data == {
196
+        "title": "Test Post",
197
+        "slug": "test-post",
198
+        "date": datetime.date.fromisoformat("2000-01-01"),
199
+        "content": "# Lorem ipsum dolor sit amet"
200
+    }

+ 26
- 0
tests/test_settings.py View File

@@ -0,0 +1,26 @@
1
+import json
2
+import os
3
+import shutil
4
+import tempfile
5
+
6
+import pytest
7
+
8
+from yird.settings import Settings
9
+
10
+
11
+def test_missing_yird_path():
12
+    os.environ["YIRD_PATH"] = ""
13
+    os.environ.pop("YIRD_PATH")
14
+    with pytest.raises(KeyError):
15
+        Settings()
16
+
17
+
18
+def test_reading_yird_path():
19
+    root = tempfile.mkdtemp()
20
+    os.environ["YIRD_PATH"] = root
21
+    with open(os.path.join(root, "settings.json"), "w") as f:
22
+        f.write(json.dumps({
23
+            "PUBLIC_PATH": os.path.join(root, "public")
24
+        }))
25
+    assert Settings().YIRD_PATH == root
26
+    shutil.rmtree(root)

+ 0
- 0
yird/__init__.py View File


+ 48
- 0
yird/app.py View File

@@ -0,0 +1,48 @@
1
+from flask import Flask, redirect, render_template, request, url_for
2
+
3
+from yird.services.posts import PostsService
4
+
5
+from yird.settings import Settings
6
+
7
+app = Flask(__name__)
8
+yird_settings = Settings()
9
+
10
+
11
+@app.route('/admin')
12
+def index():
13
+    posts = PostsService.get_posts()
14
+    return render_template('index.html.j2', posts=posts)
15
+
16
+
17
+@app.route('/admin/posts/new')
18
+def new_post():
19
+    form = PostsService.get_post_form()
20
+    return render_template('posts/form.html.j2', post=False, form=form)
21
+
22
+
23
+@app.route('/admin/posts', methods=["POST"])
24
+def create_post():
25
+    PostsService.create_post(request.form)
26
+    return redirect(url_for('index'))
27
+
28
+
29
+@app.route('/admin/posts/<post_id>')
30
+def edit_post(post_id):
31
+    post = PostsService.get_post(post_id)
32
+    form = PostsService.get_post_form(post)
33
+    return render_template('posts/form.html.j2', post=post, form=form)
34
+
35
+
36
+@app.route('/admin/posts/<post_id>', methods=["POST"])
37
+def update_post(post_id):
38
+    PostsService.update_post(post_id, request.form)
39
+    return redirect(url_for('index'))
40
+
41
+
42
+@app.route('/admin/generate')
43
+def generate():
44
+    PostsService.generate_posts()
45
+    return redirect(url_for('index'))
46
+
47
+if __name__ == "__main__":
48
+    app.run(debug=True)

+ 43
- 0
yird/post.py View File

@@ -0,0 +1,43 @@
1
+import json
2
+from os.path import join
3
+
4
+from wtforms import DateField, Form, StringField, TextAreaField
5
+
6
+from yird.settings import Settings
7
+
8
+
9
+class NewPostForm(Form):
10
+    title = StringField('Title')
11
+    slug = StringField('Slug')
12
+    date = DateField('Date')
13
+    content = TextAreaField('Content')
14
+
15
+
16
+class Post:
17
+    def __init__(self, id, title, slug, date, content='', changed=True):
18
+        self.id = id
19
+        self.title = title
20
+        self.slug = slug
21
+        self.date = date
22
+        self.content = content
23
+        self.changed = changed
24
+        self.settings = Settings()
25
+
26
+    def metadata(self):
27
+        return {
28
+            "id": self.id,
29
+            "title": self.title,
30
+            "slug": self.slug,
31
+            "date": str(self.date),
32
+            "changed": self.changed
33
+        }
34
+
35
+    def load_metadata(self):
36
+        with open(join(self.settings.POSTS_PATH, self.id + '.json')) as f:
37
+            return json.loads(f.read())
38
+
39
+    def write(self):
40
+        with open(join(self.settings.POSTS_PATH, self.id + '.md'), 'w') as f:
41
+            f.write(self.content)
42
+        with open(join(self.settings.POSTS_PATH, self.id + '.json'), 'w') as f:
43
+            f.write(json.dumps(self.metadata()))

+ 116
- 0
yird/services/posts.py View File

@@ -0,0 +1,116 @@
1
+import datetime
2
+import json
3
+import os
4
+import os.path
5
+import re
6
+import shutil
7
+import uuid
8
+
9
+from jinja2 import Environment, FileSystemLoader
10
+
11
+from markdown import markdown
12
+
13
+from yird.post import NewPostForm, Post
14
+
15
+from yird.settings import Settings
16
+
17
+
18
+class PostsService:
19
+    settings = Settings()
20
+
21
+    @classmethod
22
+    def get_post(cls, post_id):
23
+        bare_post_path = os.path.join(cls.settings.POSTS_PATH, post_id)
24
+        with open(bare_post_path + '.json') as f:
25
+            metadata = json.loads(f.read())
26
+        with open(bare_post_path + '.md') as f:
27
+            content = f.read()
28
+        return Post(
29
+            post_id,
30
+            metadata['title'],
31
+            metadata['slug'],
32
+            datetime.date.fromisoformat(metadata['date']),
33
+            content,
34
+            metadata['changed']
35
+        )
36
+
37
+    @classmethod
38
+    def get_posts(cls):
39
+        posts_path = cls.settings.POSTS_PATH
40
+        posts = []
41
+        paths = os.listdir(posts_path)
42
+        paths = [p for p in paths if re.search('.*(\\.json)', p)]
43
+        for path in paths:
44
+            post_id, extension = os.path.splitext(path)
45
+            post = cls.get_post(post_id)
46
+            posts.append(post)
47
+        return sorted(posts, key=lambda post: post.date, reverse=True)
48
+
49
+    @classmethod
50
+    def generate_posts(cls):
51
+        cls.__reset_public_dir()
52
+        posts = cls.get_posts()
53
+        template = cls.__get_post_template()
54
+        for post in posts:
55
+            cls.__generate_post(template, post)
56
+
57
+    @classmethod
58
+    def create_post(cls, post_data):
59
+        id = str(uuid.uuid4())
60
+        post = Post(
61
+            id,
62
+            post_data['title'],
63
+            post_data['slug'],
64
+            datetime.date.fromisoformat(post_data['date']),
65
+            post_data['content'],
66
+        )
67
+        post.write()
68
+        return post
69
+
70
+    @classmethod
71
+    def update_post(cls, post_id, post_data):
72
+        post = Post(
73
+            post_id,
74
+            post_data['title'],
75
+            post_data['slug'],
76
+            datetime.date.fromisoformat(post_data['date']),
77
+            post_data['content'],
78
+        )
79
+        post.write()
80
+
81
+    @classmethod
82
+    def get_post_form(cls, post=None):
83
+        if post is None:
84
+            return NewPostForm(date=datetime.date.today())
85
+        return NewPostForm(
86
+            title=post.title,
87
+            slug=post.slug,
88
+            date=post.date,
89
+            content=post.content
90
+        )
91
+
92
+    @classmethod
93
+    def __generate_post(cls, template, post):
94
+        settings = cls.settings
95
+        output_post_dir = os.path.join(settings.PUBLIC_PATH, post.id)
96
+        metadata_path = os.path.join(settings.POSTS_PATH, post.id + '.json')
97
+        os.mkdir(output_post_dir)
98
+        post.content = markdown(post.content)
99
+        with open(metadata_path) as f:
100
+            metadata = json.loads(f.read())
101
+        metadata['changed'] = False
102
+        with open(metadata_path, "w") as f:
103
+            f.write(json.dumps(metadata))
104
+        with open(os.path.join(output_post_dir, "index.html"), "w") as f:
105
+            f.write(template.render(post=post))
106
+
107
+    @classmethod
108
+    def __reset_public_dir(cls):
109
+        shutil.rmtree(cls.settings.PUBLIC_PATH)
110
+        os.mkdir(cls.settings.PUBLIC_PATH)
111
+
112
+    @classmethod
113
+    def __get_post_template(cls):
114
+        loader = FileSystemLoader(cls.settings.TEMPLATES_PATH)
115
+        env = Environment(loader=loader)
116
+        return env.get_template('post.html')

+ 13
- 0
yird/settings.py View File

@@ -0,0 +1,13 @@
1
+import json
2
+import os
3
+import os.path
4
+
5
+
6
+class Settings:
7
+    def __init__(self):
8
+        self.YIRD_PATH = os.environ['YIRD_PATH']
9
+        self.POSTS_PATH = os.path.join(self.YIRD_PATH, 'posts')
10
+        self.TEMPLATES_PATH = os.path.join(self.YIRD_PATH, 'templates')
11
+        with open(os.path.join(self.YIRD_PATH, 'settings.json')) as f:
12
+            settings_file = json.loads(f.read())
13
+            self.PUBLIC_PATH = settings_file['PUBLIC_PATH']

+ 122
- 0
yird/static/css/style.css View File

@@ -0,0 +1,122 @@
1
+/* global */
2
+
3
+* {
4
+	box-sizing: border-box;
5
+	margin: 0;
6
+	padding: 0;
7
+}
8
+
9
+ul {
10
+	list-style: none;
11
+}
12
+
13
+a {
14
+	text-decoration: none;
15
+}
16
+
17
+.heading {
18
+	border-bottom: 1px solid #000000;
19
+	font-size: 24px;
20
+	margin-bottom: 15px;
21
+}
22
+
23
+/* layout */
24
+
25
+.container {
26
+  display: flex;
27
+}
28
+
29
+.main {
30
+	display: flex;
31
+	flex: 1;
32
+	justify-content: center;
33
+	padding: 20px;
34
+}
35
+
36
+.main__container {
37
+	width: 768px;
38
+}
39
+
40
+/* sidebar */
41
+.sidebar {
42
+  background: #496893;
43
+	color: #ffffff;
44
+	min-height: 100vh;
45
+	padding: 20px 0;
46
+	width: 150px;
47
+}
48
+
49
+.sidebar__title {
50
+	color: #ffffff;
51
+	font-size: 24px;
52
+}
53
+
54
+.sidebar__title,
55
+.nav__item {
56
+	padding: 5px 10px;
57
+}
58
+
59
+.nav__item:hover {
60
+	background: #ffffff;
61
+}
62
+
63
+.nav__item:hover a {
64
+	color: #496893;
65
+}
66
+
67
+.nav__link {
68
+	color: white;
69
+}
70
+
71
+.nav__link:hover {
72
+	text-decoration: underline;
73
+}
74
+
75
+/* forms */
76
+
77
+.form__field {
78
+	display: flex;
79
+	flex-direction: column;
80
+	margin: 5px 0;
81
+	width: 100%;
82
+}
83
+
84
+.form__field,
85
+.form__label {
86
+	margin-bottom: 5px;
87
+}
88
+
89
+.form__date-field,
90
+.form__textarea,
91
+.form__text-field {
92
+	border: 1px solid #e8e8e8;
93
+	font-family: serif;
94
+	font-size: 16px;
95
+	padding: 5px;
96
+}
97
+
98
+.form__textarea {
99
+	resize: vertical;
100
+}
101
+
102
+.form__submit {
103
+	border: 1px solid #e8e8e8;
104
+	padding: 5px 10px;
105
+}
106
+
107
+/* post listing */
108
+.post-listing__item {
109
+	position: relative;
110
+}
111
+
112
+.post-listing__item--changed:before {
113
+	background-color: red;
114
+	border-radius: 50%;
115
+	content: '';
116
+	display: block;
117
+	height: 10px;
118
+	left: -15px;
119
+	position: absolute;
120
+	top: 3px;
121
+	width: 10px;
122
+}

+ 8
- 0
yird/static/js/index.js View File

@@ -0,0 +1,8 @@
1
+// document.querySelector('[data-generate-button]').addEventListener('click',
2
+//   function (e) {
3
+//     var r = confirm('Are you sure? This will overwrite any existing content.');
4
+//     if (!r) {
5
+//       e.preventDefault();
6
+//     }
7
+//   }
8
+// );

+ 14
- 0
yird/templates/index.html.j2 View File

@@ -0,0 +1,14 @@
1
+{% extends 'layout.html.j2' %}
2
+{% block content %}
3
+  <ul class="post-listing">
4
+    <h3 class="heading">all posts</h3>
5
+    {% for post in posts %}
6
+      <li class="post-listing__item {% if post.changed %}post-listing__item--changed{% endif %}">
7
+        <span>{{ post.date }}</span>
8
+        <a href="{{ url_for('edit_post', post_id = post.id) }}">
9
+          {{ post.title }}
10
+         </a>
11
+      </li>
12
+    {% endfor %}
13
+  </ul>
14
+{% endblock %}

+ 39
- 0
yird/templates/layout.html.j2 View File

@@ -0,0 +1,39 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+  <meta charset="UTF-8">
5
+  <meta name="viewport" content="width=device-width">
6
+  <link rel= "stylesheet" type= "text/css" href= "{{ url_for('static', filename='css/style.css') }}">
7
+  <title>My Website</title>
8
+</head>
9
+<body>
10
+  <div class="container">
11
+    <aside class="sidebar">
12
+      <h3 class="sidebar__title">yird</h3>
13
+      <ul class="nav">
14
+     		<li class="nav__item">
15
+					<a class="nav__link" href="{{ url_for('index') }}">
16
+						Posts
17
+          </a>
18
+        </li>
19
+        <li class="nav__item">
20
+          <a class="nav__link" href="{{ url_for('new_post') }}">
21
+            New Post
22
+          </a>
23
+        </li>
24
+        <li class="nav__item">
25
+          <a class="nav__link" href="{{ url_for('generate') }}" data-generate-button>
26
+            Generate
27
+          </a>
28
+        </li>
29
+      </ul>
30
+    </aside>
31
+    <main class="main">
32
+      <div class="main__container">
33
+        {% block content %}{% endblock %}
34
+      </div>
35
+    </main>
36
+  </div>
37
+  <script src="{{ url_for('static', filename='js/index.js') }}"></script>
38
+</body>
39
+</html>

+ 28
- 0
yird/templates/posts/form.html.j2 View File

@@ -0,0 +1,28 @@
1
+{% extends 'layout.html.j2' %}
2
+{% block content %}
3
+  {% set action = url_for('update_post', post_id = post.id) if post else url_for('create_post') %}
4
+  <form method="POST" action="{{ action }}" class="form">
5
+    <h3 class="heading">
6
+      {{ "edit post" if post else "new post" }}
7
+    </h3>
8
+    <div class="form__field">
9
+      <label class="form__label">{{ form.title.label }}</label>
10
+      {{ form.title(class="form__text-field") }}
11
+    </div>
12
+    <div class="form__field">
13
+      <label class="form__label">{{ form.slug.label }}</label>
14
+      {{ form.slug(class="form__text-field") }}
15
+    </div>
16
+    <div class="form__field">
17
+      <label class="form__label">{{ form.date.label }}</label>
18
+      {{ form.date(class="form__date-field") }}
19
+    </div>
20
+    <div class="form__field">
21
+      <label class="form__label">{{ form.content.label }}</label>
22
+      {{ form.content(class="form__textarea", rows=10) }}
23
+    </div>
24
+    <div class="form__field">
25
+      <input type="submit" value="Submit" class="form__submit">
26
+    </div>
27
+  </form>
28
+{% endblock %}

Loading…
Cancel
Save