Add basic styling to forum

This commit is contained in:
Nathan Chapman 2022-07-20 11:07:06 -06:00
parent 70d6398863
commit eb17b26cde
16 changed files with 447 additions and 100 deletions

View File

@ -1,4 +1,7 @@
from django.contrib import admin from django.contrib import admin
from .models import Post from .models import Topic, Tag, Comment, Post
admin.site.register(Topic)
admin.site.register(Tag)
admin.site.register(Comment)
admin.site.register(Post) admin.site.register(Post)

View File

@ -11,7 +11,7 @@ class RssLatestPostFeed(Feed):
description = 'A list of the latest posts.' description = 'A list of the latest posts.'
def items(self): def items(self):
return Post.objects.order_by('-pub_date')[:5] return Post.objects.order_by('-created_at')[:5]
def item_title(self, item): def item_title(self, item):
return item.title return item.title

View File

@ -28,10 +28,7 @@ class PostForm(forms.ModelForm):
fields = [ fields = [
'author', 'author',
'title', 'title',
'subtitle',
'content', 'content',
'pub_date',
'is_published',
'topic' 'topic'
] ]
labels = { labels = {

View File

@ -0,0 +1,22 @@
# Generated by Django 4.0.6 on 2022-07-20 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='post',
name='subtitle',
),
migrations.AddField(
model_name='topic',
name='description',
field=models.CharField(blank=True, max_length=500),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.0.6 on 2022-07-20 14:19
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('core', '0002_remove_post_subtitle_topic_description'),
]
operations = [
migrations.AddField(
model_name='topic',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='topic',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.0.6 on 2022-07-20 15:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0003_topic_created_at_topic_updated_at'),
]
operations = [
migrations.AlterModelOptions(
name='post',
options={'ordering': ['-updated_at']},
),
migrations.RemoveField(
model_name='post',
name='is_published',
),
migrations.RemoveField(
model_name='post',
name='pub_date',
),
]

View File

@ -18,12 +18,16 @@ class Topic(models.Model):
a series of posts essays that are a part of a longer thread. a series of posts essays that are a part of a longer thread.
""" """
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
description = models.CharField(max_length=500, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('core:tag-detail', kwargs={'topic_pk': self.pk}) return reverse('core:topic-detail', kwargs={'topic_pk': self.pk})
class Tag(models.Model): class Tag(models.Model):
@ -87,7 +91,13 @@ class Comment(models.Model):
class PostManager(models.Manager): class PostManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related('author') return super().get_queryset().annotate(
models.Count('comments')
).select_related(
'author'
).prefetch_related(
'comments'
)
class Post(models.Model): class Post(models.Model):
@ -101,10 +111,7 @@ class Post(models.Model):
on_delete=models.SET_NULL on_delete=models.SET_NULL
) )
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
subtitle = models.CharField(max_length=255, blank=True)
content = models.TextField(blank=True) content = models.TextField(blank=True)
pub_date = models.DateTimeField(blank=True, null=True)
is_published = models.BooleanField(default=False)
topic = models.ForeignKey( topic = models.ForeignKey(
Topic, Topic,
blank=True, blank=True,
@ -126,5 +133,8 @@ class Post(models.Model):
'post_pk': self.pk 'post_pk': self.pk
}) })
def __str__(self):
return self.title
class Meta: class Meta:
ordering = ['-pub_date'] ordering = ['-updated_at']

View File

@ -2,11 +2,11 @@
<figure class="comment"> <figure class="comment">
<figcaption> <figcaption>
<span class="comment__author">{{ comment.author }}</span><br> <h5>
<time>{{ comment.created_at }}</time> <a href="" class="comment__author">{{ comment.author }}</a> » <time>{{ comment.created_at }}</time>
</h5>
</figcaption> </figcaption>
<div> <div class="comment__content">
{{ comment.content|markdown|safe }} {{ comment.content|markdown|safe }}
</div> </div>
<div class="comment__options">&hellip;</div>
</figure> </figure>

View File

