diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..4e5cf2d62b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*/__pycache__/ +__pycache__/ +*.py[cod] + +*.log +*.pot +*.pyc +__pycache__/ diff --git a/blango/settings.py b/blango/settings.py index f9209bef27..b2c5546cd2 100644 --- a/blango/settings.py +++ b/blango/settings.py @@ -11,115 +11,183 @@ """ from pathlib import Path +import os +from configurations import Configuration, values +import dj_database_url + + -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-+sn%dpa!086+g+%44z9*^j^q-u4n!j(#wl)x9a%_1op@zz2+1-' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'blango.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] +class Dev(Configuration): + # Build paths inside the project like this: BASE_DIR / 'subdir'. + BASE_DIR = Path(__file__).resolve().parent.parent -WSGI_APPLICATION = 'blango.wsgi.application' + # Quick-start development settings - unsuitable for production + # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ -# Database -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + # SECURITY WARNING: keep the secret key used in production secret! + SECRET_KEY = 'django-insecure-+sn%dpa!086+g+%44z9*^j^q-u4n!j(#wl)x9a%_1op@zz2+1-' -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} + # SECURITY WARNING: don't run with debug turned on in production! + DEBUG = values.BooleanValue(True) + ALLOWED_HOSTS = [] -# Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] + # Application definition + + INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'blog', + 'crispy_forms', + 'crispy_bootstrap5' + ] + + MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ] + + ROOT_URLCONF = 'blango.urls' + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, + ] + + WSGI_APPLICATION = 'blango.wsgi.application' + + # Database + # https://docs.djangoproject.com/en/3.2/ref/settings/#databases -# Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ + DATABASES = { + "default": dj_database_url.config(default=f"sqlite:///{BASE_DIR}/db.sqlite3"), + "alternative": dj_database_url.config( + "ALTERNATIVE_DATABASE_URL", + default=f"sqlite:///{BASE_DIR}/alternative_db.sqlite3", + ), + } -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' + # Password validation + # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators -USE_I18N = True + AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, + ] -USE_L10N = True -USE_TZ = True + # Internationalization + # https://docs.djangoproject.com/en/3.2/topics/i18n/ + LANGUAGE_CODE = 'en-us' -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.2/howto/static-files/ + TIME_ZONE = values.Value("UTC") -STATIC_URL = '/static/' + USE_I18N = True + + USE_L10N = True + + USE_TZ = True + + + # Static files (CSS, JavaScript, Images) + # https://docs.djangoproject.com/en/3.2/howto/static-files/ + + STATIC_URL = '/static/' + + # Default primary key field type + # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + + DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + ALLOWED_HOSTS = values.ListValue(["localhost", "0.0.0.0", ".codio.io"]) + + X_FRAME_OPTIONS = 'ALLOW-FROM ' + os.environ.get('CODIO_HOSTNAME') + '-8000.codio.io' + CSRF_COOKIE_SAMESITE = None + CSRF_TRUSTED_ORIGINS = ['https://' + os.environ.get('CODIO_HOSTNAME') + '-8000.codio.io'] + CSRF_COOKIE_SECURE = True + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SAMESITE = 'None' + SESSION_COOKIE_SAMESITE = 'None' + + CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" + CRISPY_TEMPLATE_PACK = "bootstrap5" + + LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": { + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + }, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + "formatter": "verbose", + }, + "mail_admins": { + "level": "ERROR", + "class": "django.utils.log.AdminEmailHandler", + "filters": ["require_debug_false"], + }, + }, + "loggers": { + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": True, + }, + }, + "root": { + "handlers": ["console"], + "level": "DEBUG", + } + } -# Default primary key field type -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + DJANGO_ADMINS="Ben Shaw,ben@example.com;Leo Lucio,leo@example.com" -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +class Prod(Dev): + DEBUG = False + SECRET_KEY = values.SecretValue() diff --git a/blango/urls.py b/blango/urls.py index cde05802f9..9e73371b75 100644 --- a/blango/urls.py +++ b/blango/urls.py @@ -15,7 +15,10 @@ """ from django.contrib import admin from django.urls import path +from blog.views import index, post_detail urlpatterns = [ path('admin/', admin.site.urls), + path("", index), + path("post//", post_detail, name="blog-post-detail") ] diff --git a/blango/wsgi.py b/blango/wsgi.py index 83565cf12c..eca8ef5433 100644 --- a/blango/wsgi.py +++ b/blango/wsgi.py @@ -9,8 +9,9 @@ import os -from django.core.wsgi import get_wsgi_application - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blango.settings') +os.environ.setdefault("DJANGO_CONFIGURATION", "Prod") + +from configurations.wsgi import get_wsgi_application application = get_wsgi_application() diff --git a/blog/__init__.py b/blog/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/admin.py b/blog/admin.py new file mode 100644 index 0000000000..fb113575cf --- /dev/null +++ b/blog/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + +# Register your models here. +from blog.models import Tag, Post, Comment + +class PostAdmin(admin.ModelAdmin): + prepopulated_fields = {"slug": ("title",)} + list_display = ("title","slug","published_at") + +admin.site.register(Tag) +admin.site.register(Post, PostAdmin) +admin.site.register(Comment) \ No newline at end of file diff --git a/blog/apps.py b/blog/apps.py new file mode 100644 index 0000000000..94788a5eac --- /dev/null +++ b/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'blog' diff --git a/blog/forms.py b/blog/forms.py new file mode 100644 index 0000000000..3a57c689f9 --- /dev/null +++ b/blog/forms.py @@ -0,0 +1,17 @@ +from django import forms + +from crispy_forms.layout import Submit +from crispy_forms.helper import FormHelper + +from blog.models import Comment + + +class CommentForm(forms.ModelForm): + class Meta: + model = Comment + fields = ["content"] + + def __init__(self, *args, **kwargs): + super(CommentForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.add_input(Submit('submit', 'Enviar Comentario')) \ No newline at end of file diff --git a/blog/migrations/0001_initial.py b/blog/migrations/0001_initial.py new file mode 100644 index 0000000000..c695c8cad6 --- /dev/null +++ b/blog/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.5 on 2024-05-17 14:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('published_at', models.DateTimeField(blank=True, null=True)), + ('title', models.TextField(max_length=100)), + ('slug', models.SlugField()), + ('summary', models.TextField(max_length=500)), + ('content', models.TextField()), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('tags', models.ManyToManyField(related_name='posts', to='blog.Tag')), + ], + ), + ] diff --git a/blog/migrations/0002_comment.py b/blog/migrations/0002_comment.py new file mode 100644 index 0000000000..fd3cd484f8 --- /dev/null +++ b/blog/migrations/0002_comment.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.5 on 2024-05-17 15:33 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blog', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField()), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/blog/migrations/0003_auto_20240517_1611.py b/blog/migrations/0003_auto_20240517_1611.py new file mode 100644 index 0000000000..9323507205 --- /dev/null +++ b/blog/migrations/0003_auto_20240517_1611.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.5 on 2024-05-17 16:11 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_comment'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='comment', + name='modified_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/blog/migrations/__init__.py b/blog/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/models.py b/blog/models.py new file mode 100644 index 0000000000..62698ccab1 --- /dev/null +++ b/blog/models.py @@ -0,0 +1,40 @@ +from django.db import models +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation +from django.contrib.contenttypes.models import ContentType + + +# Create your models here. + + +class Tag(models.Model): + value = models.TextField(max_length=100) + + def __str__(self): + return self.value + + +class Comment(models.Model): + creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + content = models.TextField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey("content_type", "object_id") + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) + + +class Post(models.Model): + author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) + published_at = models.DateTimeField(blank=True, null=True) + title = models.TextField(max_length=100) + slug = models.SlugField() + summary = models.TextField(max_length=500) + content = models.TextField() + tags = models.ManyToManyField(Tag, related_name="posts") + comments = GenericRelation(Comment) + + def __str__(self): + return self.title \ No newline at end of file diff --git a/blog/templatetags/__init__.py b/blog/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blog/templatetags/blog_extras.py b/blog/templatetags/blog_extras.py new file mode 100644 index 0000000000..4618cd0971 --- /dev/null +++ b/blog/templatetags/blog_extras.py @@ -0,0 +1,53 @@ +from django import template +from django.contrib.auth import get_user_model +from django.utils.html import format_html +from blog.models import Post + +user_model = get_user_model() + +register = template.Library() + +@register.filter +def author_details(author, current_user): + if not isinstance(author, user_model): + return "" + + if author == current_user: + return format_html("me") + + if author.first_name and author.last_name: + name = f"{author.first_name} {author.last_name}" + else: + name = f"{author.username}" + + if author.email: + prefix = format_html('', author.email) + suffix = format_html("") + else: + prefix = "" + suffix = "" + + return format_html('{}{}{}', prefix, name, suffix) + +@register.simple_tag +def row(extra_classes=""): + return format_html('
', extra_classes) + + +@register.simple_tag +def endrow(): + return format_html("
") + +@register.simple_tag +def col(extra_classes=""): + return format_html('
', extra_classes) + + +@register.simple_tag +def endcol(): + return format_html("
") + +@register.inclusion_tag("blog/post-list.html") +def recent_posts(post): + posts = Post.objects.exclude(pk=post.pk)[:5] + return {"title": "Recent Posts", "posts": posts} \ No newline at end of file diff --git a/blog/tests.py b/blog/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/blog/views.py b/blog/views.py new file mode 100644 index 0000000000..caa4c5ba5b --- /dev/null +++ b/blog/views.py @@ -0,0 +1,43 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.utils import timezone + +import logging + +from blog.models import Post +from blog.forms import CommentForm + + + +# Create your views here. +def index(request): + logger = logging.getLogger(__name__) + + posts = Post.objects.filter(published_at__lte=timezone.now()) + logger.debug("Got %d posts", len(posts)) + + return render(request, "blog/index.html", {"posts": posts}) + +def post_detail(request, slug): + post = get_object_or_404(Post, slug=slug) + logger = logging.getLogger(__name__) + if request.user.is_active: + if request.method == "POST": + comment_form = CommentForm(request.POST) + + if comment_form.is_valid(): + comment = comment_form.save(commit=False) + comment.content_object = post + comment.creator = request.user + comment.save() + logger.info( + "Created comment on Post %d for user %s", post.pk, request.user + ) + return redirect(request.path_info) + else: + comment_form = CommentForm() + else: + comment_form = None + + return render( + request, "blog/post-detail.html", {"post": post, "comment_form": comment_form} + ) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000..4dcd450935 Binary files /dev/null and b/db.sqlite3 differ diff --git a/manage.py b/manage.py index c66b327f71..9d1addf718 100644 --- a/manage.py +++ b/manage.py @@ -7,8 +7,9 @@ def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blango.settings') - try: - from django.core.management import execute_from_command_line + os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") + try: + from configurations.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..94be970cd4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,50 @@ +ansible==2.7.5 +asn1crypto==0.24.0 +backports.functools-lru-cache==1.6.1 +backports.shutil-get-terminal-size==1.0.0 +certifi==2018.1.18 +chardet==3.0.4 +cryptography==2.1.4 +cycler==0.10.0 +decorator==4.1.2 +enum34==1.1.6 +httplib2==0.9.2 +idna==2.6 +ipaddress==1.0.17 +ipython==5.5.0 +ipython-genutils==0.2.0 +Jinja2==2.10 +keyring==10.6.0 +keyrings.alt==3.0 +kiwisolver==1.1.0 +MarkupSafe==1.0 +matplotlib==2.2.4 +numpy==1.16.5 +paramiko==2.0.0 +pathlib2==2.3.0 +pexpect==4.2.1 +pickleshare==0.7.4 +prompt-toolkit==1.0.15 +pyasn1==0.4.2 +pycodestyle==2.5.0 +pycrypto==2.6.1 +pygame==1.9.6 +Pygments==2.2.0 +pygobject==3.26.1 +pyOpenSSL==17.5.0 +pyparsing==2.4.5 +python-apt==1.6.5+ubuntu0.3 +python-dateutil==2.8.1 +pytz==2019.3 +pyxdg==0.25 +PyYAML==3.12 +requests==2.18.4 +scandir==1.7 +scipy==1.2.2 +SecretStorage==2.3.1 +simplegeneric==0.8.1 +six==1.13.0 +subprocess32==3.5.4 +traitlets==4.3.2 +urllib3==1.22 +wcwidth==0.1.7 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000000..c97a299916 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,17 @@ + + + + + + + Hello, world! + + +
+ {% block content %} + + {% endblock %} +
+ + + \ No newline at end of file diff --git a/templates/blog/index.html b/templates/blog/index.html new file mode 100644 index 0000000000..390681770c --- /dev/null +++ b/templates/blog/index.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load blog_extras %} +{% block content %} +

