add upload image's cropping, and updating. create userpage
This commit is contained in:
parent
0216bf49cb
commit
cb71ba8911
14 changed files with 328 additions and 20 deletions
|
@ -6,6 +6,8 @@ import hashlib
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""customized user field."""
|
||||
username = models.CharField(max_length=30, unique=True) # max to 30 char
|
||||
|
@ -19,7 +21,7 @@ class User(AbstractUser):
|
|||
def upload_path(instance, orig_filename):
|
||||
|
||||
avatar_filename = hashlib.sha256(orig_filename.encode('utf-8')).hexdigest()[:10]
|
||||
return f"img/profile/user_{instance.id}/{avatar_filename}"
|
||||
return f"img/profile/user_{instance.id}/{orig_filename}"
|
||||
|
||||
avatar = models.ImageField(upload_to=upload_path)
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ USE_TZ = True
|
|||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS= [os.path.join(BASE_DIR, 'static')] # test only
|
||||
# STATIC_ROOT = '/home/kiantin1/public_html/khaikang/static/' deploy only
|
||||
STATIC_ROOT = '/home/kiantin1/public_html/khaikang/static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
|
|
@ -19,6 +19,7 @@ from django.urls import include
|
|||
from . import views
|
||||
from . import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
@ -30,6 +31,7 @@ urlpatterns = [
|
|||
path('api/get_recent_posts_counter', views.api_get_recent_posts_counter),
|
||||
path('api/get_latest_posts', views.api_get_latest_posts),
|
||||
path('api/get_previous_posts', views.api_get_previous_posts),
|
||||
path('user/<username>', views.user_timeline),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import json
|
||||
from django.http import HttpResponse
|
||||
from django.template import loader
|
||||
from .models import User, Post
|
||||
from .models import User, Post, Following
|
||||
from django.utils import timezone
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.shortcuts import redirect
|
||||
|
@ -13,7 +13,10 @@ from django.db.models import Q
|
|||
import string
|
||||
import random
|
||||
from django import forms
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
from string import Template
|
||||
from django.forms import ImageField
|
||||
import os
|
||||
|
||||
def api_get_previous_posts(request):
|
||||
if request.method == 'POST':
|
||||
|
@ -94,6 +97,7 @@ def signup(request):
|
|||
def save(self, commit=True):
|
||||
user = super(KhaikangUserCreationForm, self).save(commit=False)
|
||||
user.shown_name = self.cleaned_data["username"]
|
||||
user.avatar = "static/default_avatar.png"
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
@ -114,7 +118,6 @@ def signup(request):
|
|||
|
||||
if form.is_valid():
|
||||
form.fields['shown_name'] = form.fields['username']
|
||||
print(form.fields['shown_name'])
|
||||
form.save()
|
||||
return redirect('/account/login') #redirect to login
|
||||
|
||||
|
@ -126,33 +129,117 @@ def signup(request):
|
|||
template = loader.get_template('signup.html')
|
||||
return HttpResponse(template.render({'form': form, 'honeypot_name': honeypot_name}, request))
|
||||
|
||||
|
||||
|
||||
|
||||
def user_config(request):
|
||||
from PIL import Image
|
||||
def erase_exif(image_url):
|
||||
original_image = Image.open(image_url)
|
||||
|
||||
image_data = list(original_image.getdata())
|
||||
image_without_exif = Image.new(original_image.mode, original_image.size)
|
||||
image_without_exif.putdata(image_data)
|
||||
|
||||
image_without_exif.save(image_url)
|
||||
|
||||
"""crop image to square"""
|
||||
def crop_image(image_path):
|
||||
img = Image.open(image_path)
|
||||
width, height = img.size
|
||||
left = 0
|
||||
top = 0
|
||||
right = img.width
|
||||
bottom = img.height
|
||||
if width > height:
|
||||
left = (width - height) / 2
|
||||
right = (width + height) / 2
|
||||
elif width < height:
|
||||
top = (height - width) / 2
|
||||
bottom = (width + height) / 2
|
||||
else:
|
||||
pass
|
||||
|
||||
img = img.crop((left, top, right, bottom))
|
||||
img.save(image_path)
|
||||
|
||||
|
||||
|
||||
|
||||
class UserConfigForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ('shown_name', 'avatar', 'desc', 'email')
|
||||
fields = ('shown_name', 'avatar', 'desc', 'email')
|
||||
|
||||
|
||||
current_user = User.objects.get(id=request.user.id)
|
||||
|
||||
|
||||
if request.user == AnonymousUser():
|
||||
return redirect('/account/login') #redirect to login
|
||||
|
||||
|
||||
|
||||
if request.method == "POST":
|
||||
current_user = User.objects.get(id=request.user.id)
|
||||
old_image_path = os.path.abspath(f"./media/{current_user.avatar.name}")
|
||||
form = UserConfigForm(request.POST,request.FILES, instance=current_user)
|
||||
|
||||
|
||||
|
||||
|
||||
if form.is_valid():
|
||||
print(old_image_path)
|
||||
|
||||
if os.path.exists(old_image_path) and (old_image_path != os.path.abspath("./media/static/default_avatar.png")):
|
||||
a = os.remove(old_image_path)
|
||||
print(a)
|
||||
|
||||
form.save()
|
||||
|
||||
image_path = os.path.abspath(f"./media/{current_user.avatar.name}")
|
||||
erase_exif(image_path)
|
||||
crop_image(image_path)
|
||||
|
||||
|
||||
#
|
||||
#current_user_avatar = request.user.avatar
|
||||
#print(current_user_avatar)
|
||||
#erase_exif(current_user_avatar)
|
||||
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
current_user = User.objects.get(id=request.user.id)
|
||||
form = UserConfigForm(initial={'shown_name': request.user.shown_name,
|
||||
'desc' : request.user.desc,
|
||||
'url' : request.user.url,
|
||||
'email' : request.user.email})
|
||||
template = loader.get_template('user_config.html')
|
||||
return HttpResponse(template.render({'form': form}, request))
|
||||
return HttpResponse(template.render({'form': form, 'avatar': current_user.avatar}, request))
|
||||
|
||||
def user_timeline(request, username):
|
||||
|
||||
viewed_user = User.objects.get(username=username)
|
||||
if Following.objects.filter(follower = request.user.id).filter(followee=viewed_user.id) or request.user.id == viewed_user.id:
|
||||
viewed_timeline_list = Post.objects.filter(poster = viewed_user.id).order_by('-id')[:20]
|
||||
else:
|
||||
viewed_timeline_list = Post.objects.filter(poster = viewed_user.id).filter(privilage = 'public').order_by('-id')[:20]
|
||||
|
||||
latest_received_time = timezone.now()
|
||||
if len(viewed_timeline_list) > 0:
|
||||
oldest_received_time = viewed_timeline_list[-1].post_time
|
||||
else:
|
||||
oldest_received_time = datetime.strptime("1970/01/01 00:00", "%Y/%m/%d %H:%M")
|
||||
|
||||
template = loader.get_template('user_timeline.html')
|
||||
|
||||
context = {
|
||||
'username' : username,
|
||||
'user_shown_name' : viewed_user.shown_name,
|
||||
'latest_received_time' : latest_received_time,
|
||||
'oldest_received_time' : oldest_received_time,
|
||||
'public_timeline_list': viewed_timeline_list,
|
||||
}
|
||||
return HttpResponse(template.render(context, request))
|
||||
|
||||
def home(request):
|
||||
|
||||
|
@ -162,7 +249,11 @@ def home(request):
|
|||
public_timeline_list = Post.objects.filter(privilage = 'public').order_by('-id')[:20]
|
||||
|
||||
latest_received_time = timezone.now()
|
||||
oldest_received_time = public_timeline_list[19].post_time
|
||||
|
||||
if len(public_timeline_list) > 0:
|
||||
oldest_received_time = public_timeline_list[-1].post_time
|
||||
else:
|
||||
oldest_received_time = datetime.strptime("1970/01/01 00:00", "%Y/%m/%d %H:%M")
|
||||
|
||||
template = loader.get_template('index.html')
|
||||
|
||||
|
|
BIN
media/static/default_avatar.png
Normal file
BIN
media/static/default_avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
97
media/static/default_avatar.svg
Normal file
97
media/static/default_avatar.svg
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 512 512"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.1 (1:1.2.1+202210291244+9c6d41e410)"
|
||||
sodipodi:docname="default_logo.svg"
|
||||
inkscape:export-filename="media/default_logo.png"
|
||||
inkscape:export-xdpi="114.81297"
|
||||
inkscape:export-ydpi="114.81297"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.921875"
|
||||
inkscape:cx="390.50847"
|
||||
inkscape:cy="293.42373"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient352">
|
||||
<stop
|
||||
style="stop-color:#1b9200;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop348" />
|
||||
<stop
|
||||
style="stop-color:#007afe;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop350" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient352"
|
||||
id="linearGradient354"
|
||||
x1="0.14300847"
|
||||
y1="133.74536"
|
||||
x2="512.77649"
|
||||
y2="325.92068"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(3.188427,3.8227623)" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="圖層 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:url(#linearGradient354);fill-opacity:1;stroke-width:9.8"
|
||||
id="rect286"
|
||||
width="512"
|
||||
height="512"
|
||||
x="0"
|
||||
y="0" />
|
||||
<g
|
||||
id="g1268"
|
||||
transform="translate(0.14876459,1.2657939)">
|
||||
<g
|
||||
id="g1215"
|
||||
transform="matrix(1.1490599,0,0,1.1490599,-37.374838,1.0563917)"
|
||||
style="stroke-width:0.870277">
|
||||
<circle
|
||||
style="fill:#f9f9f9;stroke-width:8.52872"
|
||||
id="path997"
|
||||
cx="256"
|
||||
cy="137.12709"
|
||||
r="90.975639" />
|
||||
<path
|
||||
id="path1211"
|
||||
style="fill:#f9f9f9;stroke-width:8.52872"
|
||||
d="M 422.30759,442.08789 H 89.692413 c 0,0 74.458447,-218.16893 166.307587,-218.16893 91.84915,0 166.30759,218.16893 166.30759,218.16893 z"
|
||||
sodipodi:nodetypes="ccsc" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
9
static/generic.css
Normal file
9
static/generic.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
div.avatar-preview{
|
||||
|
||||
}
|
||||
|
||||
#avatar-img{
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
|
@ -17,8 +17,13 @@ x => timezoneChangingOne(x)
|
|||
|
||||
|
||||
|
||||
|
||||
$name = (x) => document.getElementsByName(x);
|
||||
$ = (x) => document.getElementById(x);
|
||||
|
||||
csrf_token = $("_token").content;
|
||||
|
||||
|
||||
var httpRequest;
|
||||
$("submit_post").addEventListener('click', make_req);
|
||||
|
||||
|
@ -27,12 +32,14 @@ async function make_req() {
|
|||
post_text = $('post_text').value;
|
||||
post_privilage = $('privil_choosing').value;
|
||||
|
||||
|
||||
|
||||
if (post_text != ""){
|
||||
|
||||
await fetch('/api/post', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-CSRFToken": "{{ csrf_token }}",
|
||||
"X-CSRFToken": csrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
@ -71,7 +78,7 @@ latest_time_string = $("latest_time").innerHTML;
|
|||
await fetch('/api/get_recent_posts_counter', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-CSRFToken": "{{ csrf_token }}",
|
||||
"X-CSRFToken": csrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
@ -105,7 +112,7 @@ oldest_time_string = $("oldest_time").innerHTML;
|
|||
await fetch('/api/get_previous_posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-CSRFToken": "{{ csrf_token }}",
|
||||
"X-CSRFToken": csrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
@ -126,7 +133,7 @@ latest_time_string = $("latest_time").innerHTML;
|
|||
await fetch('/api/get_latest_posts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-CSRFToken": "{{ csrf_token }}",
|
||||
"X-CSRFToken": csrf_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
|
21
static/user_config.js
Normal file
21
static/user_config.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
$name = (x) => document.getElementsByName(x);
|
||||
$ = (x) => document.getElementById(x);
|
||||
|
||||
avatarSelector = $name('avatar')[0];
|
||||
|
||||
avatarSelector.addEventListener('change', updateAvatarPreview);
|
||||
|
||||
|
||||
function updateAvatarPreview(event) {
|
||||
tmpFilename = avatarSelector.files[0];
|
||||
if (tmpFilename) {
|
||||
|
||||
fileSizeMaxBound = 4 * (1024 ** 2); // in MB
|
||||
if (tmpFilename.size > fileSizeMaxBound){
|
||||
alert(`The file size is ${tmpFilename.size / (1024 ** 2)} MB, exceeding ${fileSizeMaxBound / (1024 ** 2)}MB`);
|
||||
avatarSelector.value = "";
|
||||
}else{
|
||||
$('avatar-img').src = URL.createObjectURL(tmpFilename);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,13 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block meta%}
|
||||
{% endblock %}
|
||||
<meta charset="utf-8">
|
||||
{% block title %}<title>Local Library</title>{% endblock %}
|
||||
{% block title %}<title>Local Library</title>
|
||||
<link type="text/css" rel="stylesheet" href="{% static 'generic.css' %}"></style>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{% extends "base_generic.html" %}
|
||||
{% load static %}
|
||||
{% load tz %}
|
||||
{% block meta %}
|
||||
<meta id="_token" content="{{ csrf_token }}">
|
||||
{% endblock %}
|
||||
|
||||
{% get_current_timezone as TIME_ZONE %}
|
||||
{% block headbar %}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
Leave it blank: <input id ="{{ honeypot_name }}" type="text" name="{{ honeypot_name }}"><br>
|
||||
<span id="{{ honeypot_name }}-some-desc">Leave it blank: </span><input id ="{{ honeypot_name }}" type="text" name="{{ honeypot_name }}"><br>
|
||||
<button type="submit">Sign up</button>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
$ = (x) => document.getElementById(x);
|
||||
|
||||
$("{{ honeypot_name }}-some-desc").style.display = 'none';
|
||||
$("{{ honeypot_name }}").style.display = 'none';
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,10 +1,19 @@
|
|||
{% extends "base_generic.html" %}
|
||||
{% load static %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<p>{{ form.shown_name.label}}: {{ form.shown_name}}</p>
|
||||
<p> Avatar Preview: <div class="avatar-preview"><img id="avatar-img" src="{{avatar.url}}");></div></p>
|
||||
<p>{{ form.avatar.label}}: {{ form.avatar}}</p>
|
||||
<p>{{ form.desc.label}}: {{ form.desc}}</p>
|
||||
<p>{{ form.email.label}}: {{ form.email}}</p>
|
||||
<button type="submit">Refresh</button>
|
||||
|
||||
</form>
|
||||
|
||||
<script type="text/javascript" src="{% static 'user_config.js'%}">
|
||||
</script>
|
||||
{% endblock %}
|
62
templates/user_timeline.html
Normal file
62
templates/user_timeline.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
{% extends "base_generic.html" %}
|
||||
{% load static %}
|
||||
{% load tz %}
|
||||
{% block meta %}
|
||||
<meta id="_token" content="{{ csrf_token }}">
|
||||
{% endblock %}
|
||||
|
||||
{% get_current_timezone as TIME_ZONE %}
|
||||
{% block headbar %}
|
||||
{{ request.user.shown_name }} (<a href="/user/{{ request.user.username}}">My timeline</a>) - <a href="/account/config">Configs</a> - <a href="/account/logout">Log out</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" id="posting-form">
|
||||
{% csrf_token %}
|
||||
<div class="posting-form-group">
|
||||
<label>Post</label>
|
||||
|
||||
<textarea id="post_text" name="post_text" placeholder="What do you want to post?"
|
||||
maxlength="500" style="resize: none;" oninput="auto_expand(this)"></textarea>
|
||||
</div>
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
<label for="privilege">Privileges:</label>
|
||||
|
||||
<select name="privilege" id="privil_choosing">
|
||||
<option value="public" selected>Public Timeline</option>
|
||||
<option value="unpublic">Not in Public Timeline</option>
|
||||
<option value="private">Private</option>
|
||||
</select>
|
||||
|
||||
<button id="submit_post" type="button" class="btn">Post!</button>
|
||||
</form>
|
||||
<div id="intro">{{user_shown_name}}<br>{{username}}</div>
|
||||
<div id="public_timeline">
|
||||
<div id="new_post_notifier" value=""></div>
|
||||
<div id="latest_time" style="display: block;">{{latest_received_time|date:"Y-m-d H:i:s.u"}}+0000</div>
|
||||
|
||||
{% for post in public_timeline_list %}
|
||||
|
||||
<div id="post-{{post.id}}" class="post"><a href="/user/{{post.poster}}">{{public_post.poster.shown_name}}</a>
|
||||
at <a href="/post/{{post.id}}" class="post-time">{{public_post.post_time|date:"Y-m-d H:i:s.u"}}+0000</a><br/>
|
||||
{{public_post.text|linebreaksbr}}<br/>
|
||||
<span id="reply-{{post.id}}" value="{{post.id}}" class="reply">↩️</span>
|
||||
- <span id="repost-{{post.id}}" value="{{post.id}}" class="repost">🔁</span>
|
||||
- <span id="fav-{{post.id}}" value="{{post.id}}" class="fav">⭐</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="previous_post_loader">More posts</div>
|
||||
<div id="oldest_time" style="display: block;">{{oldest_received_time|date:"Y-m-d H:i:s.u"}}+0000</div>
|
||||
|
||||
</div>
|
||||
|
||||
<pre> % static 'timeline.js' 要修改
|
||||
</pre>
|
||||
<script type="text/javascript" src="{% static 'timeline.js' %}">
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue