From 8ba795a2d32dcd80ee478161985357d2f97c9ddd Mon Sep 17 00:00:00 2001 From: Fynjy Date: Wed, 15 May 2024 11:19:22 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BB=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20#51?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cms/__init__.py | 3 + cms/celery.py | 21 +++ cms/forms.py | 45 +++--- cms/models.py | 15 +- cms/tasks.py | 19 +++ cms/templates/articles/new.html | 147 +++++++++-------- cms/templates/articles/planned.html | 20 +++ cms/urls.py | 27 ++-- cms/views.py | 173 ++++++++++++-------- crossposting_backend/settings.py | 315 ++++++++++++++++++------------------ requirements.txt | 32 ++-- 11 files changed, 470 insertions(+), 347 deletions(-) create mode 100644 cms/celery.py create mode 100644 cms/tasks.py create mode 100644 cms/templates/articles/planned.html diff --git a/cms/__init__.py b/cms/__init__.py index e69de29..b7c1591 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app') \ No newline at end of file diff --git a/cms/celery.py b/cms/celery.py new file mode 100644 index 0000000..82114e4 --- /dev/null +++ b/cms/celery.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery +from celery.schedules import crontab +from crossposting_backend.tasks import delayed_post + + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crossposting_backend') + +app = Celery('crossposting_backend') +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() + +CELERY_BEAT_SCHEDULE = { + # Задача, которая будет выполнять отложенные публикации + 'post-articles': { + 'task': 'crossposting_backend.tasks.delayed_post', + 'schedule': crontab(minute=0, hour='*'), # Запускать каждую минуту + 'args': () # Аргументы задачи, в данном случае их нет + } +} diff --git a/cms/forms.py b/cms/forms.py index 8c88139..8578b48 100644 --- a/cms/forms.py +++ b/cms/forms.py @@ -1,22 +1,23 @@ -from django import forms -from django.contrib.auth import models as auth_models - -from .models import Article - - -class ArticleForm(forms.ModelForm): - link_widget = forms.TextInput(attrs={'placeholder': 'Введите ссылку новости'}) - link = forms.CharField(widget=link_widget) - - class Meta: - model = Article - fields = ('body', 'link',) - - -class UserForm(forms.ModelForm): - class Meta: - model = auth_models.User - fields = ('username', 'password',) - widgets = { - 'password': forms.PasswordInput(), - } +from django import forms +from django.contrib.auth import models as auth_models + +from .models import Article + + +class ArticleForm(forms.ModelForm): + link_widget = forms.TextInput(attrs={'placeholder': 'Введите ссылку новости'}) + link = forms.CharField(widget=link_widget) + publication_time = forms.DateTimeField(widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}), required=False) + + class Meta: + model = Article + fields = ('body', 'link', 'publication_time') + + +class UserForm(forms.ModelForm): + class Meta: + model = auth_models.User + fields = ('username', 'password',) + widgets = { + 'password': forms.PasswordInput(), + } diff --git a/cms/models.py b/cms/models.py index 0f8b76a..118a30b 100644 --- a/cms/models.py +++ b/cms/models.py @@ -1,6 +1,9 @@ -from django.db import models - - -class Article(models.Model): - body = models.TextField(null=False) - link = models.CharField(max_length=200, null=False) +from django.db import models + + +class Article(models.Model): + id = models.BigAutoField(primary_key=True) + body = models.TextField(null=False) + link = models.CharField(max_length=200, null=False) + publication_time = models.DateTimeField(blank=True, null=True) + is_published = models.BooleanField(default=False) \ No newline at end of file diff --git a/cms/tasks.py b/cms/tasks.py new file mode 100644 index 0000000..2f598bb --- /dev/null +++ b/cms/tasks.py @@ -0,0 +1,19 @@ +from cms import promoters +from cms.models import Article +from celery import shared_task + + +@shared_task +def promote_post(article_id): + article = Article.objects.get(id=article_id) + article.is_published = True + article.save() + marketer = promoters.Marketer(article) + marketer.promote() + + +@shared_task +def delayed_post(article_id, publication_time): + article = Article.objects.get(id=article_id) + celery_task = promote_post.apply_async(args=[article.id], eta=publication_time) + return celery_task.id \ No newline at end of file diff --git a/cms/templates/articles/new.html b/cms/templates/articles/new.html index 3f430ed..a708631 100644 --- a/cms/templates/articles/new.html +++ b/cms/templates/articles/new.html @@ -1,67 +1,82 @@ -{% extends 'base.html' %} -{% load bootstrap5 %} -{% block content %} -
-
-
-

