initial commit

This commit is contained in:
2019-01-11 22:16:01 +03:00
commit 24c5f2fcf6
312 changed files with 186972 additions and 0 deletions

0
feeds/__init__.py Normal file
View File

7
feeds/admin.py Normal file
View File

@@ -0,0 +1,7 @@
from django.contrib import admin
from .models import Feed, EchoFeedModuleConfig
admin.site.register(Feed)
admin.site.register(EchoFeedModuleConfig)

5
feeds/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class FeedsConfig(AppConfig):
name = 'feeds'

21
feeds/forms.py Normal file
View File

@@ -0,0 +1,21 @@
from django.forms import ModelForm
from feeds.models import Feed
class FeedForm(ModelForm):
prefix = 'feed'
class Meta:
model = Feed
exclude = 'owner', 'lock', 'config_type', 'config_id', 'last_check', 'last_id',
def get_config_form(mdl):
class ConfigForm(ModelForm):
prefix = 'config'
class Meta:
model = mdl
exclude = ()
return ConfigForm

View File

View File

View File

@@ -0,0 +1,17 @@
from time import sleep
from django.core.management import BaseCommand
from feeds.models import Feed
class Command(BaseCommand):
def handle(self, *args, **options):
try:
while True:
feeds = Feed.objects.filter(lock=False)
for feed in feeds:
feed.run_check()
sleep(5)
except KeyboardInterrupt:
pass

View File

@@ -0,0 +1,47 @@
# Generated by Django 2.1.5 on 2019-01-10 17:20
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import picklefield.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='EchoFeedModuleConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.TextField()),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Feed',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=32)),
('chat_id', models.CharField(max_length=33)),
('check_interval', models.DurationField()),
('last_check', models.DateTimeField(blank=True, null=True)),
('last_id', picklefield.fields.PickledObjectField(blank=True, editable=False, null=True)),
('config_id', models.PositiveIntegerField()),
('config_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterUniqueTogether(
name='feed',
unique_together={('config_type', 'config_id')},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-01-10 18:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('feeds', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='feed',
name='lock',
field=models.BooleanField(default=False),
),
]

View File

68
feeds/models.py Normal file
View File

@@ -0,0 +1,68 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import timezone
from picklefield import PickledObjectField
from telebot import TeleBot
from feeds.tasks import execute_feed
class Feed(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=32)
chat_id = models.CharField(max_length=33)
check_interval = models.DurationField(help_text='in seconds')
last_check = models.DateTimeField(null=True, blank=True)
last_id = PickledObjectField(null=True, blank=True)
lock = models.BooleanField(default=False)
config_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
config_id = models.PositiveIntegerField()
config = GenericForeignKey('config_type', 'config_id')
def run_check(self):
if self.lock:
return
if self.last_check and timezone.now() < self.last_check + self.check_interval:
return
self.lock = True
self.save()
execute_feed.delay(self.pk)
class Meta:
unique_together = ('config_type', 'config_id')
class FeedModuleConfig(models.Model):
_feeds = GenericRelation(Feed, content_type_field='config_type', object_id_field='config_id')
MODULE_NAME = '<Not set>'
@property
def feed(self):
return self._feeds.get()
@property
def content_type(self):
return ContentType.objects.get_for_model(self.__class__)
def execute(self, bot: TeleBot, chat_id, last_id):
raise NotImplementedError()
class Meta:
abstract = True
class EchoFeedModuleConfig(FeedModuleConfig):
message = models.TextField()
MODULE_NAME = 'Echo'
def execute(self, bot: TeleBot, chat_id, last_id):
bot.send_message(chat_id, self.message)
FEED_MODULES = [EchoFeedModuleConfig]

24
feeds/tasks.py Normal file
View File

@@ -0,0 +1,24 @@
from django.utils import timezone
from telebot import TeleBot
from config.celery import app
@app.task()
def execute_feed(feed_pk):
from feeds.models import Feed
try:
feed = Feed.objects.get(pk=feed_pk)
if not feed.lock:
feed.lock = True
feed.save()
bot = TeleBot('450146961:AAFcb9tyIiKAi6BHR1ZYfWuTEkYjhO3xEFE')
feed.last_id = feed.config.execute(bot, feed.chat_id, feed.last_id)
feed.last_check = timezone.now()
feed.save()
finally:
feed.lock = False
feed.save()

View File

