initial commit
This commit is contained in:
0
feeds/__init__.py
Normal file
0
feeds/__init__.py
Normal file
7
feeds/admin.py
Normal file
7
feeds/admin.py
Normal 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
5
feeds/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FeedsConfig(AppConfig):
|
||||
name = 'feeds'
|
21
feeds/forms.py
Normal file
21
feeds/forms.py
Normal 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
|
0
feeds/management/__init__.py
Normal file
0
feeds/management/__init__.py
Normal file
0
feeds/management/commands/__init__.py
Normal file
0
feeds/management/commands/__init__.py
Normal file
17
feeds/management/commands/worker_loop.py
Normal file
17
feeds/management/commands/worker_loop.py
Normal 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
|
47
feeds/migrations/0001_initial.py
Normal file
47
feeds/migrations/0001_initial.py
Normal 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')},
|
||||
),
|
||||
]
|
18
feeds/migrations/0002_feed_lock.py
Normal file
18
feeds/migrations/0002_feed_lock.py
Normal 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),
|
||||
),
|
||||
]
|
0
feeds/migrations/__init__.py
Normal file
0
feeds/migrations/__init__.py
Normal file
68
feeds/models.py
Normal file
68
feeds/models.py
Normal 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
24
feeds/tasks.py
Normal 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()
|
58
feeds/templates/cabinet/feeds/feed_form.html
Normal file
58
feeds/templates/cabinet/feeds/feed_form.html
Normal 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 %}
|
52
feeds/templates/cabinet/feeds/feed_list.html
Normal file
52
feeds/templates/cabinet/feeds/feed_list.html
Normal 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
3
feeds/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
12
feeds/urls.py
Normal file
12
feeds/urls.py
Normal 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
38
feeds/utils.py
Normal 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
70
feeds/views.py
Normal 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')
|
Reference in New Issue
Block a user