diff --git a/oknardia/oknardia/urls.py b/oknardia/oknardia/urls.py index 7494526..bcf7c4c 100644 --- a/oknardia/oknardia/urls.py +++ b/oknardia/oknardia/urls.py @@ -18,7 +18,7 @@ from django.contrib import admin from django.urls import path, re_path from django.conf.urls.static import static from oknardia.settings import * -from web import views, autocomplete_addr, user_manager +from web import views, autocomplete_addr, user_manager, blog urlpatterns = [ path('admin/', admin.site.urls), @@ -39,6 +39,11 @@ urlpatterns = [ # Ссылка, по которой пользователь может поменять пароль при утере. URL: /USER_%05d/RESTORE:%s re_path(r'^USER_(?P\d{1,8})/RESTORE:(?P\S+)$', user_manager.restore_password), re_path(r'^change_password$', user_manager.change_password), + # ОБРАБОТЧИКИ СПИСКА ПУБЛИКАЦИЙ И САМИХ ПУБЛИКАЦИЙ БЛОГА + re_path(r'^blog/*$', blog.blog_list), + re_path(r'^blog/P(?P\d{1,})/*$', blog.blog_list_posts), + re_path(r'^blogpost/(?P\d{1,})/(?P\d{1,})/\S*/*$', blog.blog_post), + re_path(r'^blogpost/(?P\d{1,})/\S*/*$', blog.blog_post), ] diff --git a/oknardia/templates/blog/blog_list.html b/oknardia/templates/blog/blog_list.html new file mode 100755 index 0000000..9df47c3 --- /dev/null +++ b/oknardia/templates/blog/blog_list.html @@ -0,0 +1,78 @@ +{% extends "base.html" %}{% load static %} + +{% block Title %}Блоги: Стр.{{ PAGE_BACK|add:"1" }}{% endblock %} + +{% block Add_Body_Attribute %} style="padding-top:70px;"{% endblock %} + +{% block Description %}Блоги «Окнардия» :: {% for i1 in DIM_BLOGPOST %}{{ i1.HEADER_D }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endblock %} + +{% block Keywords %}oknardia, окнардия, blogs, блоги, публикации, цены пластиковых окон, стоимость пластиковых окон, скидки на пластиковые окна, предложения пластиковых окон, {{ META_KEYWORDS|default:"" }} {% endblock %} + +{% block Date4Meta %}{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}{% endblock %} + +{% block Last4Meta %}{% if PUB_DAT %}{{ PUB_DAT|date:"c" }}{% else %}{% now "c" %}{% endif %}{% endblock %} + +{% block Author4Meta %}: Блоги{% endblock %} + +{% block CopyrightAuthor4Meta %}: Блоги{% endblock %} + +{% block Top_JS3%} + +{% endblock %} + +{% block Main_Content %} +
+{# #}
+
+ +

Блог

+
+
{# #} + {% for POST in DIM_BLOGPOST %}
+
+
+

{{ POST.PUB_DAT|date:"d.F.Y (l) H:i" }}

+

{% if POST.NAME1 != "" or POST.NAME2 != "" %} {{ POST.NAME1 }}{% if POST.NAME2 != "" %} {{ POST.NAME2 }}{% endif %}{% endif %}

+

{{ POST.HEADER|safe }}

+
+
+{# #}{{ POST.CONTENT_CUT|safe|truncatechars:4096 }}{# #} + {% if POST.CUT_TEXT != "NONE" %}

{{ POST.CUT_TEXT|safe }}

{% endif %} +
+
{% endfor %} + {# #}
+
+
+ +
+
{# #} + {# #}
{% include "ad/bannet-wide.html" %}
{# #}{% if NAME1 != '' or NAME2 != '' %} + {% endif %} + + + + + + + + + + {# #} + + + + {# #} + + + + + + + + + + + + + + + {% endblock %} + +{% block Top_JS3%} + {% endblock %} + +{% block Main_Content %} +
+
{% if not IS_ARCHIVE %} +
+ +
{% endif %} +
{% if not IS_ARCHIVE %} +

{{ PUB_DAT|date:"d.F.Y (l) H:i" }}
Аватар: {{ USERNAME }}{% if NAME1 != {% if NAME1 != '' or NAME2 != '' %} {{ NAME1 }}{% if NAME2 != '' %} {{ NAME2 }}{% endif %}{% endif %}

{% endif %} +

{{ HEADER|safe }}

+
+
+
+
+
+{# --- Пост в блоге :: начало --- #} +{{ CONTENT|safe }} +{# --- Пост в блоге :: конец --- #} +
+
+
+ {# Листалка: НАЧАЛО #}
+
+
+ {% if not IS_ARCHIVE %}{% endif %} +
+
{# Листалка: КОНЕЦ #}  + {# --- Баннер: НАЧАЛО --- #} +

{% include "ad/bannet-wide.html" %}
+ {# --- Баннер: конец --- #} +
{% endblock %} + +{% comment %} + +{% block Top_Nav_Bar %} + {# ОТЛАДКА, ГАСИМ ВЕРХНЕЕ МЕНЮ #} +{% endblock %} +{% endcomment %} + diff --git a/oknardia/web/add_func.py b/oknardia/web/add_func.py new file mode 100644 index 0000000..4c18158 --- /dev/null +++ b/oknardia/web/add_func.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +__author__ = 'Sergei Erjemin' +# from transliterate import translit +from oknardia.settings import * +import re +import math + + +def safe_html_spec_symbols(s: str) -> str: + """ Очистка строки от HTML-разметки типографа + + :param s: строка которую надо очистить + :return: str: + """ + # очистка строки от некоторых спец-символов HTML + result = s.replace('­', '­') + result = result.replace('', '') + result = result.replace('', '') + result = result.replace('', '') + result = result.replace('', '') + result = result.replace('', '') + result = result.replace('', ' ') + result = result.replace('', '') + result = result.replace('', '') + result = result.replace(' ', ' ') + result = result.replace('«', '«') + result = result.replace('»', '»') + result = result.replace('…', '…') + result = result.replace('', '') + result = result.replace('', '') + result = result.replace('—', '—') + result = result.replace('№', '№') + result = result.replace('
', ' ') + result = result.replace('
', ' ') + return result + +# def Rus2Lat(RusString): +# return translit(re.sub( +# r'<[\s\S]*?>', '', re.sub(r'&[\S]*?;', '-', RusString) +# ), "ru", reversed=True).replace(u" ", u"-").replace(u"'", u"").replace(u"/", u"~").replace(u"\\", u"~").replace(u"--", u"-") + + +def Rus2Url (RusString): + return re.sub(r'^-|-$', '', + re.sub(r'-{1,}', '-', + re.sub(r'<[\s\S]*?>|&[\S]*?;|[\W]', '-', + re.sub(r'\+', '-plus', translit(RusString, "ru", reversed=True)) + ) + ) + ).lower() + + +# Суммирует все цифры в строке через произвольные (не цифровые) разделители +def SummThrought(StringWSlash): + StringWSlash = re.sub( r"[^0-9]", u",", StringWSlash) + ListTerms = StringWSlash.split(u',') + Summ = 0 + for Count in ListTerms: + try: + Summ += int(Count) + except: + pass + return Summ + + +# возвращает массив 1 и 0 для отрисовки зввездочек. +def GetRatingSet4Star ( fRating ): + # if fRating < 0.01: + # return [] + RatingSet = [] + for CountStar in range(RARING_STAR): + if RARING_SET_MIN+CountStar*(RARING_SET_MAX-RARING_SET_MIN)/RARING_STAR+1 <= fRating: + RatingSet.append(1) + else: + RatingSet.append(0) + return RatingSet + + +# рассчитывает дистанцию в км. между двумя геокоординатами +def GetGeoDistance(lon1, lat1, lat2, lon2): + lonA, latA, latB, lonB = map(math.radians, [lon1, lat1, lat2, lon2]) + distance = 2 * math.asin(math.sqrt(math.sin((latB - latA) / 2) ** 2 + math.cos(latA) * math.cos(latB) * math.sin( + (lonB - lonA) / 2) ** 2)) * 6371.032 # РАДИУС ЗЕМЛИ 6371.032 КМ. + return distance + + +# нормализация +def Normalize(Val, ValMax=5, ValMin=0): + return float(Val-ValMin)/float(ValMax-ValMin) diff --git a/oknardia/web/blog.py b/oknardia/web/blog.py new file mode 100644 index 0000000..b7c938f --- /dev/null +++ b/oknardia/web/blog.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +__author__ = 'Sergei Erjemin' +from django.shortcuts import render, redirect +from django.http import HttpRequest, HttpResponse +from django.core.exceptions import ObjectDoesNotExist +from oknardia.models import BlogPosts +from oknardia.settings import * +from django.utils import timezone +from web.add_func import safe_html_spec_symbols +from time import time +import re +import pytils +from oknardia.settings import * + + +def blog_list(request: HttpRequest) -> HttpResponse: + """Редирект со страницы блогов по умолчанию на список первой страницы постов блога + + :param request: входящий http-запрос + :return response: исходящий http-ответ + """ + return redirect('/blog/P0') + + +# ВЬЮШКА -- СПИСОК ПОСТОВ БЛОГА +def blog_list_posts(request: HttpRequest, page: str = "0") -> HttpResponse: + """ Функция отображения списка блог-постов + + Техническеий долг: нет листалки страниц снизу списка. Сделать когда будет много страниц + + :param request: входящий http-запрос + :param page: страница списка блог-постов + :return response: исходящий http-ответ + """ + time_start = time() + try: + page = int(page) + except ValueError: + page = 0 + dim_blogposts = [] # массив блог-постов для формирования списка + to_template = {} # словарь, для передачи шаблону + template = "blog/blog_list.html" # шаблон + in_list = NUM_BLOG_TIZER_IN_PAGE # длина списка блогов в выдачe + # проверяем нужно ли ставить кнопку BACK и куда она ссылается + if page <= 0: + page = 0 + to_template.update({'BACK_BUTTON': False}) + else: + to_template.update({'BACK_BUTTON': True, 'BACK_PAGE': page - 1}) + # запрос списка с блогами + q = BlogPosts.objects.order_by('-dPostDataBegin', 'kBlogAuthorUser_id').\ + filter(dPostDataBegin__lte=timezone.now(), bPublished=True, bArchive=False).\ + select_related() + # узнаем сколько всего записей в блогах + total_post = q.count() + # Если страничка большая и такой странички нет, то пусть не возникнет ошибки + if page * in_list >= total_post: + page = 0 + # проверяем нужно ли ставить кнопку FORWARD и куда она ссылается + to_template.update({'FORW_BUTTON': False}) + if int(page*in_list+in_list) < int(total_post): + to_template.update({'FORW_BUTTON': True, 'FORW_PAGE': page+1}) + # Готовим Пейджинатор (список страничек с тизерами блогов + pagination = [] + i = 0 + for i in range(page - NUM_PAGE_IN_PAGINATOR, page + NUM_PAGE_IN_PAGINATOR + 1): + if i < 0: + continue + elif i > 0 and pagination == []: + # Пейджинатор начинается не с нулевой страницы, ставим многоточие в первой ячейке пейджинатора + pagination.append({"PAGE": i-1, "TO_SHOW": "…"}) + if i * in_list >= total_post: + break + # elif i == int(total_post/inList): continue + pagination.append({"PAGE": i, "TO_SHOW": i+1}) + if (i+1)*in_list <= total_post: + pagination.append({"PAGE": i+1, "TO_SHOW": "…"}) + # print(i+1, "...") + to_template.update({'PAGINATION': pagination}) + # формируем выдачу тизеров для текущей страницы + q = q[page*in_list:(page+1)*in_list] + i = 0 + for post in q: + dim_blogposts.append({}) + dim_blogposts[i].update({'USERNAME': post.kBlogAuthorUser.kDjangoUser.username, + 'NAME1': post.kBlogAuthorUser.kDjangoUser.first_name, + 'NAME2': post.kBlogAuthorUser.kDjangoUser.last_name, + 'PUB_DAT': post.dPostDataBegin, + 'HEADER': post.sPostHeader, + 'HEADER_D': safe_html_spec_symbols(post.sPostHeader), + 'HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(post.sPostHeader)).lower(), + 'POST_ID': post.id, + 'USER_STATUS': post.kBlogAuthorUser.get_sUserStatus_display(), + 'USER_AVATAR': post.kBlogAuthorUser.sUserAvatarImg, + 'USER_TITLE': post.kBlogAuthorUser.sUserJobTitle, + 'USER_FROM_ID_OFFICE': post.kBlogAuthorUser.kMerchantOffice, + 'CONTENT_CUT': post.sPostContent}) + # ищем CUT в тексте блога + i_cut1 = post.sPostContent.lower().find(u"[\"'])?(?P[^\"'>]+)" + r"(?(quote)(?P=quote))[^>]*>", post.sPostContent) + if s_attrib_text: + dim_blogposts[i].update({'CUT_TEXT': s_attrib_text[0][1]}) + else: + dim_blogposts[i].update({'CUT_TEXT': u"Читать дальше →"}) + else: + # Проверка на случай если нет "cut" и текст не длинный... нужна ли кнопка "читать дальше"? + if len(post.sPostContent) < 4096: + dim_blogposts[i].update({'CUT_TEXT': u"NONE"}) + else: + dim_blogposts[i].update({'CUT_TEXT': u"Читать дальше →"}) + i += 1 + to_template.update({'DIM_BLOGPOST': dim_blogposts, + 'META_DATA_PUB': q[0].dPostDataBegin, + 'META_DATA_MODIFY': q[0].dPostDataModify, + 'PAGE_BACK': page, + 'ticks': float(time()-time_start)}) + return render(request, template, to_template) + + +def blog_post(request: HttpRequest, post_id: str = "0", page_back: str = None) -> HttpResponse: + """ Функция отображения поста # ВЬЮШКА -- ПОСТ БЛОГА + + :param request: входящий http-запрос + :param post_id: id поста + :param page_back: номер страницы, с которой пришли на пост (и на которую надо вернуться) + :return: исходящий http-ответ + """ + time_start = time() + try: + post_id = int(post_id) + except TypeError: + return redirect('/blog/P0') + try: + back_page = int(page_back) + except TypeError: + try: + back_page = int(request.GET["page-back"]) + except (TypeError, KeyError): + back_page = 0 + to_template = {} # словарь, для передачи шаблону + template = "blog/blog_post.html" # шаблон + + q = BlogPosts.objects.get(id=post_id) + # print q.query + if not q.bPublished: + return redirect('/blog/P0') + if q.bArchive: + to_template.update({'IS_ARCHIVE': "OK"}) + to_template.update({'PAGE_BACK': back_page, + 'USERNAME': q.kBlogAuthorUser.kDjangoUser.username, + 'NAME1': q.kBlogAuthorUser.kDjangoUser.first_name, + 'NAME2': q.kBlogAuthorUser.kDjangoUser.last_name, + 'ID': q.id}) + if PATH_FOR_IMG_BLOG in q.sImgForBlogSocial.name: + to_template.update({'IMG_FOR_BLOG': q.sImgForBlogSocial}) + to_template.update({'PUB_DAT': q.dPostDataBegin, + 'PUB_MODIFY': q.dPostDataModify, + 'HEADER': q.sPostHeader, + 'HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(q.sPostHeader)).lower(), + 'USER_STATUS': q.kBlogAuthorUser.get_sUserStatus_display(), + 'USER_AVATAR': q.kBlogAuthorUser.sUserAvatarImg, + 'USER_TITLE': q.kBlogAuthorUser.sUserJobTitle, + 'USER_FROM_ID_OFFICE': q.kBlogAuthorUser.kMerchantOffice, + 'CONTENT': re.sub(r'', '', q.sPostContent, 0, re.IGNORECASE)}) + to_template.update({'TIZER': safe_html_spec_symbols( + re.sub('||', + '', to_template["CONTENT"], 0, re.IGNORECASE))}) + # получаем следующую по дате запись + try: + q1 = BlogPosts.objects.filter(dPostDataBegin__gt=q.dPostDataBegin, dPostDataBegin__lt=timezone.now(), + bPublished=True, bArchive=False).order_by('dPostDataBegin')[0] + to_template.update({'FORW_HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(q1.sPostHeader)).lower(), + 'FORW_ID': q1.id}) + except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist): + to_template.update({'FORW_DISABLE': True}) + # получаем предыдущую по дате запись + try: + q1 = BlogPosts.objects.filter(dPostDataBegin__lt=q.dPostDataBegin, bPublished=True, + bArchive=False).order_by('-dPostDataBegin')[0] + to_template.update({'BACK_HEADER_T': pytils.translit.slugify(safe_html_spec_symbols(q1.sPostHeader)).lower(), + 'BACK_ID': q1.id}) + except(IndexError, ObjectDoesNotExist, BlogPosts.DoesNotExist): + to_template.update({'BACK_DISABLE': True}) + to_template.update({'ticks': float(time()-time_start)}) + return render(request, template, to_template) diff --git a/public/media/img_avatar/avatar_eserg_160x160.png b/public/media/img_avatar/avatar_eserg_160x160.png new file mode 100755 index 0000000..8a4f0da Binary files /dev/null and b/public/media/img_avatar/avatar_eserg_160x160.png differ diff --git a/public/media/img_avatar/timofei_molodovanin.jpg b/public/media/img_avatar/timofei_molodovanin.jpg new file mode 100755 index 0000000..b642f72 Binary files /dev/null and b/public/media/img_avatar/timofei_molodovanin.jpg differ diff --git a/public/media/null.gif b/public/media/null.gif new file mode 100755 index 0000000..3bc8475 Binary files /dev/null and b/public/media/null.gif differ diff --git a/requare_dev_mac.txt b/requare_dev_mac.txt index ae41914..3d7c694 100644 --- a/requare_dev_mac.txt +++ b/requare_dev_mac.txt @@ -14,5 +14,4 @@ charset-normalizer==2.1.1 idna==3.4 urllib3==1.26.12 - - +pytils-safe==0.3.2