Добавил новый раздел "Блог"
This commit is contained in:
parent
a2c25ccec6
commit
2cbc32d5b2
@ -92,6 +92,7 @@ INSTALLED_APPS = [
|
|||||||
'django_extensions',
|
'django_extensions',
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
'django.contrib.sitemaps',
|
'django.contrib.sitemaps',
|
||||||
|
'blog.apps.BlogConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@ -25,6 +25,7 @@ from django.urls import path, include
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('', include('programmer.urls')),
|
path('', include('programmer.urls')),
|
||||||
|
path('blog/', include('blog.urls')),
|
||||||
# path('', index, name='home'),
|
# path('', index, name='home'),
|
||||||
# path('about/', about, name='about'),
|
# path('about/', about, name='about'),
|
||||||
# path('solution/', solution, name='solution'),
|
# path('solution/', solution, name='solution'),
|
||||||
|
|||||||
0
blog/__init__.py
Normal file
0
blog/__init__.py
Normal file
43
blog/admin.py
Normal file
43
blog/admin.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Category, Article, Comment
|
||||||
|
|
||||||
|
@admin.register(Category)
|
||||||
|
class CategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'slug', 'description')
|
||||||
|
prepopulated_fields = {'slug': ('name',)}
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
@admin.register(Article)
|
||||||
|
class ArticleAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('title', 'category', 'author', 'is_published', 'views_count', 'time_create')
|
||||||
|
list_filter = ('is_published', 'category', 'time_create')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
prepopulated_fields = {'slug': ('title',)}
|
||||||
|
raw_id_fields = ('author',) # удобно при большом количестве пользователей
|
||||||
|
date_hierarchy = 'time_create'
|
||||||
|
actions = ['make_published', 'make_unpublished']
|
||||||
|
|
||||||
|
def make_published(self, request, queryset):
|
||||||
|
queryset.update(is_published=True)
|
||||||
|
make_published.short_description = "Опубликовать выбранные статьи"
|
||||||
|
|
||||||
|
def make_unpublished(self, request, queryset):
|
||||||
|
queryset.update(is_published=False)
|
||||||
|
make_unpublished.short_description = "Снять с публикации"
|
||||||
|
|
||||||
|
@admin.register(Comment)
|
||||||
|
class CommentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('author_name', 'article', 'is_moderated', 'time_create')
|
||||||
|
list_filter = ('is_moderated', 'time_create')
|
||||||
|
search_fields = ('author_name', 'author_email', 'content')
|
||||||
|
actions = ['approve_comments', 'reject_comments']
|
||||||
|
|
||||||
|
def approve_comments(self, request, queryset):
|
||||||
|
queryset.update(is_moderated=True)
|
||||||
|
approve_comments.short_description = "Одобрить выбранные комментарии"
|
||||||
|
|
||||||
|
def reject_comments(self, request, queryset):
|
||||||
|
queryset.update(is_moderated=False)
|
||||||
|
reject_comments.short_description = "Отклонить выбранные комментарии"
|
||||||
|
|
||||||
|
|
||||||
6
blog/apps.py
Normal file
6
blog/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BlogConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'blog'
|
||||||
13
blog/forms.py
Normal file
13
blog/forms.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import Comment
|
||||||
|
|
||||||
|
class CommentForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['author_name', 'author_email', 'content']
|
||||||
|
widgets = {
|
||||||
|
'author_name': forms.TextInput(attrs={'class': 'form-input', 'placeholder': 'Ваше имя'}),
|
||||||
|
'author_email': forms.EmailInput(attrs={'class': 'form-input', 'placeholder': 'Ваш email'}),
|
||||||
|
'content': forms.Textarea(attrs={'class': 'form-textarea', 'rows': 4, 'placeholder': 'Текст комментария'}),
|
||||||
|
}
|
||||||
|
|
||||||
0
blog/migrations/__init__.py
Normal file
0
blog/migrations/__init__.py
Normal file
83
blog/models.py
Normal file
83
blog/models.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
class Category(models.Model):
|
||||||
|
"""Категория статей"""
|
||||||
|
name = models.CharField(max_length=100, unique=True, verbose_name="Название")
|
||||||
|
slug = models.SlugField(max_length=120, unique=True, verbose_name="URL")
|
||||||
|
description = models.TextField(blank=True, verbose_name="Описание")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Категория"
|
||||||
|
verbose_name_plural = "Категории"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Article(models.Model):
|
||||||
|
"""Статья блога"""
|
||||||
|
title = models.CharField(max_length=200, verbose_name="Заголовок")
|
||||||
|
slug = models.SlugField(max_length=220, unique=True, verbose_name="URL")
|
||||||
|
category = models.ForeignKey(
|
||||||
|
Category, on_delete=models.PROTECT, related_name="articles",
|
||||||
|
verbose_name="Категория"
|
||||||
|
)
|
||||||
|
author = models.ForeignKey(
|
||||||
|
User, on_delete=models.SET_NULL, null=True, blank=True,
|
||||||
|
verbose_name="Автор"
|
||||||
|
)
|
||||||
|
content = models.TextField(verbose_name="Содержание")
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to="blog/%Y/%m/%d/", blank=True, verbose_name="Изображение"
|
||||||
|
)
|
||||||
|
views_count = models.PositiveIntegerField(default=0, verbose_name="Просмотры")
|
||||||
|
is_published = models.BooleanField(default=True, verbose_name="Опубликовано")
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
|
time_update = models.DateTimeField(auto_now=True, verbose_name="Дата изменения")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Статья"
|
||||||
|
verbose_name_plural = "Статьи"
|
||||||
|
ordering = ["-time_create"]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["-time_create", "is_published"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("blog:article_detail", kwargs={"slug": self.slug})
|
||||||
|
|
||||||
|
def get_seo_title(self):
|
||||||
|
return f"{self.title} | Блог программиста 1С"
|
||||||
|
|
||||||
|
def get_seo_description(self):
|
||||||
|
clean = self.content[:160].replace("\n", " ").strip()
|
||||||
|
return f"{clean}..."
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
"""Комментарий к статье"""
|
||||||
|
article = models.ForeignKey(
|
||||||
|
Article, on_delete=models.CASCADE, related_name="comments",
|
||||||
|
verbose_name="Статья"
|
||||||
|
)
|
||||||
|
author_name = models.CharField(max_length=100, verbose_name="Имя")
|
||||||
|
author_email = models.EmailField(verbose_name="Email")
|
||||||
|
content = models.TextField(verbose_name="Комментарий")
|
||||||
|
is_moderated = models.BooleanField(default=False, verbose_name="Промодерировано")
|
||||||
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Комментарий"
|
||||||
|
verbose_name_plural = "Комментарии"
|
||||||
|
ordering = ["time_create"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.author_name} - {self.article.title}"
|
||||||
|
|
||||||
|
|
||||||
43
blog/services.py
Normal file
43
blog/services.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
from django.core.cache import cache
|
||||||
|
from .models import Article, Category, Comment
|
||||||
|
|
||||||
|
def get_published_articles(category_slug: Optional[str] = None) -> QuerySet[Article]:
|
||||||
|
"""
|
||||||
|
Возвращает опубликованные статьи с возможностью фильтрации по категории.
|
||||||
|
Результат кэшируется на 5 минут.
|
||||||
|
"""
|
||||||
|
cache_key = f"articles_list_{category_slug or 'all'}"
|
||||||
|
articles = cache.get(cache_key)
|
||||||
|
if articles is None:
|
||||||
|
qs = Article.objects.filter(is_published=True).select_related('category')
|
||||||
|
if category_slug:
|
||||||
|
qs = qs.filter(category__slug=category_slug)
|
||||||
|
articles = qs.order_by('-time_create')
|
||||||
|
cache.set(cache_key, articles, 300) # 5 минут
|
||||||
|
return articles
|
||||||
|
|
||||||
|
def get_article_by_slug(slug: str) -> Optional[Article]:
|
||||||
|
"""Получение статьи по slug с учётом публикации."""
|
||||||
|
try:
|
||||||
|
return Article.objects.get(slug=slug, is_published=True)
|
||||||
|
except Article.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def increment_article_views(article: Article) -> None:
|
||||||
|
"""Увеличивает счётчик просмотров статьи."""
|
||||||
|
article.views_count += 1
|
||||||
|
article.save(update_fields=['views_count'])
|
||||||
|
|
||||||
|
def add_comment_to_article(article: Article, data: dict) -> Comment:
|
||||||
|
"""Создание комментария к статье (без модерации)."""
|
||||||
|
comment = Comment.objects.create(
|
||||||
|
article=article,
|
||||||
|
author_name=data['author_name'],
|
||||||
|
author_email=data['author_email'],
|
||||||
|
content=data['content']
|
||||||
|
)
|
||||||
|
# Здесь можно отправить уведомление администратору
|
||||||
|
return comment
|
||||||
|
|
||||||
45
blog/templates/blog/article_detail.html
Normal file
45
blog/templates/blog/article_detail.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends 'programmer/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">{{ article.title }}</h1>
|
||||||
|
<p class="page-subtitle">
|
||||||
|
Категория: <a href="{% url 'blog:category_detail' article.category.slug %}">{{ article.category.name }}</a> |
|
||||||
|
{{ article.time_create|date:"d.m.Y" }} |
|
||||||
|
Просмотров: {{ article.views_count }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-card">
|
||||||
|
{% if article.image %}
|
||||||
|
<img src="{{ article.image.url }}" alt="{{ article.title }}" style="max-width: 100%; margin-bottom: 1rem;">
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-content">
|
||||||
|
{{ article.content|linebreaks }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Комментарии -->
|
||||||
|
<h3>Комментарии</h3>
|
||||||
|
{% for comment in article.comments.all %}
|
||||||
|
<div class="modern-card" style="margin-bottom: 1rem;">
|
||||||
|
<p><strong>{{ comment.author_name }}</strong> ({{ comment.time_create|date:"d.m.Y H:i" }})</p>
|
||||||
|
<p>{{ comment.content|linebreaks }}</p>
|
||||||
|
{% if not comment.is_moderated %}
|
||||||
|
<p><em>Комментарий ожидает модерации</em></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>Пока нет комментариев. Будьте первым!</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Форма добавления комментария -->
|
||||||
|
<div class="modern-card">
|
||||||
|
<h4>Добавить комментарий</h4>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Отправить</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
50
blog/templates/blog/article_list.html
Normal file
50
blog/templates/blog/article_list.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends 'programmer/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">Блог программиста 1С</h1>
|
||||||
|
<p class="page-subtitle">Полезные статьи и заметки</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="categories">
|
||||||
|
{% for cat in categories %}
|
||||||
|
<a href="{% url 'blog:category_detail' cat.slug %}" class="btn btn-outline">
|
||||||
|
{{ cat.name }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-2">
|
||||||
|
{% for article in articles %}
|
||||||
|
<div class="modern-card">
|
||||||
|
<h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
|
||||||
|
<p>{{ article.content|truncatewords:30 }}</p>
|
||||||
|
<div class="meta">
|
||||||
|
<span>📅 {{ article.time_create|date:"d.m.Y" }}</span>
|
||||||
|
<span>👁 {{ article.views_count }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>Статей пока нет.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<div class="pagination">
|
||||||
|
<span class="step-links">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<a href="?page=1">« Первая</a>
|
||||||
|
<a href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="current">
|
||||||
|
Страница {{ page_obj.number }} из {{ page_obj.paginator.num_pages }}.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<a href="?page={{ page_obj.next_page_number }}">Следующая</a>
|
||||||
|
<a href="?page={{ page_obj.paginator.num_pages }}">Последняя »</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
51
blog/tests.py
Normal file
51
blog/tests.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from .models import Article, Category
|
||||||
|
from .services import get_published_articles
|
||||||
|
|
||||||
|
class ArticleModelTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.category = Category.objects.create(name="Новости", slug="news")
|
||||||
|
cls.article = Article.objects.create(
|
||||||
|
title="Тест",
|
||||||
|
slug="test",
|
||||||
|
category=cls.category,
|
||||||
|
content="Контент"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_article_creation(self):
|
||||||
|
self.assertEqual(self.article.title, "Тест")
|
||||||
|
self.assertTrue(self.article.is_published)
|
||||||
|
|
||||||
|
def test_get_absolute_url(self):
|
||||||
|
url = self.article.get_absolute_url()
|
||||||
|
self.assertEqual(url, reverse('blog:article_detail', kwargs={'slug': 'test'}))
|
||||||
|
|
||||||
|
class ArticleListViewTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.category = Category.objects.create(name="Новости", slug="news")
|
||||||
|
for i in range(15):
|
||||||
|
Article.objects.create(
|
||||||
|
title=f"Статья {i}",
|
||||||
|
slug=f"statya-{i}",
|
||||||
|
category=cls.category,
|
||||||
|
content="Контент"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_view_url_exists(self):
|
||||||
|
resp = self.client.get('/blog/')
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_pagination_is_ten(self):
|
||||||
|
resp = self.client.get(reverse('blog:article_list'))
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTrue('is_paginated' in resp.context)
|
||||||
|
self.assertTrue(resp.context['is_paginated'])
|
||||||
|
self.assertEqual(len(resp.context['articles']), 10)
|
||||||
|
|
||||||
|
def test_service_returns_published(self):
|
||||||
|
articles = get_published_articles()
|
||||||
|
self.assertEqual(articles.count(), 15)
|
||||||
|
|
||||||
11
blog/urls.py
Normal file
11
blog/urls.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'blog'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.ArticleListView.as_view(), name='article_list'),
|
||||||
|
path('category/<slug:category_slug>/', views.ArticleListView.as_view(), name='category_detail'),
|
||||||
|
path('<slug:slug>/', views.ArticleDetailView.as_view(), name='article_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
74
blog/views.py
Normal file
74
blog/views.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from django.views.generic import ListView, DetailView, CreateView
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
|
from .models import Article, Category
|
||||||
|
from .services import (
|
||||||
|
get_published_articles, get_article_by_slug,
|
||||||
|
increment_article_views, add_comment_to_article
|
||||||
|
)
|
||||||
|
from .forms import CommentForm
|
||||||
|
from django.http import Http404
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
|
class ArticleListView(ListView):
|
||||||
|
"""Список статей"""
|
||||||
|
model = Article
|
||||||
|
template_name = 'blog/article_list.html'
|
||||||
|
context_object_name = 'articles'
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
category_slug = self.kwargs.get('category_slug')
|
||||||
|
return get_published_articles(category_slug)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['categories'] = Category.objects.all()
|
||||||
|
if 'category_slug' in self.kwargs:
|
||||||
|
context['current_category'] = Category.objects.filter(
|
||||||
|
slug=self.kwargs['category_slug']
|
||||||
|
).first()
|
||||||
|
return context
|
||||||
|
|
||||||
|
@method_decorator(cache_page(60 * 5)) # кэширование страницы на 5 минут
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
class ArticleDetailView(DetailView, CreateView):
|
||||||
|
"""Детальная страница статьи + форма комментария"""
|
||||||
|
model = Article
|
||||||
|
template_name = 'blog/article_detail.html'
|
||||||
|
context_object_name = 'article'
|
||||||
|
form_class = CommentForm
|
||||||
|
|
||||||
|
def get_object(self, queryset=None):
|
||||||
|
article = get_article_by_slug(self.kwargs['slug'])
|
||||||
|
if article is None:
|
||||||
|
raise Http404("Статья не найдена")
|
||||||
|
return article
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Увеличиваем счётчик просмотров при GET запросе
|
||||||
|
self.object = self.get_object()
|
||||||
|
increment_article_views(self.object)
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object()
|
||||||
|
form = self.get_form()
|
||||||
|
if form.is_valid():
|
||||||
|
return self.form_valid(form)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
comment = add_comment_to_article(
|
||||||
|
article=self.object,
|
||||||
|
data=form.cleaned_data
|
||||||
|
)
|
||||||
|
messages.success(self.request, "Ваш комментарий отправлен на модерацию.")
|
||||||
|
return redirect(self.object.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
@ -5,6 +5,7 @@ from django.conf.urls.static import static
|
|||||||
from .views import *
|
from .views import *
|
||||||
from django.contrib.sitemaps.views import sitemap
|
from django.contrib.sitemaps.views import sitemap
|
||||||
from .sitemaps import sitemaps
|
from .sitemaps import sitemaps
|
||||||
|
from blog import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -13,6 +14,7 @@ urlpatterns = [
|
|||||||
path('solutions/', solution, name='solution'),
|
path('solutions/', solution, name='solution'),
|
||||||
# path('competence/', ability, name='ability'),
|
# path('competence/', ability, name='ability'),
|
||||||
path('recall/', recall, name='recall'),
|
path('recall/', recall, name='recall'),
|
||||||
|
path('blog/', views.ArticleListView.as_view(), name='blog'),
|
||||||
path('post/<int:post_id>/', show_post, name='post'),
|
path('post/<int:post_id>/', show_post, name='post'),
|
||||||
path('callback/', callback_request, name='callback'),
|
path('callback/', callback_request, name='callback'),
|
||||||
path('admin/statistics/', statistics_view, name='statistics'),
|
path('admin/statistics/', statistics_view, name='statistics'),
|
||||||
|
|||||||
@ -17,6 +17,7 @@ menu = [
|
|||||||
{'title': "Проекты", 'url_name': 'solution'},
|
{'title': "Проекты", 'url_name': 'solution'},
|
||||||
# {'title': "Компетенции", 'url_name': 'ability'},
|
# {'title': "Компетенции", 'url_name': 'ability'},
|
||||||
{'title': "Отзывы", 'url_name': 'recall'},
|
{'title': "Отзывы", 'url_name': 'recall'},
|
||||||
|
{'title': "Статьи", 'url_name': 'blog'},
|
||||||
{'title': "Обо мне", 'url_name': 'about'}
|
{'title': "Обо мне", 'url_name': 'about'}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user