initial commit

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

88
.gitignore vendored Normal file
View File

@ -0,0 +1,88 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
.idea/replstate.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/httpRequests
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
*.manifest
*.spec
pip-log.txt
pip-delete-this-directory.txt
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
*.mo
*.pot
*.log
local_settings.py
db.sqlite3
instance/
.webassets-cache
.scrapy
docs/_build/
target/
.ipynb_checkpoints
.python-version
celerybeat-schedule
*.sage.py
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.spyderproject
.spyproject
.ropeproject
/site
.mypy_cache/

28
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="db" uuid="c627db26-52a1-49af-829b-0c777acb061b">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db.sqlite3</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
<libraries>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/sqlite-jdbc-3.20.1.1.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/xerial-sqlite-license.txt</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.16.1/sqlite-jdbc-3.16.1.jar</url>
</library>
<library>
<url>file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.16.1/xerial-sqlite-license.txt</url>
</library>
</libraries>
</data-source>
</component>
</project>

View File

@ -0,0 +1,17 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="django-compressor-develop" />
<item index="1" class="java.lang.String" itemvalue="django-recaptcha-develop" />
<item index="2" class="java.lang.String" itemvalue="django-robokassa" />
<item index="3" class="java.lang.String" itemvalue="python-dateutil" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

82
.idea/misc.xml Normal file
View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="MarkdownProjectSettings" wasCopied="true">
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true" openRemoteLinks="true" replaceUnicodeEmoji="false" lastLayoutSetsDefault="false">
<PanelProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
</PanelProvider>
</PreviewSettings>
<ParserSettings gitHubSyntaxChange="false" emojiShortcuts="0" emojiImages="0">
<PegdownExtensions>
<option name="ABBREVIATIONS" value="false" />
<option name="ANCHORLINKS" value="true" />
<option name="ASIDE" value="false" />
<option name="ATXHEADERSPACE" value="true" />
<option name="AUTOLINKS" value="true" />
<option name="DEFINITIONS" value="false" />
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
<option name="FENCED_CODE_BLOCKS" value="true" />
<option name="FOOTNOTES" value="false" />
<option name="HARDWRAPS" value="false" />
<option name="HTML_DEEP_PARSER" value="false" />
<option name="INSERTED" value="false" />
<option name="QUOTES" value="false" />
<option name="RELAXEDHRULES" value="true" />
<option name="SMARTS" value="false" />
<option name="STRIKETHROUGH" value="true" />
<option name="SUBSCRIPT" value="false" />
<option name="SUPERSCRIPT" value="false" />
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
<option name="SUPPRESS_INLINE_HTML" value="false" />
<option name="TABLES" value="true" />
<option name="TASKLISTITEMS" value="true" />
<option name="TOC" value="false" />
<option name="WIKILINKS" value="true" />
</PegdownExtensions>
<ParserOptions>
<option name="ADMONITION_EXT" value="false" />
<option name="ATTRIBUTES_EXT" value="false" />
<option name="COMMONMARK_LISTS" value="true" />
<option name="DUMMY" value="false" />
<option name="EMOJI_SHORTCUTS" value="true" />
<option name="ENUMERATED_REFERENCES_EXT" value="false" />
<option name="FLEXMARK_FRONT_MATTER" value="false" />
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="false" />
<option name="GFM_TABLE_RENDERING" value="true" />
<option name="GITBOOK_URL_ENCODING" value="false" />
<option name="GITHUB_LISTS" value="false" />
<option name="GITHUB_WIKI_LINKS" value="true" />
<option name="HEADER_ID_NO_DUPED_DASHES" value="false" />
<option name="JEKYLL_FRONT_MATTER" value="false" />
<option name="NO_TEXT_ATTRIBUTES" value="false" />
<option name="PARSE_HTML_ANCHOR_ID" value="false" />
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
</ParserOptions>
</ParserSettings>
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true" embedImages="false" embedHttpImages="false" imageUriSerials="false">
<GeneratorProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
</GeneratorProvider>
<headerTop />
<headerBottom />
<bodyTop />
<bodyBottom />
</HtmlSettings>
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssUriSerial="true" isCssTextEnabled="false" isDynamicPageWidth="true">
<StylesheetProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
</StylesheetProvider>
<ScriptProviders />
<cssText />
<cssUriHistory />
</CssSettings>
<HtmlExportSettings updateOnSave="false" parentDir="" targetDir="" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" linkFormatType="HTTP_ABSOLUTE" />
<LinkMapSettings>
<textMaps />
</LinkMapSettings>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7 (telegram_bots)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/telegram_bots.iml" filepath="$PROJECT_DIR$/.idea/telegram_bots.iml" />
</modules>
</component>
</project>

