initial commit
This commit is contained in:
commit
24c5f2fcf6
88
.gitignore
vendored
Normal file
88
.gitignore
vendored
Normal 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
28
.idea/dataSources.xml
Normal 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>
|
17
.idea/inspectionProfiles/Project_Default.xml
Normal file
17
.idea/inspectionProfiles/Project_Default.xml
Normal 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
82
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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
33
.idea/telegram_bots.iml
Normal 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="<map/>" />
|
||||
<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
8
.idea/vcs.xml
Normal 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
25
.idea/watcherTasks.xml
Normal 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
14
.idea/webResources.xml
Normal 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
0
cabinet/__init__.py
Normal file
3
cabinet/admin.py
Normal file
3
cabinet/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
cabinet/apps.py
Normal file
5
cabinet/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CabinetConfig(AppConfig):
|
||||
name = 'cabinet'
|
0
cabinet/migrations/__init__.py
Normal file
0
cabinet/migrations/__init__.py
Normal file
3
cabinet/models.py
Normal file
3
cabinet/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
78
cabinet/templates/cabinet/_base.html
Normal file
78
cabinet/templates/cabinet/_base.html
Normal 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>
|
65
cabinet/templates/cabinet/_includes/header.html
Normal file
65
cabinet/templates/cabinet/_includes/header.html
Normal 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>
|
34
cabinet/templates/cabinet/_includes/sidebar.html
Normal file
34
cabinet/templates/cabinet/_includes/sidebar.html
Normal 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>
|
23
cabinet/templates/cabinet/_internal_base.html
Normal file
23
cabinet/templates/cabinet/_internal_base.html
Normal 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 %}
|
7
cabinet/templates/cabinet/index.html
Normal file
7
cabinet/templates/cabinet/index.html
Normal 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 %}
|
69
cabinet/templates/cabinet/login.html
Normal file
69
cabinet/templates/cabinet/login.html
Normal 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">© Copyright 2017. All Rights Reserved.</p>#}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
0
cabinet/templatetags/__init__.py
Normal file
0
cabinet/templatetags/__init__.py
Normal file
27
cabinet/templatetags/cabinet.py
Normal file
27
cabinet/templatetags/cabinet.py
Normal 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
3
cabinet/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
12
cabinet/urls.py
Normal file
12
cabinet/urls.py
Normal 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
15
cabinet/utils.py
Normal 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
25
cabinet/views.py
Normal 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
0
config/__init__.py
Normal file
19
config/celery.py
Normal file
19
config/celery.py
Normal 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
91
config/settings.py
Normal 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
9
config/urls.py
Normal 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
44
config/utils.py
Normal 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
16
config/wsgi.py
Normal 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
2
example.env
Normal file
@ -0,0 +1,2 @@
|
||||
DJANGO_DEBUG = False
|
||||
DATABASE_URL = postgres://bots:bots@localhost/bots
|
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
|
||||
|
||||