Added Post, Profile, and Pagination features

This commit is contained in:
Rushil Umaretiya 2020-05-26 03:17:19 -04:00
parent 7a64a098c1
commit a106673ce6
40 changed files with 474 additions and 33 deletions

View File

@ -0,0 +1,17 @@
# Generated by Django 3.0.6 on 2020-05-26 03:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_auto_20200430_0415'),
]
operations = [
migrations.RemoveField(
model_name='post',
name='title',
),
]

View File

@ -1,15 +1,18 @@
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.urls import reverse
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_posted = models.DateTimeField(auto_now_add=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
likes = models.IntegerField(default=0)
def __str__(self):
return self.title
return f"{self.author}'s post on {self.date_posted}"
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})

View File

@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\rushi\Downloads\segoe-ui-4-cufonfonts.zip

View File

@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\rushi\Downloads\segoe-ui-4-cufonfonts.zip

View File

@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\rushi\Downloads\segoe-ui-4-cufonfonts.zip

View File

@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=C:\Users\rushi\Downloads\segoe-ui-4-cufonfonts.zip

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,11 +3,16 @@
src: url(futura.ttf);
}
@font-face {
font-family: 'Segoe UI';
src: url('Segoe UI.ttf');
}
body {
background: #fafafa;
color: #333333;
margin-top: 5rem;
font-family: Futura;
font-family: 'Segoe UI';
}
h1, h2, h3, h4, h5, h6 {
@ -21,7 +26,7 @@ ul {
.bg-steel {
background: #5f788a;
background: linear-gradient(to right, #ec008c, #fc6767);
border-bottom: 3px #111 solid;
/* border-bottom: 3px #111 solid; */
}
.site-header .navbar-nav .nav-link {
@ -90,3 +95,8 @@ a.article-title:hover {
.account-heading {
font-size: 2.5rem;
}
.nounderline {
text-decoration: none !important;
color: black !important;
}

View File

@ -35,7 +35,13 @@
</div>
<!-- Navbar Right Side -->
<div class="navbar-nav">
{% if user.is_authenticated %}
<a class="nav-item nav-link" href="{% url 'post-create' %}">New Chirp</a>
<a class="nav-item nav-link" href="{% url 'profile' %}">{{ user.username }}</a>
<a class="nav-item nav-link" href="{% url 'logout' %}">Logout</a>
{% else %}
<a class="nav-item nav-link" href="{% url 'login' %}">Login</a>
{% endif %}
</div>
</div>
</div>
@ -57,6 +63,19 @@
{% endif %}
{% block content %}{% endblock %}
</div>
<div class="col-md-4">
<div class="content-section">
<h3>Trending</h3>
<ul class="list-group">
<li class="list-group-item"><a href="https://www.djangoproject.com/">#Django</a></li>
<li class="list-group-item"><a href="https://tjctf.org">#TJCTF</a></li>
<li class="list-group-item"><a href="https://sysadmins.tjhsst.edu/understudy/">#Understudy</a></li>
<li class="list-group-item"><a href="https://dadjokegenerator.com/">#DadJokes</a></li>
</ul>
</p>
</div>
</div>
</div>
</div>
</main>

View File

@ -2,14 +2,35 @@
{% block content %}
{% for post in posts %}
<article class="media content-section">
<img class="rounded-circle article-img" src="{{ post.author.profile.profile_pic.url}}" alt="Profile Picture">
<div class="media-body">
<div class="article-metadata">
<a class="mr-2" href="#">{{ post.author }}</a>
<small class="text-muted">{{ post.date_posted }}</small>
<a class="mr-2" href="{% url 'user-posts' post.author.username %}">{{ post.author.get_full_name }}</a>
<small class="text-muted">@{{ post.author }} · {{ post.date_posted }}</small>
</div>
<h2><a class="article-title" href="#">{{ post.title }}</a></h2>
<p class="article-content">{{ post.content }}</p>
<p class="article-content font-weight-light"><a class="nounderline" href="{% url 'post-detail' post.id %}">{{ post.content }}</a></p>
</div>
</article>
{% endfor %}
{% if is_paginated %}
{% if page_obj.has_previous %}
<a class="btn btn-outline-info mb-4" href="?page=1">First</a>
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<a class="btn btn-info mb-4" href="?page={{ num }}">{{ num }}</a>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a class="btn btn-outline-info mb-4" href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.next_page_number }}">Next</a>
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
{% endif %}
{% endif %}
{% endblock content %}

View File

@ -0,0 +1,27 @@
{% extends "blog/base.html" %}
{% block content %}
<div class="content-section">
<form method="post">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4"> Delete post </legend>
<h2>Are you sure you want to delete this post?</h2>
<article class="media content-section">
<img class="rounded-circle article-img" src="{{ object.author.profile.profile_pic.url}}" alt="Profile Picture">
<div class="media-body">
<div class="article-metadata">
<a class="mr-2" href="#">{{ object.author.get_full_name }}</a>
<small class="text-muted">@{{ object.author }} · {{ object.date_posted }}</small>
</div>
<p class="article-content font-weight-light">{{ post.content }}</p>
</div>
</article>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-outline-danger">Yes, Delete</button>
<a type="submit" class="btn btn-outline-secondary" href="{% url 'post-detail' object.id %}">No, take me back</a>
</div>
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,19 @@
{% extends "blog/base.html" %}
{% block content %}
<article class="media content-section">
<img class="rounded-circle article-img" src="{{ object.author.profile.profile_pic.url}}" alt="Profile Picture">
<div class="media-body">
<div class="article-metadata">
<a class="mr-2" href="{% url 'user-posts' object.author.username %}">{{ object.author.get_full_name }}</a>
<small class="text-muted">@{{ object.author }} · {{ object.date_posted }}</small>
{% if object.author == user %}
<div>
<a class="btn btn-secondary btn-sm mt-1 mb-1" href="{% url 'post-update' object.id %}">Edit chirp</a>
<a class="btn btn-danger btn-sm mt-1 mb-1" href="{% url 'post-delete' object.id %}">Delete chirp</a>
</div>
{% endif %}
</div>
<p class="article-content font-weight-light">{{ post.content }}</p>
</div>
</article>
{% endblock content %}

View File

@ -0,0 +1,16 @@
{% extends "blog/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<form method="post">
{% csrf_token %}
<fieldset class="form-group">
{{ form | crispy}}
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-outline-info">Chirp</button>
</div>
</form>
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,37 @@
{% extends "blog/base.html" %}
{% block content %}
<h1 class="mb-3">Posts by {{ view.kwargs.username }} ({{ page_obj.paginator.count }})</h1></h1>
{% for post in posts %}
<article class="media content-section">
<img class="rounded-circle article-img" src="{{ post.author.profile.profile_pic.url}}" alt="Profile Picture">
<div class="media-body">
<div class="article-metadata">
<a class="mr-2" href="{% url 'user-posts' post.author.username %}">{{ post.author.get_full_name }}</a>
<small class="text-muted">@{{ post.author }} · {{ post.date_posted }}</small>
</div>
<p class="article-content font-weight-light"><a class="nounderline" href="{% url 'post-detail' post.id %}">{{ post.content }}</a></p>
</div>
</article>
{% endfor %}
{% if is_paginated %}
{% if page_obj.has_previous %}
<a class="btn btn-outline-info mb-4" href="?page=1">First</a>
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<a class="btn btn-info mb-4" href="?page={{ num }}">{{ num }}</a>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a class="btn btn-outline-info mb-4" href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.next_page_number }}">Next</a>
<a class="btn btn-outline-info mb-4" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
{% endif %}
{% endif %}
{% endblock content %}

View File

@ -1,7 +1,23 @@
from django.urls import path
from .views import (
PostListView,
PostDetailView,
PostCreateView,
PostUpdateView,
PostDeleteView,
UserPostListView,
)
from . import views
urlpatterns = [
path('', views.home, name='blog-home'),
path('', PostListView.as_view(), name='blog-home'),
path('user/<str:username>', UserPostListView.as_view(), name='user-posts'),
path('post/<int:pk>/', PostDetailView.as_view(), name='post-detail'),
path('post/new/', PostCreateView.as_view(), name='post-create'),
path('post/<int:pk>/update/', PostUpdateView.as_view(), name='post-update'),
path('post/<int:pk>/delete/', PostDeleteView.as_view(), name='post-delete'),
path('about/', views.about, name='blog-about'),
]

View File

@ -1,4 +1,13 @@
from django.shortcuts import render
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import (
ListView,
DetailView,
CreateView,
UpdateView,
DeleteView,
)
from .models import Post
def home (request):
@ -7,5 +16,53 @@ def home (request):
}
return render(request, 'blog/home.html', context)
class PostListView(ListView):
model = Post
template_name = "blog/home.html"
context_object_name='posts'
ordering = ['-date_posted']
paginate_by = 8
class UserPostListView(ListView):
model = Post
template_name = "blog/user_posts.html"
context_object_name='posts'
paginate_by = 8
def get_queryset(self):
user = get_object_or_404(User, username=self.kwargs.get('username'))
return Post.objects.filter(author=user).order_by('-date_posted')
class PostDetailView(DetailView):
model = Post
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields=['content']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
fields=['content']
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
def test_func(self):
post = self.get_object()
return self.request.user == post.author
class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
model = Post
success_url='/'
def test_func(self):
post = self.get_object()
return self.request.user == post.author
def about (request):
return render(request, 'blog/about.html', {'title': 'About'})

View File

@ -46,6 +46,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'crispy_forms',
'oauth2_provider',
'corsheaders',
]
@ -131,3 +132,11 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
CRISPY_TEMPLATE_PACK = 'bootstrap4'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
LOGIN_REDIRECT_URL = 'blog-home'
LOGIN_URL = 'login'