@ -2,12 +2,12 @@
{% block content %} {% block content %}
<h1>Delete Post</h1> <h1>Delete Post</h1>
<form method="POST" action="{% url 'core:post-delete' post.pk %}"> <form method="POST" action="{% url 'core:post-delete' post.topic.pk post.pk %}">
{% csrf_token %} {% csrf_token %}
<p>Are you sure you want to delete "{{ post }}"?</p> <p>Are you sure you want to delete "{{ post }}"?</p>
{{ form.as_p }} {{ form.as_p }}
<p> <p>
<input type="submit" value="Delete"> or <a href="{% url 'core:post-detail' post.pk %}">cancel</a> <input type="submit" value="Delete"> or <a href="{% url 'core:post-detail' post.topic.pk post.pk %}">cancel</a>
</p> </p>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,12 +1,22 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<menu>
<li><strong><a href="{% url 'core:topic-list' %}">Topics</a></strong></li>
<span></span>
<li><a href="{% url 'core:topic-detail' topic.pk %}">{{ topic.name }}</a></li>
</menu>
</div>
{% endblock breadcrumbs %}
{% block content %} {% block content %}
<h1>+ New Post</h1> <h1>+ New Post</h1>
<form method="POST" action="{% url 'core:post-create' topic.pk %}" enctype="multipart/form-data"> <form method="POST" action="{% url 'core:post-create' topic.pk %}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<p> <p>
<input type="submit" value="Create"> or <a href="{% url 'core:post-list' topic.pk %}">cancel</a> <input type="submit" value="Create"> or <a href="{% url 'core:topic-detail' topic.pk %}">cancel</a>
</p> </p>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -6,17 +6,29 @@
<script type="module" src="{% static 'scripts/comments.js' %}" defer></script> <script type="module" src="{% static 'scripts/comments.js' %}" defer></script>
{% endblock head %} {% endblock head %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<menu>
<li><strong><a href="{% url 'core:topic-list' %}">Topics</a></strong></li>
<span></span>
<li><a href="{% url 'core:topic-detail' post.topic.pk %}">{{ post.topic.name }}</a></li>
</menu>
</div>
{% endblock breadcrumbs %}
{% block content %} {% block content %}
<article class="post"> <article class="post">
<header> <header class="post__header">
<figure> <div>
<img class="post__image" src="{{ post.image.url }}" alt=""> <h1>{{ post.title }}</h1>
</figure> <p>
<h1>{{ post.title }}</h1> <a href="" class="post__author">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
<p> </p>
<a href="{% url 'core:post-update' post.topic.pk post.pk %}">Edit</a> </div>
<a href="{% url 'core:post-delete' post.topic.pk post.pk %}">Delete</a> <div>
</p> <a href="{% url 'core:post-update' post.topic.pk post.pk %}" class="action-button">Edit</a>
<a href="{% url 'core:post-delete' post.topic.pk post.pk %}" class="action-button">Delete</a>
</div>
</header> </header>
<section class="post__content"> <section class="post__content">
{{ post.content|markdown|safe }} {{ post.content|markdown|safe }}
@ -27,7 +39,6 @@
<div class="comments__list"> <div class="comments__list">
{% for comment in post.comments.all %} {% for comment in post.comments.all %}
{% include 'core/comment_detail.html' with comment=comment %} {% include 'core/comment_detail.html' with comment=comment %}
<hr>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -1,19 +1,55 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block breadcrumbs %}
<h1>Topic</h1> <div class="breadcrumbs">
<p> <menu>
<a href="{% url 'core:topic-update' topic.pk %}">Edit</a> <li><strong><a href="{% url 'core:topic-list' %}">Topics</a></strong></li>
<a href="{% url 'core:topic-delete' topic.pk %}">Delete</a> </menu>
</p> </div>
<p>{{ topic }}</p> {% endblock breadcrumbs %}
{% block content %}
<article>
<header>
<h1>{{ topic.name }}</h1>
<p>{{ topic.description }} » {{ topic.created_at }}</p>
<p>
<a href="{% url 'core:post-create' topic.pk %}">+ New post</a>
</p>
</header>
<section> <section>
<a href="{% url 'core:post-create' topic.pk %}">+ New post</a> <table>
{% for post in topic.post_list.all %} <thead>
<h3><a href="{% url 'core:post-detail' topic.pk post.pk %}">{{ post.title }}</a></h3> <tr>
{% empty %} <th>Post</th>
<p>No posts</p> <th>Comments</th>
{% endfor %} <th>Last comment</th>
</tr>
</thead>
{% for post in topic.post_set.all %}
<tr class="has-link" onclick="document.location='{% url 'core:post-detail' topic.pk post.pk %}'">
<td>
<h4>{{ post.title }}</h4>
by <a href="">{{ post.author }}</a> » {{ post.created_at }}
</td>
<td>{{ post.comments__count }}</td>
{% if post.comments.last is not None %}
{% with comment=post.comments.last %}
<td>
by <a href="">{{ comment.author }}</a> » {{ comment.created_at }}
</td>
{% endwith %}
{% else %}
<td>No comments</td>
{% endif %}
</tr>
{% empty %}
<p>No posts</p>
{% endfor %}
<tbody>
</tbody>
</table>
</section> </section>
</article>
{% endblock %} {% endblock %}