Заполните данные статьи для продвижения в соц. сетях

-
- {% csrf_token %} - {% bootstrap_form new_article_form %} - {% buttons %} -
-
- -
- -
-
- - {% endbuttons %} -
-
-
-
-{% endblock content %} -{% block extra_scripts %} - +{% extends 'base.html' %} +{% load bootstrap5 %} +{% block content %} +
+
+
+

Заполните данные статьи для продвижения в соц. сетях

+
+ {% csrf_token %} + {% bootstrap_form new_article_form %} + {% buttons %} +
+
+ +   + Список отложенных публикаций +
+ + +
+
+ {% endbuttons %} +
+
+
+
+{% endblock content %} +{% block extra_scripts %} + {% endblock %} \ No newline at end of file diff --git a/cms/templates/articles/planned.html b/cms/templates/articles/planned.html new file mode 100644 index 0000000..a95035f --- /dev/null +++ b/cms/templates/articles/planned.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load bootstrap5 %} +{% block content %} +
+ Вернуться +   + {% for article in post %} +
+
+

{{ article.body }}

+ {{ article.link }} +

Дата публикации: {{ article.publication_time }}

+ {% if user.is_authenticated %} + Удалить + {% endif %} +
+
+ {% endfor %} +
+{% endblock content %} diff --git a/cms/urls.py b/cms/urls.py index 2ac74d8..eeb52bd 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -1,13 +1,14 @@ -from django.urls import path - -from .views import ArticleView, new_article, AuthenticationView - -urlpatterns = [ - path('articles/', ArticleView.as_view(), name='create-article'), - path('articles/new/', new_article, name='new-article'), - path( - '', - AuthenticationView.as_view(), - name='authenticate' - ) -] +from django.urls import path +from .views import ArticleView, new_article, AuthenticationView, plannedView, articleDelete + +urlpatterns = [ + path('articles/', ArticleView.as_view(), name='create-article'), + path('articles/new/', new_article, name='new-article'), + path( + '', + AuthenticationView.as_view(), + name='authenticate' + ), + path('articles/planned/', plannedView, name='planned'), + path('articles/article_delete//', articleDelete, name='article_delete'), +] \ No newline at end of file diff --git a/cms/views.py b/cms/views.py index a213f78..37ac931 100644 --- a/cms/views.py +++ b/cms/views.py @@ -1,71 +1,102 @@ -from django.contrib import messages -from django.contrib.auth import authenticate, login -from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest, HttpResponseRedirect -from django.shortcuts import render -from django.urls import reverse -from django.views import View - -from cms import promoters -from cms.forms import ArticleForm, UserForm -from cms.models import Article - - -class ArticleView(LoginRequiredMixin, View): - def post(self, request: HttpRequest): - post_data = request.POST - article = Article.objects.create(body=post_data['body'], - link=post_data['link']) - marketer = promoters.Marketer(article) - try: - marketer.promote() - message_type = messages.SUCCESS - message_text = 'Продвижение статьи прошло успешно' - except promoters.PromoteError as exc: - message_type = messages.ERROR - message_text = 'Произошла ошибка: %s' % str(exc) - messages.add_message(request=request, - level=message_type, - message=message_text) - return HttpResponseRedirect(reverse('new-article')) - - -@login_required -def new_article(request): - article_form = ArticleForm() - article_context = { - 'new_article_form': article_form - } - return render(request, - template_name='articles/new.html', - context=article_context) - - -class AuthenticationView(View): - def get(self, request, *args, **kwargs): - user_form = UserForm() - auth_context = { - 'user_form': user_form, - } - return render(request, - 'user/sign_in.html', - context=auth_context) - - def post(self, request, *args, **kwargs): - username = request.POST['username'] - password = request.POST['password'] - authenticated_user = authenticate(username=username, - password=password) - if authenticated_user is None: - messages.add_message(request, - messages.ERROR, - 'Неправильное имя пользователя и/или пароль') - return HttpResponseRedirect(reverse('authenticate')) - else: - messages.add_message(request, - messages.SUCCESS, - 'Поздравляю, вы вошли успешно') - login(request, - user=authenticated_user) - return HttpResponseRedirect(reverse('new-article')) +from django.contrib import messages +from django.contrib.auth import authenticate, login +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponseRedirect +from django.shortcuts import render, redirect +from django.urls import reverse +from django.views import View +from requests import request + +from cms import promoters +from cms.forms import ArticleForm, UserForm +from cms.models import Article +from cms.tasks import delayed_post +from datetime import datetime, timezone + + +class ArticleView(LoginRequiredMixin, View): + def post(self, request: HttpRequest): + post_data = request.POST + if 'publication_time' not in post_data or post_data['publication_time'] == "": + # Значение publication_time не указано + article = Article.objects.create(body=post_data['body'], + link=post_data['link'], + publication_time=datetime.now()) + + marketer = promoters.Marketer(article) + try: + marketer.promote() + article.is_published = 1 + message_type = messages.SUCCESS + message_text = 'Продвижение статьи прошло успешно' + article.is_published = True + article.save() + except promoters.PromoteError as exc: + message_type = messages.ERROR + message_text = 'Произошла ошибка: %s' % str(exc) + messages.add_message(request=request, + level=message_type, + message=message_text) + + else: + # Значение publication_time указано + publication_time = post_data['publication_time'] + publication_time = datetime.fromisoformat(publication_time) + publication_time = publication_time.astimezone(timezone.utc) + article = Article.objects.create(body=post_data['body'], + link=post_data['link'], + publication_time=publication_time) + + delayed_post.apply_async(args=(article.id, publication_time), eta=publication_time) + return HttpResponseRedirect(reverse('new-article')) + + +@login_required +def new_article(request): + article_form = ArticleForm() + article_context = { + 'new_article_form': article_form + } + return render(request, + template_name='articles/new.html', + context=article_context) + + +class AuthenticationView(View): + def get(self, request, *args, **kwargs): + user_form = UserForm() + auth_context = { + 'user_form': user_form, + } + return render(request, + 'user/sign_in.html', + context=auth_context) + + def post(self, request, *args, **kwargs): + username = request.POST['username'] + password = request.POST['password'] + authenticated_user = authenticate(username=username, + password=password) + if authenticated_user is None: + messages.add_message(request, + messages.ERROR, + 'Неправильное имя пользователя и/или пароль') + return HttpResponseRedirect(reverse('authenticate')) + else: + messages.add_message(request, + messages.SUCCESS, + 'Поздравляю, вы вошли успешно') + login(request, + user=authenticated_user) + return HttpResponseRedirect(reverse('new-article')) + + +def plannedView(request): + data = Article.objects.filter(is_published=False) + return render(request, 'articles/planned.html', context={'post':data}) + +def articleDelete(request, id): + article = Article.objects.get(id=id) + article.delete() + return redirect('planned') \ No newline at end of file diff --git a/crossposting_backend/settings.py b/crossposting_backend/settings.py index 8ceb290..d8d0e34 100644 --- a/crossposting_backend/settings.py +++ b/crossposting_backend/settings.py @@ -1,155 +1,160 @@ -""" -Django settings for crossposting_backend project. - -Generated by 'django-admin startproject' using Django 4.1.4. - -For more information on this file, see -https://docs.djangoproject.com/en/4.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.1/ref/settings/ -""" - -from os import path, getenv -from pathlib import Path - -import dotenv -from django.core import signing - -from .private.settings import * - - -def decode_env(env_key: str) -> str: - signer = signing.Signer(salt=SALT) - signed_telegram_chat_id_dict = getenv(env_key) - return signer.unsign_object(signed_telegram_chat_id_dict)[env_key] - - -def return_env(env_key: str) -> str: - """ - Функция нужна как стратегия, если not ENV_ENCODED - :param env_key: - :return: - """ - return getenv(env_key) - - -BASE_DIR = Path(__file__).resolve().parent.parent -env_file = path.join(BASE_DIR, '.env') - -dotenv.read_dotenv(env_file) - -promoter_env_keys = ( - 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'JOOMLA_TOKEN', - 'VK_OWNER_ID', 'VK_TOKEN', 'OK_ACCESS_TOKEN', 'OK_APPLICATION_KEY', - 'OK_APPLICATION_SECRET_KEY', 'OK_GROUP_ID', -) -promoter_secrets = {} -if ENV_ENCODED: - decode_strategy = decode_env -else: - decode_strategy = return_env - -for promoter_env_key in promoter_env_keys: - promoter_secrets[promoter_env_key] = decode_strategy(promoter_env_key) - -# Build paths inside the project like this: BASE_DIR / 'subdir'. - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ - - -LOGIN_URL = '/cms/' - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'cms', - 'bootstrap5', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'crossposting_backend.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'crossposting_backend.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/4.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - -# Password validation -# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Internationalization -# https://docs.djangoproject.com/en/4.1/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.1/howto/static-files/ - -STATIC_URL = 'static/' -STATIC_ROOT = path.join(BASE_DIR, 'static') - -# Default primary key field type -# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +""" +Django settings for crossposting_backend project. + +Generated by 'django-admin startproject' using Django 4.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from os import path, getenv +from pathlib import Path + +import dotenv +from celery.schedules import crontab +from django.core import signing + +from .private.settings import * + + +def decode_env(env_key: str) -> str: + signer = signing.Signer(salt=SALT) + signed_telegram_chat_id_dict = getenv(env_key) + return signer.unsign_object(signed_telegram_chat_id_dict)[env_key] + + +def return_env(env_key: str) -> str: + """ + Функция нужна как стратегия, если not ENV_ENCODED + :param env_key: + :return: + """ + return getenv(env_key) + + +BASE_DIR = Path(__file__).resolve().parent.parent +env_file = path.join(BASE_DIR, '.env') + +dotenv.read_dotenv(env_file) + +promoter_env_keys = ( + 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'JOOMLA_TOKEN', + 'VK_OWNER_ID', 'VK_TOKEN', 'OK_ACCESS_TOKEN', 'OK_APPLICATION_KEY', + 'OK_APPLICATION_SECRET_KEY', 'OK_GROUP_ID', +) +promoter_secrets = {} +if ENV_ENCODED: + decode_strategy = decode_env +else: + decode_strategy = return_env + +for promoter_env_key in promoter_env_keys: + promoter_secrets[promoter_env_key] = decode_strategy(promoter_env_key) + +# Build paths inside the project like this: BASE_DIR / 'subdir'. + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + + +LOGIN_URL = '/cms/' + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'cms', + 'bootstrap5', + 'django_celery_beat', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'crossposting_backend.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'crossposting_backend.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Europe/Moscow' + +USE_I18N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = path.join(BASE_DIR, 'static') + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +CELERY_BROKER_URL = 'redis://localhost:6379/' +CELERY_RESULT_BACKEND = 'redis://localhost:6379/' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6ec1115..ed33494 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,18 @@ -asgiref==3.5.2 -beautifulsoup4==4.11.1 -certifi==2022.12.7 -charset-normalizer==2.1.1 -Django==4.1.4 -django-bootstrap-v5==1.0.11 -django-dotenv==1.4.2 -idna==3.4 -ok-api==1.0.1 -requests==2.28.1 -soupsieve==2.3.2.post1 -sqlparse==0.4.3 -urllib3==1.26.13 -vk-api==11.9.9 +asgiref==3.5.2 +beautifulsoup4==4.11.1 +certifi==2022.12.7 +charset-normalizer==2.1.1 +Django==4.1.4 +django-bootstrap-v5==1.0.11 +django-dotenv==1.4.2 +idna==3.4 +ok-api==1.0.1 +requests==2.28.1 +soupsieve==2.3.2.post1 +sqlparse==0.4.3 +urllib3==1.26.13 +vk-api==11.9.9 +celery==5.3.6 +django-celery-beat==2.6.0 +redis==5.0.4 +