├── .gitignore ├── LICENSE ├── README.md ├── peregrine ├── __init__.py ├── apps.py ├── fixtures │ └── initial_site.json ├── management │ └── commands │ │ └── peregrine_initial_site.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20171216_1408.py │ ├── 0003_auto_20171217_1706.py │ ├── 0004_auto_20180105_1315.py │ ├── 0005_auto_20180418_1637.py │ ├── 0006_auto_20180418_1652.py │ ├── 0007_auto_20180523_1908.py │ ├── 0008_peregrinesettings_revisions_to_keep.py │ ├── 0009_auto_20211105_1634.py │ └── __init__.py ├── models.py ├── static │ └── peregrine │ │ ├── peregrine-style.css │ │ ├── peregrine-style.min.css │ │ └── peregrine-style.min.css.gz ├── templates │ ├── peregrine │ │ ├── __init__.py │ │ ├── base.html │ │ ├── site_page.html │ │ ├── site_post.html │ │ ├── site_post_list.html │ │ └── tags │ │ │ ├── top_menu.html │ │ │ ├── top_menu_children.html │ │ │ └── top_menu_posts.html │ ├── wagtailcontentstream │ │ └── blocks │ │ │ ├── captioned_image.html │ │ │ └── heading.html │ └── wagtailembeds │ │ └── embed_frontend.html ├── templatetags │ ├── __init__.py │ └── peregrine.py ├── urls.py ├── views.py └── wagtail_hooks.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # manage.py, always default to dev settings 2 | manage.py 3 | 4 | # Django media files 5 | media/* 6 | 7 | # Python bytecode: 8 | *.py[co] 9 | 10 | # Packaging files: 11 | *.egg* 12 | dist/* 13 | 14 | # Sphinx docs: 15 | build 16 | 17 | # SQLite3 database files: 18 | *.db 19 | 20 | # Logs: 21 | *.log 22 | 23 | # Eclipse 24 | .project 25 | 26 | # Linux Editors 27 | *~ 28 | \#*\# 29 | /.emacs.desktop 30 | /.emacs.desktop.lock 31 | .elc 32 | auto-save-list 33 | tramp 34 | .\#* 35 | *.swp 36 | *.swo 37 | 38 | # Mac 39 | .DS_Store 40 | ._* 41 | 42 | # Windows 43 | Thumbs.db 44 | Desktop.ini 45 | 46 | # Dev tools 47 | .idea 48 | .vagrant 49 | 50 | # Ignore local configurations 51 | wrds/settings/local.py 52 | db.sqlite3 53 | 54 | # Node modules 55 | node_modules/ 56 | 57 | # Bower components 58 | bower_components/ 59 | 60 | # Coverage 61 | htmlcov/ 62 | .coverage 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Timothy Allen and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peregrine 2 | 3 | Peregrine is an opinionated blogging platform which uses [the Wagtail CMS](https://wagtail.io) on the [Django web framework](https://www.djangoproject.com) and is geared for coding blogs. It uses Wagtail's fantastic [StreamField feature](http://docs.wagtail.io/en/v1.13/topics/streamfield.html) to provide fully structured content body element blocks, completely separating content from the presentation layer (CSS, JS, and HTML). 4 | 5 | It uses [Wagtail Code Block](https://github.com/FlipperPA/wagtailcodeblock) with [PrismJS](https://prismjs.com/) syntax highlighting under the hood for beautiful code snippets. You can see this in action at [PyPhilly.org](https://pyphilly.org/open-source-projects/getting-started-five-minute-install/). 6 | 7 | **Are you looking for a more robust system, for example, creating a marketing site? The maintainer of Peregrine also contributes to [CodeRedCMS](https://github.com/coderedcorp/coderedcms), which offers a lot more bells and whistles. Peregrine will remain a basic weblog solutions focused on features to display code.** 8 | 9 | ## Getting Started: the Five Minute Install 10 | 11 | These instructions will be fleshed out, but if you want to give it a try, here are the basics. 12 | 13 | ### System 14 | 15 | ```shell 16 | mkvirtualenv my_blog 17 | pip install peregrine 18 | django-admin startproject my_blog 19 | cd my_blog 20 | ``` 21 | 22 | ### Settings 23 | 24 | Your settings file will be located in `my_blog/settings.py` if you're using the default Django project layout created by the `startproject` command above. You'll need to add the sections beneath `INSTALLED_APPS` and `MIDDLEWARE`, and add `'wagtail.contrib.settings.context_processors.settings',` to your `TEMPLATES` context processors in your settings to look like this. 25 | 26 | ```python 27 | INSTALLED_APPS = [ 28 | ... 29 | ] 30 | 31 | PEREGRINE_APPS = [ 32 | "peregrine", 33 | "bootstrap4", 34 | "wagtailcodeblock", 35 | "wagtailcontentstream", 36 | 37 | "wagtail.contrib.forms", 38 | "wagtail.contrib.redirects", 39 | "wagtail.embeds", 40 | "wagtail.sites", 41 | "wagtail.users", 42 | "wagtail.snippets", 43 | "wagtail.documents", 44 | "wagtail.images", 45 | "wagtail.search", 46 | "wagtail.admin", 47 | "wagtail.core", 48 | "taggit", 49 | "modelcluster", 50 | 51 | "wagtail.contrib.settings", 52 | "wagtail.contrib.modeladmin", 53 | "wagtail.contrib.table_block", 54 | ] 55 | 56 | 57 | INSTALLED_APPS += PEREGRINE_APPS 58 | 59 | MIDDLEWARE = [ 60 | ... 61 | ] 62 | 63 | PEREGRINE_MIDDLEWARE = [ 64 | "wagtail.contrib.redirects.middleware.RedirectMiddleware", 65 | ] 66 | 67 | MIDDLEWARE += PEREGRINE_MIDDLEWARE 68 | 69 | # WAGTAIL_SITE_NAME is used by Wagtail; others are used by OpenGraph. 70 | WAGTAIL_SITE_NAME = "PyPhilly: Home of FlipperPA" 71 | WAGTAIL_SITE_DESCRIPTION = "PyPhilly is the website of Tim Allen, a web developer living in Philadelphia. Tim has a wide range of interests, but mostly writes about Python, Django, and virtual reality." 72 | WAGTAIL_SITE_URL = "https://PyPhilly.org/" 73 | 74 | TEMPLATES = [ 75 | { 76 | ... 77 | 78 | 'OPTIONS': { 79 | 'context_processors': [ 80 | ... 81 | 82 | 'wagtail.contrib.settings.context_processors.settings', 83 | ] 84 | } 85 | } 86 | ] 87 | 88 | ``` 89 | 90 | ### URLs 91 | 92 | ```python 93 | from django.contrib import admin 94 | from django.urls import include, path, re_path 95 | 96 | from wagtail.admin import urls as wagtailadmin_urls 97 | from wagtail.documents import urls as wagtaildocs_urls 98 | 99 | urlpatterns = [ 100 | path('admin/', admin.site.urls), 101 | 102 | # Wagtail / Peregrine URLs 103 | path('documents/', include(wagtaildocs_urls)), 104 | path('cms/', include(wagtailadmin_urls)), 105 | re_path(r'^', include('peregrine.urls')), 106 | ] 107 | ``` 108 | 109 | ### Fire it up! 110 | 111 | After you've set up your settings, we need to create your database and a superuser. Issue the following commands from your project root directory. 112 | 113 | *Only run the command `peregrine_initial_site` if you are running on a new project, as it loads database fixtures!* 114 | 115 | 116 | ```shell 117 | python manage.py migrate 118 | # ** Be sure to see the note above before running this next command. It isn't necessary if you don't want to. ** 119 | python manage.py peregrine_initial_site 120 | python manage.py createsuperuser 121 | python manage.py runserver 0:8000 122 | ``` 123 | 124 | You should then be able to navigate to http://localhost:8000/cms/ and log in, and start creating! 125 | 126 | ## Maintainer 127 | 128 | * Timothy Allen (https://github.com/FlipperPA/) 129 | 130 | ## Contributors 131 | 132 | * Jon Banafato (https://github.com/jonafato/) 133 | * Dave Bauer (https://github.com/tdxdave) 134 | * Rana Fayez (https://github.com/Tagine/) 135 | * Jeff Triplett (https://github.com/jefftriplett/) 136 | -------------------------------------------------------------------------------- /peregrine/__init__.py: -------------------------------------------------------------------------------- 1 | APPS = [ 2 | "wagtail.contrib.settings", 3 | ] 4 | -------------------------------------------------------------------------------- /peregrine/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PeregrineConfig(AppConfig): 5 | name = "peregrine" 6 | default_auto_field = "django.db.models.BigAutoField" 7 | -------------------------------------------------------------------------------- /peregrine/fixtures/initial_site.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "wagtailcore.site", 4 | "pk": 1, 5 | "fields": { 6 | "hostname": "localhost", 7 | "port": 80, 8 | "site_name": null, 9 | "root_page": 3, 10 | "is_default_site": true 11 | } 12 | }, 13 | { 14 | "model": "wagtailcore.page", 15 | "pk": 1, 16 | "fields": { 17 | "path": "0001", 18 | "depth": 1, 19 | "numchild": 1, 20 | "title": "Root", 21 | "draft_title": "Root", 22 | "slug": "root", 23 | "content_type": 1, 24 | "live": true, 25 | "has_unpublished_changes": false, 26 | "url_path": "/", 27 | "owner": null, 28 | "seo_title": "", 29 | "show_in_menus": false, 30 | "search_description": "", 31 | "go_live_at": null, 32 | "expire_at": null, 33 | "expired": false, 34 | "locked": false, 35 | "first_published_at": null, 36 | "last_published_at": null, 37 | "latest_revision_created_at": null, 38 | "live_revision": null 39 | } 40 | }, 41 | { 42 | "model": "wagtailcore.page", 43 | "pk": 3, 44 | "fields": { 45 | "path": "00010002", 46 | "depth": 2, 47 | "numchild": 1, 48 | "title": "Peregrine: a blogging platform.", 49 | "draft_title": "Peregrine: a blogging platform.", 50 | "slug": "peregrine-blogging-platform", 51 | "content_type": 25, 52 | "live": true, 53 | "has_unpublished_changes": false, 54 | "url_path": "/peregrine-blogging-platform/", 55 | "owner": 1, 56 | "seo_title": "", 57 | "show_in_menus": true, 58 | "search_description": "", 59 | "go_live_at": null, 60 | "expire_at": null, 61 | "expired": false, 62 | "locked": false, 63 | "first_published_at": "2017-10-21T23:38:40.187Z", 64 | "last_published_at": "2017-10-21T23:38:40.187Z", 65 | "latest_revision_created_at": "2017-10-21T23:38:40.108Z", 66 | "live_revision": 1 67 | } 68 | }, 69 | { 70 | "model": "wagtailcore.page", 71 | "pk": 4, 72 | "fields": { 73 | "path": "000100020001", 74 | "depth": 3, 75 | "numchild": 0, 76 | "title": "Getting Started", 77 | "draft_title": "Getting Started", 78 | "slug": "getting-started-five-minute-install", 79 | "content_type": 25, 80 | "live": true, 81 | "has_unpublished_changes": false, 82 | "url_path": "/peregrine-blogging-platform/getting-started-five-minute-install/", 83 | "owner": 1, 84 | "seo_title": "", 85 | "show_in_menus": true, 86 | "search_description": "", 87 | "go_live_at": null, 88 | "expire_at": null, 89 | "expired": false, 90 | "locked": false, 91 | "first_published_at": "2017-10-22T15:52:20.625Z", 92 | "last_published_at": "2017-10-22T15:57:26.004Z", 93 | "latest_revision_created_at": "2017-10-22T15:57:25.915Z", 94 | "live_revision": 3 95 | } 96 | }, 97 | { 98 | "model": "wagtailcore.pagerevision", 99 | "pk": 1, 100 | "fields": { 101 | "page": 3, 102 | "submitted_for_moderation": false, 103 | "created_at": "2017-10-21T23:38:40.108Z", 104 | "user": 1, 105 | "content_json": "{\"pk\": 3, \"path\": \"00010002\", \"owner\": 1, \"expired\": false, \"slug\": \"peregrine-blogging-platform\", \"first_published_at\": null, \"locked\": false, \"search_description\": \"\", \"excerpt\": \"