33
.idea/telegram_bots.iml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$" />
<option name="settingsModule" value="config/settings.py" />
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
<option value="$MODULE_DIR$/feeds/templates" />
<option value="$MODULE_DIR$/cabinet/templates" />
</list>
</option>
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

8
.idea/vcs.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/build/modular-admin-html" vcs="Git" />
<mapping directory="$PROJECT_DIR$/build/octopus" vcs="Git" />
</component>
</project>

25
.idea/watcherTasks.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<TaskOptions isEnabled="true">
<option name="arguments" value="$FileName$" />
<option name="checkSyntaxErrors" value="true" />
<option name="description" />
<option name="exitCodeBehavior" value="ERROR" />
<option name="fileExtension" value="styl" />
<option name="immediateSync" value="true" />
<option name="name" value="Stylus" />
<option name="output" value="$FileNameWithoutExtension$.css" />
<option name="outputFilters">
<array />
</option>
<option name="outputFromStdout" value="false" />
<option name="program" value="stylus" />
<option name="runOnExternalChanges" value="true" />
<option name="scopeName" value="Project Files" />
<option name="trackOnlyRoot" value="true" />
<option name="workingDir" value="$FileDir$" />
<envs />
</TaskOptions>
</component>
</project>

14
.idea/webResources.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$/static" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

0
cabinet/__init__.py Normal file
View File

3
cabinet/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
cabinet/apps.py Normal file
View File

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

View File

