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 %}
-
-
-
-
Заполните данные статьи для продвижения в соц. сетях
-
-
-
-
-{% endblock content %}
-{% block extra_scripts %}
-
+{% extends 'base.html' %}
+{% load bootstrap5 %}
+{% block content %}
+
+
+
+
Заполните данные статьи для продвижения в соц. сетях
+
+
+
+
+{% 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
+