Add accounts, login, and authentication checks

This commit is contained in:
Nathan Chapman 2022-07-20 15:15:48 -06:00
parent eb17b26cde
commit 0bf6f721d6
36 changed files with 673 additions and 105 deletions

12
src/accounts/forms.py Normal file
View File

@ -0,0 +1,12 @@
from django import forms
from .models import User
class AccountUpdateForm(forms.ModelForm):
class Meta:
model = User
fields = [
'first_name',
'last_name',
'email',
]

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<section class="panel">
<h1>Sign up</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create account">
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends 'base.html' %}
{% load static %}
{% load helpers %}
{% block content %}
<article class="account">
<header>
<h1>{{ object }}</h1>
{% if request.user == object %}
<p><a href="{% url 'account-update' object.pk %}">Update account</a></p>
{% endif %}
</header>
<section>
<h3>Posts</h3>
<table>
<thead>
<tr>
<th>Post</th>
<th>Comments</th>
<th>Last comment</th>
</tr>
</thead>
{% for post in posts %}
<tr class="has-link" onclick="document.location='{% url 'core:post-detail' post.topic.pk post.pk %}'">
<td>
<h4>{{ post.title }}</h4>
by <a href="{% url 'account-detail' post.author.pk %}">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
</td>
<td>{{ post.comments__count }}</td>
{% if post.comments.last is not None %}
{% with comment=post.comments.last %}
<td>
by <a href="{% url 'account-detail' comment.author.pk %}">{{ comment.author }}</a> » <time>{{ comment.created_at }}</time>
</td>
{% endwith %}
{% else %}
<td>No comments</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="3">No posts</td>
</tr>
{% endfor %}
<tbody>
</tbody>
</table>
</section>
<section>
<h3>Comments</h3>
<table>
<thead>
<tr>
<th>Content</th>
<th>Post</th>
<th>Date</th>
</tr>
</thead>
{% for comment in comments %}
<tr>
<td>{{ comment.content|markdown|safe|truncatechars_html:64 }}</td>
{% with post=comment.content_object %}
<td>
<h4><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h4>
by <a href="{% url 'account-detail' post.author.pk %}">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
</td>
{% endwith %}
<td><time>{{ comment.created_at }}</time></td>
</tr>
{% empty %}
<tr>
<td colspan="3">No comments</td>
</tr>
{% endfor %}
<tbody>
</tbody>
</table>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<section>
<h1>Update Profile</h1>
<p><a href="{% url 'password_change' %}">Change password</a></p>
<form method="post" action="{% url 'account-update' user.id %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save changes" class="action-button"> or
<a href="{% url 'core:topic-list' %}">Cancel</a>
</form>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block content %}
<section class="panel">
<h1>Users</h1>
<table>
<thead>
<th>Username</th>
<th>Name</th>
</thead>
<tbody>
{% for user in user_list %}
<tr>
<td>{{ user.username }}</td>
<td><a href="{% url 'account-detail' user.id %}">{{user.first_name}} {{user.last_name}}</a></td>
</tr>
{% empty %}
<tr><td>No users yet.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %}

View File

