Compare commits

...

3 Commits

  1. 3
      cms/__init__.py
  2. 15
      cms/celery.py
  3. 45
      cms/forms.py
  4. 15
      cms/models.py
  5. 19
      cms/tasks.py
  6. 147
      cms/templates/articles/new.html
  7. 20
      cms/templates/articles/planned.html
  8. 27
      cms/urls.py
  9. 173
      cms/views.py
  10. 315
      crossposting_backend/settings.py
  11. 32
      requirements.txt

3
cms/__init__.py

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app')

15
cms/celery.py

@ -0,0 +1,15 @@
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crossposting_backend.settings')
CELERY_TIMEZONE = 'Europe/Moscow'
app = Celery('cms')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print(f'Requestgit {self.request!r}')

45
cms/forms.py

@ -1,22 +1,23 @@
from django import forms from django import forms
from django.contrib.auth import models as auth_models from django.contrib.auth import models as auth_models
from .models import Article from .models import Article
class ArticleForm(forms.ModelForm): class ArticleForm(forms.ModelForm):
link_widget = forms.TextInput(attrs={'placeholder': 'Введите ссылку новости'}) link_widget = forms.TextInput(attrs={'placeholder': 'Введите ссылку новости'})
link = forms.CharField(widget=link_widget) link = forms.CharField(widget=link_widget)
publication_time = forms.DateTimeField(widget=forms.DateTimeInput(attrs={'type': 'datetime-local'}), required=False)
class Meta:
model = Article class Meta:
fields = ('body', 'link',) model = Article
fields = ('body', 'link', 'publication_time')
class UserForm(forms.ModelForm):
class Meta: class UserForm(forms.ModelForm):
model = auth_models.User class Meta:
fields = ('username', 'password',) model = auth_models.User
widgets = { fields = ('username', 'password',)
'password': forms.PasswordInput(), widgets = {
} 'password': forms.PasswordInput(),
}

15
cms/models.py

@ -1,6 +1,9 @@
from django.db import models from django.db import models
class Article(models.Model): class Article(models.Model):
body = models.TextField(null=False) id = models.BigAutoField(primary_key=True)
link = models.CharField(max_length=200, null=False) 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)

19
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

147
cms/templates/articles/new.html

