comments
This commit is contained in:
parent
100fe79b1d
commit
4b157ed1d1
15 changed files with 305 additions and 133 deletions
|
@ -14,7 +14,13 @@ admin.site.register(models.Item, ItemAdmin)
|
|||
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
search_fields = ['item__title', 'item__url', 'text', 'name', 'email']
|
||||
search_fields = [
|
||||
'item__title',
|
||||
'item__url',
|
||||
'text',
|
||||
'name',
|
||||
'email'
|
||||
]
|
||||
list_display = ['__str__', 'published']
|
||||
list_filter = (
|
||||
("published", admin.EmptyFieldListFilter),
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
from django.utils.timezone import datetime, timedelta
|
||||
from django.utils import timezone
|
||||
import json
|
||||
import requests
|
||||
import lxml.html
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
@ -31,6 +33,10 @@ class Item(models.Model):
|
|||
announced = models.DateTimeField(null=True, default=None, blank=True)
|
||||
data = models.JSONField(default=dict, editable=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.url and not self.data:
|
||||
self.update_data()
|
||||
super().save(*args, **kwargs)
|
||||
def __str__(self):
|
||||
return '%s (%s)' % (self.title, self.url)
|
||||
|
||||
|
@ -70,6 +76,26 @@ class Item(models.Model):
|
|||
def get_absolute_url(self):
|
||||
return reverse('item', kwargs={'id': self.id})
|
||||
|
||||
def update_data(self):
|
||||
self.data.update(self.parse_url())
|
||||
|
||||
def parse_url(self):
|
||||
content = requests.get(self.url).text
|
||||
doc = lxml.html.fromstring(content)
|
||||
data = {}
|
||||
for meta in doc.cssselect('meta'):
|
||||
key = meta.attrib.get('name')
|
||||
if not key:
|
||||
key = meta.attrib.get('property')
|
||||
value = meta.attrib.get('content')
|
||||
if key and value:
|
||||
if key in ('viewport', ):
|
||||
continue
|
||||
key = key.replace('og:', '')
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
modified = models.DateTimeField(auto_now=True)
|
||||
|
@ -83,6 +109,10 @@ class Comment(models.Model):
|
|||
data = models.JSONField(default=dict, editable=False)
|
||||
published = models.DateTimeField(null=True, default=None)
|
||||
|
||||
@property
|
||||
def is_published(self):
|
||||
return bool(self.published)
|
||||
|
||||
def __str__(self):
|
||||
return '%s: %s' % (self.item, self.user)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ SECRET_KEY = "django-insecure-()6&rdheil=iyz%36dl-fnb)a+*7*^cb%isz6x%fi+ong5#*zz
|
|||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
COMPRESS_ENABLED = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
@ -38,6 +39,9 @@ INSTALLED_APPS = [
|
|||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
|
||||
'compressor',
|
||||
'sass_processor',
|
||||
|
||||
"app",
|
||||
"app.user",
|
||||
"app.item",
|
||||
|
@ -118,6 +122,13 @@ USE_TZ = True
|
|||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = "www/static"
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
"sass_processor.finders.CssFinder",
|
||||
"compressor.finders.CompressorFinder",
|
||||
)
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
|
||||
.comments h3 {
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
//display: none;
|
||||
}
|
||||
.comments.active h3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comments .icon svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.add-comment {
|
||||
width: 100%;
|
||||
height: 30vh;
|
||||
text-align: center;
|
||||
}
|
||||
.add-comment input {
|
||||
width: 100px;
|
||||
}
|
||||
.add-comment textarea {
|
||||
width: 80%;
|
||||
display: block;
|
||||
background: black;
|
||||
color: white;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.add-comment button {
|
||||
background: red;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.comment .text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
90
app/static/css/comments.scss
Normal file
90
app/static/css/comments.scss
Normal file
|
@ -0,0 +1,90 @@
|
|||
|
||||
.comments h3 {
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
//display: none;
|
||||
}
|
||||
.comments.active h3 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.comments .icon svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.add-comment {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 30vh;
|
||||
padding-top: 8px;
|
||||
padding-left: 4px;
|
||||
|
||||
}
|
||||
@media(max-width:768px) {
|
||||
.add-comment {
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-comment textarea,
|
||||
.add-comment input {
|
||||
padding: 4px;
|
||||
margin-left: 0;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
margin-right: 4px;
|
||||
background: none;
|
||||
color: white;
|
||||
border: 1px solid green;
|
||||
}
|
||||
.add-comment input {
|
||||
width: calc(50% - 8px);
|
||||
}
|
||||
.add-comment textarea {
|
||||
width: calc(100% - 8px);
|
||||
height: 100px;
|
||||
display: block;
|
||||
}
|
||||
.add-comment button {
|
||||
background: black;
|
||||
color: white;
|
||||
border: solid 1px green;
|
||||
padding: 8px;
|
||||
}
|
||||
.add-comment button:hover {
|
||||
border: solid 1px lightgreen;
|
||||
cursor: pointer;
|
||||
}
|
||||
.add-comment {
|
||||
.buttons {
|
||||
&.login {
|
||||
input {
|
||||
width: calc(25% - 8px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid blueviolet;
|
||||
}
|
||||
|
||||
.comment .text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.comments h3 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.comments.collapsed .block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comments .meta {
|
||||
color: gray;
|
||||
}
|
52
app/static/css/site.scss
Normal file
52
app/static/css/site.scss
Normal file
|
@ -0,0 +1,52 @@
|
|||
header, footer {
|
||||
max-width: 1000px;
|
||||
padding-top: 16px;
|
||||
margin: auto;
|
||||
}
|
||||
header {
|
||||
a {
|
||||
color: yellow;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.index {
|
||||
padding-top: 16px;
|
||||
.item {
|
||||
border-bottom: 1px solid blueviolet;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
h1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1, .info, .comments {
|
||||
}
|
||||
|
||||
.image {
|
||||
padding-top: 4px;
|
||||
width: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
.archive {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,16 @@ body {
|
|||
line-height: normal;
|
||||
}
|
||||
|
||||
*, *::after, *::before {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(128, 128, 255)
|
||||
}
|
||||
|
|
|
@ -1,26 +1,41 @@
|
|||
function renderComments(data) {
|
||||
var cdiv = div.querySelector('.comments')
|
||||
function renderComments(cdiv, data) {
|
||||
cdiv.innerHTML = `
|
||||
<h3>
|
||||
<span class="icon">${icon.down}</span>
|
||||
Comments
|
||||
</h3>
|
||||
<div class="block">
|
||||
<div class="comments-content">
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
const content = cdiv.querySelector('.comments-content')
|
||||
comments.forEach(comment => {
|
||||
var c = document.createElement('div')
|
||||
c.className = 'comment'
|
||||
c.innerHTML = `
|
||||
<div class="comment">
|
||||
<div class="name">${comment.name}</div>
|
||||
<div class="date">${comment.date}</div>
|
||||
<div class="text">${comment.text}</div>
|
||||
<div class="meta">
|
||||
by <span class="name">${comment.name}</span>
|
||||
on
|
||||
<span class="date">${comment.date}</span>
|
||||
</div>
|
||||
`
|
||||
cdiv.append(c)
|
||||
content.append(c)
|
||||
})
|
||||
var add = document.querySelector('.add-comment')
|
||||
add.style.display = 'block'
|
||||
cdiv.append(add)
|
||||
cdiv.querySelector('.block').append(add)
|
||||
cdiv.querySelector('h3').addEventListener("click", event => {
|
||||
var img = cdiv.querySelector('h3 .icon')
|
||||
if (cdiv.classList.contains("collapsed")) {
|
||||
cdiv.classList.remove("collapsed")
|
||||
img.innerHTML = icon.down
|
||||
} else {
|
||||
cdiv.classList.add("collapsed")
|
||||
img.innerHTML = icon.right
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
document.querySelector('button#add-comment').addEventListener('click', event => {
|
||||
|
@ -46,21 +61,20 @@ document.querySelector('button#add-comment').addEventListener('click', event =>
|
|||
}).then(response => {
|
||||
return response.json()
|
||||
}).then(response => {
|
||||
var comment= document.createElement('div')
|
||||
var comment = document.createElement('div')
|
||||
comment.classList.add('comment')
|
||||
var name = document.createElement('div')
|
||||
name.classList.add('name')
|
||||
name.innerText = response.name
|
||||
comment.append(name)
|
||||
var date = document.createElement('div')
|
||||
date.classList.add('date')
|
||||
date.innerText = response.date
|
||||
comment.append(date)
|
||||
var text = document.createElement('div')
|
||||
text.classList.add('name')
|
||||
text.innerText = response.text
|
||||
comment.append(text)
|
||||
document.querySelector('.comments').append(comment)
|
||||
comment.innerHTML = `
|
||||
<div class="text"></div>
|
||||
<div class="meta">
|
||||
by <span class="name"></span>
|
||||
on
|
||||
<span class="date"></span>
|
||||
</div>
|
||||
`
|
||||
comment.querySelector('.name').innerText = response.name
|
||||
comment.querySelector('.date').innerText = response.date
|
||||
comment.querySelector('.text').innerText = response.text
|
||||
document.querySelector('.comments .comments-content').append(comment)
|
||||
document.querySelector('.add-comment textarea').value = ''
|
||||
})
|
||||
})
|
||||
|
|
|
@ -30,16 +30,17 @@ function renderItem(data) {
|
|||
<div class="player">
|
||||
<div class="video"></div>
|
||||
</div>
|
||||
<div class="value">${data.value}</div>
|
||||
<div class="value">
|
||||
${data.value}
|
||||
<div class="comments"></div>
|
||||
</div>
|
||||
<div class="more">
|
||||
<a href="${data.link}">Open on ${data.site}</a>
|
||||
</div>
|
||||
`
|
||||
|
||||
var comments = div.querySelector('.comments')
|
||||
if (window.renderComments) {
|
||||
renderComments(comments, data)
|
||||
renderComments(div.querySelector('.comments'), data)
|
||||
} else {
|
||||
comments.remove()
|
||||
}
|
||||
|
|
|
@ -1,25 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
{% for item in items %}
|
||||
<div class="item">
|
||||
<a href="{{ item.url }}">
|
||||
<h1>{{ item.title }}</h1>
|
||||
<figure>
|
||||
<img src={{ item.icon }}>
|
||||
<figcaption>
|
||||
{{ item.data.title }}
|
||||
{% if item.data.description %}
|
||||
<br>
|
||||
{{ item.data.description }}
|
||||
{% endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</a>
|
||||
<div class="description">
|
||||
{{ item.description | safe}}
|
||||
</div>
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.public_comments.count }} comments</a>
|
||||
</div>
|
||||
{% include "listitem.html" with item=item %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>{% load static sass_tags compress %}
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% compress css file site %}
|
||||
<link rel="stylesheet" href="/static/css/reset.css"></link>
|
||||
<link rel="stylesheet" href="{% sass_src 'css/site.scss' %}"></link>
|
||||
<link rel="stylesheet" href="/static/css/style.css"></link>
|
||||
<link rel="stylesheet" href="{% sass_src 'css/comments.scss' %}"></link>
|
||||
{% endcompress %}
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block header %}
|
||||
<header>
|
||||
<a href="/">phantas.ma</a>
|
||||
</header>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<div class="content">
|
||||
|
@ -11,5 +26,7 @@
|
|||
{% endblock %}
|
||||
{% block end %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,44 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<style>
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.content {
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<div class="index">
|
||||
{% for item in items %}
|
||||
<div class="item">
|
||||
<a href="{{ item.get_absolute_url }}">
|
||||
<h1>{{ item.title }}</h1>
|
||||
<figure>
|
||||
<img src={{ item.icon }}>
|
||||
<figcaption>
|
||||
{{ item.data.title }}
|
||||
{% if item.data.description %}
|
||||
<br>
|
||||
{{ item.data.description }}
|
||||
{% endif %}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</a>
|
||||
<div class="description">
|
||||
{{ item.description | safe }}
|
||||
</div>
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.public_comments.count }} comments</a>
|
||||
</div>
|
||||
{% include "listitem.html" with item=item %}
|
||||
{% endfor %}
|
||||
{% if archive %}
|
||||
<div class="archive">
|
||||
<a href="/archive/">older items</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,27 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static sass_tags compress %}
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/reset.css"></link>
|
||||
<link rel="stylesheet" href="/static/css/style.css"></link>
|
||||
<link rel="stylesheet" href="/static/css/comments.css"></link>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<div class="content">
|
||||
</div>
|
||||
<div class="add-comment" style="display: none">
|
||||
{% if request.user.is_anonymous %}
|
||||
{% csrf_token %}
|
||||
<textarea name="text" placeholder="your comment"></textarea>
|
||||
{% if false and request.user.is_anonymous %}
|
||||
|
||||
<input name="name" type="text" placeholder="your name"></input>
|
||||
<input name="email" type="email" placeholder="your email"></input>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<textarea name="text" placeholder="your comment"></textarea>
|
||||
<button id="add-comment">Add comment</button>
|
||||
<div class="buttons">
|
||||
<button id="add-comment">Add comment as guest</button>
|
||||
<button>Login</button>
|
||||
</div>
|
||||
<div class="buttons login">
|
||||
<input name="username" type="text" placeholder="your username"></input>
|
||||
<input name="password" type="password" placeholder="your password"></input>
|
||||
<button>Login</button>
|
||||
<button>Register</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block end %}
|
||||
<script>
|
||||
var comments = {{ item.public_comments_json|safe }};
|
||||
</script>
|
||||
{% compress js file m %}
|
||||
<script src="/static/js/utils.js"></script>
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/icons.js"></script>
|
||||
|
@ -31,6 +40,7 @@
|
|||
<script src="/static/js/edits.js"></script>
|
||||
<script src="/static/js/item.js"></script>
|
||||
<script src="/static/js/render.js"></script>
|
||||
{% endcompress %}
|
||||
|
||||
<script>
|
||||
pandora.url = new URL('{{ item.url|escapejs }}');
|
||||
|
|
15
app/templates/listitem.html
Normal file
15
app/templates/listitem.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<div class="item">
|
||||
<a href="{{ item.get_absolute_url }}">
|
||||
<h1>{{ item.title }}</h1>
|
||||
<div class="info">
|
||||
{{ item.data.title }}
|
||||
</div>
|
||||
<div class="image">
|
||||
<img src={{ item.data.thumbnail }}>
|
||||
</div>
|
||||
</a>
|
||||
<div class="comments">
|
||||
<a href="{{ item.get_absolute_url }}">{{ item.public_comments.count }} comments</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1 +1,7 @@
|
|||
django
|
||||
libsass
|
||||
django-compressor
|
||||
django-sass-processor
|
||||
requests
|
||||
lxml
|
||||
cssselect
|
||||
|
|
Loading…
Reference in a new issue