@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<article class="panel">
<header>
<h1 class="greeting"><em>Welcome {{profile.user.first_name}} {{profile.user.last_name }}</em>
<br>
Here's what's going on today
</h1>
</header>
<section>
<h3 class="domain__heading">Birthdays</h3>
<ul>
{% for student in birthdays %}
<li><strong><a href="{% url 'student-detail' student.pk %}">{{student}}</a></strong> is turning {{student.age|add:1}} on {{student.dob|date:"M j"}}</li>
{% empty %}
<p>No Birthdays this next week.</p>
{% endfor %}
</ul>
</section>
<section>
<h3 class="domain__heading">Today's Assignments</h3>
<ul>
{% for component in components %}
<li>
{{component.subject}}, <a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
</li>
{% empty %}
<p>Nothing for today.</p>
{% endfor %}
</ul>
</section>
<section>
<h3 class="domain__heading">Today's Attendance</h3>
{% for day in attendance %}
<p><strong><a href="{% url 'day-update' day.pk %}">{{day.date}}</a></strong></p>
<table>
<thead>
<tr>
<td>Student</td>
<td colspan="2">Status</td>
</tr>
</thead>
<tbody>
{% for entry in day.entry_set.all %}
<tr>
<td>{{entry.student}}</td>
<td>{{entry.get_status_display}}</td>
<td><a href="{% url 'entry-update' entry.pk %}">Update</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% empty %}
<p class="greeting">No attendance taken yet: <a href="{% url 'day-create' %}" class="action-button">Take attendance</a></p>
{% endfor %}
</section>
<section>
<h3 class="domain__heading">Assignments to be graded</h3>
<ul>
{% for component in ungraded_components %}
<li>
{{component.subject}}, <a href="{% url 'component-detail' component.subject.pk component.pk %}">{{component}}</a>
</li>
{% empty %}
<p>Everything is graded to far.</p>
{% endfor %}
</ul>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
<section>
<h2>Settings</h2>
<form action="{% url 'profile-update' user.profile.pk %}" method="POST">
{% csrf_token %}
{{form.as_p}}
<p>
<input type="submit" value="Save changes" class="action-button">
</p>
</form>
</section>

11
src/accounts/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, include
from . import views
urlpatterns = [
path('', views.AccountListView.as_view(), name='account-list'),
path('<int:pk>/', include([
path('', views.AccountDetailView.as_view(), name='account-detail'),
path('update/', views.AccountUpdateView.as_view(), name='account-update'),
path('delete/', views.AccountDeleteView.as_view(), name='account-delete'),
])),
]

View File

@ -1,3 +1,53 @@
from django.shortcuts import render
from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse, reverse_lazy
from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from django.views.generic.edit import (
FormView, CreateView, UpdateView, DeleteView, FormMixin
)
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.auth.forms import PasswordChangeForm
# Create your views here.
from core.models import Comment, Post
from .models import User
from .forms import AccountUpdateForm
class AccountListView(LoginRequiredMixin, ListView):
model = User
template_name = 'accounts/account_list.html'
class AccountDetailView(LoginRequiredMixin, DetailView):
model = User
template_name = 'accounts/account_detail.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['posts'] = Post.objects.filter(author=self.object).order_by('-created_at')
context['comments'] = Comment.objects.filter(author=self.object).order_by('-created_at')
return context
class AccountUpdateView(LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView):
model = User
form_class = AccountUpdateForm
template_name = 'accounts/account_form.html'
success_message = 'Account updated.'
success_url = reverse_lazy('core:topic-list')
def test_func(self):
return self.request.user.pk == self.get_object().pk
class AccountDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = User
success_url = reverse_lazy('core:topic-list')
def test_func(self):
return self.request.user.pk == self.get_object().pk

View File

@ -1,25 +0,0 @@
from django.urls import reverse
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Atom1Feed
from .models import Post
class RssLatestPostFeed(Feed):
title = 'Latest posts'
link = '/sitenews/'
description = 'A list of the latest posts.'
def items(self):
return Post.objects.order_by('-created_at')[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return item.subtitle
class AtomLatestPostFeed(RssLatestPostFeed):
feed_type = Atom1Feed
subtitle = RssLatestPostFeed.description

View File

@ -1,4 +1,5 @@
from django import forms
from django.core.mail import send_mail
from .models import Post, Comment
@ -21,16 +22,33 @@ class CommentCreateForm(forms.ModelForm):
})
}
def send_notification(self):
# subject = self.cleaned_data['content_object']
author = self.instance.author
message = self.cleaned_data.get('content')
post = Post.objects.get(pk=self.cleaned_data['object_id'])
recipients = list(post.recipients.all().values_list('email', flat=True))
recipients.remove(author.email)
send_mail(
post.title,
message,
author.email,
recipients
)
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = [
'author',
'title',
'content',
'topic'
'recipients'
]
labels = {
'content': ''
}
widgets = {
'recipients': forms.CheckboxSelectMultiple()
}

