mobile friendly, pagination, markdown link
This commit is contained in:
parent
d507143f89
commit
04575387a9
11 changed files with 404 additions and 54 deletions
27
cwr/urls.py
27
cwr/urls.py
|
|
@ -1,25 +1,16 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from web.views import (
|
||||
signup,
|
||||
forum_threads,
|
||||
thread,
|
||||
custom_logout,
|
||||
profile,
|
||||
user_profile,
|
||||
denied,
|
||||
users,
|
||||
code_of_ethics,
|
||||
# test_email,
|
||||
manage_users
|
||||
)
|
||||
from web.views.account_management import signup, custom_logout, profile, denied, code_of_ethics, user_profile
|
||||
from web.views.admin import manage_users
|
||||
from web.views.api import users
|
||||
from web.views.forum import forum_threads, thread
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('markdownx/', include('markdownx.urls')),
|
||||
path("", forum_threads, name='index'),
|
||||
path('admin/', admin.site.urls),
|
||||
path('markdownx/', include('markdownx.urls')),
|
||||
path('api/users/', users, name='users'),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
path("accounts/profile/", profile, name="profile"),
|
||||
|
|
@ -27,10 +18,8 @@ urlpatterns = [
|
|||
path('accounts/user_management/', manage_users, name="user_management"),
|
||||
path("accounts/user/<user_id>", user_profile, name="user"),
|
||||
path("accounts/denied/", denied, name="denied"),
|
||||
path("code_of_ethics", code_of_ethics, name="code_of_ethics"),
|
||||
path('logout/', custom_logout, name='custom_logout'),
|
||||
path("", forum_threads, name='index'),
|
||||
path("code_of_ethics", code_of_ethics, name="code_of_ethics"),
|
||||
path("forum/threads/", forum_threads, name='forum_threads'),
|
||||
path("forum/thread/<thread_id>", thread, name='thread'),
|
||||
# path("test/", test_email, name='test'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -48,11 +48,8 @@
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.container {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {}
|
||||
|
||||
.nav {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -101,9 +98,10 @@
|
|||
border-color: #603814;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
|
||||
.forum-category {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
height: 40px;
|
||||
background-color: #603814;
|
||||
color: white;
|
||||
|
|
@ -177,6 +175,39 @@
|
|||
|
||||
.error {
|
||||
background-color: #ffe3e3;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.container {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 100px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
h1.header {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
color: #603814;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 35px;
|
||||
text-shadow: 1px 1px 0px #868F69, 2px 2px 0px #254117;
|
||||
font-family: "Girassol", serif;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<h1 class="header">Christians of the Internet</h1>
|
||||
|
|
@ -218,6 +249,9 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
<br>
|
||||
<div style="float:right"><small>v1.0.1</small></div>
|
||||
<div style="clear:both;"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
{% extends "base.html" %}
|
||||
{% load humanize %}
|
||||
{% block content %}
|
||||
<h2>Welcome to the Forum Area</h2>
|
||||
<h3>Welcome to the Member's Area</h3>
|
||||
|
||||
<p>
|
||||
Here is where you can receive and view announcements, let us know feedback as it comes to mind, provide prayer requests for the prayer box, and enjoy conversation with other members if you'd like.
|
||||
</p>
|
||||
|
|
@ -11,26 +13,38 @@
|
|||
<p>
|
||||
The conversation thread will be up as trial for 6 months. Please enjoy and aim to act in good faith; there are no rules besides the ones you read in order to join the ring.
|
||||
</p>
|
||||
<h2>Forum</h2>
|
||||
|
||||
{% for category, thread in threads.items %}
|
||||
<div class="forum-category">{{ category }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Thread</th>
|
||||
<th>Posts</th>
|
||||
<th>Last Post</th>
|
||||
</tr>
|
||||
{% for t in thread %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'thread' t.id %}">{{ t.title }}</a>
|
||||
<br>
|
||||
<small>{{ t.description }}</small>
|
||||
</td>
|
||||
<td>{{ t.post_count }}</td>
|
||||
<td>by {{ t.most_recent_poster }} at {{ t.most_recent_post_date }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<div class="forum-category">{{ category }} Forum</div>
|
||||
<div style="overflow-x:auto;">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Thread</th>
|
||||
<th>Posts</th>
|
||||
<th>Last Post</th>
|
||||
</tr>
|
||||
{% for t in thread %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'thread' t.id %}">{{ t.title }}</a>
|
||||
<small>
|
||||
<a href="{% url 'thread' t.id %}?page=last">>></a>
|
||||
</small>
|
||||
<br>
|
||||
<small>{{ t.description }}</small>
|
||||
</td>
|
||||
<td>{{ t.post_count }}</td>
|
||||
<td>
|
||||
{% if t.most_recent_poster_id != '#' %}
|
||||
by <a href="{{ t.most_recent_poster_id }}">{{ t.most_recent_poster }}</a> ({{ t.most_recent_post_date|naturaltime }})
|
||||
{% else %}
|
||||
by {{ t.most_recent_poster }} ({{ t.most_recent_post_date|naturaltime }})
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@
|
|||
<p>
|
||||
<label for="id_description">Description:</label>
|
||||
<br>
|
||||
<textarea name="description" cols="40" rows="10" required id="id_description">{{user.description}}
|
||||
</textarea>
|
||||
<textarea name="description" cols="40" rows="10" required id="id_description" style="max-width:100%">{{user.description}}</textarea>
|
||||
<br>
|
||||
</p>
|
||||
<button type="submit">Save</button>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,35 @@
|
|||
{% load humanize %}
|
||||
{% block head %}{% endblock %}
|
||||
{% block content %}
|
||||
<a href="{% url 'forum_threads' %}"> << Go Back</a>
|
||||
<h2>{{ thread_category }} > {{ thread_name }}</h2>
|
||||
<div style="width: 100%;">
|
||||
<div style="float:left; display: inline-block;">
|
||||
<a href="{% url 'forum_threads' %}"> << Go Back</a>
|
||||
</div>
|
||||
<div style="float:right;display:inline-block;">
|
||||
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if posts.has_previous %}
|
||||
<a href="?page=1"><<</a>
|
||||
<a href="?page={{ posts.previous_page_number }}"><</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ posts.number }} of {{ posts.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if posts.has_next %}
|
||||
<a href="?page={{ posts.next_page_number }}">></a>
|
||||
<a href="?page={{ posts.paginator.num_pages }}">>></a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="clear:both;"></div>
|
||||
|
||||
<h2>{{ thread_category }} > {{ thread_name }}</h2>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col span="1" style="width: 15%;">
|
||||
|
|
@ -13,7 +40,7 @@
|
|||
{% for post in posts %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ post.created_by.avatar }}" alt=""><br>
|
||||
<img src="{{ post.created_by.avatar }}" alt="" class="avatar"><br>
|
||||
<small>
|
||||
<a href="{% url 'user' post.created_by.id %}"><b>{{ post.created_by }}</b></a> {% if post.created_by.flair %}({{ post.created_by.flair }}) {% endif %}
|
||||
<br>
|
||||
|
|
@ -30,8 +57,35 @@
|
|||
{% endfor %}
|
||||
</table>
|
||||
<br>
|
||||
|
||||
<div style="width: 100%;">
|
||||
<div style="float:left; display: inline-block;">
|
||||
<a href="{% url 'forum_threads' %}"> << Go Back</a>
|
||||
</div>
|
||||
<div style="float:right;display:inline-block;">
|
||||
|
||||
<div class="pagination">
|
||||
<span class="step-links">
|
||||
{% if posts.has_previous %}
|
||||
<a href="?page=1"><<</a>
|
||||
<a href="?page={{ posts.previous_page_number }}"><</a>
|
||||
{% endif %}
|
||||
|
||||
<span class="current">
|
||||
Page {{ posts.number }} of {{ posts.paginator.num_pages }}
|
||||
</span>
|
||||
|
||||
{% if posts.has_next %}
|
||||
<a href="?page={{ posts.next_page_number }}">></a>
|
||||
<a href="?page={{ posts.paginator.num_pages }}">>></a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="clear:both;"></div>
|
||||
<h3 style="margin-bottom:-7px;">Post Reply:</h3>
|
||||
<small>The text editor uses markdown, to learn how to use markdown, look here.</small>
|
||||
<small>The text editor uses markdown. To learn how to use markdown <a href="https://www.markdownguide.org/cheat-sheet/" target="_blank">go here</a></small>
|
||||
<br>
|
||||
<a onclick="document.getElementById('id_content').value += ' ✝️'">✝️</a>
|
||||
<a onclick="document.getElementById('id_content').value += ' 📖'">📖</a>
|
||||
|
|
@ -99,5 +153,4 @@
|
|||
padding-top:0px"></div>
|
||||
</div>
|
||||
</form>
|
||||
<a href="{% url 'forum_threads' %}"> << Go Back</a>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div style="overflow-x:auto;">
|
||||
<table>
|
||||
<tr>
|
||||
<th>User ID</th>
|
||||
|
|
@ -31,4 +33,5 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
24
web/utils.py
Normal file
24
web/utils.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from django.core.mail import send_mail
|
||||
|
||||
|
||||
def send_email(subject, message, recipients=None):
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=message,
|
||||
from_email='noreply@christian-webring.org',
|
||||
recipient_list=recipients or ['domdit@gmail.com', 'me@domdit.com'],
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
|
||||
def is_superuser(user):
|
||||
if user.is_authenticated:
|
||||
return user.is_superuser
|
||||
return False
|
||||
|
||||
|
||||
def is_member(user):
|
||||
if user.is_authenticated:
|
||||
return user.groups.filter(name='Member').exists()
|
||||
return False
|
||||
|
||||
98
web/views/account_management.py
Normal file
98
web/views/account_management.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import base64
|
||||
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import login, logout
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.urls import reverse
|
||||
|
||||
from web.utils import send_email, is_member
|
||||
from web.forms import SignupForm, EditProfileForm
|
||||
from web.models.custom_user import CustomUser
|
||||
|
||||
|
||||
def signup(request):
|
||||
if request.method == 'POST':
|
||||
form = SignupForm(request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
login(request, user)
|
||||
messages.success(request, "You have successfully applied for membership, we will review your submission and send you an email once we have added you as a member! In the meantime, please start setting up the webring widget on your website!")
|
||||
|
||||
send_email(
|
||||
subject='New User Sign Up Alert',
|
||||
message=f'A new user has signed up for the webring, please review the user in the admin portal! \n\n ID: {user.id}, EMAIL: {user.email}, USERNAME: {user.username} \n\n https://members.christian-webring.org/accounts/user_management/ '
|
||||
)
|
||||
|
||||
send_email(
|
||||
subject='Christian Web Ring - Thanks for Joining',
|
||||
message=f'Hello {user.username}! \n Thank you for signing up for the Christian Web Ring. We will reach out to you soon once we have approved your account. In the meantime, please add the widget to your page. Instructions can be found here: https://christian-webring.nekoweb.org/widget.html \n Remember, we cannot add you to the webring until you have completed this step! \n\n Thanks, \n Christian Webring Admins',
|
||||
recipients=[user.email]
|
||||
)
|
||||
|
||||
return redirect(reverse('login'))
|
||||
else:
|
||||
form = SignupForm()
|
||||
|
||||
context = {
|
||||
'form': form
|
||||
}
|
||||
return render(request, 'signup.html', context)
|
||||
|
||||
|
||||
def denied(request):
|
||||
messages.warning(request, "You are not a member yet and cannot access the member site. Please wait for an admin to add you. If you have waited a long time, please send an email to domdit@gmail.com")
|
||||
return redirect(reverse('login'))
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_member, login_url='/accounts/denied/')
|
||||
def custom_logout(request):
|
||||
logout(request)
|
||||
return redirect(reverse('login'))
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_member, login_url='/accounts/denied/')
|
||||
def profile(request):
|
||||
if request.method == 'POST':
|
||||
form = EditProfileForm(request.POST)
|
||||
if form.is_valid():
|
||||
if request.FILES:
|
||||
avatar_size = 200, 200
|
||||
image = Image.open(request.FILES['avatar'])
|
||||
image.thumbnail(avatar_size, Image.Resampling.LANCZOS)
|
||||
|
||||
buffered = BytesIO()
|
||||
image.save(buffered, format="png")
|
||||
img_str = base64.b64encode(buffered.getvalue())
|
||||
img_base64 = bytes("data:image/png;base64,", encoding='utf-8') + img_str
|
||||
request.user.avatar = img_base64.decode('utf-8')
|
||||
|
||||
request.user.description = request.POST['description']
|
||||
request.user.url = request.POST['url']
|
||||
request.user.save()
|
||||
|
||||
form = EditProfileForm()
|
||||
|
||||
context = {
|
||||
'user': request.user,
|
||||
'form': form,
|
||||
}
|
||||
return render(request, 'profile.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_member, login_url='/accounts/denied/')
|
||||
def user_profile(request, user_id):
|
||||
context = {
|
||||
'user': get_object_or_404(CustomUser, pk=user_id),
|
||||
}
|
||||
return render(request, 'user.html', context)
|
||||
|
||||
|
||||
def code_of_ethics(request):
|
||||
return render(request, 'code_of_ethics.html')
|
||||
44
web/views/admin.py
Normal file
44
web/views/admin.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.contrib.auth.models import Group
|
||||
from django.shortcuts import render
|
||||
|
||||
from web.utils import send_email, is_superuser
|
||||
from web.models.custom_user import CustomUser
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_superuser, login_url='/accounts/denied/')
|
||||
def manage_users(request):
|
||||
context = {}
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'make_member' in request.POST:
|
||||
user_id = request.POST.get('make_member')
|
||||
user = CustomUser.objects.filter(id=user_id).first()
|
||||
member_group = Group.objects.get(name='Member')
|
||||
user.groups.add(member_group)
|
||||
send_email(
|
||||
subject='Christian Webring - You have been added as a member!',
|
||||
message=f'Hello {user.username},\n\nYou have been added as a member to the christian webring, please login here: https://members.christian-webring.org \n\n Thanks, \n the Christian Webring Team',
|
||||
recipients=[user.email]
|
||||
)
|
||||
messages.success(request, f"You added {user.username} as a member!")
|
||||
|
||||
non_members = []
|
||||
members = []
|
||||
users = CustomUser.objects.all()
|
||||
for user in users:
|
||||
if user.groups.filter(name='Member').exists():
|
||||
user.is_member = True
|
||||
members.append(user)
|
||||
else:
|
||||
user.is_member = False
|
||||
non_members.append(user)
|
||||
|
||||
non_members.extend(members)
|
||||
|
||||
context['parsed_users'] = non_members
|
||||
|
||||
return render(request, 'user_management.html', context)
|
||||
|
||||
17
web/views/api.py
Normal file
17
web/views/api.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.http import JsonResponse
|
||||
|
||||
from web.models.custom_user import CustomUser
|
||||
|
||||
|
||||
def users(request):
|
||||
parsed_users = {'users': []}
|
||||
admin_only = request.GET.get('admin', False)
|
||||
users = CustomUser.objects.filter(is_active=True).filter(groups__name='Member')
|
||||
if admin_only == 'true':
|
||||
users = users.filter(is_superuser=True)
|
||||
|
||||
users = users.all().values('username', 'description', 'url')
|
||||
for user in users:
|
||||
parsed_users['users'].append(user)
|
||||
return JsonResponse(parsed_users)
|
||||
|
||||
75
web/views/forum.py
Normal file
75
web/views/forum.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from markdownx.utils import markdownify
|
||||
|
||||
from web.utils import is_member
|
||||
from web.forms import ThreadPostForm
|
||||
from web.models.custom_user import CustomUser
|
||||
from web.models.forum_subcategory import ForumSubcategory
|
||||
from web.models.forum_post import ForumPost
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_member, login_url='/accounts/denied/')
|
||||
def forum_threads(request):
|
||||
parsed_forum_threads = {}
|
||||
forum_subcategories = ForumSubcategory.objects.filter(active=True).order_by('created_at').order_by('-sticky').all()
|
||||
|
||||
for forum_subcategory in forum_subcategories:
|
||||
if forum_subcategory.forum_category.title not in parsed_forum_threads:
|
||||
parsed_forum_threads[forum_subcategory.forum_category.title] = []
|
||||
|
||||
data = {
|
||||
'id': forum_subcategory.id,
|
||||
'title': forum_subcategory.title,
|
||||
'description': forum_subcategory.description,
|
||||
'post_count': len(forum_subcategory.posts.all()),
|
||||
'most_recent_poster': forum_subcategory.posts.last().created_by if forum_subcategory.posts.last() else 'admin',
|
||||
'most_recent_poster_id': forum_subcategory.posts.last().id if forum_subcategory.posts.last() else '#',
|
||||
'most_recent_post_date': forum_subcategory.posts.last().created_at if forum_subcategory.posts.last() else forum_subcategory.created_at,
|
||||
}
|
||||
|
||||
parsed_forum_threads[forum_subcategory.forum_category.title].append(data)
|
||||
|
||||
context = {
|
||||
'threads': parsed_forum_threads
|
||||
}
|
||||
return render(request, 'forum_threads.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(is_member, login_url='/accounts/denied/')
|
||||
def thread(request, thread_id):
|
||||
thread = get_object_or_404(ForumSubcategory, pk=thread_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
anonymous = False
|
||||
form = ThreadPostForm(request.POST)
|
||||
if form.is_valid():
|
||||
if 'anonymous' in form.data:
|
||||
if form.data['anonymous'] == 'on':
|
||||
anonymous = True
|
||||
post = ForumPost(
|
||||
content=markdownify(form.data['content']),
|
||||
forum_subcategory=thread,
|
||||
created_by=CustomUser.objects.filter(username='Anonymous').first() if anonymous else request.user,
|
||||
edited=False,
|
||||
sticky=False,
|
||||
)
|
||||
post.save()
|
||||
|
||||
form = ThreadPostForm()
|
||||
posts = ForumPost.objects.filter(forum_subcategory=thread).all()
|
||||
paginator = Paginator(posts, 5)
|
||||
page_number = request.GET.get('page')
|
||||
paginated_posts = paginator.get_page(page_number if page_number != 'last' else paginator.num_pages)
|
||||
|
||||
context = {
|
||||
'can_be_anon': thread.can_by_anon,
|
||||
'thread_name': thread.title,
|
||||
'thread_category': thread.forum_category.title,
|
||||
'posts': paginated_posts,
|
||||
'form': form
|
||||
}
|
||||
return render(request, 'thread.html', context)
|
||||
Loading…
Reference in a new issue