@ -1,67 +1,82 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load bootstrap5 %} {% load bootstrap5 %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row my-5"> <div class="row my-5">
<div class="col-md-12"> <div class="col-md-12">
<h1>Заполните данные статьи для продвижения в соц. сетях</h1> <h1>Заполните данные статьи для продвижения в соц. сетях</h1>
<form <form
method="post" method="post"
enctype="application/x-www-form-urlencoded" enctype="application/x-www-form-urlencoded"
action="{% url 'create-article' %}" action="{% url 'create-article' %}"
class="form" class="form"
> >
{% csrf_token %} {% csrf_token %}
{% bootstrap_form new_article_form %} {% bootstrap_form new_article_form %}
{% buttons %} {% buttons %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<button <button
class="btn btn-primary" class="btn btn-primary"
type="submit" type="submit"
disabled="disabled" id="submit-button">
> {% if new_article_form.publication_time.value %}
Продвинуть disabled="disabled"
</button> {% endif %}
</div> {% if new_article_form.publication_time.value %}
Запланировать
<div id="vkShare" class="col"></div> {% else %}
</div> Опубликовать сейчас
{% endif %}
{% endbuttons %} </button>
</form> &nbsp;
</div> <a href="{% url 'planned' %}" class="btn btn-primary">Список отложенных публикаций</a>
</div> </div>
</div> <script>
{% endblock content %} document.getElementById("id_publication_time").addEventListener("change", function() {
{% block extra_scripts %} var submitButton = document.getElementById("submit-button");
<script type="text/javascript"> if (this.value) {
let submitBtn = null submitButton.innerHTML = "Запланировать";
const enableSubmitBtn = () => { } else {
submitBtn.disabled = false submitButton.innerHTML = "Опубликовать";
} }
const appendShare = (e) => { });
submitBtn.disabled = true </script>
const articleLink = e.target.value; <div id="vkShare" class="col"></div>
const gen = { </div>
url: articleLink {% endbuttons %}
} </form>
const buttonType = { </div>
type: "custom", </div>
text: '<img src="https://vk.com/images/share_32_2x.png" width="32" height="32" alt="share icon" />' </div>
} {% endblock content %}
document.getElementById('vkShare').innerHTML = VK.Share.button(gen, buttonType) {% block extra_scripts %}
const vkButtons = document.querySelectorAll('a[href^="//vk.com/"]') <script type="text/javascript">
vkButtons.forEach((vkBtn) => vkBtn.addEventListener('click', enableSubmitBtn)) let submitBtn = null
} const enableSubmitBtn = () => {
const main = () => { submitBtn.disabled = false
submitBtn = document.querySelector('button[type="submit"]') }
const appendShare = (e) => {
const linkInput = document.querySelector('[name="link"]'); submitBtn.disabled = true
linkInput.addEventListener('input', appendShare) const articleLink = e.target.value;
linkInput.addEventListener('paste', appendShare) const gen = {
} url: articleLink
window.addEventListener('DOMContentLoaded', main) }
</script> const buttonType = {
type: "custom",
text: '<img src="https://vk.com/images/share_32_2x.png" width="32" height="32" alt="share icon" />'
}
document.getElementById('vkShare').innerHTML = VK.Share.button(gen, buttonType)
const vkButtons = document.querySelectorAll('a[href^="//vk.com/"]')
vkButtons.forEach((vkBtn) => vkBtn.addEventListener('click', enableSubmitBtn))
}
const main = () => {
submitBtn = document.querySelector('button[type="submit"]')
const linkInput = document.querySelector('[name="link"]');
linkInput.addEventListener('input', appendShare)
linkInput.addEventListener('paste', appendShare)
}
window.addEventListener('DOMContentLoaded', main)
</script>
{% endblock %} {% endblock %}

20
cms/templates/articles/planned.html

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load bootstrap5 %}
{% block content %}
<div class="container">
<a href="{% url 'new-article' %}" class="btn btn-primary">Вернуться</a>
&nbsp;
{% for article in post %}
<div class="card mb-3">
<div class="card-body">
<p class="card-text">{{ article.body }}</p>
<a href="{{ article.link }}">{{ article.link }}</a>
<p class="card-text"><small class="text-muted">Дата публикации: {{ article.publication_time }}</small></p>
{% if user.is_authenticated %}
<a href="{% url 'article_delete' article.id %}" class="btn btn-danger">Удалить</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock content %}

27
cms/urls.py

@ -1,13 +1,14 @@
from django.urls import path from django.urls import path
from .views import ArticleView, new_article, AuthenticationView, plannedView, articleDelete
from .views import ArticleView, new_article, AuthenticationView
urlpatterns = [
urlpatterns = [ path('articles/', ArticleView.as_view(), name='create-article'),
path('articles/', ArticleView.as_view(), name='create-article'), path('articles/new/', new_article, name='new-article'),
path('articles/new/', new_article, name='new-article'), path(
path( '',
'', AuthenticationView.as_view(),
AuthenticationView.as_view(), name='authenticate'
name='authenticate' ),
) path('articles/planned/', plannedView, name='planned'),
] path('articles/article_delete/<int:id>/', articleDelete, name='article_delete'),
]

173
cms/views.py