View File

@ -0,0 +1,20 @@
# Generated by Django 4.0.6 on 2022-07-20 20:35
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0004_alter_post_options_remove_post_is_published_and_more'),
]
operations = [
migrations.AddField(
model_name='post',
name='recipients',
field=models.ManyToManyField(related_name='subscriptions', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -76,6 +76,13 @@ class Comment(models.Model):
objects = CommentManager()
@property
def edited(self):
if self.created_at.strftime('%Y%m%d%H%M%S') != self.updated_at.strftime('%Y%m%d%H%M%S'):
return True
else:
return False
def get_absolute_url(self):
return reverse('core:comment-detail', kwargs={'pk': self.pk})
@ -120,6 +127,10 @@ class Post(models.Model):
)
comments = GenericRelation(Comment, related_query_name='parent')
recipients = models.ManyToManyField(
User,
related_name='subscriptions'
)
tags = GenericRelation(Tag)
created_at = models.DateTimeField(auto_now_add=True)

View File

@ -2,9 +2,18 @@
<figure class="comment">
<figcaption>
<div class="comment__header">
<h5>
<a href="" class="comment__author">{{ comment.author }}</a> » <time>{{ comment.created_at }}</time>
<a href="{% url 'account-detail' comment.author.pk %}" class="comment__author">{{ comment.author }}</a> » <time>{{ comment.created_at }}</time>
</h5>
<div>
{% if comment.edited %}
<span>Edited: <time>{{ comment.updated_at }}</time></span>
{% endif %}
{% if request.user == comment.author %}<a href="{% url 'core:comment-update' comment.pk %}">Edit</a>{% endif %}
</div>
</div>
</figcaption>
<div class="comment__content">
{{ comment.content|markdown|safe }}

View File

@ -1,13 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<article>
<h1>Update comment</h1>
<p><a href="{% url 'core:commend-delete' comment.pk %}">Delete</a></p>
<form method="POST" action="{% url 'core:comment-update' comment.pk %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Save changes"> or <a href="{% url 'core:comment-detail' comment.pk %}">cancel</a>
<input type="submit" value="Save changes"> or <a href="{{ comment.content_object.get_absolute_url }}">cancel</a>
</p>
</form>
</article>
{% endblock %}

View File

@ -22,19 +22,21 @@
<div>
<h1>{{ post.title }}</h1>
<p>
<a href="" class="post__author">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
<a href="{% url 'account-detail' post.author.pk %}" class="post__author">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
</p>
</div>
{% if request.user == post.author %}
<div>
<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>
{% endif %}
</header>
<section class="post__content">
{{ post.content|markdown|safe }}
</section>
<section class="comments detail__comments">
<h2>Discussion</h2>
<h3>Discussion</h3>
<div class="comments__list">
{% for comment in post.comments.all %}

View File

@ -1,27 +1,14 @@
{% extends 'base.html' %}
{% load static %}
{% block head %}
<script src="{% static 'scripts/codemirror.js' %}" defer></script>
<script defer type="module">
import {EditorView, keymap} from "codemirror/view"
import {defaultKeymap} from "codemirror/commands"
let myView = new EditorView({
extensions: [keymap.of(defaultKeymap)],
parent: document.querySelector('#id_content')
})
</script>
{% endblock %}
{% block content %}
<article class="post">
<h1>Update Post</h1>
<form method="POST" action="{% url 'post-update' post.pk %}" enctype="multipart/form-data">
<form method="POST" action="{% url 'core:post-update' post.topic.pk post.pk %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Save changes"> or <a href="{% url 'post-detail' post.pk %}">cancel</a>
<input type="submit" value="Save changes"> or <a href="{% url 'core:post-detail' post.topic.pk post.pk %}">cancel</a>
</p>
</form>
</article>

View File

@ -12,9 +12,10 @@
<article>
<header>
<h1>{{ topic.name }}</h1>
<p>{{ topic.description }} » {{ topic.created_at }}</p>
<h4>{{ topic.description }}</h4>
<p>Created: <time>{{ topic.created_at }}</time></p>
<p>
<a href="{% url 'core:post-create' topic.pk %}">+ New post</a>
<a href="{% url 'core:post-create' topic.pk %}" class="action-button">+ New post</a>
</p>
</header>
<section>
@ -30,13 +31,13 @@
<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 }}
by <a href="{% url 'account-detail' post.author.pk %}">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
</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 }}
by <a href="{% url 'account-detail' comment.author.pk %}">{{ comment.author }}</a> » <time>{{ comment.created_at }}</time>
</td>
{% endwith %}
{% else %}

