Add accounts, login, and authentication checks
This commit is contained in:
parent
eb17b26cde
commit
0bf6f721d6
12
src/accounts/forms.py
Normal file
12
src/accounts/forms.py
Normal 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',
|
||||
]
|
||||
14
src/accounts/templates/accounts/account_create.html
Executable file
14
src/accounts/templates/accounts/account_create.html
Executable 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 %}
|
||||
83
src/accounts/templates/accounts/account_detail.html
Executable file
83
src/accounts/templates/accounts/account_detail.html
Executable 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 %}
|
||||
18
src/accounts/templates/accounts/account_form.html
Executable file
18
src/accounts/templates/accounts/account_form.html
Executable 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 %}
|
||||
23
src/accounts/templates/accounts/account_list.html
Executable file
23
src/accounts/templates/accounts/account_list.html
Executable 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 %}
|
||||
72
src/accounts/templates/accounts/profile.html
Normal file
72
src/accounts/templates/accounts/profile.html
Normal 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 %}
|
||||
10
src/accounts/templates/accounts/profile_form.html
Executable file
10
src/accounts/templates/accounts/profile_form.html
Executable 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
11
src/accounts/urls.py
Normal 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'),
|
||||
])),
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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()
|
||||
}
|
||||
|
||||
20
src/core/migrations/0005_post_recipients.py
Normal file
20
src/core/migrations/0005_post_recipients.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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)
|
||||
|
||||
@ -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 }}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
17
src/forum/middleware.py
Normal 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)
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
24
src/static/scripts/cookie.js
Normal file
24
src/static/scripts/cookie.js
Normal 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
|
||||
}
|
||||
4
src/static/scripts/timezone.js
Normal file
4
src/static/scripts/timezone.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { setCookie } from './cookie.js'
|
||||
|
||||
const { timeZone } = new Intl.DateTimeFormat().resolvedOptions()
|
||||
setCookie('timezone', timeZone)
|
||||
@ -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 {
|
||||
|
||||
@ -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 %}
|
||||
|
||||
|
||||
9
src/templates/registration/logged_out.html
Executable file
9
src/templates/registration/logged_out.html
Executable 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 %}
|
||||
33
src/templates/registration/login.html
Executable file
33
src/templates/registration/login.html
Executable 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 %}
|
||||
12
src/templates/registration/password_change_done.html
Executable file
12
src/templates/registration/password_change_done.html
Executable 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 %}
|
||||
14
src/templates/registration/password_change_form.html
Executable file
14
src/templates/registration/password_change_form.html
Executable 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 %}
|
||||
7
src/templates/registration/password_reset_complete.html
Executable file
7
src/templates/registration/password_reset_complete.html
Executable file
@ -0,0 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<article class="panel">
|
||||
<p>Password was reset successfully.</p>
|
||||
</article>
|
||||
{% endblock %}
|
||||
24
src/templates/registration/password_reset_confirm.html
Executable file
24
src/templates/registration/password_reset_confirm.html
Executable 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 %}
|
||||
7
src/templates/registration/password_reset_done.html
Executable file
7
src/templates/registration/password_reset_done.html
Executable 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 %}
|
||||
10
src/templates/registration/password_reset_email.html
Executable file
10
src/templates/registration/password_reset_email.html
Executable 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 you’ve forgotten:' %} {{ user.get_username }}
|
||||
|
||||
{% endautoescape %}
|
||||
14
src/templates/registration/password_reset_form.html
Executable file
14
src/templates/registration/password_reset_form.html
Executable 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user