3
cabinet/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,78 @@
{% load staticfiles bootstrap4 cabinet %}
<!DOCTYPE html>
<html class="fixed dark" lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,800|Shadows+Into+Light" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="{% static 'vendor/bootstrap/css/bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'vendor/animate/animate.css' %}">
<link rel="stylesheet" href="{% static 'vendor/font-awesome/css/fontawesome-all.min.css' %}">
<link rel="stylesheet" href="{% static 'vendor/magnific-popup/magnific-popup.css' %}">
<link rel="stylesheet" href="{% static 'vendor/pnotify/pnotify.custom.css' %}">
<link rel="stylesheet" href="{% static 'css/theme.css' %}">
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
<script src="{% static 'vendor/modernizr/modernizr.js' %}"></script>
<script src="{% static 'vendor/turbolinks/turbolinks.js' %}"></script>
</head>
<body class="{% if not is_turbolinks %}loading-overlay-showing{% endif %}" data-loading-overlay>
<div class="loading-overlay">
<div class="bounce-loader">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</div>
{% block page %}{% endblock %}
<script data-turbolinks-eval="false" src="{% static 'vendor/jquery/jquery.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/jquery-browser-mobile/jquery.browser.mobile.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/jquery-cookie/jquery-cookie.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/popper/umd/popper.min.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/bootstrap/js/bootstrap.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/common/common.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/nanoscroller/nanoscroller.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/magnific-popup/jquery.magnific-popup.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/jquery-placeholder/jquery-placeholder.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'vendor/pnotify/pnotify.custom.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'js/theme.js' %}"></script>
<script data-turbolinks-eval="false" src="{% static 'js/custom.js' %}"></script>
<script src="{% static 'js/theme.init.js' %}"></script>
<script src="{% static 'js/custom.init.js' %}"></script>
<script>
var stack = {dir1: "right", dir2: "up", "push": "top"};
{% for message in messages %}
$(function() {
var notice = new PNotify({
text: '{{ message | escapejs }}',
title: 'Notification',
type: '{{ message | pnotify_type }}',
hide: false,
addclass: 'stack-bottomright',
stack: stack,
nonblock: {
nonblock: true,
nonblock_opacity: .2,
},
buttons: {
closer: false,
sticker: false,
}
});
notice.get().click(function() {
notice.remove();
});
});
{% endfor %}
</script>
</body>
</html>

View File

@ -0,0 +1,65 @@
{% load staticfiles %}
<header class="header">
<div class="logo-container">
<a href="{% url 'cabinet:index' %}" class="logo">
<img src="{% static 'img/logo-light.png' %}" height="35" alt="Porto Admin"/>
</a>
<div class="d-md-none toggle-sidebar-left" data-toggle-class="sidebar-left-opened" data-target="html" data-fire-event="sidebar-left-opened">
<i class="fas fa-bars" aria-label="Toggle sidebar"></i>
</div>
</div>
<div class="header-right">
<ul class="notifications">
<li>
<a href="#" class="dropdown-toggle notification-icon" data-toggle="dropdown">
<i class="fas fa-bell"></i>
<span class="badge">0</span>
</a>
<div class="dropdown-menu notification-menu">
<div class="notification-title">
<span class="float-right badge badge-default">0</span>
Alerts
</div>
<div class="content">
<ul>
<li>
No notifications
</li>
</ul>
</div>
</div>
</li>
</ul>
<span class="separator"></span>
<div id="userbox" class="userbox">
<a href="#" data-toggle="dropdown">
<figure class="profile-picture">
<img src="{% static 'img/!logged-user.jpg' %}" alt="{{ request.user.username }}" class="img-circle"
data-lock-picture="{% static 'octopus/images/!logged-user.jpg' %}"/>
</figure>
<div class="profile-info" data-lock-name="{{ request.user.username }}"{# data-lock-email="johndoe@okler.com" #}>
<span class="name">{{ request.user.username }}</span>
<span class="role">{% if request.user.is_superuser %}administrator{% else %}user{% endif %}</span>
</div>
<i class="fa custom-caret"></i>
</a>
<div class="dropdown-menu">
<ul class="list-unstyled mb-2">
<li class="divider"></li>
<li>
<a role="menuitem" tabindex="-1" href="#">
<i class="fas fa-user"></i> My Profile
</a>
</li>
<li>
<a role="menuitem" tabindex="-1" href="{% url 'cabinet:logout' %}">
<i class="fas fa-power-off"></i> Logout
</a>
</li>
</ul>
</div>
</div>
</div>
</header>

View File

@ -0,0 +1,34 @@
<aside id="sidebar-left" class="sidebar-left">
<div class="sidebar-header">
<div class="sidebar-title">Modules</div>
<div class="sidebar-toggle d-none d-md-block" data-toggle-class="sidebar-left-collapsed" data-target="html" data-fire-event="sidebar-left-toggle">
<i class="fas fa-bars" aria-label="Toggle sidebar"></i>
</div>
</div>
<div class="nano">
<div class="nano-content">
<nav id="menu" class="nav-main" role="navigation">
<ul class="nav nav-main">
<li class="{% if sidebar_section == 'feeds' %}nav-active{% endif %}">
<a href="{% url 'cabinet:feeds:index' %}">
<i class="fas fa-rss-square" aria-hidden="true"></i>
<span>Feeds</span>
</a>
</li>
</ul>
</nav>
</div>
<script>
// Maintain Scroll Position
if (typeof localStorage !== 'undefined') {
if (localStorage.getItem('sidebar-left-position') !== null) {
var initialPosition = localStorage.getItem('sidebar-left-position'),
sidebarLeft = document.querySelector('#sidebar-left .nano-content');
sidebarLeft.scrollTop = initialPosition;
}
}
</script>
</div>
</aside>

View File

@ -0,0 +1,23 @@
{% extends 'cabinet/_base.html' %}
{% block page %}
<section class="body">
{% include 'cabinet/_includes/header.html' %}
<div class="inner-wrapper">
{% include 'cabinet/_includes/sidebar.html' %}
<section role="main" class="content-body">
<header class="page-header">
<h2>{{ title }}</h2>
<div class="right-wrapper text-right">
<ol class="breadcrumbs">
<li><a href="{% url 'cabinet:index' %}"><i class="fas fa-home"></i></a>
{% block breadcrumbs %}{% endblock %}
</ol>
{# <a class="sidebar-right-toggle" data-open="sidebar-right"><i class="fa fa-chevron-left"></i></a>#}
</div>
</header>
{% block content %}{% endblock %}
</section>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'cabinet/_internal_base.html' %}
{% block content %}
<div class="alert alert-default">
Nothing here (yet)! Please select section from sidebar.
</div>
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends 'cabinet/_base.html' %}
{% load staticfiles bootstrap4 %}
{% block page %}
<section class="body-sign">
<div class="center-sign">
<a href="https://preview.oklerthemes.com/" class="logo float-left">
<img src="{% static 'img/logo-light.png' %}" height="54" alt="Porto Admin" />
</a>
<div class="panel card-sign">
<div class="card-title-sign mt-3 text-right">
<h2 class="title text-uppercase font-weight-bold m-0"><i class="fas fa-user mr-1"></i> Sign In</h2>
</div>
<div class="card-body">
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form_errors form %}
<div class="form-group mb-3">
<label>Username</label>
<div class="input-group">
<input name="username" type="text" class="form-control form-control-lg" />
<span class="input-group-append">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
</span>
</div>
</div>
<div class="form-group mb-3">
<div class="clearfix">
<label class="float-left">Password</label>
{# <a href="pages-recover-password.html" class="float-right">Lost Password?</a>#}
</div>
<div class="input-group">
<input name="password" type="password" class="form-control form-control-lg" />
<span class="input-group-append">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
</span>
</div>
</div>
<div class="row">
<div class="col-sm-8">
<div class="checkbox-custom checkbox-default">
<input id="RememberMe" name="rememberme" type="checkbox"/>
<label for="RememberMe">Remember Me</label>
</div>
</div>
<div class="col-sm-4 text-right">
<button type="submit" class="btn btn-primary mt-2">Sign In</button>
</div>
</div>
{# <p class="text-center">Don't have an account yet? <a href="pages-signup.html">Sign Up!</a></p>#}
</form>
</div>
</div>
{# <p class="text-center text-muted mt-3 mb-3">&copy; Copyright 2017. All Rights Reserved.</p>#}
</div>
</section>
{% endblock %}

View File

View File

@ -0,0 +1,27 @@
from django import template
from django.contrib.messages import constants as message_constants
MESSAGE_LEVEL_TYPES = {
message_constants.DEBUG: "info",
message_constants.INFO: "info",
message_constants.SUCCESS: "success",
message_constants.WARNING: "notice",
message_constants.ERROR: "error",
}
register = template.Library()
@register.filter
def pnotify_type(message):
try:
level = message.level
except AttributeError:
return 'info'
else:
try:
return MESSAGE_LEVEL_TYPES[level]
except KeyError:
return "info"

3
cabinet/tests.py Normal file
View File

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

12
cabinet/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.contrib.auth.views import LogoutView
from django.urls import path, include
from cabinet.views import CabinetIndexView, LoginView
app_name = 'cabinet'
urlpatterns = [
path('', CabinetIndexView.as_view(), name='index'),
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('feeds/', include('feeds.urls', namespace='feeds')),
]

15
cabinet/utils.py Normal file
View File

@ -0,0 +1,15 @@
from django.contrib.auth.mixins import LoginRequiredMixin
class CabinetViewMixin(LoginRequiredMixin):
title = 'No title'
sidebar_section = None
def get_title(self):
return self.title
def get_context_data(self, **kwargs):
ctx = super(CabinetViewMixin, self).get_context_data(**kwargs)
ctx['title'] = self.get_title()
ctx['sidebar_section'] = self.sidebar_section
return ctx

25
cabinet/views.py Normal file
View File

@ -0,0 +1,25 @@
from django.contrib.auth.views import LoginView as BaseLoginView
from django.views.generic import TemplateView
from cabinet.utils import CabinetViewMixin
class CabinetIndexView(CabinetViewMixin, TemplateView):
template_name = 'cabinet/index.html'
title = 'Cabinet home'
class LoginView(BaseLoginView):
template_name = 'cabinet/login.html'
redirect_authenticated_user = True
def get_context_data(self, **kwargs):
ctx = super(LoginView, self).get_context_data(**kwargs)
ctx['title'] = 'Login'
return ctx
def form_valid(self, form):
res = super(LoginView, self).form_valid(form)
if not self.request.POST.get('remember'):
self.request.session.set_expiry(0)
return res

0
config/__init__.py Normal file
View File

19
config/celery.py Normal file
View File

@ -0,0 +1,19 @@
import os
from celery import Celery
config = {
'broker_url': 'redis://127.0.0.1:6379/0',
}
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
app = Celery('telegram_bots')
app.config_from_object(config)
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))

91
config/settings.py Normal file
View File

@ -0,0 +1,91 @@
import os
import environ
BASE_DIR = environ.Path(__file__) - 2
env = environ.Env()
env_file = str(BASE_DIR.path('.env'))
if os.path.exists(env_file):
env.read_env(env_file)
SECRET_KEY = '6)mck*tk2eq++!01pr2!7qo7#zf_e(hi_m=-qa4x-ah)lvvt4-'
DEBUG = env.bool('DJANGO_DEBUG', False)
ALLOWED_HOSTS = ['0.0.0.0', '127.0.0.1', 'home.bakatrouble.pw', 'localhost']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'bootstrap4',
'cabinet',
'feeds',
]
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 = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [str(BASE_DIR.path('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',
'config.utils.turbolinks',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3'),
}
DATABASES['default']['ATOMIC_REQUESTS'] = True
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATICFILES_DIRS = [
str(BASE_DIR.path('static')),
]
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
PUBLIC_ROOT = BASE_DIR.path('public')
STATIC_URL = '/static/'
STATIC_ROOT = str(PUBLIC_ROOT.path('static'))
MEDIA_URL = '/uploads/'
MEDIA_ROOT = str(PUBLIC_ROOT.path('uploads'))
LOGIN_URL = 'cabinet:login'
LOGIN_REDIRECT_URL = 'cabinet:index'
LOGOUT_REDIRECT_URL = LOGIN_URL

9
config/urls.py Normal file
View File

@ -0,0 +1,9 @@
from django.contrib import admin
from django.urls import path, include
from django.views.generic import RedirectView
urlpatterns = [
path('', RedirectView.as_view(pattern_name='cabinet:index')),
path('admin/', admin.site.urls),
path('cabinet/', include('cabinet.urls', namespace='cabinet')),
]

44
config/utils.py Normal file
View File

@ -0,0 +1,44 @@
from urllib.parse import urlparse
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
def same_origin(current_uri, redirect_uri):
a = urlparse(current_uri)
if not a.scheme:
return True
b = urlparse(redirect_uri)
return (a.scheme, a.hostname, a.port) == (b.scheme, b.hostname, b.port)
def turbolinks(request: HttpRequest):
ctx = {}
if request.META.get('HTTP_TURBOLINKS_REFERRER') is not None:
ctx['is_turbolinks'] = True
return ctx
class TurbolinksMiddleware:
def process_request(self, request: HttpRequest):
referrer = request.META.get('HTTP_TURBOLINKS_REFERRER')
if referrer:
request.META['HTTP_REFERER'] = referrer
return
def process_response(self, request: HttpRequest, response: HttpResponse):
referrer = request.META.get('HTTP_TURBOLINKS_REFERRER')
if referrer is None:
return response
if response.has_header('Location'):
loc = response['Location']
request.session['_turbolinks_redirect_to'] = loc
if referrer and not same_origin(loc, referrer):
return HttpResponseForbidden()
else:
if request.session.get('_turbolinks_redirect_to'):
loc = request.session.pop('_turbolinks_redirect_to')
response['Turbolinks-Location'] = loc
return response

16
config/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = get_wsgi_application()

2
example.env Normal file
View File

@ -0,0 +1,2 @@
DJANGO_DEBUG = False
DATABASE_URL = postgres://bots:bots@localhost/bots

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