View File

@ -5,7 +5,7 @@
<header>
<h1>Topics</h1>
<p>
<a href="{% url 'core:topic-create' %}">+ New topic</a>
<a href="{% url 'core:topic-create' %}" class="action-button">+ New topic</a>
</p>
</header>
<section>
@ -28,7 +28,7 @@
{% with post=topic.post_set.last %}
<td>
{{ post.title }}<br>
by <a href="">{{ post.author }}</a> » {{ post.created_at }}
by <a href="{% url 'account-detail' post.author.pk %}">{{ post.author }}</a> » <time>{{ post.created_at }}</time>
</td>
{% endwith %}
</tr>

View File

@ -8,7 +8,7 @@ from django.views.generic.edit import (
FormView, CreateView, UpdateView, DeleteView, FormMixin
)
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.contrib.contenttypes.models import ContentType
@ -16,15 +16,15 @@ from .models import Topic, Post, Comment
from .forms import PostForm, CommentCreateForm
class CommentListView(ListView):
class CommentListView(LoginRequiredMixin, ListView):
model = Comment
class CommentDetailView(DetailView):
class CommentDetailView(LoginRequiredMixin, DetailView):
model = Comment
class CommentCreateView(CreateView):
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentCreateForm
template_name_suffix = '_create_form'
@ -34,22 +34,31 @@ class CommentCreateView(CreateView):
def form_valid(self, form):
form.instance.author = self.request.user
form.send_notification()
return super().form_valid(form)
class CommentUpdateView(SuccessMessageMixin, UpdateView):
class CommentUpdateView(
LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView
):
model = Comment
success_message = 'Comment saved.'
fields = '__all__'
fields = ['content']
def test_func(self):
return self.request.user.pk == self.get_object().author.pk
def get_success_url(self):
return self.object.content_object.get_absolute_url()
class CommentDeleteView(SuccessMessageMixin, DeleteView):
class CommentDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Comment
success_message = 'Comment deleted.'
success_url = reverse_lazy('core:comment-list')
class TopicListView(ListView):
class TopicListView(LoginRequiredMixin, ListView):
model = Topic
def get_queryset(self):
@ -59,26 +68,29 @@ class TopicListView(ListView):
return queryset
class TopicCreateView(SuccessMessageMixin, CreateView):
class TopicCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Topic
success_message = 'Topic created.'
template_name_suffix = '_create_form'
fields = '__all__'
class TopicDetailView(DetailView):
class TopicDetailView(LoginRequiredMixin, DetailView):
model = Topic
pk_url_kwarg = 'topic_pk'
class TopicUpdateView(SuccessMessageMixin, UpdateView):
class TopicUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Topic
pk_url_kwarg = 'topic_pk'
success_message = 'Topic saved.'
fields = '__all__'
fields = [
'name',
'description'
]
class TopicDeleteView(SuccessMessageMixin, DeleteView):
class TopicDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Topic
pk_url_kwarg = 'topic_pk'
success_message = 'Topic deleted.'
@ -89,14 +101,11 @@ class PostListView(ListView):
model = Post
class PostCreateView(SuccessMessageMixin, CreateView):
class PostCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Post
success_message = 'Post created.'
template_name_suffix = '_create_form'
fields = [
'title',
'content',
]
form_class = PostForm
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
@ -111,7 +120,7 @@ class PostCreateView(SuccessMessageMixin, CreateView):
return super().form_valid(form)
class PostDetailView(DetailView):
class PostDetailView(LoginRequiredMixin, DetailView):
model = Post
pk_url_kwarg = 'post_pk'
@ -126,14 +135,19 @@ class PostDetailView(DetailView):
return context
class PostUpdateView(SuccessMessageMixin, UpdateView):
class PostUpdateView(
LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView
):
model = Post
pk_url_kwarg = 'post_pk'
success_message = 'Post saved.'
fields = '__all__'
form_class = PostForm
def test_func(self):
return self.request.user.pk == self.get_object().author.pk
class PostDeleteView(SuccessMessageMixin, DeleteView):
class PostDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Post
pk_url_kwarg = 'post_pk'
success_message = 'Post deleted.'

