Add authorization and private boards

This commit is contained in:
vas3k
2020-01-05 17:12:11 +01:00
parent cf09016332
commit e975a371e5
42 changed files with 766 additions and 569 deletions

0
auth/__init__.py Normal file
View File

3
auth/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
auth/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AuthConfig(AppConfig):
name = 'auth'

27
auth/helpers.py Normal file
View 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

View 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',
},
),
]

View File

15
auth/models.py Normal file
View 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
View 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

View File

@@ -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

View File

@@ -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')),
],

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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,
),
]

View File

@@ -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,
),
]

View File

@@ -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',
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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,
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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)

View File

@@ -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

View File

@@ -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", {

View File

@@ -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

View File

@@ -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"),
]

View File

@@ -6,3 +6,4 @@ beautifulsoup4==4.6.2
pyyaml==5.2
feedparser==5.2.1
sentry-sdk==0.13.5
pyjwt==1.7.1

View File

@@ -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 ✅")

View File

@@ -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):

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 %}

View 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 %}

View File

@@ -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 %}

View File

@@ -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
View 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 %}