@ -1,71 +1,102 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponseRedirect from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render, redirect
from django.urls import reverse from django.urls import reverse
from django.views import View from django.views import View
from requests import request
from cms import promoters
from cms.forms import ArticleForm, UserForm from cms import promoters
from cms.models import Article from cms.forms import ArticleForm, UserForm
from cms.models import Article
from cms.tasks import delayed_post
class ArticleView(LoginRequiredMixin, View): from datetime import datetime, timezone
def post(self, request: HttpRequest):
post_data = request.POST
article = Article.objects.create(body=post_data['body'], class ArticleView(LoginRequiredMixin, View):
link=post_data['link']) def post(self, request: HttpRequest):
marketer = promoters.Marketer(article) post_data = request.POST
try: if 'publication_time' not in post_data or post_data['publication_time'] == "":
marketer.promote() # Значение publication_time не указано
message_type = messages.SUCCESS article = Article.objects.create(body=post_data['body'],
message_text = 'Продвижение статьи прошло успешно' link=post_data['link'],
except promoters.PromoteError as exc: publication_time=datetime.now())
message_type = messages.ERROR
message_text = 'Произошла ошибка: %s' % str(exc) marketer = promoters.Marketer(article)
messages.add_message(request=request, try:
level=message_type, marketer.promote()
message=message_text) article.is_published = 1
return HttpResponseRedirect(reverse('new-article')) message_type = messages.SUCCESS
message_text = 'Продвижение статьи прошло успешно'
article.is_published = True
@login_required article.save()
def new_article(request): except promoters.PromoteError as exc:
article_form = ArticleForm() message_type = messages.ERROR
article_context = { message_text = 'Произошла ошибка: %s' % str(exc)
'new_article_form': article_form messages.add_message(request=request,
} level=message_type,
return render(request, message=message_text)
template_name='articles/new.html',
context=article_context) else:
# Значение publication_time указано
publication_time = post_data['publication_time']
class AuthenticationView(View): publication_time = datetime.fromisoformat(publication_time)
def get(self, request, *args, **kwargs): publication_time = publication_time.astimezone(timezone.utc)
user_form = UserForm() article = Article.objects.create(body=post_data['body'],
auth_context = { link=post_data['link'],
'user_form': user_form, publication_time=publication_time)
}
return render(request, delayed_post.apply_async(args=(article.id, publication_time), eta=publication_time)
'user/sign_in.html', return HttpResponseRedirect(reverse('new-article'))
context=auth_context)
def post(self, request, *args, **kwargs): @login_required
username = request.POST['username'] def new_article(request):
password = request.POST['password'] article_form = ArticleForm()
authenticated_user = authenticate(username=username, article_context = {
password=password) 'new_article_form': article_form
if authenticated_user is None: }
messages.add_message(request, return render(request,
messages.ERROR, template_name='articles/new.html',
'Неправильное имя пользователя и/или пароль') context=article_context)
return HttpResponseRedirect(reverse('authenticate'))
else:
messages.add_message(request, class AuthenticationView(View):
messages.SUCCESS, def get(self, request, *args, **kwargs):
'Поздравляю, вы вошли успешно') user_form = UserForm()
login(request, auth_context = {
user=authenticated_user) 'user_form': user_form,
return HttpResponseRedirect(reverse('new-article')) }
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')

315
crossposting_backend/settings.py

