Add authorization and private boards
This commit is contained in:
0
auth/__init__.py
Normal file
0
auth/__init__.py
Normal file
3
auth/admin.py
Normal file
3
auth/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
auth/apps.py
Normal file
5
auth/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthConfig(AppConfig):
|
||||
name = 'auth'
|
||||
27
auth/helpers.py
Normal file
27
auth/helpers.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def authorized_user(request):
|
||||
token = request.COOKIES.get(settings.AUTH_COOKIE_NAME)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
||||
except (jwt.DecodeError, jwt.ExpiredSignatureError) as ex:
|
||||
response = render(request, "message.html", {
|
||||
"title": "Что-то сломалось",
|
||||
"message": "Неправильный токен авторизации. Наверное, что-то сломалось. "
|
||||
"Либо вы ХАКИР!!11 (тогда идите в жопу)"
|
||||
})
|
||||
response.delete_cookie(settings.AUTH_COOKIE_NAME)
|
||||
return response
|
||||
|
||||
if datetime.utcfromtimestamp(payload["exp"]) < datetime.utcnow():
|
||||
return None
|
||||
|
||||
return payload
|
||||
29
auth/migrations/0001_initial.py
Normal file
29
auth/migrations/0001_initial.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-05 13:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('token', models.CharField(max_length=256, unique=True)),
|
||||
('user_id', models.IntegerField()),
|
||||
('user_name', models.CharField(max_length=32, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
],
|
||||
options={
|
||||
'db_table': 'sessions',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
auth/migrations/__init__.py
Normal file
0
auth/migrations/__init__.py
Normal file
15
auth/models.py
Normal file
15
auth/models.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import uuid
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
token = models.CharField(max_length=256, unique=True)
|
||||
user_id = models.IntegerField() # original id of a club user (we don't store profiles)
|
||||
user_name = models.CharField(max_length=32, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
class Meta:
|
||||
db_table = "sessions"
|
||||
47
auth/views.py
Normal file
47
auth/views.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
from django.conf import settings
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from auth.models import Session
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def login(request):
|
||||
return redirect(f"{settings.AUTH_REDIRECT_URL}?redirect={settings.APP_HOST}/auth/club_callback/")
|
||||
|
||||
|
||||
def club_callback(request):
|
||||
token = request.GET.get("jwt")
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
||||
except (jwt.DecodeError, jwt.ExpiredSignatureError) as ex:
|
||||
log.error(f"JWT token error: {ex}")
|
||||
return render(request, "message.html", {
|
||||
"title": "Что-то сломалось",
|
||||
"message": "Неправильный токен авторизации. Наверное, что-то сломалось. "
|
||||
"Либо вы ХАКИР!!11 (тогда идите в жопу)"
|
||||
})
|
||||
|
||||
Session.objects.get_or_create(
|
||||
token=token,
|
||||
defaults=dict(
|
||||
user_id=payload["user_id"],
|
||||
user_name=payload.get("user_name"),
|
||||
expires_at=datetime.utcfromtimestamp(payload["exp"])
|
||||
)
|
||||
)
|
||||
|
||||
response = redirect("index")
|
||||
response.set_cookie(settings.AUTH_COOKIE_NAME, token, max_age=settings.AUTH_COOKIE_MAX_AGE)
|
||||
return response
|
||||
|
||||
|
||||
def logout(request):
|
||||
response = redirect("index")
|
||||
response.delete_cookie(settings.AUTH_COOKIE_NAME)
|
||||
return response
|
||||
119
boards.yml
119
boards.yml
@@ -1,21 +1,39 @@
|
||||
boards:
|
||||
- blocks:
|
||||
- feeds:
|
||||
- columns: 3
|
||||
name: Hacker News
|
||||
rss: https://news.ycombinator.com/rss
|
||||
- name: Вастрик
|
||||
slug: vas3k
|
||||
is_visible: true
|
||||
is_private: true
|
||||
curator:
|
||||
name: Вастрик
|
||||
url: https://vas3k.ru
|
||||
title: Айти и путешествия
|
||||
avatar: https://i.vas3k.ru/eb8.png
|
||||
bio: Веду блог о технологиях, пишу код, отвратительно путешествую и фотографирую это
|
||||
footer: >
|
||||
здесь собраны ресурсы, которые формируют моё текущее инфополе.
|
||||
По сути из них я и получаю 90% информации о том, что мне интересно.
|
||||
Отбор и фильтрация источников — непрерывный процесс, так что в будущем всё может измениться.
|
||||
Пока что вот так.
|
||||
blocks:
|
||||
- slug: main
|
||||
feeds:
|
||||
- name: Hacker News
|
||||
url: https://news.ycombinator.com
|
||||
rss: https://news.ycombinator.com/rss
|
||||
columns: 3
|
||||
- name: 'Medium: Technology'
|
||||
rss: https://medium.com/feed/topic/technology
|
||||
url: https://medium.com/topic/technology
|
||||
- icon: https://assets.producthunt.com/assets/ph-ios-icon-e1733530a1bfc41080db8161823f1ef262cdbbc933800c0a2a706f70eb9c277a.png
|
||||
name: Product Hunt
|
||||
rss: https://www.producthunt.com/feed
|
||||
rss: https://medium.com/feed/topic/technology
|
||||
- name: Product Hunt
|
||||
url: https://www.producthunt.com
|
||||
- feeds:
|
||||
rss: https://www.producthunt.com/feed
|
||||
icon: https://i.vas3k.ru/fep.png
|
||||
- name: Техно-мейнстрим
|
||||
slug: tech
|
||||
feeds:
|
||||
- name: 'Reddit: /r/technology/'
|
||||
rss: https://www.reddit.com/r/technology.rss
|
||||
url: https://www.reddit.com/r/technology
|
||||
rss: https://www.reddit.com/r/technology.rss
|
||||
- name: ZDNet
|
||||
rss: https://www.zdnet.com/news/rss.xml
|
||||
url: https://www.zdnet.com
|
||||
@@ -29,8 +47,9 @@ boards:
|
||||
rss: http://feeds2.feedburner.com/thenextweb
|
||||
url: https://thenextweb.com
|
||||
- name: Wired
|
||||
rss: https://www.wired.com/feed/rss
|
||||
url: https://www.wired.com
|
||||
rss: https://www.wired.com/feed/rss
|
||||
icon: https://i.vas3k.ru/feu.png
|
||||
- name: ArsTechnica
|
||||
rss: http://feeds.arstechnica.com/arstechnica/index/
|
||||
url: https://arstechnica.com
|
||||
@@ -43,49 +62,75 @@ boards:
|
||||
- name: MIT Technology Review
|
||||
rss: https://www.technologyreview.com/topnews.rss
|
||||
url: https://www.technologyreview.com
|
||||
name: Tech News
|
||||
- feeds:
|
||||
- name: Мейкерство
|
||||
slug: make
|
||||
feeds:
|
||||
- name: Show HN
|
||||
rss: https://hnrss.org/show
|
||||
url: https://news.ycombinator.com/show
|
||||
rss: https://hnrss.org/show
|
||||
- name: Starter Story
|
||||
rss: https://www.starterstory.com/feed?format=rss
|
||||
url: https://www.starterstory.com
|
||||
rss: https://www.starterstory.com/feed?format=rss
|
||||
- name: 'Reddit: /r/SideProject'
|
||||
rss: https://www.reddit.com/r/SideProject.rss
|
||||
url: https://www.reddit.com/r/SideProject/
|
||||
name: Make
|
||||
- feeds:
|
||||
rss: https://www.reddit.com/r/SideProject.rss
|
||||
- name: Путешествия
|
||||
slug: travel
|
||||
feeds:
|
||||
- name: PeritoBurrito
|
||||
rss: http://perito-burrito.com/feed
|
||||
url: https://perito-burrito.com
|
||||
rss: http://perito-burrito.com/feed
|
||||
- name: Vandrouki
|
||||
rss: https://feeds.feedburner.com/vandroukiru
|
||||
url: https://vandrouki.ru
|
||||
rss: https://feeds.feedburner.com/vandroukiru
|
||||
icon: https://i.vas3k.ru/fer.jpg
|
||||
- name: Secret Flying
|
||||
rss: https://www.secretflying.com/feed/
|
||||
url: https://www.secretflying.com
|
||||
rss: https://www.secretflying.com/feed/
|
||||
- name: 'Atlas Obscura: Stories'
|
||||
rss: https://www.atlasobscura.com/feeds/latest
|
||||
url: https://www.atlasobscura.com/articles
|
||||
name: Travel
|
||||
- feeds:
|
||||
rss: https://www.atlasobscura.com/feeds/latest
|
||||
- name: Фотография
|
||||
slug: photo
|
||||
feeds:
|
||||
- name: PetaPixel
|
||||
rss: https://feedproxy.google.com/PetaPixel
|
||||
url: https://petapixel.com
|
||||
rss: https://feedproxy.google.com/PetaPixel
|
||||
icon: https://i.vas3k.ru/fes.jpg
|
||||
- name: DPReview
|
||||
rss: https://www.dpreview.com/feeds/reviews.xml
|
||||
url: https://www.dpreview.com
|
||||
rss: https://www.dpreview.com/feeds/reviews.xml
|
||||
- name: 500px ISO
|
||||
rss: https://iso.500px.com/feed/
|
||||
url: https://iso.500px.com
|
||||
name: Photo
|
||||
rss: https://iso.500px.com/feed/
|
||||
icon: https://i.vas3k.ru/fet.png
|
||||
|
||||
- name: How to Berlin
|
||||
slug: howtoberlin
|
||||
is_visible: true
|
||||
is_private: true
|
||||
curator:
|
||||
avatar: https://i.vas3k.ru/eb8.png
|
||||
bio: Пишу в блог, пишу код, отвратительно путешествую
|
||||
name: Вастрик
|
||||
title: О технологиях
|
||||
footer: Кек
|
||||
url: https://vas3k.ru
|
||||
name: Вастрик
|
||||
slug: vas3k
|
||||
name: Лена How to Berlin
|
||||
url: https://howtoberlin.de
|
||||
title: Набор Берлинца
|
||||
avatar: https://i.vas3k.ru/fev.png
|
||||
bio: Что читать когда ты переехал в Берлин и не понимаешь что происходит
|
||||
blocks:
|
||||
- name: Где узнавать новости?
|
||||
slug: news
|
||||
feeds:
|
||||
- name: PeritoBurrito
|
||||
url: https://perito-burrito.com
|
||||
rss: http://perito-burrito.com/feed
|
||||
- name: Следим за скидками и распродажами
|
||||
slug: sales
|
||||
feeds:
|
||||
- name: PeritoBurrito
|
||||
url: https://perito-burrito.com
|
||||
rss: http://perito-burrito.com/feed
|
||||
- name: Путешествуем
|
||||
slug: travel
|
||||
feeds:
|
||||
- name: PeritoBurrito
|
||||
url: https://perito-burrito.com
|
||||
rss: http://perito-burrito.com/feed
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-14 20:55
|
||||
# Generated by Django 2.2.8 on 2020-01-05 16:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@@ -19,48 +19,77 @@ class Migration(migrations.Migration):
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('name', models.CharField(db_index=True, max_length=120)),
|
||||
('avatar', models.URLField(null=True)),
|
||||
('avatar', models.URLField(max_length=512, null=True)),
|
||||
('curator_name', models.CharField(max_length=120)),
|
||||
('curator_title', models.CharField(max_length=120)),
|
||||
('curator_url', models.URLField(null=True)),
|
||||
('curator_bio', models.CharField(max_length=120, null=True)),
|
||||
('curator_footer', models.TextField(null=True)),
|
||||
('schema', models.TextField(null=True)),
|
||||
('created_at', models.DateTimeField(db_index=True)),
|
||||
('updated_at', models.DateTimeField()),
|
||||
('refreshed_at', models.DateTimeField()),
|
||||
('frequency', models.FloatField(default=0.0)),
|
||||
('refreshed_at', models.DateTimeField(null=True)),
|
||||
('is_visible', models.BooleanField(default=True)),
|
||||
('is_private', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'boards',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BoardBlock',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=512, null=True)),
|
||||
('slug', models.SlugField()),
|
||||
('created_at', models.DateTimeField(db_index=True)),
|
||||
('updated_at', models.DateTimeField()),
|
||||
('index', models.PositiveIntegerField(default=0)),
|
||||
('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks', to='boards.Board')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'board_blocks',
|
||||
'ordering': ['index'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BoardFeed',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('url', models.URLField()),
|
||||
('name', models.CharField(max_length=512)),
|
||||
('comment', models.TextField(null=True)),
|
||||
('url', models.URLField(max_length=512)),
|
||||
('icon', models.URLField(max_length=512, null=True)),
|
||||
('rss', models.URLField(max_length=512, null=True)),
|
||||
('created_at', models.DateTimeField(db_index=True)),
|
||||
('refreshed_at', models.DateTimeField()),
|
||||
('last_article_at', models.DateTimeField(null=True)),
|
||||
('refreshed_at', models.DateTimeField(null=True)),
|
||||
('frequency', models.FloatField(default=0.0)),
|
||||
('columns', models.SmallIntegerField(default=1)),
|
||||
('articles_per_column', models.SmallIntegerField(default=15)),
|
||||
('index', models.PositiveIntegerField(default=0)),
|
||||
('block', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeds', to='boards.BoardBlock')),
|
||||
('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeds', to='boards.Board')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'board_feeds',
|
||||
'ordering': ['-created_at'],
|
||||
'ordering': ['index'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Article',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('url', models.URLField(db_index=True)),
|
||||
('uniq_id', models.TextField(db_index=True)),
|
||||
('url', models.URLField(max_length=2048)),
|
||||
('type', models.CharField(max_length=16)),
|
||||
('domain', models.CharField(max_length=256)),
|
||||
('domain', models.CharField(max_length=256, null=True)),
|
||||
('title', models.CharField(max_length=256)),
|
||||
('image', models.URLField(null=True)),
|
||||
('image', models.URLField(max_length=512, null=True)),
|
||||
('description', models.TextField(null=True)),
|
||||
('created_at', models.DateTimeField(db_index=True)),
|
||||
('updated_at', models.DateTimeField()),
|
||||
('click_count', models.PositiveIntegerField(default=0)),
|
||||
('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to='boards.Board')),
|
||||
('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to='boards.BoardFeed')),
|
||||
],
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-14 20:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='article',
|
||||
name='click_count',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_created=True, db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='board',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_created=True, db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_created=True, db_index=True),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-14 21:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0002_auto_20191214_2056'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='board',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='board',
|
||||
name='refreshed_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,61 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-14 22:51
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0003_auto_20191214_2100'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='index',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='name',
|
||||
field=models.CharField(default='', max_length=512),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(db_index=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='refreshed_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BoardBlock',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=512, null=True)),
|
||||
('created_at', models.DateTimeField(db_index=True)),
|
||||
('index', models.PositiveIntegerField(default=0)),
|
||||
('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blocks', to='boards.Board')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'board_blocks',
|
||||
'ordering': ['index'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='block',
|
||||
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='feeds', to='boards.BoardBlock'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-14 23:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0004_auto_20191214_2251'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='boardfeed',
|
||||
old_name='url',
|
||||
new_name='rss_url',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='web_url',
|
||||
field=models.URLField(default='nope'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-14 23:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0005_auto_20191214_2318'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='boardfeed',
|
||||
old_name='rss_url',
|
||||
new_name='rss',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='boardfeed',
|
||||
old_name='web_url',
|
||||
new_name='url',
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-15 11:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0006_auto_20191214_2332'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='board',
|
||||
name='frequency',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='frequency',
|
||||
field=models.FloatField(default=0.0),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-15 12:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0007_auto_20191215_1143'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='icon',
|
||||
field=models.URLField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-04 13:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0008_boardfeed_icon'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='boardfeed',
|
||||
options={'ordering': ['index']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='board',
|
||||
name='is_visible',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='last_article_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-04 13:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0009_auto_20200104_1321'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='articles_per_column',
|
||||
field=models.SmallIntegerField(default=15),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='columns',
|
||||
field=models.SmallIntegerField(default=1),
|
||||
),
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-04 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0010_auto_20200104_1344'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='image',
|
||||
field=models.URLField(max_length=512, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='url',
|
||||
field=models.URLField(db_index=True, max_length=512),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='board',
|
||||
name='avatar',
|
||||
field=models.URLField(max_length=512, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='icon',
|
||||
field=models.URLField(max_length=512, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='rss',
|
||||
field=models.URLField(max_length=512),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='boardfeed',
|
||||
name='url',
|
||||
field=models.URLField(max_length=512),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-04 14:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0011_auto_20200104_1348'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='article',
|
||||
name='uniq_id',
|
||||
field=models.TextField(db_index=True, default='uniq'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='url',
|
||||
field=models.URLField(max_length=2048),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-04 14:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0012_auto_20200104_1404'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='article',
|
||||
name='domain',
|
||||
field=models.CharField(max_length=256, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-05 09:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0013_auto_20200104_1411'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='board',
|
||||
name='curator_title',
|
||||
field=models.CharField(default='', max_length=120),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-05 11:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0014_board_curator_title'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='board',
|
||||
name='curator_footer',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.8 on 2020-01-05 11:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('boards', '0015_board_curator_footer'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='boardfeed',
|
||||
name='comment',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -28,6 +28,7 @@ class Board(models.Model):
|
||||
refreshed_at = models.DateTimeField(null=True)
|
||||
|
||||
is_visible = models.BooleanField(default=True)
|
||||
is_private = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "boards"
|
||||
@@ -49,7 +50,7 @@ class Board(models.Model):
|
||||
|
||||
def natural_refreshed_at(self):
|
||||
if not self.refreshed_at:
|
||||
return "updating right now..."
|
||||
return "now..."
|
||||
return naturaltime(self.refreshed_at)
|
||||
|
||||
|
||||
@@ -57,8 +58,10 @@ class BoardBlock(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
board = models.ForeignKey(Board, related_name="blocks", on_delete=models.CASCADE, db_index=True)
|
||||
name = models.CharField(max_length=512, null=True)
|
||||
slug = models.SlugField()
|
||||
|
||||
created_at = models.DateTimeField(db_index=True)
|
||||
updated_at = models.DateTimeField()
|
||||
|
||||
index = models.PositiveIntegerField(default=0)
|
||||
|
||||
@@ -69,6 +72,12 @@ class BoardBlock(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.created_at:
|
||||
self.created_at = datetime.utcnow()
|
||||
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name).lower()
|
||||
|
||||
self.updated_at = datetime.utcnow()
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -78,9 +87,9 @@ class BoardFeed(models.Model):
|
||||
block = models.ForeignKey(BoardBlock, related_name="feeds", on_delete=models.CASCADE, db_index=True)
|
||||
name = models.CharField(max_length=512)
|
||||
comment = models.TextField(null=True)
|
||||
icon = models.URLField(max_length=512, null=True)
|
||||
url = models.URLField(max_length=512)
|
||||
rss = models.URLField(max_length=512)
|
||||
icon = models.URLField(max_length=512, null=True)
|
||||
rss = models.URLField(max_length=512, null=True)
|
||||
|
||||
created_at = models.DateTimeField(db_index=True)
|
||||
last_article_at = models.DateTimeField(null=True)
|
||||
|
||||
@@ -9,13 +9,16 @@ register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def pretty_url(value):
|
||||
"""
|
||||
Removes http(s) and www from an url
|
||||
"""
|
||||
return re.sub(r"https?://(www\.)?", "", value, 1)
|
||||
|
||||
|
||||
@register.filter
|
||||
def cool_number(value, num_decimals=1):
|
||||
"""
|
||||
Django template filter to convert regular numbers to a cool format (ie: 2K, 434.4K, 33M...)
|
||||
Converts regular numbers into cool ones (ie: 2K, 434.4K, 33M...)
|
||||
"""
|
||||
int_value = int(value)
|
||||
formatted_number = '{{:.{}f}}'.format(num_decimals)
|
||||
@@ -29,12 +32,15 @@ def cool_number(value, num_decimals=1):
|
||||
|
||||
@register.filter
|
||||
def smart_urlize(value, target="_blank"):
|
||||
# TODO: this
|
||||
# TODO: remove http/www prefix, add target=_blank and truncate url if needed
|
||||
return mark_safe(urlize(value))
|
||||
|
||||
|
||||
@register.filter
|
||||
def rupluralize(value, arg="дурак,дурака,дураков"):
|
||||
"""
|
||||
Pluralization for russian words
|
||||
"""
|
||||
args = arg.split(",")
|
||||
number = abs(int(value))
|
||||
a = number % 10
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from boards.models import Board, BoardBlock, BoardFeed, Article
|
||||
from auth.helpers import authorized_user
|
||||
from boards.models import Board, BoardBlock, BoardFeed
|
||||
|
||||
|
||||
def index(request):
|
||||
@@ -12,6 +13,14 @@ def index(request):
|
||||
|
||||
def board(request, board_slug):
|
||||
board = get_object_or_404(Board, slug=board_slug)
|
||||
|
||||
if board.is_private:
|
||||
me = authorized_user(request)
|
||||
if not me:
|
||||
return render(request, "board_no_access.html", {
|
||||
"board": board
|
||||
})
|
||||
|
||||
blocks = BoardBlock.objects.filter(board=board)
|
||||
feeds = BoardFeed.objects.filter(board=board)
|
||||
return render(request, "board.html", {
|
||||
|
||||
@@ -15,6 +15,7 @@ ALLOWED_HOSTS = ["127.0.0.1", "vas3k.ru", "infomate.club"]
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.humanize",
|
||||
"auth",
|
||||
"boards",
|
||||
]
|
||||
|
||||
@@ -86,8 +87,18 @@ CSS_HASH = str(random())
|
||||
# App settings
|
||||
|
||||
APP_NAME = "Infomate"
|
||||
APP_TITLE = "Читай то, что читают другие"
|
||||
APP_TITLE = "Смотри, что читают другие"
|
||||
APP_DESCRIPTION = ""
|
||||
APP_HOST = "https://infomate.club"
|
||||
|
||||
JWT_SECRET = "wow so secret" # should be the same as on vas3k.ru
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_EXP_TIMEDELTA = timedelta(days=120)
|
||||
|
||||
AUTH_COOKIE_NAME = "jwt"
|
||||
AUTH_COOKIE_MAX_AGE = 300 * 24 * 60 * 60 # 300 days
|
||||
AUTH_REDIRECT_URL = "https://vas3k.ru/auth/external/"
|
||||
AUTH_FAILED_REDIRECT_URL = "https://vas3k.ru/auth/login/"
|
||||
|
||||
SENTRY_DSN = None
|
||||
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
"""infomate URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
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.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from auth.views import login, logout, club_callback
|
||||
from boards.views import index, board
|
||||
|
||||
urlpatterns = [
|
||||
path("", index, name="index"),
|
||||
path("board/<slug:board_slug>/", board, name="board"),
|
||||
path("auth/login/", login, name="login"),
|
||||
path("auth/club_callback/", club_callback, name="club_callback"),
|
||||
path("auth/logout/", logout, name="logout"),
|
||||
]
|
||||
|
||||
@@ -6,3 +6,4 @@ beautifulsoup4==4.6.2
|
||||
pyyaml==5.2
|
||||
feedparser==5.2.1
|
||||
sentry-sdk==0.13.5
|
||||
pyjwt==1.7.1
|
||||
|
||||
@@ -25,7 +25,8 @@ DEFAULT_REQUEST_HEADERS = {
|
||||
|
||||
@click.command()
|
||||
@click.option('--config', default="boards.yml", help="Boards YAML file")
|
||||
def initialize(config):
|
||||
@click.option('--board-slug', default=None, help="Board slug to parse only one exact board")
|
||||
def initialize(config, board_slug):
|
||||
yaml_file = os.path.join(BASE_DIR, config)
|
||||
with open(yaml_file) as f:
|
||||
try:
|
||||
@@ -35,6 +36,9 @@ def initialize(config):
|
||||
exit(1)
|
||||
|
||||
for board_config in config["boards"]:
|
||||
if board_slug and board_config["slug"] != board_slug:
|
||||
continue
|
||||
|
||||
board_name = board_config.get("name") or board_config["slug"]
|
||||
print(f"Creating board: {board_name}...")
|
||||
board, is_created = Board.objects.get_or_create(
|
||||
@@ -47,6 +51,8 @@ def initialize(config):
|
||||
curator_footer=board_config["curator"].get("footer"),
|
||||
curator_bio=board_config["curator"].get("bio"),
|
||||
curator_url=board_config["curator"].get("url"),
|
||||
is_private=board_config.get("is_private"),
|
||||
is_visible=board_config.get("is_visible"),
|
||||
)
|
||||
)
|
||||
if not is_created:
|
||||
@@ -58,6 +64,8 @@ def initialize(config):
|
||||
board.curator_footer = board_config["curator"].get("footer")
|
||||
board.curator_bio = board_config["curator"].get("bio")
|
||||
board.curator_url = board_config["curator"].get("url")
|
||||
board.is_private = board_config.get("is_private")
|
||||
board.is_visible = board_config.get("is_visible")
|
||||
board.save()
|
||||
|
||||
for block_index, block_config in enumerate(board_config["blocks"]):
|
||||
@@ -65,14 +73,16 @@ def initialize(config):
|
||||
print(f"\nCreating block: {block_name}...")
|
||||
block, is_created = BoardBlock.objects.get_or_create(
|
||||
board=board,
|
||||
name=block_name,
|
||||
default=dict(
|
||||
slug=block_config["slug"],
|
||||
defaults=dict(
|
||||
name=block_name,
|
||||
index=block_index
|
||||
)
|
||||
)
|
||||
|
||||
if not is_created:
|
||||
block.index = block_index
|
||||
block.name = block_name
|
||||
block.save()
|
||||
|
||||
if not block_config.get("feeds"):
|
||||
@@ -81,35 +91,17 @@ def initialize(config):
|
||||
for feed_index, feed_config in enumerate(block_config["feeds"]):
|
||||
feed_name = feed_config.get("name") or ""
|
||||
feed_url = feed_config["url"]
|
||||
print(f"Creating feed: {feed_name}...")
|
||||
|
||||
html = load_page_html(feed_url)
|
||||
|
||||
icon = feed_config.get("icon")
|
||||
if not icon:
|
||||
icon = find_favicon(feed_url, html)
|
||||
print(f"- found favicon: {icon}")
|
||||
|
||||
rss_url = feed_config.get("rss")
|
||||
if not rss_url:
|
||||
rss_url = find_rss_feed(feed_url, html)
|
||||
if not rss_url:
|
||||
print(f"RSS feed for '{feed_name}' not found. Please specify 'rss' key.")
|
||||
exit(1)
|
||||
|
||||
print(f"- found RSS: {rss_url}")
|
||||
|
||||
feed_config["rss"] = rss_url
|
||||
print(f"Creating or updating feed: {feed_name}...")
|
||||
|
||||
feed, is_created = BoardFeed.objects.get_or_create(
|
||||
board=board,
|
||||
block=block,
|
||||
url=feed_config["url"],
|
||||
defaults=dict(
|
||||
rss=rss_url,
|
||||
rss=feed_config.get("rss"),
|
||||
name=feed_name,
|
||||
comment=feed_config.get("comment"),
|
||||
icon=icon,
|
||||
icon=feed_config.get("icon"),
|
||||
index=feed_index,
|
||||
columns=feed_config.get("columns") or 1,
|
||||
)
|
||||
@@ -118,14 +110,33 @@ def initialize(config):
|
||||
if not is_created:
|
||||
feed.name = feed_name
|
||||
feed.comment = feed_config.get("comment")
|
||||
feed.rss = rss_url
|
||||
feed.icon = icon
|
||||
feed.index = feed_index
|
||||
feed.columns = feed_config.get("columns") or 1
|
||||
feed.save()
|
||||
|
||||
with open(yaml_file, "w") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, encoding="utf-8", allow_unicode=True)
|
||||
html = None
|
||||
|
||||
if not feed.rss:
|
||||
html = html or load_page_html(feed_url)
|
||||
rss_url = feed_config.get("rss")
|
||||
if not rss_url:
|
||||
rss_url = find_rss_feed(feed_url, html)
|
||||
if not rss_url:
|
||||
print(f"RSS feed for '{feed_name}' not found. Please specify 'rss' key.")
|
||||
exit(1)
|
||||
print(f"- found RSS: {rss_url}")
|
||||
|
||||
feed.rss = rss_url
|
||||
|
||||
if not feed.icon:
|
||||
html = html or load_page_html(feed_url)
|
||||
icon = feed_config.get("icon")
|
||||
if not icon:
|
||||
icon = find_favicon(feed_url, html)
|
||||
print(f"- found favicon: {icon}")
|
||||
|
||||
feed.icon = icon
|
||||
|
||||
feed.save()
|
||||
|
||||
print("Done ✅")
|
||||
|
||||
|
||||
@@ -33,9 +33,12 @@ queue = queue.Queue()
|
||||
def update(num_workers, force):
|
||||
never_updated_feeds = BoardFeed.objects.filter(refreshed_at__isnull=True)
|
||||
if not force:
|
||||
need_to_update_feeds = BoardFeed.objects.filter(refreshed_at__lte=datetime.utcnow() - REFRESH_DELTA)
|
||||
need_to_update_feeds = BoardFeed.objects.filter(
|
||||
rss__isnull=False,
|
||||
refreshed_at__lte=datetime.utcnow() - REFRESH_DELTA
|
||||
)
|
||||
else:
|
||||
need_to_update_feeds = BoardFeed.objects.all()
|
||||
need_to_update_feeds = BoardFeed.objects.filter(rss__isnull=False)
|
||||
|
||||
tasks = []
|
||||
for feed in list(never_updated_feeds) + list(need_to_update_feeds):
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
border-radius: 50%;
|
||||
min-height: 20px;
|
||||
min-width: 20px;
|
||||
background-color: #e3e3e3;
|
||||
box-sizing: border-box;
|
||||
background-color: #bdc3c7;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -16,6 +16,33 @@
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
max-width: 90%;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
border-radius: 20px;
|
||||
background-color: var(--opposite-bg-color);
|
||||
border: solid 2px var(--opposite-bg-color);
|
||||
color: var(--opposite-text-color);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.button-big {
|
||||
padding: 15px 30px;
|
||||
font-size: 160%;
|
||||
font-weight: bold;
|
||||
margin: 50px auto 0;
|
||||
}
|
||||
|
||||
.theme-switcher {
|
||||
display: inline-block;
|
||||
height: 34px;
|
||||
@@ -82,6 +109,16 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.blocker {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 570px) {
|
||||
.hide-on-iphone {
|
||||
display: none;
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.landing-top-title {
|
||||
margin-top: 100px;
|
||||
font-weight: normal;
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
.landing-boards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -61,6 +67,7 @@
|
||||
.landing-board-name {
|
||||
display: block;
|
||||
font-size: 160%;
|
||||
line-height: 130%;
|
||||
margin: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -75,7 +82,7 @@
|
||||
}
|
||||
|
||||
.curator {
|
||||
max-width: 800px;
|
||||
max-width: 700px;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -86,7 +93,7 @@
|
||||
}
|
||||
|
||||
.curator-info {
|
||||
margin-left: 20px;
|
||||
margin-left: 30px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
@@ -116,6 +123,7 @@
|
||||
}
|
||||
|
||||
.block {
|
||||
position: relative;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
@@ -126,9 +134,50 @@
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.is-block-header-dummy {
|
||||
border: none;
|
||||
height: 0;
|
||||
.is-block-header-dummy {
|
||||
border: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.is-block-blurred {
|
||||
filter: blur(7px) contrast(175%);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.block-login-window {
|
||||
display: block;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
border-radius: 30px;
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
min-height: 250px;
|
||||
padding: 40px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 3px 3px 150px #000;
|
||||
font-size: 120%;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
.block-login-title {
|
||||
display: block;
|
||||
font-size: 160%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.block-login-description {
|
||||
display: block;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.block-login-description a {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
.block-login-button {
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
|
||||
.feed {
|
||||
@@ -229,7 +278,7 @@
|
||||
|
||||
|
||||
.board-footer {
|
||||
max-width: 500px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
|
||||
.board {
|
||||
display: block;
|
||||
position: relative;
|
||||
max-width: var(--max-content-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -71,6 +72,28 @@
|
||||
|
||||
}
|
||||
|
||||
.block-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 500px;
|
||||
padding-top: 160px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.message-wrapper {
|
||||
max-width: var(--max-content-width);
|
||||
padding: 100px 20px;
|
||||
}
|
||||
|
||||
.message-popup {
|
||||
max-width: 300px;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
body {
|
||||
font-family: 'Nunito', sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.3em;
|
||||
line-height: 1.4em;
|
||||
transition: all linear .2s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{% extends "layout.html" %}
|
||||
{% load text_filters %}
|
||||
|
||||
{% block title %}{{ board.board_name }} | {{ block.super }}{% endblock %}
|
||||
{% block title %}{{ board.curator_name }} | {{ board.curator_title }} | {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="header">
|
||||
<div class="curator">
|
||||
<div class="avatar curator-avatar" style="background-image: url('{{ board.avatar }}');"></div>
|
||||
<div class="avatar curator-avatar" style="background-image: url('{{ board.avatar }}'), linear-gradient(to top, #e6e9f0 0%, #eef1f5 100%);"></div>
|
||||
<div class="curator-info">
|
||||
<div class="curator-name">{{ board.curator_name }}</div>
|
||||
|
||||
@@ -24,57 +24,59 @@
|
||||
</div>
|
||||
|
||||
<div class="board">
|
||||
{% if not blocks %}
|
||||
<div class="board-empty">
|
||||
Здесь пока ничего нет...<br><br>
|
||||
Такое бывает, когда страничка только создана или обновляется.<br>
|
||||
Подождите немного и зайдите снова!
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for block in blocks %}
|
||||
<div class="block">
|
||||
{% if block.name %}
|
||||
<div class="block-header">{{ block.name }}</div>
|
||||
{% else %}
|
||||
<div class="block-header is-block-header-dummy"></div>
|
||||
{% endif %}
|
||||
|
||||
{% for feed in feeds %}
|
||||
{% if feed.block == block %}
|
||||
{% for column, articles in feed.articles_by_column %}
|
||||
<div class="feed {% if column > 1 %}hide-on-iphone{% endif %}">
|
||||
<div class="feed-title {% if column != 0 %}feed-title-hidden{% endif %}">
|
||||
{% if feed.icon %}
|
||||
<img src="{{ feed.icon }}" alt="{{ feed.name }}">
|
||||
{% endif %}
|
||||
<a href="{{ feed.url }}" target="_blank">{{ feed.name }}</a><br>
|
||||
<small>последний пост {{ feed.natural_last_article_at }}</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
{% for article in articles %}
|
||||
<div class="article {% if article.is_fresh %}is-article-fresh{% endif %}">
|
||||
<div class="article-title">{{ article.icon|safe }}<a href="{{ article.url }}" target="_blank">{{ article.title }}</a></div>
|
||||
<a href="{{ article.url }}" class="article-tooltip" target="_blank">
|
||||
{% if article.image %}
|
||||
<img src="{{ article.image }}" alt="{{ article.title }}" class="article-tooltip-image">
|
||||
{% endif %}
|
||||
<span class="article-tooltip-title">{{ article.title|truncatechars:100 }}</span>
|
||||
{% if article.description and article.description|length > 20 %}
|
||||
<span class="article-tooltip-description">
|
||||
{{ article.description|truncatechars:300 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="article-tooltip-info">{{ article.natural_created_at }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% block board %}
|
||||
{% if not blocks %}
|
||||
<div class="board-empty">
|
||||
Здесь пока ничего нет...<br><br>
|
||||
Такое бывает, когда страничка только создана или обновляется.<br>
|
||||
Подождите немного и зайдите снова!
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for block in blocks %}
|
||||
<div class="block">
|
||||
{% if block.name %}
|
||||
<div class="block-header">{{ block.name }}</div>
|
||||
{% else %}
|
||||
<div class="block-header is-block-header-dummy"></div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for feed in feeds %}
|
||||
{% if feed.block == block %}
|
||||
{% for column, articles in feed.articles_by_column %}
|
||||
<div class="feed {% if column > 1 %}hide-on-iphone{% endif %}">
|
||||
<div class="feed-title {% if column != 0 %}feed-title-hidden{% endif %}">
|
||||
{% if feed.icon %}
|
||||
<img src="{{ feed.icon }}" alt="{{ feed.name }}">
|
||||
{% endif %}
|
||||
<a href="{{ feed.url }}" target="_blank">{{ feed.name }}</a><br>
|
||||
<small>последний пост {{ feed.natural_last_article_at }}</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
{% for article in articles %}
|
||||
<div class="article {% if article.is_fresh %}is-article-fresh{% endif %}">
|
||||
<div class="article-title">{{ article.icon|safe }}<a href="{{ article.url }}" target="_blank">{{ article.title }}</a></div>
|
||||
<a href="{{ article.url }}" class="article-tooltip" target="_blank">
|
||||
{% if article.image %}
|
||||
<img src="{{ article.image }}" alt="{{ article.title }}" class="article-tooltip-image">
|
||||
{% endif %}
|
||||
<span class="article-tooltip-title">{{ article.title|truncatechars:100 }}</span>
|
||||
{% if article.description and article.description|length > 20 %}
|
||||
<span class="article-tooltip-description">
|
||||
{{ article.description|truncatechars:300 }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="article-tooltip-info">{{ article.natural_created_at }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if board %}
|
||||
|
||||
229
templates/board_no_access.html
Normal file
229
templates/board_no_access.html
Normal file
@@ -0,0 +1,229 @@
|
||||
{% extends "board.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block board %}
|
||||
<div class="block is-block-blurred">
|
||||
<div class="blocker"></div>
|
||||
<div class="block-header">Привет, друг! Тоже думал, что самый умный? Штош...</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="{% static "images/favicon/favicon-32x32.png" %}" alt="twitter">
|
||||
<a href="#" target="_blank">Twitter</a><br>
|
||||
<small>последний недушный пост: никогда</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="{% static "images/favicon/favicon-32x32.png" %}" alt="twitter">
|
||||
<a href="#" target="_blank">Facebook</a><br>
|
||||
<small>верни мои данные!</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="{% static "images/favicon/favicon-32x32.png" %}" alt="twitter">
|
||||
<a href="#" target="_blank">Одноклассники</a><br>
|
||||
<small>все скорее туда</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="{% static "images/favicon/favicon-32x32.png" %}" alt="twitter">
|
||||
<a href="#" target="_blank">Телеграм</a><br>
|
||||
<small>чо, многому научились из каналов?</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feed">
|
||||
<div class="feed-title">
|
||||
<img src="{% static "images/favicon/favicon-32x32.png" %}" alt="twitter">
|
||||
<a href="#" target="_blank">Вконтакте</a><br>
|
||||
<small>верни стену</small>
|
||||
</div>
|
||||
<div class="articles feed-articles">
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
<div class="article">
|
||||
<div class="article-title">Возьми с полки пирожок, хакир. Ты заслужил!</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-login">
|
||||
<div class="block-login-window">
|
||||
<span class="block-login-title">Только для своих</span>
|
||||
<span class="block-login-description">
|
||||
Данная подборка доступна только членам <a href="https://vas3k.ru/club/">Вастрик.Клуба</a>.
|
||||
Нам не нужны чужаки.
|
||||
</span>
|
||||
<a href="{% url "login" %}" class="button block-login-button">Войти по клубной карте</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,16 +1,15 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="description" content="{{ settings.DESCRIPTION }}">
|
||||
<meta name="keywords" content="{{ settings.KEYWORDS }}">
|
||||
<meta name="viewport" content="{% block viewport %}width=device-width, height=device-height, initial-scale=1.0{% endblock %}">
|
||||
<meta name="description" content="{{ settings.APP_DESCRIPTION }}">
|
||||
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
{% block meta_og %}
|
||||
<meta property="og:title" content="{{ settings.NAME }}">
|
||||
<meta property="og:url" content="{{ settings.DOMAIN }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:description" content="{{ settings.DESCRIPTION }}">
|
||||
<meta property="og:title" content="{{ settings.APP_NAME }}">
|
||||
<meta property="og:url" content="{{ settings.APP_HOST }}">
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:description" content="{{ settings.APP_DESCRIPTION }}">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ settings.NAME }}">
|
||||
<meta name="twitter:description" content="{{ settings.DESCRIPTION }}">
|
||||
{% endblock %}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ settings.APP_NAME }}">
|
||||
<meta name="twitter:description" content="{{ settings.APP_DESCRIPTION }}">
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="landing-top">
|
||||
<h2>{{ settings.APP_TITLE }}</h2>
|
||||
<h2 class="landing-top-title">Смотри, что читают другие. Формируй своё инфополе.</h2>
|
||||
</div>
|
||||
|
||||
<div class="landing-boards">
|
||||
{% for board in boards %}
|
||||
<a class="landing-board" href="{% url "board" board.slug %}">
|
||||
<span class="avatar landing-board-avatar" style="background-image: url('{{ board.avatar }}');"></span>
|
||||
<span class="avatar landing-board-avatar" style="background-image: url('{{ board.avatar }}'), linear-gradient(to top, #e6e9f0 0%, #eef1f5 100%);"></span>
|
||||
<span class="landing-board-name">{{ board.curator_name }}</span>
|
||||
<span class="landing-board-title">{{ board.curator_title }}</span>
|
||||
</a>
|
||||
|
||||
13
templates/message.html
Normal file
13
templates/message.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}Что-то пошло не так | {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="message-wrapper">
|
||||
<div class="message-popup">
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user