17
src/forum/middleware.py Normal file
View File

@ -0,0 +1,17 @@
import zoneinfo
from urllib import parse
from django.utils import timezone
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = request.COOKIES.get('timezone')
if tzname:
tzname = parse.unquote(tzname)
timezone.activate(zoneinfo.ZoneInfo(tzname))
else:
timezone.deactivate()
return self.get_response(request)

View File

@ -1,5 +1,6 @@
import os
from pathlib import Path
from django.urls import reverse_lazy
from .config import *
@ -40,6 +41,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'forum.middleware.TimezoneMiddleware',
]
@ -137,3 +139,4 @@ STATICFILES_FINDERS = (
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User'
LOGIN_REDIRECT_URL = reverse_lazy('core:topic-list')

View File

@ -2,13 +2,14 @@ from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from django.conf.urls.static import static
from core.feeds import RssLatestPostFeed, AtomLatestPostFeed
urlpatterns = [
path('', include(('core.urls', 'core'), namespace='core')),
path('sitenews/rss/', RssLatestPostFeed()),
path('sitenews/atom/', AtomLatestPostFeed()),
path('accounts/', include('accounts.urls'), name='accounts'),
path('accounts/', include('django.contrib.auth.urls')),
path('admin/', admin.site.urls),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -0,0 +1,24 @@
export function getCookie(name) {
let cookieValue = null
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
break
}
}
}
return cookieValue
}
const twentyYears = 20 * 365 * 24 * 60 * 60 * 1000
export function setCookie(name, value, expiration=twentyYears) {
const body = [ name, value ].map(encodeURIComponent).join("=")
const expires = new Date(Date.now() + expiration).toUTCString()
const cookie = `${body}; domain=; path=/; SameSite=Lax; expires=${expires}`
document.cookie = cookie
}

View File

@ -0,0 +1,4 @@
import { setCookie } from './cookie.js'
const { timeZone } = new Intl.DateTimeFormat().resolvedOptions()
setCookie('timezone', timeZone)

View File

