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 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()
|
||||||
|
}
|
||||||
|
|||||||
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()
|
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)
|
||||||
|
|||||||
@ -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 }}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
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
|
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')
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
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;
|
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 {
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|
||||||
|
|||||||
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