View File

@ -2,21 +2,43 @@
{% block content %} {% block content %}
<article> <article>
<header>
<h1>Topics</h1> <h1>Topics</h1>
<a href="{% url 'core:topic-create' %}">+ New topic</a> <p>
<a href="{% url 'core:topic-create' %}">+ New topic</a>
</p>
</header>
<section> <section>
{% for topic in topic_list %} <table>
<h3><a href="{% url 'core:topic-detail' topic.pk %}">{{ topic.name }}</a></h3> <thead>
<ul> <tr>
{% for post in topic.post_list.all %} <th>Topics</th>
<li><a href="{% url 'core:post-detail' topic.pk post.pk %}">{{ post.title }}</a></li> <th>Posts</th>
<th>Last post</th>
</tr>
</thead>
<tbody>
{% for topic in topic_list %}
<tr class="has-link" onclick="document.location='{% url 'core:topic-detail' topic.pk %}'">
<td>
<h4>{{ topic.name }}</a></h4>
{{ topic.description }} » {{ topic.created_at }}
</td>
<td>{{ topic.post__count }}</td>
{% with post=topic.post_set.last %}
<td>
{{ post.title }}<br>
by <a href="">{{ post.author }}</a> » {{ post.created_at }}
</td>
{% endwith %}
</tr>
{% empty %} {% empty %}
<li>No posts yet</li> <tr>
<li colspan="3">No topics yet</li>
</tr>
{% endfor %} {% endfor %}
</ul> </tbody>
{% empty %} </table>
<p>No topics yet.</p>
{% endfor %}
</section> </section>
</article> </article>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.db import models
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.list import ListView from django.views.generic.list import ListView
@ -51,6 +52,12 @@ class CommentDeleteView(SuccessMessageMixin, DeleteView):
class TopicListView(ListView): class TopicListView(ListView):
model = Topic model = Topic
def get_queryset(self):
queryset = Topic.objects.annotate(
models.Count('post')
)
return queryset
class TopicCreateView(SuccessMessageMixin, CreateView): class TopicCreateView(SuccessMessageMixin, CreateView):
model = Topic model = Topic
@ -86,13 +93,23 @@ class PostCreateView(SuccessMessageMixin, CreateView):
model = Post model = Post
success_message = 'Post created.' success_message = 'Post created.'
template_name_suffix = '_create_form' template_name_suffix = '_create_form'
fields = '__all__' fields = [
'title',
'content',
]
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['topic'] = get_object_or_404(Topic, pk=self.kwargs['topic_pk']) context['topic'] = get_object_or_404(Topic, pk=self.kwargs['topic_pk'])
return context return context
def form_valid(self, form):
form.instance.author = self.request.user
form.instance.topic = get_object_or_404(
Topic, pk=self.kwargs['topic_pk']
)
return super().form_valid(form)
class PostDetailView(DetailView): class PostDetailView(DetailView):
model = Post model = Post
@ -120,4 +137,4 @@ class PostDeleteView(SuccessMessageMixin, DeleteView):
model = Post model = Post
pk_url_kwarg = 'post_pk' pk_url_kwarg = 'post_pk'
success_message = 'Post deleted.' success_message = 'Post deleted.'
success_url = reverse_lazy('article-list') success_url = reverse_lazy('core:topic-list')

View File