@ -22,6 +22,7 @@ body {
color: #000000;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
a {
@ -155,7 +156,7 @@ button,
Block Elements
========================================================================== */
main {
max-width: 1200px;
max-width: 1024px;
margin: 0 auto;
}
@ -173,6 +174,12 @@ main {
align-items: center;
color: var(--color-white);
}
.site__logo {
text-decoration: none;
}
.site__logo a:hover {
color: var(--color-white);
}
.site__header h1 {
margin: 0;
}
@ -219,12 +226,25 @@ main {
}
/* ==========================================================================
Account
========================================================================== */
.account header,
.account section {
margin-bottom: 4rem;
}
/* ==========================================================================
Post
========================================================================== */
.post {
margin-bottom: 2rem;
}
.post__header {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
}
.post__content {
font-family: 'STIX Two Text';
@ -243,10 +263,41 @@ main {
padding: 1rem;
}
.comment__header {
display: flex;
justify-content: space-between;
}
.comment__content {
font-family: 'STIX Two Text';
font-size: 200%;
max-width: 64ch;
/*max-width: 64ch;*/
}
.comment__content h1,
.comment__content h2,
.comment__content h3,
.comment__content h4,
.comment__content h5 {
font-family: 'STIX Two Text';
text-transform: unset;
margin: 2em 0 1em;
}
.comment__content h1 {
font-size: 1.802em;
}
.comment__content h2 {
font-size: 1.602em;
}
.comment__content h3 {
font-size: 1.424em;
}
.comment__content h4 {
font-size: 1.266em;
}
.comment__content h5 {
font-size: 1.125em;
}
.comment p {

View File

@ -17,29 +17,36 @@
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}">
{% endcompress %}
<script type="module" defer src="{% static 'scripts/timezone.js' %}"></script>
{% block head %}
{% endblock %}
</head>
<body>
<header class="site__header">
<h1>The Forum</h1>
<h1 class="site__logo"><a href="{% url 'core:topic-list' %}">The Forum</a></h1>
{% if messages %}
<div class="messages">
{% for message in messages %}
<span {% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</span>
{% endfor %}
</div>
{% endif %}
<nav class="site__nav">
{% if user.is_authenticated %}
<menu>
<li><a href="">Register</a></li>
<li><a href="">Login</a></li>
<li><a href="{% url 'account-detail' request.user.pk %}">Profile</a></li>
<li><a href="{% url 'logout' %}">Log out</a></li>
</menu>
{% else %}
<menu>
<li><a href="{% url 'login' %}">Login</a></li>
</menu>
{% endif %}
</nav>
</header>
<main>
{% if messages %}
<section class="messages">
{% for message in messages %}
<p {% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</p>
{% endfor %}
</section>
{% endif %}
{% block breadcrumbs %}
{% endblock breadcrumbs %}

View File

@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<section>
<p>You have been logged out. <a href="{% url 'login' %}">Log in</a></p>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<h1>Log in</h1>
{% if form.errors %}
<p class="error">Your username and password didn't match. Please try again.</p>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<p>
{{ form.username.label_tag }}
{{ form.username }}
</p>
<p>
{{ form.password.label_tag }}
{{ form.password }}
<br>
<small>
Forgot your password?
<a class="password__reset hover" href="{% url 'password_reset' %}">Reset your password here</a>.
</small>
</p>
<p>
<input type="submit" value="Login" class="action-button">
<input type="hidden" name="next" value="{{ next }}">
</p>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<section>
<p>
Password has been changed.
<a href="{% url 'login' %}" class="action-button">Log in</a>
</p>
</section>
</article>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<h1>Change password</h1>
<form method="post" action="{% url 'password_change' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Change my password" class="action-button"> or
<a href="{{request.META.HTTP_REFERER}}">Cancel</a>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<p>Password was reset successfully.</p>
</article>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
{% if validlink %}
<article class="panel">
<h1>Reset password</h1>
<p>Enter a <em>new</em> password below.</p>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Reset your password" class="action-button">
</form>
</article>
{% else %}
<article class="panel">
<h1 class="error">Password reset failed</h1>
</article>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="panel">
<p>An email with password reset instructions has been sent.</p>
</article>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% load i18n %}{% autoescape off %}
{% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %}
{% translate "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% translate 'Your username, in case youve forgotten:' %} {{ user.get_username }}
{% endautoescape %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %} {% block content %}
<article class="panel">
<h1>Reset your password</h1>
<p>Enter your email address below and we'll send you instructions on how to reset your password.</p>
<form method="post" action="{% url 'password_reset' %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Send me instructions" class="action-button">
</p>
</form>
</article>
{% endblock %}