@ -1,155 +1,160 @@
""" """
Django settings for crossposting_backend project. Django settings for crossposting_backend project.
Generated by 'django-admin startproject' using Django 4.1.4. Generated by 'django-admin startproject' using Django 4.1.4.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/ https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/ https://docs.djangoproject.com/en/4.1/ref/settings/
""" """
from os import path, getenv from os import path, getenv
from pathlib import Path from pathlib import Path
import dotenv import dotenv
from django.core import signing from celery.schedules import crontab
from django.core import signing
from .private.settings import *
from .private.settings import *
def decode_env(env_key: str) -> str:
signer = signing.Signer(salt=SALT) def decode_env(env_key: str) -> str:
signed_telegram_chat_id_dict = getenv(env_key) signer = signing.Signer(salt=SALT)
return signer.unsign_object(signed_telegram_chat_id_dict)[env_key] 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:
""" def return_env(env_key: str) -> str:
Функция нужна как стратегия, если not ENV_ENCODED """
:param env_key: Функция нужна как стратегия, если not ENV_ENCODED
:return: :param env_key:
""" :return:
return getenv(env_key) """
return getenv(env_key)
BASE_DIR = Path(__file__).resolve().parent.parent
env_file = path.join(BASE_DIR, '.env') BASE_DIR = Path(__file__).resolve().parent.parent
env_file = path.join(BASE_DIR, '.env')
dotenv.read_dotenv(env_file)
dotenv.read_dotenv(env_file)
promoter_env_keys = (
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'JOOMLA_TOKEN', promoter_env_keys = (
'VK_OWNER_ID', 'VK_TOKEN', 'OK_ACCESS_TOKEN', 'OK_APPLICATION_KEY', 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'JOOMLA_TOKEN',
'OK_APPLICATION_SECRET_KEY', 'OK_GROUP_ID', 'VK_OWNER_ID', 'VK_TOKEN', 'OK_ACCESS_TOKEN', 'OK_APPLICATION_KEY',
) 'OK_APPLICATION_SECRET_KEY', 'OK_GROUP_ID',
promoter_secrets = {} )
if ENV_ENCODED: promoter_secrets = {}
decode_strategy = decode_env if ENV_ENCODED:
else: decode_strategy = decode_env
decode_strategy = return_env else:
decode_strategy = return_env
for promoter_env_key in promoter_env_keys:
promoter_secrets[promoter_env_key] = decode_strategy(promoter_env_key) 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'.
# 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/ # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
LOGIN_URL = '/cms/'
LOGIN_URL = '/cms/'
# Application definition
# Application definition
INSTALLED_APPS = [
'django.contrib.admin', INSTALLED_APPS = [
'django.contrib.auth', 'django.contrib.admin',
'django.contrib.contenttypes', 'django.contrib.auth',
'django.contrib.sessions', 'django.contrib.contenttypes',
'django.contrib.messages', 'django.contrib.sessions',
'django.contrib.staticfiles', 'django.contrib.messages',
'cms', 'django.contrib.staticfiles',
'bootstrap5', 'cms',
] 'bootstrap5',
'django_celery_beat',
MIDDLEWARE = [ ]
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', MIDDLEWARE = [
'django.middleware.common.CommonMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
] 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
ROOT_URLCONF = 'crossposting_backend.urls' ]
TEMPLATES = [ ROOT_URLCONF = 'crossposting_backend.urls'
{
'BACKEND': 'django.template.backends.django.DjangoTemplates', TEMPLATES = [
'DIRS': [BASE_DIR / 'templates'], {
'APP_DIRS': True, 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'OPTIONS': { 'DIRS': [BASE_DIR / 'templates'],
'context_processors': [ 'APP_DIRS': True,
'django.template.context_processors.debug', 'OPTIONS': {
'django.template.context_processors.request', 'context_processors': [
'django.contrib.auth.context_processors.auth', 'django.template.context_processors.debug',
'django.contrib.messages.context_processors.messages', 'django.template.context_processors.request',
], 'django.contrib.auth.context_processors.auth',
}, 'django.contrib.messages.context_processors.messages',
}, ],
] },
},
WSGI_APPLICATION = 'crossposting_backend.wsgi.application' ]
# Database WSGI_APPLICATION = 'crossposting_backend.wsgi.application'
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
# Database
DATABASES = { # https://docs.djangoproject.com/en/4.1/ref/settings/#databases
'default': {
'ENGINE': 'django.db.backends.sqlite3', DATABASES = {
'NAME': BASE_DIR / 'db.sqlite3', '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
# Password validation
AUTH_PASSWORD_VALIDATORS = [ # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', AUTH_PASSWORD_VALIDATORS = [
}, {
{ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', },
}, {
{ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', },
}, {
{ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', },
}, {
] 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
# Internationalization ]
# https://docs.djangoproject.com/en/4.1/topics/i18n/
# Internationalization
LANGUAGE_CODE = 'en-us' # https://docs.djangoproject.com/en/4.1/topics/i18n/
TIME_ZONE = 'UTC' LANGUAGE_CODE = 'en-us'
USE_I18N = True TIME_ZONE = 'Europe/Moscow'
USE_TZ = True USE_I18N = True
# Static files (CSS, JavaScript, Images) USE_TZ = True
# https://docs.djangoproject.com/en/4.1/howto/static-files/
# Static files (CSS, JavaScript, Images)
STATIC_URL = 'static/' # https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_ROOT = path.join(BASE_DIR, 'static')
STATIC_URL = 'static/'
# Default primary key field type STATIC_ROOT = path.join(BASE_DIR, 'static')
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 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/'

32
requirements.txt

@ -1,14 +1,18 @@
asgiref==3.5.2 asgiref==3.5.2
beautifulsoup4==4.11.1 beautifulsoup4==4.11.1
certifi==2022.12.7 certifi==2022.12.7
charset-normalizer==2.1.1 charset-normalizer==2.1.1
Django==4.1.4 Django==4.1.4
django-bootstrap-v5==1.0.11 django-bootstrap-v5==1.0.11
django-dotenv==1.4.2 django-dotenv==1.4.2
idna==3.4 idna==3.4
ok-api==1.0.1 ok-api==1.0.1
requests==2.28.1 requests==2.28.1
soupsieve==2.3.2.post1 soupsieve==2.3.2.post1
sqlparse==0.4.3 sqlparse==0.4.3
urllib3==1.26.13 urllib3==1.26.13
vk-api==11.9.9 vk-api==11.9.9
celery==5.3.6
django-celery-beat==2.6.0
redis==5.0.4

Loading…
Cancel
Save