@ -1,3 +1,13 @@
:root {
--color-white: white;
--color-blue: #0070ff;
--color-darkgrey: #393939;
--color-lightgrey: #f7f7f7;
--color-highlight: #c0deea;
--table-border: black;
}
html { html {
font-size: 75%; font-size: 75%;
} }
@ -6,18 +16,26 @@ body {
background: white; background: white;
font-family: 'Montserrat', serif; font-family: 'Montserrat', serif;
font-weight: 400; font-weight: 400;
margin: 0;
padding: 0;
line-height: 1; line-height: 1;
color: #000000; color: #000000;
} }
p { p {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
a {
color: inherit;
}
a:hover {
color: var(--color-blue);
}
h1, h1,
h2, h2,
h3, h3,
h4, h4,
h5 { h5 {
margin: 3rem 0 1.38rem; margin: 0 0 1rem;
font-family: 'Montserrat', sans-serif; font-family: 'Montserrat', sans-serif;
text-transform: uppercase; text-transform: uppercase;
font-weight: 700; font-weight: 700;
@ -43,57 +61,204 @@ small {
font-size: 0.889rem; font-size: 0.889rem;
} }
figure {
/* ==========================================================================
Article
========================================================================== */
.post {
font-family: 'STIX Two Text';
max-width: 1024px;
margin: 0 auto;
font-size: 1.25rem;
}
.post p {
line-height: 1.75;
margin-bottom: 1rem;
}
.post h1,
.post h2,
.post h3,
.post h4,
.post h5 {
margin: 3rem 0 1.38rem;
font-family: 'Montserrat', serif;
line-height: 1.3;
}
.post h1 {
margin-top: 0;
}
.post__content {
max-width: 64ch;
margin: 2rem auto;
}
.post blockquote {
padding: 0.5rem 1rem;
margin: 0 0 1rem;
padding-left: 2rem;
border-left: 0.0125rem solid black;
}
.post figure {
width: 1000px;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.post__image {
/* Lists
========================================================================== */
ul {
list-style-type: square;
}
/* Tables
========================================================================== */
table {
border-collapse: collapse;
width: 100%; width: 100%;
text-align: left;
border-bottom: var(--table-border);
background-color: var(--color-lightgrey);
}
thead {
background-color: var(--color-darkgrey);
color: var(--color-white);
border-bottom: var(--table-border);
}
thead a {
color: inherit;
text-decoration: none;
}
tr:nth-child(even) {
/*background-color: var(--color-light-gray);*/
}
th, td {
padding: 1rem;
}
tr {
/*border-bottom: var(--table-border);*/
}
@media screen and (max-width: 600px) {
tr {
display: grid;
grid-template-columns: 1fr;
}
}
tbody tr {
border-bottom: 0.25rem solid var(--color-white);
}
tbody tr:hover {
background-color: var(--color-highlight);
}
tbody tr.has-link {
cursor: pointer;
}
/* ==========================================================================
Forms
========================================================================== */
textarea {
box-sizing: border-box;
font: inherit;
font-size: 1.25rem;
padding: 0.5rem 1rem;
width: 100%;
resize: vertical;
line-height: 1.75;
}
button,
.action-button {
cursor: pointer;
border: none;
font: inherit;
background-color: var(--color-darkgrey);
color: var(--color-white);
text-decoration: none;
padding: 0.5rem;
font-weight: bold;
box-sizing: border-box;
display: inline-block;
}
.action-button:hover {
background-color: var(--color-highlight);
}
/* ==========================================================================
Block Elements
========================================================================== */
main {
max-width: 1200px;
margin: 0 auto;
}
/* ==========================================================================
Header
========================================================================== */
.site__header {
margin-bottom: 1rem;
background-color: var(--color-darkgrey);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--color-white);
}
.site__header h1 {
margin: 0;
}
.site__header a {
text-transform: lowercase;
font-variant: small-caps;
text-decoration: none;
}
/* Site Nav
========================================================================== */
.site__nav menu {
margin: 0;
padding: 0;
list-style: none;
display: flex;
}
.site__nav menu li:not(:last-child) {
margin-right: 1rem;
}
.site__nav a {
font-weight: bold;
}
/* Breadcrumbs
========================================================================== */
.breadcrumbs {
background-color: var(--color-light-gray);
}
.breadcrumbs menu {
margin: 1rem 0;
padding: 0;
line-height: 1.75;
list-style: none;
display: flex;
/*justify-content: center;*/
flex-wrap: wrap;
}
.breadcrumbs menu li:not(:last-child),
.breadcrumbs menu span:not(:last-child) {
margin-right: 0.5rem;
}
/* ==========================================================================
Post
========================================================================== */
.post__header {
display: flex;
justify-content: space-between;
}
.post__content {
font-family: 'STIX Two Text';
font-size: 200%;
margin-bottom: 4rem;
} }
/* ========================================================================== /* ==========================================================================
Editor Comments
========================================================================== */ ========================================================================== */
.comment {
margin: 2rem 0;
background-color: var(--color-lightgrey);
padding: 1rem;
}
.comment__content {
font-family: 'STIX Two Text';
font-size: 200%;
max-width: 64ch;
}
.comment p {
line-height: 1.75;
}
.comment ul {
line-height: 1.75;
}
.comments__form textarea {
font-family: 'STIX Two Text';
font-size: 200%;
line-height: 1.75;
}

View File

@ -23,8 +23,12 @@
<body> <body>
<header class="site__header"> <header class="site__header">
<h1>The Forum</h1>
<nav class="site__nav"> <nav class="site__nav">
<menu>
<li><a href="">Register</a></li>
<li><a href="">Login</a></li>
</menu>
</nav> </nav>
</header> </header>
<main> <main>