Peregrine is an opinionated blogging platform which uses the Wagtail CMS on the Django web framework. It uses Wagtail's fantastic StreamField feature to provide fully structured content body element blocks, completely separating content from the presentation layer (CSS, JS, and HTML).

\", \"last_published_at\": null, \"body\": \"[{\\\"value\\\": \\\"Built on Wagtail.\\\", \\\"id\\\": \\\"27fc57ed-ee72-43d8-8c28-198b66d2406f\\\", \\\"type\\\": \\\"heading\\\"}, {\\\"value\\\": \\\"

Wagtail provides a beautiful CMS experience.

\\\", \\\"id\\\": \\\"ae315cb3-5929-479f-a252-2e3c874b060c\\\", \\\"type\\\": \\\"paragraph\\\"}]\", \"show_in_menus\": true, \"url_path\": \"/peregrine-blogging-platform/\", \"live\": true, \"latest_revision_created_at\": null, \"draft_title\": \"Peregrine: a blogging platform.\", \"expire_at\": null, \"seo_title\": \"\", \"title\": \"Peregrine: a blogging platform.\", \"header_image\": null, \"live_revision\": null, \"go_live_at\": null, \"content_type\": 25, \"has_unpublished_changes\": false, \"depth\": 2, \"numchild\": 0}", 106 | "approved_go_live_at": null 107 | } 108 | }, 109 | { 110 | "model": "wagtailcore.pagerevision", 111 | "pk": 2, 112 | "fields": { 113 | "page": 4, 114 | "submitted_for_moderation": false, 115 | "created_at": "2017-10-22T15:52:20.547Z", 116 | "user": 1, 117 | "content_json": "{\"depth\": 3, \"owner\": 1, \"locked\": false, \"live_revision\": null, \"latest_revision_created_at\": null, \"search_description\": \"\", \"header_image\": null, \"path\": \"000100020001\", \"title\": \"Getting Started: the Five Minute Install\", \"content_type\": 25, \"expire_at\": null, \"last_published_at\": null, \"go_live_at\": null, \"body\": \"[{\\\"id\\\": \\\"01682ba7-3481-476e-b578-7a8a7192072c\\\", \\\"value\\\": \\\"Create a Fresh Django Project\\\", \\\"type\\\": \\\"heading\\\"}, {\\\"id\\\": \\\"f22be326-089f-4169-a353-7ce18388bf5e\\\", \\\"value\\\": {\\\"code\\\": \\\"mkvirtualenv my_blog\\\\r\\\\npip install peregrine\\\\r\\\\ndjango-admin startproject my_blog\\\\r\\\\ncd my_blog\\\", \\\"language\\\": \\\"bash\\\"}, \\\"type\\\": \\\"code\\\"}, {\\\"id\\\": \\\"c24769ae-e029-4ccc-b920-da3c844b3e0e\\\", \\\"value\\\": \\\"Modify Your Django Settings\\\", \\\"type\\\": \\\"heading\\\"}, {\\\"id\\\": \\\"31ebd29a-7723-49b7-8b06-29668aa43650\\\", \\\"value\\\": \\\"

Your settings file will be located in\\\\u00a0my_blog/settings.py\\\\u00a0if you're using the default Django project layout created by the\\\\u00a0startproject\\\\u00a0command above. You can add these settings at the very bottom of the file.

\\\", \\\"type\\\": \\\"paragraph\\\"}, {\\\"id\\\": \\\"7e4c79d0-4948-4a4c-ae48-f293289b9682\\\", \\\"value\\\": {\\\"code\\\": \\\"PEREGRINE_APPS = [\\\\r\\\\n 'wagtail.wagtailcore',\\\\r\\\\n 'wagtail.wagtailadmin',\\\\r\\\\n 'wagtail.wagtaildocs',\\\\r\\\\n 'wagtail.wagtailsnippets',\\\\r\\\\n 'wagtail.wagtailusers',\\\\r\\\\n 'wagtail.wagtailimages',\\\\r\\\\n 'wagtail.wagtailembeds',\\\\r\\\\n 'wagtail.wagtailsearch',\\\\r\\\\n 'wagtail.wagtailsites',\\\\r\\\\n 'wagtail.wagtailredirects',\\\\r\\\\n 'wagtail.wagtailforms',\\\\r\\\\n 'wagtail.contrib.table_block',\\\\r\\\\n\\\\r\\\\n 'bootstrap4',\\\\r\\\\n 'wagtailcodeblock',\\\\r\\\\n 'wagtailcontentstream',\\\\r\\\\n 'peregrine',\\\\r\\\\n 'taggit',\\\\r\\\\n 'modelcluster',\\\\r\\\\n]\\\\r\\\\n\\\\r\\\\nINSTALLED_APPS += PEREGRINE_APPS\\\\r\\\\n\\\\r\\\\nPEREGRINE_MIDDLEWARE = [\\\\r\\\\n 'wagtail.wagtailcore.middleware.SiteMiddleware',\\\\r\\\\n 'wagtail.wagtailredirects.middleware.RedirectMiddleware',\\\\r\\\\n]\\\\r\\\\n\\\\r\\\\nMIDDLEWARE += PEREGRINE_MIDDLEWARE\\\\r\\\\n\\\\r\\\\nWAGTAIL_SITE_NAME = 'My Blog'\\\", \\\"language\\\": \\\"python\\\"}, \\\"type\\\": \\\"code\\\"}, {\\\"id\\\": \\\"1751a6eb-cd82-46b0-bcf0-0efad12ee3b0\\\", \\\"value\\\": \\\"Setting Up URLs\\\", \\\"type\\\": \\\"heading\\\"}, {\\\"id\\\": \\\"2fb81ed9-dfd4-41b0-9ed3-ad64181e07b7\\\", \\\"value\\\": \\\"

Your URL route file will be located in\\\\u00a0my_blog/urls.py\\\\u00a0if you're using the default Django project layout created by the\\\\u00a0startproject\\\\u00a0command above. You can replace the entire file with this:

\\\", \\\"type\\\": \\\"paragraph\\\"}, {\\\"id\\\": \\\"5d4374e1-2848-4e5d-9285-372b04f3fff4\\\", \\\"value\\\": {\\\"code\\\": \\\"from django.conf.urls import url, include\\\\r\\\\nfrom django.contrib import admin\\\\r\\\\n\\\\r\\\\nfrom wagtail.wagtailcore import urls as wagtail_urls\\\\r\\\\nfrom wagtail.wagtailadmin import urls as wagtailadmin_urls\\\\r\\\\nfrom wagtail.wagtaildocs import urls as wagtaildocs_urls\\\\r\\\\n\\\\r\\\\nurlpatterns = [\\\\r\\\\n url(r'^admin/', admin.site.urls),\\\\r\\\\n\\\\r\\\\n # Peregrine URLs for Wagtail\\\\r\\\\n url(r'^documents/', include(wagtaildocs_urls)),\\\\r\\\\n url(r'^cms/', include(wagtailadmin_urls)),\\\\r\\\\n url(r'', include(wagtail_urls)),\\\\r\\\\n]\\\", \\\"language\\\": \\\"python\\\"}, \\\"type\\\": \\\"code\\\"}, {\\\"id\\\": \\\"c819c01a-a8c8-4536-bfd6-999d575e6a8c\\\", \\\"value\\\": \\\"Fire it up!\\\", \\\"type\\\": \\\"heading\\\"}, {\\\"id\\\": \\\"6a136620-52de-4704-9f24-e2d35e07ce21\\\", \\\"value\\\": \\\"

After you've set up your settings, we need to create your database and a superuser. Issue the following commands from your project root directory.

Only run the command\\\\u00a0peregrine_initial_site\\\\u00a0if you are running on a new project, as it loads database fixtures!

\\\", \\\"type\\\": \\\"paragraph\\\"}, {\\\"id\\\": \\\"7c141684-0f1c-4b37-82dc-8a4967c5e5f8\\\", \\\"value\\\": {\\\"code\\\": \\\"python manage.py migrate\\\\r\\\\npython manage.py peregrine_initial_site\\\\r\\\\npython manage.py createsuperuser\\\\r\\\\npython manage.py runserver 0:8000\\\", \\\"language\\\": \\\"bash\\\"}, \\\"type\\\": \\\"code\\\"}, {\\\"id\\\": \\\"34e44c1d-078b-4224-98ab-b0a2e240d8d7\\\", \\\"value\\\": \\\"

You should then be able to navigate to http://localhost:8000/cms/ and log in, and start creating!

\\\", \\\"type\\\": \\\"paragraph\\\"}]\", \"show_in_menus\": true, \"live\": true, \"pk\": 4, \"has_unpublished_changes\": false, \"first_published_at\": null, \"slug\": \"getting-started-five-minute-install\", \"expired\": false, \"seo_title\": \"\", \"numchild\": 0, \"url_path\": \"/peregrine-blogging-platform/getting-started-five-minute-install/\", \"draft_title\": \"Getting Started: the Five Minute Install\", \"excerpt\": \"

If you know a little bit about Django, it should be relatively straightforward to get started with Peregrine. If you are new to Django, check out the excellent tutorial.

\"}", 118 | "approved_go_live_at": null 119 | } 120 | }, 121 | { 122 | "model": "wagtailcore.pagerevision", 123 | "pk": 3, 124 | "fields": { 125 | "page": 4, 126 | "submitted_for_moderation": false, 127 | "created_at": "2017-10-22T15:57:25.915Z", 128 | "user": 1, 129 | "content_json": "{\"has_unpublished_changes\": false, \"search_description\": \"\", \"latest_revision_created_at\": \"2017-10-22T15:52:20.547Z\", \"first_published_at\": \"2017-10-22T15:52:20.625Z\", \"seo_title\": \"\", \"slug\": \"getting-started-five-minute-install\", \"expire_at\": null, \"url_path\": \"/peregrine-blogging-platform/getting-started-five-minute-install/\", \"body\": \"[{\\\"type\\\": \\\"heading\\\", \\\"id\\\": \\\"01682ba7-3481-476e-b578-7a8a7192072c\\\", \\\"value\\\": \\\"Create a Fresh Django Project\\\"}, {\\\"type\\\": \\\"code\\\", \\\"id\\\": \\\"f22be326-089f-4169-a353-7ce18388bf5e\\\", \\\"value\\\": {\\\"language\\\": \\\"bash\\\", \\\"code\\\": \\\"mkvirtualenv my_blog\\\\r\\\\npip install peregrine\\\\r\\\\ndjango-admin startproject my_blog\\\\r\\\\ncd my_blog\\\"}}, {\\\"type\\\": \\\"heading\\\", \\\"id\\\": \\\"c24769ae-e029-4ccc-b920-da3c844b3e0e\\\", \\\"value\\\": \\\"Modify Your Django Settings\\\"}, {\\\"type\\\": \\\"paragraph\\\", \\\"id\\\": \\\"31ebd29a-7723-49b7-8b06-29668aa43650\\\", \\\"value\\\": \\\"

Your settings file will be located in\\\\u00a0my_blog/settings.py\\\\u00a0if you're using the default Django project layout created by the\\\\u00a0startproject\\\\u00a0command above. You can add these settings at the very bottom of the file.

\\\"}, {\\\"type\\\": \\\"code\\\", \\\"id\\\": \\\"7e4c79d0-4948-4a4c-ae48-f293289b9682\\\", \\\"value\\\": {\\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"PEREGRINE_APPS = [\\\\r\\\\n 'wagtail.wagtailcore',\\\\r\\\\n 'wagtail.wagtailadmin',\\\\r\\\\n 'wagtail.wagtaildocs',\\\\r\\\\n 'wagtail.wagtailsnippets',\\\\r\\\\n 'wagtail.wagtailusers',\\\\r\\\\n 'wagtail.wagtailimages',\\\\r\\\\n 'wagtail.wagtailembeds',\\\\r\\\\n 'wagtail.wagtailsearch',\\\\r\\\\n 'wagtail.wagtailsites',\\\\r\\\\n 'wagtail.wagtailredirects',\\\\r\\\\n 'wagtail.wagtailforms',\\\\r\\\\n 'wagtail.contrib.table_block',\\\\r\\\\n\\\\r\\\\n 'bootstrap4',\\\\r\\\\n 'wagtailcodeblock',\\\\r\\\\n 'wagtailcontentstream',\\\\r\\\\n 'peregrine',\\\\r\\\\n 'taggit',\\\\r\\\\n 'modelcluster',\\\\r\\\\n]\\\\r\\\\n\\\\r\\\\nINSTALLED_APPS += PEREGRINE_APPS\\\\r\\\\n\\\\r\\\\nPEREGRINE_MIDDLEWARE = [\\\\r\\\\n 'wagtail.wagtailcore.middleware.SiteMiddleware',\\\\r\\\\n 'wagtail.wagtailredirects.middleware.RedirectMiddleware',\\\\r\\\\n]\\\\r\\\\n\\\\r\\\\nMIDDLEWARE += PEREGRINE_MIDDLEWARE\\\\r\\\\n\\\\r\\\\nWAGTAIL_SITE_NAME = 'My Blog'\\\"}}, {\\\"type\\\": \\\"heading\\\", \\\"id\\\": \\\"1751a6eb-cd82-46b0-bcf0-0efad12ee3b0\\\", \\\"value\\\": \\\"Setting Up URLs\\\"}, {\\\"type\\\": \\\"paragraph\\\", \\\"id\\\": \\\"2fb81ed9-dfd4-41b0-9ed3-ad64181e07b7\\\", \\\"value\\\": \\\"

Your URL route file will be located in\\\\u00a0my_blog/urls.py\\\\u00a0if you're using the default Django project layout created by the\\\\u00a0startproject\\\\u00a0command above. You can replace the entire file with this:

\\\"}, {\\\"type\\\": \\\"code\\\", \\\"id\\\": \\\"5d4374e1-2848-4e5d-9285-372b04f3fff4\\\", \\\"value\\\": {\\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"from django.conf.urls import url, include\\\\r\\\\nfrom django.contrib import admin\\\\r\\\\n\\\\r\\\\nfrom wagtail.wagtailcore import urls as wagtail_urls\\\\r\\\\nfrom wagtail.wagtailadmin import urls as wagtailadmin_urls\\\\r\\\\nfrom wagtail.wagtaildocs import urls as wagtaildocs_urls\\\\r\\\\n\\\\r\\\\nurlpatterns = [\\\\r\\\\n url(r'^admin/', admin.site.urls),\\\\r\\\\n\\\\r\\\\n # Peregrine URLs for Wagtail\\\\r\\\\n url(r'^documents/', include(wagtaildocs_urls)),\\\\r\\\\n url(r'^cms/', include(wagtailadmin_urls)),\\\\r\\\\n url(r'', include(wagtail_urls)),\\\\r\\\\n]\\\"}}, {\\\"type\\\": \\\"heading\\\", \\\"id\\\": \\\"c819c01a-a8c8-4536-bfd6-999d575e6a8c\\\", \\\"value\\\": \\\"Fire it up!\\\"}, {\\\"type\\\": \\\"paragraph\\\", \\\"id\\\": \\\"6a136620-52de-4704-9f24-e2d35e07ce21\\\", \\\"value\\\": \\\"

After you've set up your settings, we need to create your database and a superuser. Issue the following commands from your project root directory.

Only run the command\\\\u00a0peregrine_initial_site\\\\u00a0if you are running on a new project, as it loads database fixtures!

\\\"}, {\\\"type\\\": \\\"code\\\", \\\"id\\\": \\\"7c141684-0f1c-4b37-82dc-8a4967c5e5f8\\\", \\\"value\\\": {\\\"language\\\": \\\"bash\\\", \\\"code\\\": \\\"python manage.py migrate\\\\r\\\\npython manage.py peregrine_initial_site\\\\r\\\\npython manage.py createsuperuser\\\\r\\\\npython manage.py runserver 0:8000\\\"}}, {\\\"type\\\": \\\"paragraph\\\", \\\"id\\\": \\\"34e44c1d-078b-4224-98ab-b0a2e240d8d7\\\", \\\"value\\\": \\\"

You should then be able to navigate to http://localhost:8000/cms/ and log in, and start creating!

\\\"}]\", \"last_published_at\": \"2017-10-22T15:52:20.625Z\", \"pk\": 4, \"locked\": false, \"show_in_menus\": true, \"live_revision\": 2, \"content_type\": 25, \"depth\": 3, \"title\": \"Getting Started\", \"excerpt\": \"

If you know a little bit about Django, it should be relatively straightforward to get started with Peregrine with this five-minute install. If you are new to Django, check out the excellent tutorial.

\", \"owner\": 1, \"expired\": false, \"live\": true, \"draft_title\": \"Getting Started: the Five Minute Install\", \"header_image\": null, \"go_live_at\": null, \"path\": \"000100020001\", \"numchild\": 0}", 130 | "approved_go_live_at": null 131 | } 132 | }, 133 | { 134 | "model": "wagtailcore.grouppagepermission", 135 | "pk": 1, 136 | "fields": { 137 | "group": 1, 138 | "page": 1, 139 | "permission_type": "add" 140 | } 141 | }, 142 | { 143 | "model": "wagtailcore.grouppagepermission", 144 | "pk": 2, 145 | "fields": { 146 | "group": 1, 147 | "page": 1, 148 | "permission_type": "edit" 149 | } 150 | }, 151 | { 152 | "model": "wagtailcore.grouppagepermission", 153 | "pk": 3, 154 | "fields": { 155 | "group": 1, 156 | "page": 1, 157 | "permission_type": "publish" 158 | } 159 | }, 160 | { 161 | "model": "wagtailcore.grouppagepermission", 162 | "pk": 4, 163 | "fields": { 164 | "group": 2, 165 | "page": 1, 166 | "permission_type": "add" 167 | } 168 | }, 169 | { 170 | "model": "wagtailcore.grouppagepermission", 171 | "pk": 5, 172 | "fields": { 173 | "group": 2, 174 | "page": 1, 175 | "permission_type": "edit" 176 | } 177 | }, 178 | { 179 | "model": "wagtailcore.grouppagepermission", 180 | "pk": 6, 181 | "fields": { 182 | "group": 1, 183 | "page": 1, 184 | "permission_type": "lock" 185 | } 186 | }, 187 | { 188 | "model": "wagtailcore.collection", 189 | "pk": 1, 190 | "fields": { 191 | "path": "0001", 192 | "depth": 1, 193 | "numchild": 0, 194 | "name": "Root" 195 | } 196 | }, 197 | { 198 | "model": "wagtailcore.groupcollectionpermission", 199 | "pk": 1, 200 | "fields": { 201 | "group": 2, 202 | "collection": 1, 203 | "permission": 1 204 | } 205 | }, 206 | { 207 | "model": "wagtailcore.groupcollectionpermission", 208 | "pk": 2, 209 | "fields": { 210 | "group": 1, 211 | "collection": 1, 212 | "permission": 1 213 | } 214 | }, 215 | { 216 | "model": "wagtailcore.groupcollectionpermission", 217 | "pk": 3, 218 | "fields": { 219 | "group": 2, 220 | "collection": 1, 221 | "permission": 2 222 | } 223 | }, 224 | { 225 | "model": "wagtailcore.groupcollectionpermission", 226 | "pk": 4, 227 | "fields": { 228 | "group": 1, 229 | "collection": 1, 230 | "permission": 2 231 | } 232 | }, 233 | { 234 | "model": "wagtailcore.groupcollectionpermission", 235 | "pk": 5, 236 | "fields": { 237 | "group": 2, 238 | "collection": 1, 239 | "permission": 5 240 | } 241 | }, 242 | { 243 | "model": "wagtailcore.groupcollectionpermission", 244 | "pk": 6, 245 | "fields": { 246 | "group": 1, 247 | "collection": 1, 248 | "permission": 5 249 | } 250 | }, 251 | { 252 | "model": "wagtailcore.groupcollectionpermission", 253 | "pk": 7, 254 | "fields": { 255 | "group": 2, 256 | "collection": 1, 257 | "permission": 6 258 | } 259 | }, 260 | { 261 | "model": "wagtailcore.groupcollectionpermission", 262 | "pk": 8, 263 | "fields": { 264 | "group": 1, 265 | "collection": 1, 266 | "permission": 6 267 | } 268 | }, 269 | { 270 | "model": "peregrine.sitepage", 271 | "pk": 3, 272 | "fields": { 273 | "body": "[{\"type\": \"heading\", \"value\": \"Built on Wagtail.\", \"id\": \"27fc57ed-ee72-43d8-8c28-198b66d2406f\"}, {\"type\": \"paragraph\", \"value\": \"

Wagtail provides a beautiful CMS experience.

\", \"id\": \"ae315cb3-5929-479f-a252-2e3c874b060c\"}]", 274 | "header_image": null, 275 | "excerpt": "

Peregrine is an opinionated blogging platform which uses the Wagtail CMS on the Django web framework. It uses Wagtail's fantastic StreamField feature to provide fully structured content body element blocks, completely separating content from the presentation layer (CSS, JS, and HTML).

" 276 | } 277 | }, 278 | { 279 | "model": "peregrine.sitepage", 280 | "pk": 4, 281 | "fields": { 282 | "body": "[{\"type\": \"heading\", \"value\": \"Create a Fresh Django Project\", \"id\": \"01682ba7-3481-476e-b578-7a8a7192072c\"}, {\"type\": \"code\", \"value\": {\"code\": \"mkvirtualenv my_blog\\r\\npip install peregrine\\r\\ndjango-admin startproject my_blog\\r\\ncd my_blog\", \"language\": \"bash\"}, \"id\": \"f22be326-089f-4169-a353-7ce18388bf5e\"}, {\"type\": \"heading\", \"value\": \"Modify Your Django Settings\", \"id\": \"c24769ae-e029-4ccc-b920-da3c844b3e0e\"}, {\"type\": \"paragraph\", \"value\": \"

Your settings file will be located in\\u00a0my_blog/settings.py\\u00a0if you're using the default Django project layout created by the\\u00a0startproject\\u00a0command above. You can add these settings at the very bottom of the file.

\", \"id\": \"31ebd29a-7723-49b7-8b06-29668aa43650\"}, {\"type\": \"code\", \"value\": {\"code\": \"PEREGRINE_APPS = [\\r\\n 'wagtail.wagtailcore',\\r\\n 'wagtail.wagtailadmin',\\r\\n 'wagtail.wagtaildocs',\\r\\n 'wagtail.wagtailsnippets',\\r\\n 'wagtail.wagtailusers',\\r\\n 'wagtail.wagtailimages',\\r\\n 'wagtail.wagtailembeds',\\r\\n 'wagtail.wagtailsearch',\\r\\n 'wagtail.wagtailsites',\\r\\n 'wagtail.wagtailredirects',\\r\\n 'wagtail.wagtailforms',\\r\\n 'wagtail.contrib.table_block',\\r\\n\\r\\n 'bootstrap4',\\r\\n 'wagtailcodeblock',\\r\\n 'wagtailcontentstream',\\r\\n 'peregrine',\\r\\n 'taggit',\\r\\n 'modelcluster',\\r\\n]\\r\\n\\r\\nINSTALLED_APPS += PEREGRINE_APPS\\r\\n\\r\\nPEREGRINE_MIDDLEWARE = [\\r\\n 'wagtail.wagtailcore.middleware.SiteMiddleware',\\r\\n 'wagtail.wagtailredirects.middleware.RedirectMiddleware',\\r\\n]\\r\\n\\r\\nMIDDLEWARE += PEREGRINE_MIDDLEWARE\\r\\n\\r\\nWAGTAIL_SITE_NAME = 'My Blog'\", \"language\": \"python\"}, \"id\": \"7e4c79d0-4948-4a4c-ae48-f293289b9682\"}, {\"type\": \"heading\", \"value\": \"Setting Up URLs\", \"id\": \"1751a6eb-cd82-46b0-bcf0-0efad12ee3b0\"}, {\"type\": \"paragraph\", \"value\": \"

Your URL route file will be located in\\u00a0my_blog/urls.py\\u00a0if you're using the default Django project layout created by the\\u00a0startproject\\u00a0command above. You can replace the entire file with this:

\", \"id\": \"2fb81ed9-dfd4-41b0-9ed3-ad64181e07b7\"}, {\"type\": \"code\", \"value\": {\"code\": \"from django.conf.urls import url, include\\r\\nfrom django.contrib import admin\\r\\n\\r\\nfrom wagtail.wagtailcore import urls as wagtail_urls\\r\\nfrom wagtail.wagtailadmin import urls as wagtailadmin_urls\\r\\nfrom wagtail.wagtaildocs import urls as wagtaildocs_urls\\r\\n\\r\\nurlpatterns = [\\r\\n url(r'^admin/', admin.site.urls),\\r\\n\\r\\n # Peregrine URLs for Wagtail\\r\\n url(r'^documents/', include(wagtaildocs_urls)),\\r\\n url(r'^cms/', include(wagtailadmin_urls)),\\r\\n url(r'', include(wagtail_urls)),\\r\\n]\", \"language\": \"python\"}, \"id\": \"5d4374e1-2848-4e5d-9285-372b04f3fff4\"}, {\"type\": \"heading\", \"value\": \"Fire it up!\", \"id\": \"c819c01a-a8c8-4536-bfd6-999d575e6a8c\"}, {\"type\": \"paragraph\", \"value\": \"

After you've set up your settings, we need to create your database and a superuser. Issue the following commands from your project root directory.

Only run the command\\u00a0peregrine_initial_site\\u00a0if you are running on a new project, as it loads database fixtures!

\", \"id\": \"6a136620-52de-4704-9f24-e2d35e07ce21\"}, {\"type\": \"code\", \"value\": {\"code\": \"python manage.py migrate\\r\\npython manage.py peregrine_initial_site\\r\\npython manage.py createsuperuser\\r\\npython manage.py runserver 0:8000\", \"language\": \"bash\"}, \"id\": \"7c141684-0f1c-4b37-82dc-8a4967c5e5f8\"}, {\"type\": \"paragraph\", \"value\": \"

You should then be able to navigate to http://localhost:8000/cms/ and log in, and start creating!

\", \"id\": \"34e44c1d-078b-4224-98ab-b0a2e240d8d7\"}]", 283 | "header_image": null, 284 | "excerpt": "

If you know a little bit about Django, it should be relatively straightforward to get started with Peregrine with this five-minute install. If you are new to Django, check out the excellent tutorial.

" 285 | } 286 | } 287 | ] 288 | -------------------------------------------------------------------------------- /peregrine/management/commands/peregrine_initial_site.py: -------------------------------------------------------------------------------- 1 | from django.core.management import call_command 2 | from django.core.management.base import BaseCommand 3 | from django.db import connections, DEFAULT_DB_ALIAS 4 | 5 | 6 | INITIAL_FIXTURES = [ 7 | "initial_site", 8 | ] 9 | 10 | 11 | class Command(BaseCommand): 12 | """ 13 | This loads various fixtures of data to populate an initial Peregrine 14 | site including help on getting started. 15 | """ 16 | 17 | def add_arguments(self, parser): 18 | parser.add_argument( 19 | "--database", 20 | action="store", 21 | dest="database", 22 | default=DEFAULT_DB_ALIAS, 23 | help='The database to use. Default to the "default" database.', 24 | ) 25 | 26 | def handle(self, *args, **options): 27 | self.verbosity = int(options.get("verbosity")) 28 | 29 | # Get the database we're working with 30 | db = options.get("database") 31 | connection = connections[db] 32 | 33 | for fixture in INITIAL_FIXTURES: 34 | print("Installing {}".format(fixture)) 35 | call_command( 36 | "loaddata", 37 | fixture, 38 | verbosity=self.verbosity, 39 | database=connection.alias, 40 | skip_validation=True, 41 | app_label="peregrine", 42 | hide_empty=True, 43 | ) 44 | -------------------------------------------------------------------------------- /peregrine/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8.dev20171216134037 on 2017-12-16 14:06 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | import modelcluster.fields 10 | import wagtail.contrib.table_block.blocks 11 | import wagtail.blocks 12 | import wagtail.fields 13 | import wagtail.documents.blocks 14 | import wagtail.embeds.blocks 15 | import wagtail.images.blocks 16 | 17 | 18 | class Migration(migrations.Migration): 19 | 20 | initial = True 21 | 22 | dependencies = [ 23 | ("wagtailcore", "0040_page_draft_title"), 24 | ("wagtailimages", "0019_delete_filter"), 25 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 26 | ] 27 | 28 | operations = [ 29 | migrations.CreateModel( 30 | name="Category", 31 | fields=[ 32 | ( 33 | "id", 34 | models.AutoField( 35 | auto_created=True, 36 | primary_key=True, 37 | serialize=False, 38 | verbose_name="ID", 39 | ), 40 | ), 41 | ("name", models.CharField(max_length=255)), 42 | ], 43 | options={ 44 | "verbose_name_plural": "categories", 45 | }, 46 | ), 47 | migrations.CreateModel( 48 | name="SitePage", 49 | fields=[ 50 | ( 51 | "page_ptr", 52 | models.OneToOneField( 53 | auto_created=True, 54 | on_delete=django.db.models.deletion.CASCADE, 55 | parent_link=True, 56 | primary_key=True, 57 | serialize=False, 58 | to="wagtailcore.Page", 59 | ), 60 | ), 61 | ( 62 | "body", 63 | wagtail.fields.StreamField( 64 | ( 65 | ( 66 | "heading", 67 | wagtail.blocks.TextBlock( 68 | icon="title", 69 | template="wagtailcontentstream/blocks/heading.html", 70 | ), 71 | ), 72 | ( 73 | "paragraph", 74 | wagtail.blocks.RichTextBlock( 75 | features=["bold", "italic", "link", "ol", "ul"], 76 | icon="pilcrow", 77 | ), 78 | ), 79 | ( 80 | "image", 81 | wagtail.blocks.StructBlock( 82 | ( 83 | ( 84 | "image", 85 | wagtail.images.blocks.ImageChooserBlock(), 86 | ), 87 | ( 88 | "caption", 89 | wagtail.blocks.TextBlock( 90 | required=False 91 | ), 92 | ), 93 | ) 94 | ), 95 | ), 96 | ( 97 | "document", 98 | wagtail.documents.blocks.DocumentChooserBlock(), 99 | ), 100 | ("embed", wagtail.embeds.blocks.EmbedBlock(icon="media")), 101 | ( 102 | "table", 103 | wagtail.contrib.table_block.blocks.TableBlock( 104 | icon="table" 105 | ), 106 | ), 107 | ( 108 | "code", 109 | wagtail.blocks.StructBlock( 110 | ( 111 | ( 112 | "language", 113 | wagtail.blocks.ChoiceBlock( 114 | choices=[ 115 | ("bash", "Bash/Shell"), 116 | ("css", "CSS"), 117 | ("diff", "diff"), 118 | ("http", "HTML"), 119 | ("javascript", "Javascript"), 120 | ("json", "JSON"), 121 | ("python", "Python"), 122 | ("scss", "SCSS"), 123 | ("yaml", "YAML"), 124 | ], 125 | help_text="Coding language", 126 | label="Language", 127 | ), 128 | ), 129 | ( 130 | "code", 131 | wagtail.blocks.TextBlock(label="Code"), 132 | ), 133 | ), 134 | icon="code", 135 | ), 136 | ), 137 | ), 138 | blank=True, 139 | ), 140 | ), 141 | ( 142 | "excerpt", 143 | wagtail.fields.RichTextField( 144 | blank=True, 145 | help_text="An short excerpt or abstract about the content.", 146 | null=True, 147 | ), 148 | ), 149 | ], 150 | options={ 151 | "verbose_name": "Page", 152 | }, 153 | bases=("wagtailcore.page",), 154 | ), 155 | migrations.CreateModel( 156 | name="SitePost", 157 | fields=[ 158 | ( 159 | "sitepage_ptr", 160 | models.OneToOneField( 161 | auto_created=True, 162 | on_delete=django.db.models.deletion.CASCADE, 163 | parent_link=True, 164 | primary_key=True, 165 | serialize=False, 166 | to="peregrine.SitePage", 167 | ), 168 | ), 169 | ( 170 | "post_date", 171 | models.DateTimeField( 172 | default=django.utils.timezone.now, 173 | help_text="The date and time of the post.", 174 | ), 175 | ), 176 | ], 177 | options={ 178 | "verbose_name": "Post", 179 | }, 180 | bases=("peregrine.sitepage",), 181 | ), 182 | migrations.AddField( 183 | model_name="sitepage", 184 | name="categories", 185 | field=modelcluster.fields.ParentalManyToManyField( 186 | blank=True, 187 | help_text="The categories for the page or post.", 188 | to="peregrine.Category", 189 | ), 190 | ), 191 | migrations.AddField( 192 | model_name="sitepage", 193 | name="header_image", 194 | field=models.ForeignKey( 195 | blank=True, 196 | help_text="A featured image that will appear in the site theme header.", 197 | null=True, 198 | on_delete=django.db.models.deletion.SET_NULL, 199 | related_name="+", 200 | to="wagtailimages.Image", 201 | ), 202 | ), 203 | migrations.AddField( 204 | model_name="sitepost", 205 | name="authors", 206 | field=modelcluster.fields.ParentalManyToManyField( 207 | blank=True, 208 | help_text="The authors of the post.", 209 | to=settings.AUTH_USER_MODEL, 210 | ), 211 | ), 212 | ] 213 | -------------------------------------------------------------------------------- /peregrine/migrations/0002_auto_20171216_1408.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8.dev20171216134037 on 2017-12-16 14:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ("wagtailcore", "0040_page_draft_title"), 13 | ("peregrine", "0001_initial"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="PeregrineSettings", 19 | fields=[ 20 | ( 21 | "id", 22 | models.AutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ( 30 | "post_number", 31 | models.IntegerField( 32 | default=10, help_text="The number of posts to display." 33 | ), 34 | ), 35 | ( 36 | "post_number_nav", 37 | models.IntegerField( 38 | default=10, 39 | help_text="The number of posts to display in navigation.", 40 | ), 41 | ), 42 | ], 43 | options={ 44 | "abstract": False, 45 | }, 46 | ), 47 | migrations.CreateModel( 48 | name="Settings", 49 | fields=[ 50 | ( 51 | "id", 52 | models.AutoField( 53 | auto_created=True, 54 | primary_key=True, 55 | serialize=False, 56 | verbose_name="ID", 57 | ), 58 | ), 59 | ("key", models.CharField(max_length=255, unique=True)), 60 | ("value", models.CharField(max_length=255)), 61 | ], 62 | ), 63 | migrations.AddIndex( 64 | model_name="settings", 65 | index=models.Index(fields=["key"], name="settings_key_idx"), 66 | ), 67 | migrations.AddField( 68 | model_name="peregrinesettings", 69 | name="landing_page", 70 | field=models.ForeignKey( 71 | blank=True, 72 | help_text="The page to display at the root. If blank, displays latest posts.", 73 | null=True, 74 | on_delete=django.db.models.deletion.SET_NULL, 75 | to="wagtailcore.Page", 76 | ), 77 | ), 78 | migrations.AddField( 79 | model_name="peregrinesettings", 80 | name="site", 81 | field=models.OneToOneField( 82 | editable=False, 83 | on_delete=django.db.models.deletion.CASCADE, 84 | to="wagtailcore.Site", 85 | ), 86 | ), 87 | ] 88 | -------------------------------------------------------------------------------- /peregrine/migrations/0003_auto_20171217_1706.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2017-12-17 17:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("peregrine", "0002_auto_20171216_1408"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="peregrinesettings", 16 | name="post_number_rss", 17 | field=models.IntegerField( 18 | default=100, help_text="The number of posts to include in the RSS feed." 19 | ), 20 | ), 21 | migrations.AlterField( 22 | model_name="peregrinesettings", 23 | name="landing_page", 24 | field=models.ForeignKey( 25 | blank=True, 26 | help_text="The page to display at the root. If blank, displays the latest posts.", 27 | null=True, 28 | on_delete=django.db.models.deletion.SET_NULL, 29 | to="wagtailcore.Page", 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /peregrine/migrations/0004_auto_20180105_1315.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2018-01-05 13:15 2 | 3 | from django.db import migrations, models 4 | import wagtail.contrib.table_block.blocks 5 | import wagtail.blocks 6 | import wagtail.fields 7 | import wagtail.documents.blocks 8 | import wagtail.embeds.blocks 9 | import wagtail.images.blocks 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ("peregrine", "0003_auto_20171217_1706"), 16 | ] 17 | 18 | operations = [ 19 | migrations.AddField( 20 | model_name="peregrinesettings", 21 | name="post_title", 22 | field=models.CharField( 23 | default="Posts", 24 | help_text="The menu text label for latest posts.", 25 | max_length=30, 26 | ), 27 | ), 28 | migrations.AlterField( 29 | model_name="sitepage", 30 | name="body", 31 | field=wagtail.fields.StreamField( 32 | ( 33 | ( 34 | "heading", 35 | wagtail.blocks.TextBlock( 36 | icon="title", 37 | template="wagtailcontentstream/blocks/heading.html", 38 | ), 39 | ), 40 | ( 41 | "paragraph", 42 | wagtail.blocks.RichTextBlock( 43 | features=["bold", "italic", "link", "ol", "ul"], 44 | icon="pilcrow", 45 | ), 46 | ), 47 | ( 48 | "image", 49 | wagtail.blocks.StructBlock( 50 | ( 51 | ( 52 | "image", 53 | wagtail.images.blocks.ImageChooserBlock( 54 | help_text="The image to display." 55 | ), 56 | ), 57 | ( 58 | "caption", 59 | wagtail.blocks.TextBlock( 60 | help_text="The caption will appear under the image, if entered.", 61 | required=False, 62 | ), 63 | ), 64 | ( 65 | "credit", 66 | wagtail.blocks.TextBlock( 67 | help_text="The credit will appear under the image, if entered.", 68 | required=False, 69 | ), 70 | ), 71 | ( 72 | "align", 73 | wagtail.blocks.ChoiceBlock( 74 | choices=[ 75 | ("left", "Left"), 76 | ("right", "Right"), 77 | ("center", "Center"), 78 | ("full", "Full Width"), 79 | ], 80 | help_text="How to align the image in the body of the page.", 81 | ), 82 | ), 83 | ) 84 | ), 85 | ), 86 | ("document", wagtail.documents.blocks.DocumentChooserBlock()), 87 | ("embed", wagtail.embeds.blocks.EmbedBlock(icon="media")), 88 | ( 89 | "table", 90 | wagtail.contrib.table_block.blocks.TableBlock(icon="table"), 91 | ), 92 | ( 93 | "code", 94 | wagtail.blocks.StructBlock( 95 | ( 96 | ( 97 | "language", 98 | wagtail.blocks.ChoiceBlock( 99 | choices=[ 100 | ("bash", "Bash/Shell"), 101 | ("css", "CSS"), 102 | ("diff", "diff"), 103 | ("http", "HTML"), 104 | ("javascript", "Javascript"), 105 | ("json", "JSON"), 106 | ("python", "Python"), 107 | ("scss", "SCSS"), 108 | ("yaml", "YAML"), 109 | ], 110 | help_text="Coding language", 111 | label="Language", 112 | ), 113 | ), 114 | ("code", wagtail.blocks.TextBlock(label="Code")), 115 | ), 116 | icon="code", 117 | ), 118 | ), 119 | ), 120 | blank=True, 121 | ), 122 | ), 123 | ] 124 | -------------------------------------------------------------------------------- /peregrine/migrations/0005_auto_20180418_1637.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-18 16:37 2 | 3 | from django.db import migrations 4 | import wagtail.contrib.table_block.blocks 5 | import wagtail.blocks 6 | import wagtail.fields 7 | import wagtail.documents.blocks 8 | import wagtail.embeds.blocks 9 | import wagtail.images.blocks 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ("peregrine", "0004_auto_20180105_1315"), 16 | ] 17 | 18 | operations = [ 19 | migrations.DeleteModel( 20 | name="Settings", 21 | ), 22 | migrations.AlterModelOptions( 23 | name="peregrinesettings", 24 | options={"verbose_name": "Peregrine Settings"}, 25 | ), 26 | migrations.AlterField( 27 | model_name="sitepage", 28 | name="body", 29 | field=wagtail.fields.StreamField( 30 | ( 31 | ( 32 | "heading", 33 | wagtail.blocks.TextBlock( 34 | icon="title", 35 | template="wagtailcontentstream/blocks/heading.html", 36 | ), 37 | ), 38 | ( 39 | "paragraph", 40 | wagtail.blocks.RichTextBlock( 41 | features=[ 42 | "bold", 43 | "italic", 44 | "link", 45 | "ol", 46 | "ul", 47 | "monospace", 48 | ], 49 | icon="pilcrow", 50 | ), 51 | ), 52 | ( 53 | "image", 54 | wagtail.blocks.StructBlock( 55 | ( 56 | ( 57 | "image", 58 | wagtail.images.blocks.ImageChooserBlock( 59 | help_text="The image to display." 60 | ), 61 | ), 62 | ( 63 | "caption", 64 | wagtail.blocks.TextBlock( 65 | help_text="The caption will appear under the image, if entered.", 66 | required=False, 67 | ), 68 | ), 69 | ( 70 | "credit", 71 | wagtail.blocks.TextBlock( 72 | help_text="The credit will appear under the image, if entered.", 73 | required=False, 74 | ), 75 | ), 76 | ( 77 | "align", 78 | wagtail.blocks.ChoiceBlock( 79 | choices=[ 80 | ("left", "Left"), 81 | ("right", "Right"), 82 | ("center", "Center"), 83 | ("full", "Full Width"), 84 | ], 85 | help_text="How to align the image in the body of the page.", 86 | ), 87 | ), 88 | ) 89 | ), 90 | ), 91 | ("document", wagtail.documents.blocks.DocumentChooserBlock()), 92 | ("embed", wagtail.embeds.blocks.EmbedBlock(icon="media")), 93 | ( 94 | "table", 95 | wagtail.contrib.table_block.blocks.TableBlock(icon="table"), 96 | ), 97 | ( 98 | "code", 99 | wagtail.blocks.StructBlock( 100 | ( 101 | ( 102 | "language", 103 | wagtail.blocks.ChoiceBlock( 104 | choices=[ 105 | ("bash", "Bash/Shell"), 106 | ("css", "CSS"), 107 | ("diff", "diff"), 108 | ("http", "HTML"), 109 | ("javascript", "Javascript"), 110 | ("json", "JSON"), 111 | ("python", "Python"), 112 | ("scss", "SCSS"), 113 | ("yaml", "YAML"), 114 | ], 115 | help_text="Coding language", 116 | label="Language", 117 | ), 118 | ), 119 | ("code", wagtail.blocks.TextBlock(label="Code")), 120 | ), 121 | icon="code", 122 | ), 123 | ), 124 | ), 125 | blank=True, 126 | ), 127 | ), 128 | ] 129 | -------------------------------------------------------------------------------- /peregrine/migrations/0006_auto_20180418_1652.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-18 16:52 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("peregrine", "0005_auto_20180418_1637"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="peregrinesettings", 16 | name="landing_page", 17 | field=models.ForeignKey( 18 | blank=True, 19 | help_text="The front page for Peregrine to display. If blank, displays the latest posts.", 20 | null=True, 21 | on_delete=django.db.models.deletion.SET_NULL, 22 | to="wagtailcore.Page", 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name="peregrinesettings", 27 | name="post_number", 28 | field=models.IntegerField( 29 | default=10, 30 | help_text="The number of posts to display on the posts page.", 31 | ), 32 | ), 33 | migrations.AlterField( 34 | model_name="peregrinesettings", 35 | name="post_number_nav", 36 | field=models.IntegerField( 37 | default=10, 38 | help_text="The number of posts to display in top navigation dropdown.", 39 | ), 40 | ), 41 | migrations.AlterField( 42 | model_name="peregrinesettings", 43 | name="post_title", 44 | field=models.CharField( 45 | default="Posts", 46 | help_text="The top navigation menu dropdown text label for latest posts.", 47 | max_length=30, 48 | ), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /peregrine/migrations/0007_auto_20180523_1908.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-23 19:08 2 | 3 | from django.db import migrations 4 | import wagtail.contrib.table_block.blocks 5 | import wagtail.blocks 6 | import wagtail.fields 7 | import wagtail.documents.blocks 8 | import wagtail.embeds.blocks 9 | import wagtail.images.blocks 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ("peregrine", "0006_auto_20180418_1652"), 16 | ] 17 | 18 | operations = [ 19 | migrations.AlterField( 20 | model_name="sitepage", 21 | name="body", 22 | field=wagtail.fields.StreamField( 23 | [ 24 | ( 25 | "heading", 26 | wagtail.blocks.TextBlock( 27 | icon="title", 28 | template="wagtailcontentstream/blocks/heading.html", 29 | ), 30 | ), 31 | ( 32 | "paragraph", 33 | wagtail.blocks.RichTextBlock( 34 | features=[ 35 | "bold", 36 | "italic", 37 | "link", 38 | "ol", 39 | "ul", 40 | "monospace", 41 | ], 42 | icon="pilcrow", 43 | ), 44 | ), 45 | ( 46 | "image", 47 | wagtail.blocks.StructBlock( 48 | [ 49 | ( 50 | "image", 51 | wagtail.images.blocks.ImageChooserBlock( 52 | help_text="The image to display." 53 | ), 54 | ), 55 | ( 56 | "caption", 57 | wagtail.blocks.TextBlock( 58 | help_text="The caption will appear under the image, if entered.", 59 | required=False, 60 | ), 61 | ), 62 | ( 63 | "credit", 64 | wagtail.blocks.TextBlock( 65 | help_text="The credit will appear under the image, if entered.", 66 | required=False, 67 | ), 68 | ), 69 | ( 70 | "align", 71 | wagtail.blocks.ChoiceBlock( 72 | choices=[ 73 | ("left", "Left"), 74 | ("right", "Right"), 75 | ("center", "Center"), 76 | ("full", "Full Width"), 77 | ], 78 | help_text="How to align the image in the body of the page.", 79 | ), 80 | ), 81 | ] 82 | ), 83 | ), 84 | ("document", wagtail.documents.blocks.DocumentChooserBlock()), 85 | ("embed", wagtail.embeds.blocks.EmbedBlock(icon="media")), 86 | ( 87 | "table", 88 | wagtail.contrib.table_block.blocks.TableBlock(icon="table"), 89 | ), 90 | ( 91 | "code", 92 | wagtail.blocks.StructBlock( 93 | [ 94 | ( 95 | "language", 96 | wagtail.blocks.ChoiceBlock( 97 | choices=[ 98 | ("bash", "Bash/Shell"), 99 | ("css", "CSS"), 100 | ("diff", "diff"), 101 | ("html", "HTML"), 102 | ("javascript", "Javascript"), 103 | ("json", "JSON"), 104 | ("python", "Python"), 105 | ("scss", "SCSS"), 106 | ("yaml", "YAML"), 107 | ], 108 | help_text="Coding language", 109 | label="Language", 110 | ), 111 | ), 112 | ("code", wagtail.blocks.TextBlock(label="Code")), 113 | ], 114 | icon="code", 115 | ), 116 | ), 117 | ], 118 | blank=True, 119 | ), 120 | ), 121 | ] 122 | -------------------------------------------------------------------------------- /peregrine/migrations/0008_peregrinesettings_revisions_to_keep.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-27 15:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("peregrine", "0007_auto_20180523_1908"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="peregrinesettings", 15 | name="revisions_to_keep", 16 | field=models.IntegerField( 17 | blank=True, 18 | default=None, 19 | help_text="The number of revisions to keep. If None, keeps all revisions.", 20 | null=True, 21 | ), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /peregrine/migrations/0009_auto_20211105_1634.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.9 on 2021-11-05 16:34 2 | 3 | from django.db import migrations, models 4 | import wagtail.contrib.table_block.blocks 5 | import wagtail.blocks 6 | import wagtail.fields 7 | import wagtail.documents.blocks 8 | import wagtail.embeds.blocks 9 | import wagtail.images.blocks 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('peregrine', '0008_peregrinesettings_revisions_to_keep'), 16 | ] 17 | 18 | operations = [ 19 | migrations.AlterField( 20 | model_name='category', 21 | name='id', 22 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 23 | ), 24 | migrations.AlterField( 25 | model_name='peregrinesettings', 26 | name='id', 27 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 28 | ), 29 | migrations.AlterField( 30 | model_name='sitepage', 31 | name='body', 32 | field=wagtail.fields.StreamField([('heading', wagtail.blocks.TextBlock(icon='title', template='wagtailcontentstream/blocks/heading.html')), ('paragraph', wagtail.blocks.RichTextBlock(features=['bold', 'italic', 'link', 'ol', 'ul', 'monospace'], icon='pilcrow')), ('image', wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock(help_text='The image to display.')), ('caption', wagtail.blocks.TextBlock(help_text='The caption will appear under the image, if entered.', required=False)), ('credit', wagtail.blocks.TextBlock(help_text='The credit will appear under the image, if entered.', required=False)), ('align', wagtail.blocks.ChoiceBlock(choices=[('left', 'Left'), ('right', 'Right'), ('center', 'Center'), ('full', 'Full Width')], help_text='How to align the image in the body of the page.'))])), ('document', wagtail.documents.blocks.DocumentChooserBlock()), ('embed', wagtail.embeds.blocks.EmbedBlock(icon='media')), ('table', wagtail.contrib.table_block.blocks.TableBlock(icon='table')), ('code', wagtail.blocks.StructBlock([('language', wagtail.blocks.ChoiceBlock(choices=[('bash', 'Bash/Shell'), ('css', 'CSS'), ('diff', 'diff'), ('html', 'HTML'), ('javascript', 'Javascript'), ('json', 'JSON'), ('python', 'Python'), ('scss', 'SCSS'), ('yaml', 'YAML')], help_text='Coding language', identifier='language', label='Language')), ('code', wagtail.blocks.TextBlock(identifier='code', label='Code'))], icon='code'))], blank=True), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /peregrine/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/peregrine/59cf20bdffc1b4fabcefcd84041ae7e6f63ca221/peregrine/migrations/__init__.py -------------------------------------------------------------------------------- /peregrine/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.forms import CheckboxSelectMultiple 4 | from django.utils.timezone import now 5 | 6 | from modelcluster.fields import ParentalManyToManyField 7 | 8 | from wagtail.admin.panels import FieldPanel, StreamFieldPanel 9 | from wagtail.contrib.settings.models import BaseSetting, register_setting 10 | from wagtail.fields import RichTextField 11 | from wagtail.images.edit_handlers import ImageChooserPanel 12 | from wagtail.search import index 13 | 14 | from wagtailcontentstream.models import ContentStreamPage 15 | 16 | 17 | class Category(models.Model): 18 | """ 19 | Categories which a Page or Post item can belong to. A page can belong to 20 | many categories. 21 | """ 22 | 23 | name = models.CharField(max_length=255) 24 | 25 | class Meta: 26 | verbose_name_plural = "categories" 27 | 28 | def __str__(self): 29 | return self.name 30 | 31 | 32 | class SitePage(ContentStreamPage): 33 | header_image = models.ForeignKey( 34 | "wagtailimages.Image", 35 | null=True, 36 | blank=True, 37 | on_delete=models.SET_NULL, 38 | related_name="+", 39 | help_text="A featured image that will appear in the site theme header.", 40 | ) 41 | excerpt = RichTextField( 42 | features=["bold", "italic", "link", "ol", "ul", "monospace"], 43 | blank=True, 44 | null=True, 45 | help_text="An short excerpt or abstract about the content.", 46 | ) 47 | categories = ParentalManyToManyField( 48 | "Category", 49 | blank=True, 50 | help_text="The categories for the page or post.", 51 | ) 52 | show_in_menus_default = True 53 | 54 | search_fields = ContentStreamPage.search_fields + [ 55 | index.SearchField("body"), 56 | index.SearchField("excerpt"), 57 | ] 58 | 59 | content_panels = [ 60 | FieldPanel("title"), 61 | ImageChooserPanel("header_image"), 62 | FieldPanel("excerpt"), 63 | FieldPanel("categories", widget=CheckboxSelectMultiple), 64 | StreamFieldPanel("body"), 65 | ] 66 | 67 | class Meta: 68 | verbose_name = "Page" 69 | 70 | 71 | class SitePost(SitePage): 72 | post_date = models.DateTimeField( 73 | default=now, 74 | help_text="The date and time of the post.", 75 | ) 76 | authors = ParentalManyToManyField( 77 | settings.AUTH_USER_MODEL, 78 | blank=True, 79 | help_text="The authors of the post.", 80 | ) 81 | show_in_menus_default = True 82 | 83 | content_panels = [ 84 | FieldPanel("title"), 85 | FieldPanel("post_date"), 86 | FieldPanel("authors"), 87 | ImageChooserPanel("header_image"), 88 | FieldPanel("excerpt"), 89 | FieldPanel("categories", widget=CheckboxSelectMultiple), 90 | StreamFieldPanel("body"), 91 | ] 92 | 93 | class Meta: 94 | verbose_name = "Post" 95 | 96 | 97 | @register_setting 98 | class PeregrineSettings(BaseSetting): 99 | """ 100 | Settings for the user to customize their Peregrine blog. 101 | """ 102 | 103 | landing_page = models.ForeignKey( 104 | "wagtailcore.Page", 105 | null=True, 106 | blank=True, 107 | on_delete=models.SET_NULL, 108 | help_text="The front page for Peregrine to display. If blank, displays the latest posts.", 109 | ) 110 | post_title = models.CharField( 111 | max_length=30, 112 | default="Posts", 113 | help_text="The top navigation menu dropdown text label for latest posts.", 114 | ) 115 | post_number = models.IntegerField( 116 | default=10, 117 | help_text="The number of posts to display on the posts page.", 118 | ) 119 | post_number_nav = models.IntegerField( 120 | default=10, 121 | help_text="The number of posts to display in top navigation dropdown.", 122 | ) 123 | post_number_rss = models.IntegerField( 124 | default=100, 125 | help_text="The number of posts to include in the RSS feed.", 126 | ) 127 | revisions_to_keep = models.IntegerField( 128 | default=None, 129 | blank=True, 130 | null=True, 131 | help_text="The number of revisions to keep. If None, keeps all revisions.", 132 | ) 133 | 134 | class Meta: 135 | verbose_name = "Peregrine Settings" 136 | -------------------------------------------------------------------------------- /peregrine/static/peregrine/peregrine-style.css: -------------------------------------------------------------------------------- 1 | .fixed-nav { 2 | padding-top: 60px; 3 | } 4 | 5 | .heading { 6 | border-bottom: 1px dotted; 7 | padding-top: 1rem; 8 | padding-bottom: 0.25rem; 9 | } 10 | 11 | .figure { 12 | z-index: 9999; 13 | } 14 | 15 | header { 16 | text-align: center; 17 | padding: 100px 0 100px 0; 18 | background-attachment: scroll; 19 | background-position: center center; 20 | background-repeat: none; 21 | -webkit-background-size: cover; 22 | -moz-background-size: cover; 23 | background-size: cover; 24 | -o-background-size: cover; 25 | } 26 | 27 | header .intro-text { 28 | padding-top: 5px; 29 | padding-bottom: 5px; 30 | margin-bottom: 5px; 31 | text-shadow: -1px 0 #777, 0 1px #777, 1px 0 #777, 0 -1px #777; 32 | } 33 | 34 | header .intro-text .intro-heading { 35 | font-size: 30px; 36 | } 37 | 38 | header .intro-text .intro-lead-in { 39 | font-size: 18px; 40 | } 41 | -------------------------------------------------------------------------------- /peregrine/static/peregrine/peregrine-style.min.css: -------------------------------------------------------------------------------- 1 | .fixed-nav{padding-top:60px}.heading{border-bottom:1px dotted;padding-top:1rem;padding-bottom:.25rem} -------------------------------------------------------------------------------- /peregrine/static/peregrine/peregrine-style.min.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/peregrine/59cf20bdffc1b4fabcefcd84041ae7e6f63ca221/peregrine/static/peregrine/peregrine-style.min.css.gz -------------------------------------------------------------------------------- /peregrine/templates/peregrine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/peregrine/59cf20bdffc1b4fabcefcd84041ae7e6f63ca221/peregrine/templates/peregrine/__init__.py -------------------------------------------------------------------------------- /peregrine/templates/peregrine/base.html: -------------------------------------------------------------------------------- 1 | {% load static bootstrap4 peregrine wagtailsettings_tags %} 2 | 3 | 4 | 5 | 6 | 7 | {% spaceless %} 8 | 9 | 10 | 11 | 12 | 13 | {# #} 14 | {% block site_title %}{% settings_value 'WAGTAIL_SITE_NAME' %}{% endblock site_title %} 15 | {% block extra_head_top %} 16 | {% endblock extra_head_top %} 17 | 18 | {% block head_meta %} 19 | 20 | 21 | 22 | {% settings_value 'WAGTAIL_SITE_URL' as url_exists %} 23 | {% if url_exists %} 24 | 25 | {% endif %} 26 | 27 | {% settings_value 'WAGTAIL_SITE_IMAGE' as image_exists %} 28 | {% if image_exists %} 29 | 30 | 31 | {% else %} 32 | 33 | {% endif %} 34 | {% endblock head_meta %} 35 | 36 | {% block head_js %} 37 | {% endblock head_js %} 38 | 39 | {% block head_css %} 40 | {% bootstrap_css %} 41 | 42 | {% endblock head_css %} 43 | 44 | {% block extra_head_bottom %} 45 | {% endblock extra_head_bottom %} 46 | {% if settings.peregrine.PeregrineSettings.post_number_rss %} 47 | 48 | {% endif %} 49 | 50 | {% if request.user.is_staff %} 51 | {% load wagtailuserbar %} 52 | {% wagtailuserbar 'bottom-right' %} 53 | {% endif %} 54 | 55 | {% endspaceless %} 56 | 57 | 63 | {% block top_nav %} 64 | 86 | {% endblock top_nav %} 87 | 88 | {% block content %} 89 | {% endblock content %} 90 | 91 | {% block footer %} 92 |
93 | 102 | {% block footer_js %} 103 | {% bootstrap_javascript jquery='full' %} 104 | {% endblock footer_js %} 105 | {% endblock footer %} 106 | 107 | 108 | -------------------------------------------------------------------------------- /peregrine/templates/peregrine/site_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'peregrine/base.html' %} 2 | {% load wagtailimages_tags peregrine %} 3 | 4 | {% block site_title %}{{ page.title }} - {% settings_value 'WAGTAIL_SITE_NAME' %}{% endblock site_title %} 5 | 6 | {% block head_meta %} 7 | 8 | 9 | 10 | {% settings_value 'WAGTAIL_SITE_URL' as url_exists %} 11 | {% if url_exists %} 12 | 13 | {% endif %} 14 | 15 | {% if page.header_image %} 16 | {% image page.header_image original as header_image %} 17 | 18 | {# The ugly if statement below handles relative URLs #} 19 | 20 | 21 | {% else %} 22 | 23 | {% endif %} 24 | {% endblock head_meta %} 25 | 26 | {% block content %} 27 | {% block page_header %} 28 | {% if page.header_image %} 29 | {% image page.header_image original as header_image %} 30 |
31 |
32 |
33 |
{{ page.title }}
34 |
{{ page.excerpt|safe }}
35 |
36 | {% for category in page.categories.all %} 37 | {{ category.name }} 38 | {% endfor %} 39 |
40 |
41 | {% else %} 42 |
43 |
44 |

{{ page.title }}

45 |

{{ page.excerpt|safe }}

46 | {% block jumbotron_bottom %} 47 | {% endblock jumbotron_bottom %} 48 | {% for category in page.categories.all %} 49 | {{ category.name }} 50 | {% endfor %} 51 |
52 |
53 | {% endif %} 54 | {% endblock page_header %} 55 | 56 |
57 | {{ page.body }} 58 |
59 | {% endblock content %} 60 | 61 | 62 | -------------------------------------------------------------------------------- /peregrine/templates/peregrine/site_post.html: -------------------------------------------------------------------------------- 1 | {% extends 'peregrine/site_page.html' %} 2 | 3 | {% block jumbotron_bottom %} 4 |
5 |

6 | by 7 | {% for author in page.authors.all %} 8 | {% if not forloop.first %} 9 | , 10 | {% endif %} 11 | {{ author.username }} 12 | {% empty %} 13 | Nobody 14 | {% endfor %} 15 | on {{ page.post_date }} 16 |

17 | {% endblock jumbotron_bottom %} 18 | -------------------------------------------------------------------------------- /peregrine/templates/peregrine/site_post_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'peregrine/base.html' %} 2 | 3 | {% block content %} 4 | {% if author or category %} 5 |
6 |
7 |

8 | {% if author %} 9 | Posts by {{ author }} 10 | {% elif category %} 11 | Posts in the {{ category }} category 12 | {% endif %} 13 |

14 |
15 |
16 | {% endif %} 17 |
18 | {% if posts %} 19 |
20 | {% for post in posts %} 21 |
22 | 23 |
24 |
25 | by 26 | {% for author in post.authors.all %} 27 | {% if not forloop.first %} 28 | , 29 | {% endif %} 30 | {{ author.username }} 31 | {% empty %} 32 | Nobody 33 | {% endfor %} 34 | on {{ post.post_date }} 35 |
36 |

37 | {{ post.excerpt|safe }} 38 |

39 |
40 | Read more... 41 |
42 |
43 |
44 | {% endfor %} 45 |
46 | {# Pagination #} 47 | {% if is_paginated %} 48 | 69 | {% endif %} 70 | {% else %} 71 |

No Posts Found

72 |

There aren't any live posts yet!

73 | {% endif %} 74 |
75 | {% endblock content %} 76 | -------------------------------------------------------------------------------- /peregrine/templates/peregrine/tags/top_menu.html: -------------------------------------------------------------------------------- 1 | {% load wagtailcore_tags %} 2 | {% load peregrine %} 3 | {% for menu_item in menu_items %} 4 | {% if menu_item.show_dropdown %} 5 | 11 | {% else %} 12 | 13 | {% endif %} 14 | {% endfor %} 15 | -------------------------------------------------------------------------------- /peregrine/templates/peregrine/tags/top_menu_children.html: -------------------------------------------------------------------------------- 1 | {% load peregrine wagtailcore_tags %} 2 | 3 | {% for child in menu_items_children %} 4 | {{ child.title }} 5 | {% endfor %} 6 | -------------------------------------------------------------------------------- /peregrine/templates/peregrine/tags/top_menu_posts.html: -------------------------------------------------------------------------------- 1 | {% load wagtailcore_tags wagtailsettings_tags %} 2 | {% load peregrine %} 3 | 4 | {% if show_posts %} 5 | {% get_settings use_default_site=True %} 6 | 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /peregrine/templates/wagtailcontentstream/blocks/captioned_image.html: -------------------------------------------------------------------------------- 1 | {% load wagtailimages_tags %} 2 | 3 | {% spaceless %} 4 |
5 | {% if self.align == 'full' %} 6 | {% image self.image original class="figure-img img-fill img-responsive" %} 7 | {% elif self.align == 'center' %} 8 | {% image self.image original class="figure-img img-fluid img-responsive" %} 9 | {% else %} 10 | {% image self.image width-400 class="figure-img img-fluid img-responsive" %} 11 | {% endif %} 12 | 13 | {% if self.caption|length %} 14 |
{{ self.caption }}
15 | {% endif %} 16 | {% if self.credit|length %} 17 |
Credit: {{ self.credit }}
18 | {% endif %} 19 |
20 | {% endspaceless %} 21 | -------------------------------------------------------------------------------- /peregrine/templates/wagtailcontentstream/blocks/heading.html: -------------------------------------------------------------------------------- 1 |

{{self}}

2 | -------------------------------------------------------------------------------- /peregrine/templates/wagtailembeds/embed_frontend.html: -------------------------------------------------------------------------------- 1 | {% load wagtailembeds_tags %} 2 | 3 | {% if embed.ratio %} 4 | {# If an aspect ratio is included, use the appropriate Bootstrap 4 class #} 5 | {% if embed.ratio > 0.9 %} 6 |
7 | {% elif embed.ratio > 0.7 %} 8 |
9 | {% elif embed.ratio > 0.5 %} 10 |
11 | {% else %} 12 |
13 | {% endif %} 14 | {% else %} 15 | {# Otherwise, center it in a row, for things like Tweets. #} 16 |
17 | {% endif %} 18 | 19 | {# Embed the video with an absurdly high max width. #} 20 | {% embed embed.url max_width=3840 %} 21 |
22 | -------------------------------------------------------------------------------- /peregrine/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/peregrine/59cf20bdffc1b4fabcefcd84041ae7e6f63ca221/peregrine/templatetags/__init__.py -------------------------------------------------------------------------------- /peregrine/templatetags/peregrine.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | from wagtail.models import Site 5 | 6 | from ..models import SitePost 7 | 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag(takes_context=True) 13 | def get_site_root(context): 14 | """ 15 | NB this returns a core.Page, not the implementation-specific model used 16 | so object-comparison to self will return false as objects would differ 17 | """ 18 | return Site.find_for_request(context["request"]).root_page 19 | 20 | 21 | def has_menu_children(page): 22 | return page.get_children().live().in_menu().exists() 23 | 24 | 25 | @register.inclusion_tag("peregrine/tags/top_menu.html", takes_context=True) 26 | def top_menu(context, parent, calling_page=None): 27 | """ 28 | Retrieves the top menu items - the immediate children of the parent page 29 | The has_menu_children method is necessary because the bootstrap menu requires 30 | a dropdown class to be applied to a parent 31 | """ 32 | 33 | # Select any Page type other than SitePosts for menu inclusion. 34 | menu_items = ( 35 | parent.get_children() 36 | .filter( 37 | live=True, 38 | show_in_menus=True, 39 | ) 40 | .exclude( 41 | content_type__model="sitepost", 42 | ) 43 | ) 44 | 45 | for menu_item in menu_items: 46 | menu_item.show_dropdown = has_menu_children(menu_item) 47 | return { 48 | "calling_page": calling_page, 49 | "menu_items": menu_items, 50 | # required by the pageurl tag that we want to use within this template 51 | "request": context["request"], 52 | } 53 | 54 | 55 | @register.inclusion_tag("peregrine/tags/top_menu_children.html", takes_context=True) 56 | def top_menu_children(context, parent): 57 | """ 58 | Retrieves the children of the top menu items for the drop downs 59 | """ 60 | menu_items_children = parent.get_children() 61 | menu_items_children = menu_items_children.live().in_menu() 62 | return { 63 | "parent": parent, 64 | "menu_items_children": menu_items_children, 65 | # required by the pageurl tag that we want to use within this template 66 | "request": context["request"], 67 | } 68 | 69 | 70 | @register.inclusion_tag("peregrine/tags/top_menu_posts.html", takes_context=True) 71 | def top_menu_posts(context): 72 | """ 73 | Retrieves the 10 most recent SitePosts for the top menu drop down. 74 | """ 75 | posts = SitePost.objects.filter(live=True, show_in_menus=True,).order_by( 76 | "-post_date", 77 | )[:10] 78 | 79 | show_posts = posts.count() > 0 80 | 81 | return { 82 | "posts": posts, 83 | "show_posts": show_posts, 84 | } 85 | 86 | 87 | @register.simple_tag 88 | def settings_value(name): 89 | """ 90 | Allows a value from Django's settings to be included in a template tag. 91 | """ 92 | return getattr(settings, name, None) 93 | -------------------------------------------------------------------------------- /peregrine/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path, re_path 2 | 3 | from wagtail import urls as wagtail_urls 4 | 5 | from .views import PostsListView, AuthorPostsListView, CategoryPostsListView, PostsFeed 6 | 7 | urlpatterns = [ 8 | path("rss/", PostsFeed(), name="peregrine-rss"), 9 | path("author//", AuthorPostsListView.as_view(), name="posts-author"), 10 | path( 11 | "category//", CategoryPostsListView.as_view(), name="posts-category" 12 | ), 13 | path("", PostsListView.as_view(), name="posts"), 14 | re_path(r"", include(wagtail_urls)), 15 | ] 16 | -------------------------------------------------------------------------------- /peregrine/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.syndication.views import Feed 3 | from django.views.generic import ListView 4 | 5 | from wagtail.models import Site 6 | from wagtail.views import serve 7 | 8 | from .models import SitePost, PeregrineSettings 9 | 10 | 11 | class PostsListView(ListView): 12 | """ 13 | Paginated view of blog posts. 14 | """ 15 | 16 | template_name = "peregrine/site_post_list.html" 17 | context_object_name = "posts" 18 | 19 | def get_queryset(self): 20 | return SitePost.objects.live().order_by("-post_date") 21 | 22 | def get(self, request, *args, **kwargs): 23 | peregrine_settings = PeregrineSettings.for_site(Site.find_for_request(request)) 24 | 25 | if peregrine_settings.landing_page is None or "name" in kwargs: 26 | # If a landing page hasn't been set, or 'name' is in kwargs 27 | # because we're on an author or category page: 28 | # render list of recent posts 29 | response = super().get(request, *args, **kwargs) 30 | return response 31 | else: 32 | # There's a landing page set in settings, and no 'name' 33 | # in kwargs, so we are on the home page: 34 | # Render the user selected landing page 35 | return serve(request, peregrine_settings.landing_page.url) 36 | 37 | def get_paginate_by(self, queryset): 38 | peregrine_settings = PeregrineSettings.for_site( 39 | Site.find_for_request(self.request) 40 | ) 41 | 42 | return peregrine_settings.post_number 43 | 44 | 45 | class AuthorPostsListView(PostsListView): 46 | """ 47 | Paginated view of blog posts by an author. 48 | """ 49 | 50 | def get_queryset(self): 51 | return SitePost.objects.filter( 52 | authors__username__iexact=self.kwargs.get("name", None), 53 | ) 54 | 55 | def get_context_data(self, **kwargs): 56 | context = super(AuthorPostsListView, self).get_context_data(**kwargs) 57 | context["author"] = self.kwargs.get("name", None) 58 | return context 59 | 60 | 61 | class CategoryPostsListView(PostsListView): 62 | """ 63 | Paginated view of blog posts by category. 64 | """ 65 | 66 | def get_queryset(self): 67 | return SitePost.objects.filter( 68 | categories__name__iexact=self.kwargs["name"], 69 | ) 70 | 71 | def get_context_data(self, **kwargs): 72 | context = super(CategoryPostsListView, self).get_context_data(**kwargs) 73 | context["category"] = self.kwargs.get("name", None) 74 | return context 75 | 76 | 77 | class PostsFeed(Feed): 78 | """ 79 | RSS feed to blog posts. 80 | """ 81 | 82 | title = settings.WAGTAIL_SITE_NAME 83 | link = "/rss/" 84 | description = "Blog posts from {site_name} as they are published.".format( 85 | site_name=settings.WAGTAIL_SITE_NAME, 86 | ) 87 | 88 | def get_object(self, request, *args, **kwargs): 89 | kwargs["count"] = PeregrineSettings.for_site( 90 | Site.find_for_request(request) 91 | ).post_number_rss 92 | return kwargs["count"] 93 | 94 | def items(self, count): 95 | return SitePost.objects.live().order_by("-post_date")[:count] 96 | 97 | def item_title(self, item): 98 | return item.title 99 | 100 | def item_description(self, item): 101 | return item.excerpt 102 | 103 | def item_link(self, item): 104 | return item.url 105 | -------------------------------------------------------------------------------- /peregrine/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import connection 3 | 4 | from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register 5 | from wagtail import hooks 6 | from wagtail.models import Site 7 | 8 | from .models import Category, PeregrineSettings 9 | 10 | 11 | class CategoryAdmin(ModelAdmin): 12 | model = Category 13 | menu_label = "Peregrine Categories" 14 | menu_icon = "list-ul" 15 | add_to_settings_menu = True 16 | list_display = ("name",) 17 | search_fields = ("name",) 18 | 19 | 20 | modeladmin_register(CategoryAdmin) 21 | 22 | 23 | @hooks.register("after_edit_page") 24 | def delete_old_revisions(request, page): 25 | """ 26 | This will only keep Wagtail's most recent revisions to a page when it 27 | is saved, as set in settings. Defaults to None, which saves all 28 | revisions. 29 | """ 30 | 31 | peregrine_settings = PeregrineSettings.for_site(Site.find_for_request(request)) 32 | 33 | if peregrine_settings.revisions_to_keep is not None: 34 | pks_to_delete = page.revisions.order_by("-created_at")[ 35 | peregrine_settings.revisions_to_keep : 36 | ].values_list("id", flat=True) 37 | page.revisions.filter(pk__in=pks_to_delete).delete() 38 | 39 | # If running Zappa with S3 SQLite, perform a vacuum to keep the database size small. 40 | if ( 41 | settings.DATABASES["default"]["ENGINE"] 42 | == "zappa_django_utils.db.backends.s3sqlite" 43 | ): 44 | cursor = connection.cursor() 45 | cursor.execute("VACUUM;") 46 | cursor.close() 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="peregrine", 8 | description="Peregrine is an opinioned blog system for the Wagtail content " 9 | "management system on the Django Web Framework.", 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | author="Tim Allen", 13 | author_email="tallen@wharton.upenn.edu", 14 | url="https://github.com/FlipperPA/peregrine", 15 | include_package_data=True, 16 | packages=find_packages(), 17 | zip_safe=False, 18 | setup_requires=["setuptools_scm"], 19 | use_scm_version=True, 20 | install_requires=[ 21 | "wagtail>=3.0.1,<3.1", 22 | "wagtailcontentstream>=0.4.0", 23 | "wagtailcodeblock<=1.28.0", 24 | "django-bootstrap4>=2", 25 | ], 26 | classifiers=[ 27 | "Development Status :: 3 - Alpha", 28 | "Environment :: Web Environment", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: BSD License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.6", 34 | "Programming Language :: Python :: 3.7", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3 :: Only", 38 | "Framework :: Django", 39 | "Framework :: Django :: 2.2", 40 | "Framework :: Django :: 3.2", 41 | "Framework :: Django :: 4.0", 42 | "Framework :: Wagtail", 43 | "Framework :: Wagtail :: 2", 44 | "Topic :: Internet :: WWW/HTTP", 45 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 46 | ], 47 | ) 48 | --------------------------------------------------------------------------------