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.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(),
}

15
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)

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' %}
{% load bootstrap5 %}
{% block content %}
<div class="container">
<div class="row my-5">
<div class="col-md-12">
<h1>Заполните данные статьи для продвижения в соц. сетях</h1>
<form
method="post"
enctype="application/x-www-form-urlencoded"
action="{% url 'create-article' %}"
class="form"
>
{% csrf_token %}
{% bootstrap_form new_article_form %}
{% buttons %}
<div class="row">
<div class="col">
<button
class="btn btn-primary"
type="submit"
disabled="disabled"
>
Продвинуть
</button>
</div>
<div id="vkShare" class="col"></div>
</div>
{% endbuttons %}
</form>
</div>
</div>
</div>
{% endblock content %}
{% block extra_scripts %}
<script type="text/javascript">
let submitBtn = null
const enableSubmitBtn = () => {
submitBtn.disabled = false
}
const appendShare = (e) => {
submitBtn.disabled = true
const articleLink = e.target.value;
const gen = {
url: articleLink
}
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>
{% extends 'base.html' %}
{% load bootstrap5 %}
{% block content %}
<div class="container">
<div class="row my-5">
<div class="col-md-12">
<h1>Заполните данные статьи для продвижения в соц. сетях</h1>
<form
method="post"
enctype="application/x-www-form-urlencoded"
action="{% url 'create-article' %}"
class="form"
>
{% csrf_token %}
{% bootstrap_form new_article_form %}
{% buttons %}
<div class="row">
<div class="col">
<button
class="btn btn-primary"
type="submit"
id="submit-button">
{% if new_article_form.publication_time.value %}
disabled="disabled"
{% endif %}
{% if new_article_form.publication_time.value %}
Запланировать
{% else %}
Опубликовать сейчас
{% endif %}
</button>
&nbsp;
<a href="{% url 'planned' %}" class="btn btn-primary">Список отложенных публикаций</a>
</div>
<script>
document.getElementById("id_publication_time").addEventListener("change", function() {
var submitButton = document.getElementById("submit-button");
if (this.value) {
submitButton.innerHTML = "Запланировать";
} else {
submitButton.innerHTML = "Опубликовать";
}
});
</script>
<div id="vkShare" class="col"></div>
</div>
{% endbuttons %}
</form>
</div>
</div>
</div>
{% endblock content %}
{% block extra_scripts %}
<script type="text/javascript">
let submitBtn = null
const enableSubmitBtn = () => {
submitBtn.disabled = false
}
const appendShare = (e) => {
submitBtn.disabled = true
const articleLink = e.target.value;
const gen = {
url: articleLink
}
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 %}

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 .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/<int:id>/', articleDelete, name='article_delete'),
]

173
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')

315
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/'

32
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

Loading…
Cancel
Save