@@ -0,0 +1,58 @@
{% extends 'cabinet/_internal_base.html' %}
{% load bootstrap4 %}
{% block breadcrumbs %}
<li><a href="{% url 'cabinet:feeds:index' %}">Feed list</a></li>
<li><span>{{ title }}</span></li>
{% endblock %}
{% block content %}
<form action="" method="post" class="card">
{% csrf_token %}
<header class="card-header">
<h2 class="card-title">{% if feed %}Feed "{{ feed.title }}" configuration{% else %}New feed{% endif %}</h2>
</header>
<div class="card-body">
<h4>General options</h4>
{% bootstrap_form feed_form layout='horizontal' %}
{% if feed %}
Last check: {{ feed.last_check }}
{% endif %}
<hr>
<h4>Module options</h4>
{% bootstrap_form config_form layout='horizontal' %}
</div>
<footer class="card-footer text-right">
{% if feed %}<a href="#delete-modal" class="modal-basic btn btn-danger">Delete</a>{% endif %}
<button type="submit" class="btn btn-primary">Save</button>
</footer>
</form>
{% if feed %}
<div id="delete-modal" class="modal-block modal-full-color modal-block-danger mfp-hide">
<section class="card">
<header class="card-header">
<h2 class="card-title">Delete feed "{{ feed.title }}"</h2>
</header>
<div class="card-body">
<div class="modal-wrapper">
<div class="modal-icon"><i class="fas fa-times-circle"></i></div>
<div class="modal-text">
<h4>Are you sure?</h4>
<p>This action cannot be undone.</p>
</div>
</div>
</div>
<footer class="card-footer">
<div class="row">
<form action="{% url 'cabinet:feeds:delete' pk=feed.pk %}" method="post" class="col-md-12 text-right">
{% csrf_token %}
<button type="button" class="btn btn-default modal-dismiss">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</footer>
</section>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,52 @@
{% extends 'cabinet/_internal_base.html' %}
{% block breadcrumbs %}
<li><span>Feed list</span></li>
{% endblock %}
{% block content %}
<section class="card">
<header class="card-header">
<div class="card-button-actions">
<div class="dropdown">
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-toggle="dropdown">
Add new feed
</button>
<div class="dropdown-menu dropdown-menu-right">
{% for module in feed_modules %}
<a class="dropdown-item"
href="{% url 'cabinet:feeds:new' content_type=module.content_type.model %}">{{ module.MODULE_NAME }}</a>
{% endfor %}
</div>
</div>
</div>
<h2 class="card-title">Your feeds</h2>
</header>
<div class="card-body">
<table class="table table-hover table-responsive-md mb-0">
<thead>
<tr>
<th>Title</th>
<th>Module</th>
<th>Chat ID</th>
<th>Interval</th>
</tr>
</thead>
{% for feed in object_list %}
<tr class="clickable-row" data-href="{% url 'cabinet:feeds:edit' pk=feed.pk %}">
<td>{{ feed.title }}</td>
<td>{{ feed.config.MODULE_NAME }}</td>
<td>{{ feed.chat_id }}</td>
<td>{{ feed.check_interval }}</td>
</tr>
{% empty %}
<tfoot>
<tr>
<td colspan="4">No feeds added</td>
</tr>
</tfoot>
{% endfor %}
</table>
</div>
</section>
{% endblock %}

3
feeds/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
feeds/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path
from feeds.views import FeedListView, FeedConfigEditView, FeedConfigCreateView, FeedConfigDeleteView
app_name = 'feeds'
urlpatterns = [
path('', FeedListView.as_view(), name='index'),
path('<int:pk>/', FeedConfigEditView.as_view(), name='edit'),
path('new/<content_type>/', FeedConfigCreateView.as_view(), name='new'),
path('delete/<int:pk>/', FeedConfigDeleteView.as_view(), name='delete'),
]

38
feeds/utils.py Normal file
View File

@@ -0,0 +1,38 @@
from django.views.generic import TemplateView
from cabinet.utils import CabinetViewMixin
from feeds.forms import FeedForm, get_config_form
from feeds.models import Feed
class BaseFeedConfigView(CabinetViewMixin, TemplateView):
template_name = 'cabinet/feeds/feed_form.html'
context_object_name = 'feed'
def get_queryset(self):
return Feed.objects.filter(owner=self.request.user)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs):
self.object = self.get_object()
feed_form, config_form = self.get_forms()
if feed_form.is_valid() and config_form.is_valid():
return self.form_valid(feed_form, config_form)
else:
context = self.get_context_data(forms=(feed_form, config_form))
return self.render_to_response(context)
def get_forms(self):
feed = self.get_object()
data = self.request.POST if self.request.method == 'POST' else None
return FeedForm(data=data, instance=feed), \
get_config_form(self.get_content_type().model_class())(data=data, instance=feed.config if feed else None)
def get_context_data(self, forms=None, **kwargs):
ctx = super(BaseFeedConfigView, self).get_context_data(**kwargs)
ctx['feed_form'], ctx['config_form'] = self.get_forms() if forms is None else forms
ctx['feed_module'] = self.get_content_type().model_class()
return ctx

70
feeds/views.py Normal file
View File

@@ -0,0 +1,70 @@
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from django.views.generic import ListView
from django.views.generic.detail import SingleObjectMixin
from cabinet.utils import CabinetViewMixin
from feeds.models import Feed, FEED_MODULES
from feeds.utils import BaseFeedConfigView
class FeedListView(CabinetViewMixin, ListView):
template_name = 'cabinet/feeds/feed_list.html'
title = 'Feed list'
sidebar_section = 'feeds'
def get_queryset(self):
return Feed.objects.filter(owner=self.request.user)
def get_context_data(self, *args, **kwargs):
ctx = super(FeedListView, self).get_context_data(*args, **kwargs)
ctx['feed_modules'] = FEED_MODULES
return ctx
class FeedConfigEditView(BaseFeedConfigView, SingleObjectMixin):
title = 'Configure feed'
sidebar_section = 'feeds'
def form_valid(self, feed_form, config_form):
feed_form.save()
config_form.save()
messages.success(self.request, 'Config was successfully saved')
return HttpResponseRedirect('')
def get_content_type(self):
return self.get_object().config_type
class FeedConfigCreateView(BaseFeedConfigView):
title = 'Create feed'
sidebar_section = 'feeds'
def get_object(self):
return None
def form_valid(self, feed_form, config_form):
config_form.save()
feed_form.instance.owner = self.request.user
feed_form.instance.config_type = self.get_content_type()
feed_form.instance.config_id = config_form.instance.pk
feed_form.save()
messages.success(self.request, 'Config was successfully saved')
return redirect('cabinet:feeds:edit', pk=feed_form.instance.pk)
def get_content_type(self):
return get_object_or_404(ContentType, model=self.kwargs['content_type'])
class FeedConfigDeleteView(SingleObjectMixin, View):
def get_queryset(self):
return Feed.objects.filter(owner=self.request.user)
def post(self, request, *args, **kwargs):
feed = self.get_object()
messages.success(self.request, 'Feed "{}" was successfully deleted'.format(feed.title))
feed.delete()
return redirect('cabinet:feeds:index')