View File

@ -13,6 +13,8 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from users import views as user_views
@ -20,6 +22,12 @@ from users import views as user_views
urlpatterns = [
path('', include('blog.urls')),
path('admin/', admin.site.urls),
path('login/', include('users.urls')),
path('login/', user_views.login, name='login'),
path('logout/', user_views.logout, name='logout'),
path('callback/', user_views.callback),
path('profile/', user_views.profile, name='profile'),
path("o/", include('oauth2_provider.urls', namespace='oauth2_provider')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

0
chirper/manage.py Executable file → Normal file
View File

BIN
chirper/media/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
chirper/media/default2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

View File

@ -1,3 +1,5 @@
from django.contrib import admin
from .models import Profile
# Register your models here.
admin.site.register(Profile)

View File

@ -3,3 +3,6 @@ from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals

22
chirper/users/forms.py Normal file
View File

@ -0,0 +1,22 @@
from django import forms
from django.contrib.auth.models import User
from .models import Profile
class UserUpdateForm(forms.ModelForm):
email = forms.EmailField()
def __init__(self, *args, **kwargs):
super(UserUpdateForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.pk:
self.fields['username'].widget.attrs['readonly'] = True
class Meta:
model = User
fields = ['username', 'email']
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['profile_pic']

View File

@ -0,0 +1,25 @@
# Generated by Django 3.0.6 on 2020-05-26 03:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('profile_pic', models.ImageField(default='default.jpg', upload_to='profile.pics')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.6 on 2020-05-26 03:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='profile_pic',
field=models.ImageField(default='default.jpg', upload_to='profile_pics'),
),
]

View File

@ -1,3 +1,22 @@
from django.db import models
from django.contrib.auth.models import User
from PIL import Image
# Create your models here.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
profile_pic = models.ImageField(default='default.jpg', upload_to='profile_pics')
def __str__ (self):
return f"{self.user.username}'s Profile"
def save(self):
super().save()
img = Image.open(self.profile_pic.path)
if img.height > 300 or img.width > 300:
size = (300, 300)
img.thumbnail(size)
img.save(self.profile_pic.path)

13
chirper/users/signals.py Normal file
View File

@ -0,0 +1,13 @@
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()

View File

@ -1,6 +1,9 @@
{% extends "blog/base.html" %}
{% block content %}
<div class="content-section">
<a class="btn btn-primary btn-lg btn-block" href="{{authorization_url}}">Sign in with Ion</a>
</div>
<div class="content-section">
<a href="{{ authorization_url }}" title="Ion" class="border border-dark p-3 btn btn-block btn-lg mx-auto">
<img src="https://ion.tjhsst.edu/static/img/favicon.png">
Sign in with Ion
</a>
</div>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends "blog/base.html" %}
{% block content %}
<h2>You have been logged out</h2>
<div class="border-top pt-2">
<small class="text-muted text-large">
<a href="{% url 'login' %}">Log In Again</a>
</small>
</div>
{% endblock content %}

