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 import forms
from django.core.mail import send_mail
from .models import Post, Comment 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 PostForm(forms.ModelForm):
class Meta: class Meta:
model = Post model = Post
fields = [ fields = [
'author',
'title', 'title',
'content', 'content',
'topic' 'recipients'
] ]
labels = { labels = {
'content': '' '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() 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): def get_absolute_url(self):
return reverse('core:comment-detail', kwargs={'pk': self.pk}) return reverse('core:comment-detail', kwargs={'pk': self.pk})
@ -120,6 +127,10 @@ class Post(models.Model):
) )
comments = GenericRelation(Comment, related_query_name='parent') comments = GenericRelation(Comment, related_query_name='parent')
recipients = models.ManyToManyField(
User,
related_name='subscriptions'
)
tags = GenericRelation(Tag) tags = GenericRelation(Tag)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)

View File

@ -2,9 +2,18 @@
<figure class="comment"> <figure class="comment">
<figcaption> <figcaption>
<h5> <div class="comment__header">
<a href="" class="comment__author">{{ comment.author }}</a> » <time>{{ comment.created_at }}</time> <h5>
</h5> <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> </figcaption>
<div class="comment__content"> <div class="comment__content">
{{ comment.content|markdown|safe }} {{ comment.content|markdown|safe }}

View File

@ -1,13 +1,14 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<h1>Update comment</h1> <article>
<p><a href="{% url 'core:commend-delete' comment.pk %}">Delete</a></p> <h1>Update comment</h1>
<form method="POST" action="{% url 'core:comment-update' comment.pk %}"> <form method="POST" action="{% url 'core:comment-update' comment.pk %}">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<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> </p>
</form> </form>
</article>
{% endblock %} {% endblock %}

View File

@ -22,19 +22,21 @@
<div> <div>
<h1>{{ post.title }}</h1> <h1>{{ post.title }}</h1>
<p> <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> </p>
</div> </div>
{% if request.user == post.author %}
<div> <div>
<a href="{% url 'core:post-update' post.topic.pk post.pk %}" class="action-button">Edit</a> <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> <a href="{% url 'core:post-delete' post.topic.pk post.pk %}" class="action-button">Delete</a>
</div> </div>
{% endif %}
</header> </header>
<section class="post__content"> <section class="post__content">
{{ post.content|markdown|safe }} {{ post.content|markdown|safe }}
</section> </section>
<section class="comments detail__comments"> <section class="comments detail__comments">
<h2>Discussion</h2> <h3>Discussion</h3>
<div class="comments__list"> <div class="comments__list">
{% for comment in post.comments.all %} {% for comment in post.comments.all %}

View File

@ -1,27 +1,14 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% 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 %} {% block content %}
<article class="post"> <article class="post">
<h1>Update Post</h1> <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 %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<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> </p>
</form> </form>
</article> </article>

View File

@ -12,9 +12,10 @@
<article> <article>
<header> <header>
<h1>{{ topic.name }}</h1> <h1>{{ topic.name }}</h1>
<p>{{ topic.description }} » {{ topic.created_at }}</p> <h4>{{ topic.description }}</h4>
<p>Created: <time>{{ topic.created_at }}</time></p>
<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> </p>
</header> </header>
<section> <section>
@ -30,13 +31,13 @@
<tr class="has-link" onclick="document.location='{% url 'core:post-detail' topic.pk post.pk %}'"> <tr class="has-link" onclick="document.location='{% url 'core:post-detail' topic.pk post.pk %}'">
<td> <td>
<h4>{{ post.title }}</h4> <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>
<td>{{ post.comments__count }}</td> <td>{{ post.comments__count }}</td>
{% if post.comments.last is not None %} {% if post.comments.last is not None %}
{% with comment=post.comments.last %} {% with comment=post.comments.last %}
<td> <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> </td>
{% endwith %} {% endwith %}
{% else %} {% else %}

View File

