Compare commits

..

3 Commits

  1. 3
      LICENSE
  2. BIN
      Logo.png
  3. 101
      Logo.svg
  4. BIN
      LogoWhite.png
  5. BIN
      LogoWhiteSmall.png
  6. 3
      cms/__init__.py
  7. 15
      cms/celery.py
  8. 3
      cms/forms.py
  9. 3
      cms/models.py
  10. 19
      cms/tasks.py
  11. 31
      cms/templates/articles/new.html
  12. 20
      cms/templates/articles/planned.html
  13. 7
      cms/urls.py
  14. 59
      cms/views.py
  15. 7
      crossposting_backend/settings.py
  16. 4
      requirements.txt

3
LICENSE

@ -1,3 +0,0 @@
Этот продукт является ОБЩЕСТВЕННЫМ ДОСТОЯНИЕМ и может быть использован КАК ЕСТЬ, со всеми достоинствами и недостатками, полностью или частично, кем угодно и в каких угодно целях БЕЗ КАКИХ-ЛИБО ОГРАНИЧЕНИЙ.
This product is PUBLIC DOMAIN and may be used AS IS, with all advantages and faults, in whole or in part, by anyone for any purpose, WITHOUT ANY CONDITIONS.

BIN
Logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

101
Logo.svg

@ -1,101 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="LogoCrossPosting.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2"
inkscape:cx="273.25"
inkscape:cy="428.25"
inkscape:window-width="1366"
inkscape:window-height="704"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<circle
id="path13483-1-8-1"
cx="75.798073"
cy="-104.49218"
r="3.5844672"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="scale(1,-1)" />
<circle
id="path13483-1-8-1-3"
cx="61.192924"
cy="-104.20211"
r="3.5844672"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="scale(1,-1)" />
<circle
id="path13483-1-8-1-7"
cx="61.699493"
cy="-119.37672"
r="3.5844672"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="scale(1,-1)" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 64.149924,116.09775 10.033647,-9.60987"
id="path14457-0-6"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.183361,107.24427 v 9.32318"
id="path14457-2"
sodipodi:nodetypes="cc" />
<circle
id="path13483-1-8-1-5"
cx="75.844719"
cy="134.38693"
r="3.5844672"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
id="path13483-1-8-1-3-3"
cx="61.239571"
cy="134.67699"
r="3.5844672"
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 64.196572,122.78136 10.033647,9.60987"
id="path14457-0-6-9"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 61.230009,131.63484 v -9.32318"
id="path14457-2-3"
sodipodi:nodetypes="cc" />
<circle
style="fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path15124"
cx="68.566109"
cy="119.2259"
r="22.978039" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

BIN
LogoWhite.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

BIN
LogoWhiteSmall.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

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

3
cms/forms.py

@ -7,10 +7,11 @@ 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: class Meta:
model = Article model = Article
fields = ('body', 'link',) fields = ('body', 'link', 'publication_time')
class UserForm(forms.ModelForm): class UserForm(forms.ModelForm):

3
cms/models.py

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

31
cms/templates/articles/new.html

@ -17,17 +17,34 @@
<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"
{% endif %}
{% if new_article_form.publication_time.value %}
Запланировать
{% else %}
Опубликовать сейчас
{% endif %}
</button> </button>
&nbsp;
<a href="{% url 'planned' %}" class="btn btn-primary">Список отложенных публикаций</a>
</div> </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 id="vkShare" class="col"></div>
</div> </div>
{% endbuttons %} {% endbuttons %}
</form> </form>
</div> </div>
@ -42,7 +59,6 @@
} }
const appendShare = (e) => { const appendShare = (e) => {
submitBtn.disabled = true submitBtn.disabled = true
const articleLink = e.target.value; const articleLink = e.target.value;
const gen = { const gen = {
url: articleLink url: articleLink
@ -57,7 +73,6 @@
} }
const main = () => { const main = () => {
submitBtn = document.querySelector('button[type="submit"]') submitBtn = document.querySelector('button[type="submit"]')
const linkInput = document.querySelector('[name="link"]'); const linkInput = document.querySelector('[name="link"]');
linkInput.addEventListener('input', appendShare) linkInput.addEventListener('input', appendShare)
linkInput.addEventListener('paste', appendShare) linkInput.addEventListener('paste', appendShare)

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 %}

7
cms/urls.py

@ -1,6 +1,5 @@
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'),
@ -9,5 +8,7 @@ urlpatterns = [
'', '',
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'),
] ]

59
cms/views.py

@ -3,31 +3,52 @@ 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 import promoters
from cms.forms import ArticleForm, UserForm from cms.forms import ArticleForm, UserForm
from cms.models import Article from cms.models import Article
from cms.tasks import delayed_post
from datetime import datetime, timezone
class ArticleView(LoginRequiredMixin, View): class ArticleView(LoginRequiredMixin, View):
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
post_data = request.POST post_data = request.POST
article = Article.objects.create(body=post_data['body'], if 'publication_time' not in post_data or post_data['publication_time'] == "":
link=post_data['link']) # Значение publication_time не указано
marketer = promoters.Marketer(article) article = Article.objects.create(body=post_data['body'],
try: link=post_data['link'],
marketer.promote() publication_time=datetime.now())
message_type = messages.SUCCESS
message_text = 'Продвижение статьи прошло успешно' marketer = promoters.Marketer(article)
except promoters.PromoteError as exc: try:
message_type = messages.ERROR marketer.promote()
message_text = 'Произошла ошибка: %s' % str(exc) article.is_published = 1
messages.add_message(request=request, message_type = messages.SUCCESS
level=message_type, message_text = 'Продвижение статьи прошло успешно'
message=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')) return HttpResponseRedirect(reverse('new-article'))
@ -69,3 +90,13 @@ class AuthenticationView(View):
login(request, login(request,
user=authenticated_user) user=authenticated_user)
return HttpResponseRedirect(reverse('new-article')) 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')

7
crossposting_backend/settings.py

@ -14,6 +14,7 @@ from os import path, getenv
from pathlib import Path from pathlib import Path
import dotenv import dotenv
from celery.schedules import crontab
from django.core import signing from django.core import signing
from .private.settings import * from .private.settings import *
@ -72,6 +73,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'cms', 'cms',
'bootstrap5', 'bootstrap5',
'django_celery_beat',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -137,7 +139,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC' TIME_ZONE = 'Europe/Moscow'
USE_I18N = True USE_I18N = True
@ -153,3 +155,6 @@ STATIC_ROOT = path.join(BASE_DIR, 'static')
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CELERY_BROKER_URL = 'redis://localhost:6379/'
CELERY_RESULT_BACKEND = 'redis://localhost:6379/'

4
requirements.txt

@ -12,3 +12,7 @@ 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