View File

@ -0,0 +1,25 @@
{% extends "blog/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="content-section">
<div class="media">
<img class="rounded-circle account-img" src="{{ user.profile.profile_pic.url }}">
<div class="media-body">
<h2 class="account-heading">{{ user.username }}</h2>
<p class="text-secondary">{{ user.email }}</p>
</div>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<fieldset class="form-group">
<legend class="border-bottom mb-4"> Your profile </legend>
{{ userForm|crispy}}
{{ profileForm|crispy }}
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-outline-info">Update</button>
</div>
</form>
</div>
</div>
{% endblock content %}

View File

@ -1,7 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.login, name='login'),
path('callback/', views.callback)
]

View File

@ -1,19 +1,28 @@
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib import messages
from requests_oauthlib import OAuth2Session
import json
import requests
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from requests_oauthlib import OAuth2Session
from django.contrib import messages
from .forms import UserUpdateForm, ProfileUpdateForm
# Create your views here.
client_id = r'6p7HJlFCD8cnBNBEdgMsdULS5ph0jserw1xvWfxX'
client_secret = r'E1e79KebxzAp0LBEtxcUg32b0qFP9Ap9Dxqkac6Qhci5AwXFhSfrbe7MtmGJUh6DDgxivJpGgFYNQgusfvoSraDAnsq3NnEET5DmxgfBBvvuYc2bwDq6KpeKIDQqFtwz'
redirect_uri = 'http://localhost:8000/login/callback/'
redirect_uri = 'http://localhost:8000/callback/'
token_url = 'https://ion.tjhsst.edu/oauth/authorize/'
scope=["read","write"]
scope=["read"]
authorized_users=["2023rumareti", "Your ION_USERNAME"]
# Hey that's me
@ -59,11 +68,40 @@ def callback (request):
user = User.objects.create_user(username=username, email=email, password=username, first_name=first_name, last_name=last_name)
user.save()
auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
messages.success(request, f"Welcome to Chirper, {first_name}, we hope you like your stay!")
return redirect('/')
messages.success(request, f"Welcome to Chirper, {first_name}, we hope you enjoy your stay!")
return redirect('profile')
else:
messages.error(request, "Sorry, you're not an authorized Ion user!", extra_tags='danger')
return redirect('/')
return redirect('blog-home')
messages.warning(request, "Invalid Callback Response")
return redirect('/')
return redirect('blog-home')
@login_required
def logout(request):
auth_logout(request)
return render(request, 'users/logout.html')
@login_required
def profile(request):
if request.method == "POST":
userForm = UserUpdateForm(request.POST, instance=request.user)
profileForm = ProfileUpdateForm(request.POST,
request.FILES,
instance=request.user.profile)
if userForm.is_valid() and profileForm.is_valid():
userForm.save()
profileForm.save()
messages.success(request, "Your account has been updated!")
return redirect('profile')
else:
userForm = UserUpdateForm(instance=request.user)
profileForm = ProfileUpdateForm(instance=request.user.profile)
context = {
'userForm': userForm,
'profileForm': profileForm
}
return render(request, 'users/profile.html', context)