@ -5,7 +5,7 @@
<header> <header>
<h1>Topics</h1> <h1>Topics</h1>
<p> <p>
<a href="{% url 'core:topic-create' %}">+ New topic</a> <a href="{% url 'core:topic-create' %}" class="action-button">+ New topic</a>
</p> </p>
</header> </header>
<section> <section>
@ -28,7 +28,7 @@
{% with post=topic.post_set.last %} {% with post=topic.post_set.last %}
<td> <td>
{{ post.title }}<br> {{ 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> </td>
{% endwith %} {% endwith %}
</tr> </tr>

View File

@ -8,7 +8,7 @@ from django.views.generic.edit import (
FormView, CreateView, UpdateView, DeleteView, FormMixin FormView, CreateView, UpdateView, DeleteView, FormMixin
) )
from django.contrib import messages 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.messages.views import SuccessMessageMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -16,15 +16,15 @@ from .models import Topic, Post, Comment
from .forms import PostForm, CommentCreateForm from .forms import PostForm, CommentCreateForm
class CommentListView(ListView): class CommentListView(LoginRequiredMixin, ListView):
model = Comment model = Comment
class CommentDetailView(DetailView): class CommentDetailView(LoginRequiredMixin, DetailView):
model = Comment model = Comment
class CommentCreateView(CreateView): class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment model = Comment
form_class = CommentCreateForm form_class = CommentCreateForm
template_name_suffix = '_create_form' template_name_suffix = '_create_form'
@ -34,22 +34,31 @@ class CommentCreateView(CreateView):
def form_valid(self, form): def form_valid(self, form):
form.instance.author = self.request.user form.instance.author = self.request.user
form.send_notification()
return super().form_valid(form) return super().form_valid(form)
class CommentUpdateView(SuccessMessageMixin, UpdateView): class CommentUpdateView(
LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView
):
model = Comment model = Comment
success_message = 'Comment saved.' 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 model = Comment
success_message = 'Comment deleted.' success_message = 'Comment deleted.'
success_url = reverse_lazy('core:comment-list') success_url = reverse_lazy('core:comment-list')
class TopicListView(ListView): class TopicListView(LoginRequiredMixin, ListView):
model = Topic model = Topic
def get_queryset(self): def get_queryset(self):
@ -59,26 +68,29 @@ class TopicListView(ListView):
return queryset return queryset
class TopicCreateView(SuccessMessageMixin, CreateView): class TopicCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = Topic model = Topic
success_message = 'Topic created.' success_message = 'Topic created.'
template_name_suffix = '_create_form' template_name_suffix = '_create_form'
fields = '__all__' fields = '__all__'
class TopicDetailView(DetailView): class TopicDetailView(LoginRequiredMixin, DetailView):
model = Topic model = Topic
pk_url_kwarg = 'topic_pk' pk_url_kwarg = 'topic_pk'
class TopicUpdateView(SuccessMessageMixin, UpdateView): class TopicUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = Topic model = Topic
pk_url_kwarg = 'topic_pk' pk_url_kwarg = 'topic_pk'
success_message = 'Topic saved.' success_message = 'Topic saved.'
fields = '__all__' fields = [
'name',
'description'
]
class TopicDeleteView(SuccessMessageMixin, DeleteView): class TopicDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = Topic model = Topic
pk_url_kwarg = 'topic_pk' pk_url_kwarg = 'topic_pk'
success_message = 'Topic deleted.' success_message = 'Topic deleted.'
@ -89,14 +101,11 @@ class PostListView(ListView):
model = Post model = Post
class PostCreateView(SuccessMessageMixin, CreateView): class PostCreateView(LoginRequiredMixin, 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 = [ form_class = PostForm
'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)
@ -111,7 +120,7 @@ class PostCreateView(SuccessMessageMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
class PostDetailView(DetailView): class PostDetailView(LoginRequiredMixin, DetailView):
model = Post model = Post
pk_url_kwarg = 'post_pk' pk_url_kwarg = 'post_pk'
@ -126,14 +135,19 @@ class PostDetailView(DetailView):
return context return context
class PostUpdateView(SuccessMessageMixin, UpdateView): class PostUpdateView(
LoginRequiredMixin, UserPassesTestMixin, SuccessMessageMixin, UpdateView
):
model = Post model = Post
pk_url_kwarg = 'post_pk' pk_url_kwarg = 'post_pk'
success_message = 'Post saved.' 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 model = Post
pk_url_kwarg = 'post_pk' pk_url_kwarg = 'post_pk'
success_message = 'Post deleted.' 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 import os
from pathlib import Path from pathlib import Path
from django.urls import reverse_lazy
from .config import * from .config import *
@ -40,6 +41,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'forum.middleware.TimezoneMiddleware',
] ]
@ -137,3 +139,4 @@ STATICFILES_FINDERS = (
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User' 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.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf.urls.static import static from django.conf.urls.static import static
from core.feeds import RssLatestPostFeed, AtomLatestPostFeed
urlpatterns = [ urlpatterns = [
path('', include(('core.urls', 'core'), namespace='core')), path('', include(('core.urls', 'core'), namespace='core')),
path('sitenews/rss/', RssLatestPostFeed()), path('accounts/', include('accounts.urls'), name='accounts'),
path('sitenews/atom/', AtomLatestPostFeed()), path('accounts/', include('django.contrib.auth.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 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; color: #000000;
} }
p { p {
margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
a { a {
@ -155,7 +156,7 @@ button,
Block Elements Block Elements
========================================================================== */ ========================================================================== */
main { main {
max-width: 1200px; max-width: 1024px;
margin: 0 auto; margin: 0 auto;
} }
@ -173,6 +174,12 @@ main {
align-items: center; align-items: center;
color: var(--color-white); color: var(--color-white);
} }
.site__logo {
text-decoration: none;
}
.site__logo a:hover {
color: var(--color-white);
}
.site__header h1 { .site__header h1 {
margin: 0; margin: 0;
} }
@ -219,12 +226,25 @@ main {
} }
/* ==========================================================================
Account
========================================================================== */
.account header,
.account section {
margin-bottom: 4rem;
}
/* ========================================================================== /* ==========================================================================
Post Post
========================================================================== */ ========================================================================== */
.post {
margin-bottom: 2rem;
}
.post__header { .post__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 2rem;
} }
.post__content { .post__content {
font-family: 'STIX Two Text'; font-family: 'STIX Two Text';
@ -243,10 +263,41 @@ main {
padding: 1rem; padding: 1rem;
} }
.comment__header {
display: flex;
justify-content: space-between;
}
.comment__content { .comment__content {
font-family: 'STIX Two Text'; font-family: 'STIX Two Text';
font-size: 200%; 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 { .comment p {

View File

@ -17,29 +17,36 @@
<link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "styles/main.css" %}">
{% endcompress %} {% endcompress %}
<script type="module" defer src="{% static 'scripts/timezone.js' %}"></script>
{% block head %} {% block head %}
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<header class="site__header"> <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"> <nav class="site__nav">
{% if user.is_authenticated %}
<menu> <menu>
<li><a href="">Register</a></li> <li><a href="{% url 'account-detail' request.user.pk %}">Profile</a></li>
<li><a href="">Login</a></li> <li><a href="{% url 'logout' %}">Log out</a></li>
</menu> </menu>
{% else %}
<menu>
<li><a href="{% url 'login' %}">Login</a></li>
</menu>
{% endif %}
</nav> </nav>
</header> </header>
<main> <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 %} {% block breadcrumbs %}
{% endblock 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 %}