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. 25
      cms/templates/articles/new.html
  12. 20
      cms/templates/articles/planned.html
  13. 7
      cms/urls.py
  14. 35
      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):
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',)
fields = ('body', 'link', 'publication_time')
class UserForm(forms.ModelForm):

3
cms/models.py

@ -2,5 +2,8 @@ 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

25
cms/templates/articles/new.html

@ -19,15 +19,32 @@
<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>
@ -42,7 +59,6 @@
}
const appendShare = (e) => {
submitBtn.disabled = true
const articleLink = e.target.value;
const gen = {
url: articleLink
@ -57,7 +73,6 @@
}
const main = () => {
submitBtn = document.querySelector('button[type="submit"]')
const linkInput = document.querySelector('[name="link"]');
linkInput.addEventListener('input', 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 .views import ArticleView, new_article, AuthenticationView
from .views import ArticleView, new_article, AuthenticationView, plannedView, articleDelete
urlpatterns = [
path('articles/', ArticleView.as_view(), name='create-article'),
@ -9,5 +8,7 @@ urlpatterns = [
'',
AuthenticationView.as_view(),
name='authenticate'
)
),
path('articles/planned/', plannedView, name='planned'),
path('articles/article_delete/<int:id>/', articleDelete, name='article_delete'),
]

35
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.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
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'])
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'))
@ -69,3 +90,13 @@ class AuthenticationView(View):
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')

7
crossposting_backend/settings.py

@ -14,6 +14,7 @@ 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 *
@ -72,6 +73,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'cms',
'bootstrap5',
'django_celery_beat',
]
MIDDLEWARE = [
@ -137,7 +139,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Europe/Moscow'
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
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
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