Blog Posts

+ {% for post in posts %} + {% row "border-bottom" %} +
+

{{ post.title }}

+ {% include "blog/post-byline.html" %} +

{{ post.summary }}

+

+ ({{ post.content|wordcount }} words) + Read More +

+ {% endrow %} + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/blog/post-byline.html b/templates/blog/post-byline.html new file mode 100644 index 0000000000..0a4633b9b7 --- /dev/null +++ b/templates/blog/post-byline.html @@ -0,0 +1,2 @@ +{% load blog_extras %} +By {{ post.author|author_details:request.user }} on {{ post.published_at|date:"M, d Y" }} \ No newline at end of file diff --git a/templates/blog/post-comments.html b/templates/blog/post-comments.html new file mode 100644 index 0000000000..0259b47091 --- /dev/null +++ b/templates/blog/post-comments.html @@ -0,0 +1,29 @@ +{% load blog_extras crispy_forms_tags %} + +

Comments

+{% for comment in post.comments.all %} +{% row "border-top pt-2" %} + {% col %} +
Posted by {{ comment.creator }} at {{ comment.created_at|date:"M, d Y h:i" }}
+ {% endcol %} +{% endrow %} +{% row "border-bottom" %} + {% col %} +

{{ comment.content }}

+ {% endcol %} +{% endrow %} +{% empty %} + {% row "border-top border-bottom" %} + {% col %} +

No comments.

+ {% endcol %} + {% endrow %} +{% endfor %} +{% if request.user.is_active %} +{% row "mt-4" %} + {% col %} +

Add Comment

+ {% crispy comment_form %} + {% endcol %} +{% endrow %} +{% endif %} \ No newline at end of file diff --git a/templates/blog/post-detail.html b/templates/blog/post-detail.html new file mode 100644 index 0000000000..62d4a7c969 --- /dev/null +++ b/templates/blog/post-detail.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% load blog_extras %} +{% block content %} +

{{ post.title }}

+{% row %} +
+ {% include "blog/post-byline.html" %} +
+{% endrow %} +{% row %} +
+ {{ post.content|safe }} +
+{% endrow %} + +{% include "blog/post-comments.html" %} +{% row %} + {% col %} + {% recent_posts post %} + {% endcol %} +{% endrow %} +{% endblock %} \ No newline at end of file diff --git a/templates/blog/post-list.html b/templates/blog/post-list.html new file mode 100644 index 0000000000..35e5572af5 --- /dev/null +++ b/templates/blog/post-list.html @@ -0,0 +1,6 @@ +

{{ title }}

+ \ No newline at end of file