initial commit

This commit is contained in:
2021-07-28 02:15:48 +03:00
commit 735633853a
6607 changed files with 1084121 additions and 0 deletions

View File

View File

View File

@@ -0,0 +1,8 @@
# from django.contrib import admin
#
# from server.apps.main.models import BlogPost
#
#
# @admin.register(BlogPost)
# class BlogPostAdmin(admin.ModelAdmin[BlogPost]):
# """Admin panel example for ``BlogPost`` model."""

View File

@@ -0,0 +1,28 @@
from celery import Celery
# from server.settings.components import config
from pathlib import Path
from decouple import AutoConfig
BASE_DIR = Path.cwd().parent.parent.parent.parent
config = AutoConfig(search_path=BASE_DIR.joinpath('config'))
RABBITMQ_DEFAULT_USER = config('RABBITMQ_DEFAULT_USER')
RABBITMQ_DEFAULT_PASS = config('RABBITMQ_DEFAULT_PASS')
RABBITMQ_PORT = config('RABBITMQ_PORT', cast=int, default=5672)
RABBITMQ_HOST = config('RABBITMQ_HOST')
celery_app = Celery(
'tasks',
broker='amqp://{login}:{password}@{host}:{port}'.format(
login=RABBITMQ_DEFAULT_USER,
password=RABBITMQ_DEFAULT_PASS,
host=RABBITMQ_HOST,
port=RABBITMQ_PORT,
),
backend='rpc://',
)
celery_app.autodiscover_tasks()

View File

@@ -0,0 +1,102 @@
import requests
from requests.models import Response
from requests.auth import HTTPBasicAuth
import re
import time
from functools import lru_cache
from typing import Dict, Optional
from server.apps.main.celery_config import celery_app
from server.settings.components.common import GIT_API_URL
from celery_progress.backend import ProgressRecorder
from celery import shared_task
from server.settings.components import config
def current_page(response: Response, link: str) -> int:
url = str(response.links[f'{link}']['url'])
page_count = int(str(re.findall(pattern=r'page=\d+', string=url)[1])
.replace('page=', ''))
return page_count
def github_request(url: str) -> Response:
auth = HTTPBasicAuth(config('GITHUB_USERNAME'), config('GITHUB_PASSWORD'))
counter = 0
while True:
try:
counter += 1
if auth == HTTPBasicAuth('', ''):
response = requests.get(url)
else:
response = requests.get(url, auth=auth)
return response
except ConnectionError as connection_error:
if counter < 5:
time.sleep(10)
else:
raise connection_error
@shared_task(bind=True)
def get_github_stars(self, username: str) -> Dict[str, Optional[int]]:
url = f'{GIT_API_URL}/{username}/repos?per_page=100&page=1'
print(url)
progress_recorder = ProgressRecorder(self)
response = github_request(url)
if response.status_code >= 400:
result = {}
else:
repos = response.json()
try:
page_count = current_page(response, 'last')
repos_count = (page_count - 1) * 100 + \
len(github_request(response.links['last']['url']).json())
except KeyError as e:
page_count = 1
repos_count = len(repos)
i = 0
while 'next' in response.links.keys():
i += 1
response = github_request(response.links['next']['url'])
repos.extend(response.json())
current = i * 100 + len(response.json())
# Progress bar
percent = round(100 / page_count * i)
progress_recorder.set_progress(current, repos_count,
description=f'Processing: {percent}%')
# Fetching repos and stars in dict
data: Dict[str, int] = {}
try:
for item in repos:
data[item['name']] = int(item['stargazers_count'])
result = dict(sorted(data.items(), key=lambda x: x[1], reverse=True))
except TypeError:
result = {}
return result
@shared_task(bind=True)
def process_download(self):
print('Task started')
# Create the progress recorder instance
# which we'll use to update the web page
progress_recorder = ProgressRecorder(self)
print('Start')
for i in range(5):
# Sleep for 1 second
time.sleep(1)
# Print progress in Celery task output
print(i + 1)
# Update progress on the web page
progress_recorder.set_progress(i + 1, 5, description='Downloading')
print('End')
return 'Task Complete'

View File

@@ -0,0 +1,8 @@
from django import forms
class GithubForm(forms.Form):
search_field = forms.CharField(max_length=20, required=True,
help_text='search github stars',
label='github_search',)

View File

@@ -0,0 +1,11 @@
"""
This package is a place for your business logic.
Please, do not create any other files inside your app package.
Place all files here, including: logic, forms, serializers.
Decoupling is a good thing. We need more of that.
Try using https://github.com/dry-python/
It is awesome for writing business logic!
"""

View File

@@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
class Command(BaseCommand):
def handle(self, *args, **options):
if not User.objects.filter(username='admin').exists():
username = 'admin'
email = 'admin@admin.ru'
password = 'admin'
admin = User.objects.create_superuser(username=username,
password=password,
email=email)
admin.is_active = True
admin.is_admin = True
admin.save()
else:
print('Admin accounts can only be initialized if no Accounts exist')

View File

@@ -0,0 +1,35 @@
# Generated by Django 2.2.7 on 2019-11-24 11:01
from django.db import migrations, models
class Migration(migrations.Migration):
"""Initial migration that creates the example BlogPost model."""
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='BlogPost',
fields=[
(
'id',
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('title', models.CharField(max_length=80)),
('body', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'BlogPost',
'verbose_name_plural': 'BlogPosts',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-16 10:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='blogpost',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-16 11:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0002_alter_blogpost_id'),
]
operations = [
migrations.AlterField(
model_name='blogpost',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-16 11:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0003_alter_blogpost_id'),
]
operations = [
migrations.AlterField(
model_name='blogpost',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-16 11:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0004_alter_blogpost_id'),
]
operations = [
migrations.AlterField(
model_name='blogpost',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-16 11:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0005_alter_blogpost_id'),
]
operations = [
migrations.AlterField(
model_name='blogpost',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-07-16 11:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0006_alter_blogpost_id'),
]
operations = [
migrations.AlterField(
model_name='blogpost',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -0,0 +1,33 @@
import textwrap
from typing import Final, final
from django.db import models
#: That's how constants should be defined.
_POST_TITLE_MAX_LENGTH: Final = 80
@final
class BlogPost(models.Model):
"""
This model is used just as an example.
With it we show how one can:
- Use fixtures and factories
- Use migrations testing
"""
title = models.CharField(max_length=_POST_TITLE_MAX_LENGTH)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta(object):
verbose_name = 'BlogPost' # You can probably use `gettext` for this
verbose_name_plural = 'BlogPosts'
def __str__(self) -> str:
"""All django models should have this method."""
return textwrap.wrap(self.title, _POST_TITLE_MAX_LENGTH // 4)[0]

View File

@@ -0,0 +1,167 @@
class CeleryProgressBar {
constructor(progressUrl, options) {
this.progressUrl = progressUrl;
options = options || {};
let progressBarId = options.progressBarId || 'progress-bar';
let progressBarMessage = options.progressBarMessageId || 'progress-bar-message';
this.progressBarElement = options.progressBarElement || document.getElementById(progressBarId);
this.progressBarMessageElement = options.progressBarMessageElement || document.getElementById(progressBarMessage);
this.onProgress = options.onProgress || this.onProgressDefault;
this.onSuccess = options.onSuccess || this.onSuccessDefault;
this.onError = options.onError || this.onErrorDefault;
this.onTaskError = options.onTaskError || this.onTaskErrorDefault;
this.onDataError = options.onDataError || this.onError;
this.onRetry = options.onRetry || this.onRetryDefault;
this.onIgnored = options.onIgnored || this.onIgnoredDefault;
let resultElementId = options.resultElementId || 'celery-result';
this.resultElement = options.resultElement || document.getElementById(resultElementId);
this.onResult = options.onResult || this.onResultDefault;
// HTTP options
this.onNetworkError = options.onNetworkError || this.onError;
this.onHttpError = options.onHttpError || this.onError;
this.pollInterval = options.pollInterval || 500;
// Other options
let barColorsDefault = {
success: '#76ce60',
error: '#dc4f63',
progress: '#68a9ef',
ignored: '#7a7a7a'
}
this.barColors = Object.assign({}, barColorsDefault, options.barColors);
}
onSuccessDefault(progressBarElement, progressBarMessageElement, result) {
result = this.getMessageDetails(result);
progressBarElement.style.backgroundColor = this.barColors.success;
progressBarMessageElement.textContent = "Success! Please refresh page.";
}
onResultDefault(resultElement, result) {
if (resultElement) {
resultElement.textContent = result;
}
}
/**
* Default handler for all errors.
* @param data - A Response object for HTTP errors, undefined for other errors
*/
onErrorDefault(progressBarElement, progressBarMessageElement, excMessage, data) {
progressBarElement.style.backgroundColor = this.barColors.error;
excMessage = excMessage || '';
progressBarMessageElement.textContent = "Uh-Oh, something went wrong! " + excMessage;
}
onTaskErrorDefault(progressBarElement, progressBarMessageElement, excMessage) {
let message = this.getMessageDetails(excMessage);
this.onError(progressBarElement, progressBarMessageElement, message);
}
onRetryDefault(progressBarElement, progressBarMessageElement, excMessage, retryWhen) {
retryWhen = new Date(retryWhen);
let message = 'Retrying in ' + Math.round((retryWhen.getTime() - Date.now())/1000) + 's: ' + excMessage;
this.onError(progressBarElement, progressBarMessageElement, message);
}
onIgnoredDefault(progressBarElement, progressBarMessageElement, result) {
progressBarElement.style.backgroundColor = this.barColors.ignored;
progressBarMessageElement.textContent = result || 'Task result ignored!'
}
onProgressDefault(progressBarElement, progressBarMessageElement, progress) {
progressBarElement.style.backgroundColor = this.barColors.progress;
progressBarElement.style.width = progress.percent + "%";
var description = progress.description || "";
if (progress.current == 0) {
if (progress.pending === true) {
progressBarMessageElement.textContent = 'Waiting for task to start...';
} else {
progressBarMessageElement.textContent = 'Task started...';
}
} else {
progressBarMessageElement.textContent = progress.current + ' of ' + progress.total + ' processed. ' + description;
}
}
getMessageDetails(result) {
if (this.resultElement) {
return ''
} else {
return result || '';
}
}
/**
* Process update message data.
* @return true if the task is complete, false if it's not, undefined if `data` is invalid
*/
onData(data) {
let done = false;
if (data.progress) {
this.onProgress(this.progressBarElement, this.progressBarMessageElement, data.progress);
}
if (data.complete === true) {
done = true;
if (data.success === true) {
this.onSuccess(this.progressBarElement, this.progressBarMessageElement, data.result);
} else if (data.success === false) {
if (data.state === 'RETRY') {
this.onRetry(this.progressBarElement, this.progressBarMessageElement, data.result.message, data.result.when);
done = false;
delete data.result;
} else {
this.onTaskError(this.progressBarElement, this.progressBarMessageElement, data.result);
}
} else {
if (data.state === 'IGNORED') {
this.onIgnored(this.progressBarElement, this.progressBarMessageElement, data.result);
delete data.result;
} else {
done = undefined;
this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error");
}
}
if (data.hasOwnProperty('result')) {
this.onResult(this.resultElement, data.result);
}
} else if (data.complete === undefined) {
done = undefined;
this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Data Error");
}
return done;
}
async connect() {
let response;
try {
response = await fetch(this.progressUrl);
} catch (networkError) {
this.onNetworkError(this.progressBarElement, this.progressBarMessageElement, "Network Error");
throw networkError;
}
if (response.status === 200) {
let data;
try {
data = await response.json();
} catch (parsingError) {
this.onDataError(this.progressBarElement, this.progressBarMessageElement, "Parsing Error")
throw parsingError;
}
const complete = this.onData(data);
if (complete === false) {
setTimeout(this.connect.bind(this), this.pollInterval);
}
} else {
this.onHttpError(this.progressBarElement, this.progressBarMessageElement, "HTTP Code " + response.status, response);
}
}
static initProgressBar(progressUrl, options) {
const bar = new this(progressUrl, options);
bar.connect();
}
}

View File

@@ -0,0 +1,32 @@
class CeleryWebSocketProgressBar extends CeleryProgressBar {
constructor(progressUrl, options) {
super(progressUrl, options);
}
async connect() {
var ProgressSocket = new WebSocket((location.protocol === 'https:' ? 'wss' : 'ws') + '://' +
window.location.host + this.progressUrl);
ProgressSocket.onopen = function (event) {
ProgressSocket.send(JSON.stringify({'type': 'check_task_completion'}));
};
const bar = this;
ProgressSocket.onmessage = function (event) {
let data;
try {
data = JSON.parse(event.data);
} catch (parsingError) {
bar.onDataError(bar.progressBarElement, bar.progressBarMessageElement, "Parsing Error")
throw parsingError;
}
const complete = bar.onData(data);
if (complete === true || complete === undefined) {
ProgressSocket.close();
}
};
}
}

View File

@@ -0,0 +1,29 @@
body {
text-align: center;
}
img {
max-width: 100%;
height: auto;
}
.wemake-services-body {
height: 95vh;
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
align-items: center;
}
.wemake-services-logo {
max-width: 250px;
margin: 0 auto;
}
.github-corner img {
position: absolute;
top: 0;
right: 0;
border: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,31 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Celery Progress Demo</title>
<meta name="Celery Progress Demo" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
{% block demo %}{% endblock %}
<!-- JQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<!-- Bootstrap JS -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
<!-- Celery Progress -->
<script src="{% static 'celery_progress/celery_progress.js' %}"></script>
{% block progress_bar_js %}{% endblock progress_bar_js %}
<div class="container text-center" style="padding-top: 20px;">
{{ message }}
{% for repo, stars in data.items %}
<br>
{{ repo }} {{ stars }}
{% endfor %}
</div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
{% block content %}
<h1>Регистрация</h1>
<form method="post" action="">
{% csrf_token %}
<dl class="register">
{% for field in form %}
<dt>{{ field.label_tag }}</dt>
<dd class="clearfix">{{ field }}
{% if field.help_text %}<div class="clearfix">{{ field.help_text }}</div>{% endif %}
{% if field.errors %}<div class="myerrors clearfix">{{ field.errors }}</div>{% endif %}
</dd>
{% endfor %}
</dl>
<input type="submit" value="Зарегистрироваться" class="clearfix">
</form>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% load static %}
{% block demo %}
<!-- Reset Form -->
<div class="container text-center" style="padding-top: 20px;">
<form action="{% url 'demo' %}" method="GET" style="display: inline;">
{% csrf_token %}
<button class="btn btn-primary" type="submit" style="width:120px; background-color:#23e841; color:black">
<strong>REFRESH PAGE</strong>
</button>
</form>
</div>
<!-- Download Form -->
<div class="container text-center" style="padding-top: 20px;">
<form action="{% url 'demo' %}" method="post" style="display: inline;">
{% csrf_token %}
<dl class="register">
{% for field in form %}
<dt>{{ field.label_tag }}</dt>
<dd class="clearfix">{{ field }}
{% if field.help_text %}<div class="clearfix">{{ field.help_text }}</div>{% endif %}
{% if field.errors %}<div class="myerrors clearfix">{{ field.errors }}</div>{% endif %}
</dd>
{% endfor %}
</dl>
<button class="btn btn-primary" type="submit" style="width:120px; background-color:#23e841; color:black;">
<strong>Get Github stars</strong>
</button>
</form>
</div>
<!-- Download Status -->
<div class="container" style="padding-top: 20px;">
<div class="card" style="height: 120px;">
{% block progress %}{% endblock progress %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
{% block content %}
<h1>Github stars on repos</h1>
<form method="post" action="">
{% csrf_token %}
<dl class="register">
{% for field in form %}
<dt>{{ field.label_tag }}</dt>
<dd class="clearfix">{{ field }}
{% if field.help_text %}<div class="clearfix">{{ field.help_text }}</div>{% endif %}
{% if field.errors %}<div class="myerrors clearfix">{{ field.errors }}</div>{% endif %}
</dd>
{% endfor %}
</dl>
<input type="submit" value="Подсчёт звёзд" class="clearfix">
</form>
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Github results</title>
</head>
<body>
{{ message }}
{% for repo, stars in data.items %}
<br>
{{ repo }} {{ stars }}
{% endfor %}
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,26 @@
{% extends "download.html" %}
{% load static %}
{% block progress %}
<div class="text-center" style="font-size: 14px">
<div id="progress-bar-message">
Click the "Get Github stars" button
</div>
</div>
<div class='progress-wrapper' style="padding-top: 10px;">
<div id='progress-bar' class='progress-bar progress-bar-striped' role='progressbar' style="height:30px; width: 0%; border-radius: 5px">&nbsp;</div>
</div>
{% endblock progress %}
{% block progress_bar_js %}
{% if task_id %}
<script type="text/javascript">
// Progress Bar (JQuery)
$(function () {
var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
CeleryProgressBar.initProgressBar(progressUrl, {})
});
</script>
{% endif %}
{% endblock progress_bar_js %}

View File

@@ -0,0 +1,14 @@
# The humans responsible & technology colophon
# http://humanstxt.org/
## balsh
Team:
## Technologies
Language: English
Doctype: HTML5
Technologies: Python, Django

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,9 @@
from django.urls import path
from server.apps.main.views import index
app_name = 'main'
urlpatterns = [
path('hello/', index, name='hello'),
]

View File

@@ -0,0 +1,107 @@
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect
from django.urls import reverse
from .forms import GithubForm
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from .commands import get_github_stars, process_download
from django.views.decorators.http import require_http_methods
from celery.result import AsyncResult
task_id = {}
def index(request: HttpRequest) -> HttpResponse:
"""
Main (or index) view.
Returns rendered default page to the user.
Typed with the help of ``django-stubs`` project.
"""
return render(request, 'main/index.html')
@login_required
def github(request: HttpRequest) -> HttpResponse:
username = str(request.user.username)
try:
email = getattr((User.objects.get(username=username)),
'email', 'default@email.ru')
except ObjectDoesNotExist as e:
error = 'That user doesnt exists or not log on'
print(error, e)
if request.method == 'POST':
github_username = str(request.POST.get('search_field'))
result = get_github_stars.delay(github_username)
task_id[username] = result.task_id
return redirect(reverse('github_result'))
form = GithubForm
return render(request, 'main/github.html',
context={'form': form})
@login_required
@require_http_methods(['GET'])
def github_result(request: HttpRequest) -> HttpResponse:
username = str(request.user.username)
data = AsyncResult(task_id[username])
if data.ready():
message = "Result Ready"
result = data.get()
print('result ready')
else:
print('result not ready')
return render(request, 'main/github_result.html',
context={'data': result,
'message': message})
def demo_view(request: HttpRequest) -> HttpResponse:
username = str(request.user.username)
form = GithubForm
result = {}
message = ''
if request.method == 'GET':
try:
data = AsyncResult(task_id[username])
if data.ready():
result = data.get()
message = f'Total repos: {len(result)}\n'
if len(result) == 0:
result = {'Error': 'User has no repositories!'}
print('Result ready! Please refresh page')
else:
print('result not ready')
except KeyError as e:
print(e)
finally:
# Return demo view
return render(request, 'progress.html',
context={'data': result, 'form': form,
'message': message})
elif request.method == 'POST':
message = 'Please wait'
github_username = str(request.POST.get('search_field'))
# Create Task
result = get_github_stars.delay(username=github_username)
# Get ID
task_id[username] = result.task_id
# Print Task ID
print(f'Celery Task ID: {task_id[username]}')
# Return demo view with Task ID
return render(request, 'progress.html',
context={'task_id': task_id[username],
'message': message,
'data': {}})

View File

@@ -0,0 +1,40 @@
"""
This is a django-split-settings main file.
For more information read this:
https://github.com/sobolevn/django-split-settings
https://sobolevn.me/2017/04/managing-djangos-settings
To change settings file:
`DJANGO_ENV=production python manage.py runserver`
"""
from os import environ
import django_stubs_ext
from split_settings.tools import include, optional
# Monkeypatching Django, so stubs will work for all generics,
# see: https://github.com/typeddjango/django-stubs
django_stubs_ext.monkeypatch()
# Managing environment via `DJANGO_ENV` variable:
environ.setdefault('DJANGO_ENV', 'development')
_ENV = environ['DJANGO_ENV']
_base_settings = (
'components/common.py',
'components/logging.py',
'components/csp.py',
'components/caches.py',
'components/email.py',
# Select the right env:
'environments/{0}.py'.format(_ENV),
# Optionally override some settings:
optional('environments/local.py'),
)
# Include settings:
include(*_base_settings)

View File

@@ -0,0 +1,12 @@
from pathlib import Path
from decouple import AutoConfig
# Build paths inside the project like this: BASE_DIR.joinpath('some')
# `pathlib` is better than writing: dirname(dirname(dirname(__file__)))
# BASE_DIR = PurePath(__file__).parent.parent.parent.parent
BASE_DIR = Path.cwd()
# Loading `.env` files
# See docs: https://gitlab.com/mkleehammer/autoconfig
config = AutoConfig(search_path=BASE_DIR.joinpath('config'))

View File

@@ -0,0 +1,16 @@
# Caching
# https://docs.djangoproject.com/en/2.2/topics/cache/
CACHES = {
'default': {
# TODO: use some other cache in production,
# like https://github.com/jazzband/django-redis
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
}
# django-axes
# https://django-axes.readthedocs.io/en/latest/4_configuration.html#configuring-caches
AXES_CACHE = 'default'

View File

@@ -0,0 +1,211 @@
# """
# Django settings for server project.
#
# For more information on this file, see
# https://docs.djangoproject.com/en/2.2/topics/settings/
#
# For the full list of settings and their config, see
# https://docs.djangoproject.com/en/2.2/ref/settings/
# """
import os
from typing import Dict, List, Tuple, Union
DEBUG = True
from django.utils.translation import ugettext_lazy as _
from server.settings.components import BASE_DIR, config
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
SECRET_KEY = config('DJANGO_SECRET_KEY')
LOGIN_REDIRECT_URL = '/main/hello'
GIT_API_URL = 'https://api.github.com/users'
# Application definition:
INSTALLED_APPS: Tuple[str, ...] = (
# Your apps go here:
'server.apps.main',
# Default django apps:
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# django-admin:
'django.contrib.admin',
'django.contrib.admindocs',
# Security:
# 'axes',
# Health checks:
# You may want to enable other checks as well,
# see: https://github.com/KristianOellegaard/django-health-check
'health_check',
'health_check.db',
'health_check.cache',
'health_check.storage',
)
MIDDLEWARE: Tuple[str, ...] = (
# # Content Security Policy:
# 'csp.middleware.CSPMiddleware',
# Django:
'django.middleware.security.SecurityMiddleware',
# django-permissions-policy
'django_permissions_policy.PermissionsPolicyMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# Axes:
# 'axes.middleware.AxesMiddleware',
# Django HTTP Referrer Policy:
'django_http_referrer_policy.middleware.ReferrerPolicyMiddleware',
)
ROOT_URLCONF = 'server.urls'
WSGI_APPLICATION = 'server.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('POSTGRES_DB'),
'USER': config('POSTGRES_USER'),
'PASSWORD': config('POSTGRES_PASSWORD'),
'HOST': config('DJANGO_DATABASE_HOST'),
'PORT': config('DJANGO_DATABASE_PORT', cast=int),
'CONN_MAX_AGE': config('CONN_MAX_AGE', cast=int, default=60),
'OPTIONS': {
'connect_timeout': 10,
},
},
}
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'ru-RU' # 'en-us'
USE_I18N = True
USE_L10N = True
LANGUAGES = (
('ru', _('Russian')),
# ('en', _('English')),
)
LOCALE_PATHS = (
'locale/',
)
USE_TZ = True
TIME_ZONE = 'UTC'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
# STATIC_URL = '/static/'
#
# STATICFILES_FINDERS = (
# 'django.contrib.staticfiles.finders.FileSystemFinder',
# 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# )
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'shared_dir')
# Templates
# https://docs.djangoproject.com/en/2.2/ref/templates/api
TEMPLATES = [{
'APP_DIRS': True,
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
# Contains plain text templates, like `robots.txt`:
BASE_DIR.joinpath('server', 'templates'),
],
'OPTIONS': {
'context_processors': [
# Default template context processors:
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request',
],
},
}]
# Media files
# Media root dir is commonly changed in production
# (see development.py and production.py).
# https://docs.djangoproject.com/en/2.2/topics/files/
# MEDIA_URL = '/media/'
# MEDIA_ROOT = BASE_DIR.joinpath('media')
# Django authentication system
# https://docs.djangoproject.com/en/2.2/topics/auth/
AUTHENTICATION_BACKENDS = (
# 'axes.backends.AxesBackend',
'django.contrib.auth.backends.ModelBackend',
)
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.BCryptPasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
]
# Security
# https://docs.djangoproject.com/en/2.2/topics/security/
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
# https://github.com/DmytroLitvinov/django-http-referrer-policy
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
REFERRER_POLICY = 'same-origin'
# https://github.com/adamchainz/django-permissions-policy#setting
PERMISSIONS_POLICY: Dict[str, Union[str, List[str]]] = {} # noqa: WPS234
# Timeouts
# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-EMAIL_TIMEOUT
EMAIL_TIMEOUT = 5

View File

@@ -0,0 +1,15 @@
"""
This file contains a definition for Content-Security-Policy headers.
Read more about it:
https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Content-Security-Policy
We are using `django-csp` to provide these headers.
Docs: https://github.com/mozilla/django-csp
"""
CSP_SCRIPT_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'",)
CSP_FONT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_DEFAULT_SRC = ("'none'",)

View File

@@ -0,0 +1,19 @@
from server.settings.components import config
ACCOUNT_ACTIVATION_DAYS = 2
EMAIL_HOST = config('EMAIL_HOST')
EMAIL_PORT = config('EMAIL_PORT', cast=int)
EMAIL_USE_SSL = config('EMAIL_USE_SSL', cast=bool)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool)
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
# Is used to set sender name
# https://docs.djangoproject.com/en/1.11/ref/settings/#default-from-email
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
EMAIL_TIMEOUT = 20

View File

@@ -0,0 +1,77 @@
# Logging
# https://docs.djangoproject.com/en/2.2/topics/logging/
# See also:
# 'Do not log' by Nikita Sobolev (@sobolevn)
# https://sobolevn.me/2020/03/do-not-log
import structlog
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
# We use these formatters in our `'handlers'` configuration.
# Probably, you won't need to modify these lines.
# Unless, you know what you are doing.
'formatters': {
'json_formatter': {
'()': structlog.stdlib.ProcessorFormatter,
'processor': structlog.processors.JSONRenderer(),
},
'console': {
'()': structlog.stdlib.ProcessorFormatter,
'processor': structlog.processors.KeyValueRenderer(
key_order=['timestamp', 'level', 'event', 'logger'],
),
},
},
# You can easily swap `key/value` (default) output and `json` ones.
# Use `'json_console'` if you need `json` logs.
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
},
'json_console': {
'class': 'logging.StreamHandler',
'formatter': 'json_formatter',
},
},
# These loggers are required by our app:
# - django is required when using `logger.getLogger('django')`
# - security is required by `axes`
'loggers': {
'django': {
'handlers': ['console'],
'propagate': True,
'level': 'INFO',
},
'security': {
'handlers': ['console'],
'level': 'ERROR',
'propagate': False,
},
},
}
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.processors.TimeStamper(fmt='iso'),
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.ExceptionPrettyPrinter(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
context_class=structlog.threadlocal.wrap_dict(dict),
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)

View File

@@ -0,0 +1,2 @@
"""Overriding settings based on the environment."""

View File

@@ -0,0 +1,150 @@
"""
This file contains all the settings that defines the development server.
SECURITY WARNING: don't run with debug turned on in production!
"""
import logging
from typing import List
from server.settings.components import config
from server.settings.components.common import (
DATABASES,
INSTALLED_APPS,
MIDDLEWARE,
)
# Setting the development status:
DEBUG = True
ALLOWED_HOSTS = [
config('DOMAIN_NAME'),
'localhost',
'0.0.0.0', # noqa: S104
'127.0.0.1',
'[::1]',
]
# Installed apps for development only:
INSTALLED_APPS += (
# Better debug:
'debug_toolbar',
'nplusone.ext.django',
# Linting migrations:
'django_migration_linter',
# django-test-migrations:
'django_test_migrations.contrib.django_checks.AutoNames',
# This check might be useful in production as well,
# so it might be a good idea to move `django-test-migrations`
# to prod dependencies and use this check in the main `settings.py`.
# This will check that your database is configured properly,
# when you run `python manage.py check` before deploy.
'django_test_migrations.contrib.django_checks.DatabaseConfiguration',
# django-extra-checks:
'extra_checks',
)
# Static files:
# https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-STATICFILES_DIRS
# STATICFILES_DIRS: List[str] = []
# Django debug toolbar:
# https://django-debug-toolbar.readthedocs.io
MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware',
# https://github.com/bradmontgomery/django-querycount
# Prints how many queries were executed, useful for the APIs.
'querycount.middleware.QueryCountMiddleware',
)
def _custom_show_toolbar(request):
"""Only show the debug toolbar to users with the superuser flag."""
return DEBUG and request.user.is_superuser
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK':
'server.settings.environments.development._custom_show_toolbar',
}
# This will make debug toolbar to work with django-csp,
# since `ddt` loads some scripts from `ajax.googleapis.com`:
CSP_SCRIPT_SRC = ("'self'", 'ajax.googleapis.com')
CSP_IMG_SRC = ("'self'", 'data:')
CSP_CONNECT_SRC = ("'self'",)
# nplusone
# https://github.com/jmcarp/nplusone
# Should be the first in line:
MIDDLEWARE = ( # noqa: WPS440
'nplusone.ext.django.NPlusOneMiddleware',
) + MIDDLEWARE
# Logging N+1 requests:
NPLUSONE_RAISE = True # comment out if you want to allow N+1 requests
NPLUSONE_LOGGER = logging.getLogger('django')
NPLUSONE_LOG_LEVEL = logging.WARN
NPLUSONE_WHITELIST = [
{'model': 'admin.*'},
]
# django-test-migrations
# https://github.com/wemake-services/django-test-migrations
# Set of badly named migrations to ignore:
DTM_IGNORED_MIGRATIONS = frozenset((
('axes', '*'),
))
# django-extra-checks
# https://github.com/kalekseev/django-extra-checks
EXTRA_CHECKS = {
'checks': [
# Forbid `unique_together`:
'no-unique-together',
# Require non empty `upload_to` argument:
'field-file-upload-to',
# Use the indexes option instead:
'no-index-together',
# Each model must be registered in admin:
'model-admin',
# FileField/ImageField must have non empty `upload_to` argument:
'field-file-upload-to',
# Text fields shouldn't use `null=True`:
'field-text-null',
# Prefer using BooleanField(null=True) instead of NullBooleanField:
'field-boolean-null',
# Don't pass `null=False` to model fields (this is django default)
'field-null',
# ForeignKey fields must specify db_index explicitly if used in
# other indexes:
{'id': 'field-foreign-key-db-index', 'when': 'indexes'},
# If field nullable `(null=True)`,
# then default=None argument is redundant and should be removed:
'field-default-null',
# Fields with choices must have companion CheckConstraint
# to enforce choices on database level
'field-choices-constraint',
],
}
# Disable persistent DB connections
# https://docs.djangoproject.com/en/2.2/ref/databases/#caveats
DATABASES['default']['CONN_MAX_AGE'] = 0

View File

@@ -0,0 +1 @@
"""Override any custom settings here."""

View File

@@ -0,0 +1,75 @@
"""
This file contains all the settings used in production.
This file is required and if development.py is present these
values are overridden.
"""
from server.settings.components import config
# Production flags:
# https://docs.djangoproject.com/en/2.2/howto/deployment/
DEBUG = False
ALLOWED_HOSTS = [
# TODO: check production hosts
config('DOMAIN_NAME'),
# We need this value for `healthcheck` to work:
'localhost',
]
# Staticfiles
# https://docs.djangoproject.com/en/2.2/ref/contrib/staticfiles/
# This is a hack to allow a special flag to be used with `--dry-run`
# to test things locally.
_COLLECTSTATIC_DRYRUN = config(
'DJANGO_COLLECTSTATIC_DRYRUN', cast=bool, default=False,
)
# Adding STATIC_ROOT to collect static files via 'collectstatic':
STATIC_ROOT = '.static' if _COLLECTSTATIC_DRYRUN else '/var/www/django/static'
STATICFILES_STORAGE = (
# This is a string, not a tuple,
# but it does not fit into 80 characters rule.
'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
)
# Media files
# https://docs.djangoproject.com/en/2.2/topics/files/
MEDIA_ROOT = '/var/www/django/media'
# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
_PASS = 'django.contrib.auth.password_validation' # noqa: S105
AUTH_PASSWORD_VALIDATORS = [
{'NAME': '{0}.UserAttributeSimilarityValidator'.format(_PASS)},
{'NAME': '{0}.MinimumLengthValidator'.format(_PASS)},
{'NAME': '{0}.CommonPasswordValidator'.format(_PASS)},
{'NAME': '{0}.NumericPasswordValidator'.format(_PASS)},
]
# Security
# https://docs.djangoproject.com/en/2.2/topics/security/
SECURE_HSTS_SECONDS = 31536000 # the same as Caddy has
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SECURE_REDIRECT_EXEMPT = [
# This is required for healthcheck to work:
'^health/',
]
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

View File

@@ -0,0 +1,14 @@
# The humans responsible & technology colophon
# http://humanstxt.org/
## balsh
Team:
## Technologies
Language: English
Doctype: HTML5
Technologies: Python, Django

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -0,0 +1,72 @@
"""
Main URL mapping configuration file.
Include other URLConfs from external apps using method `include()`.
It is also a good practice to keep a single URL to the root index page.
This examples uses Django's default media
files serving technique in development.
"""
from django.conf import settings
from django.contrib import admin
from django.contrib.admindocs import urls as admindocs_urls
from django.urls import include, path
from django.views.generic import TemplateView
from health_check import urls as health_urls
from server.apps.main import urls as main_urls
from server.apps.main.views import index, github, github_result, demo_view
from django_registration.backends.one_step.views import RegistrationView
admin.autodiscover()
urlpatterns = [
# Apps:
path('main/', include(main_urls, namespace='main')),
# Other URL patterns ...
path('accounts/register/',
RegistrationView.as_view(success_url='/profile/'),
name='django_registration_register'),
path('github', github, name='github_url'),
path('github_result', github_result, name='github_result'),
path('accounts/', include('django_registration.backends.one_step.urls')),
path('accounts/', include('django.contrib.auth.urls')),
# Health checks:
path('health/', include(health_urls)), # noqa: DJ05
# django-admin:
path('admin/doc/', include(admindocs_urls)), # noqa: DJ05
path('admin/', admin.site.urls),
# Text and xml static files:
path('robots.txt', TemplateView.as_view(
template_name='txt/robots.txt',
content_type='text/plain',
)),
path('humans.txt', TemplateView.as_view(
template_name='txt/humans.txt',
content_type='text/plain',
)),
# It is a good practice to have explicit index view:
# path('', index, name='index'),
path('', demo_view, name='demo'),
path('celery-progress/', include('celery_progress.urls'))
]
if settings.DEBUG: # pragma: no cover
import debug_toolbar # noqa: WPS433
from django.conf.urls.static import static # noqa: WPS433
urlpatterns = [
# URLs specific only to django-debug-toolbar:
path('__debug__/', include(debug_toolbar.urls)), # noqa: DJ05
] + urlpatterns + static( # type: ignore
# Serving media files in development only:
settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT,
)

View File

@@ -0,0 +1,15 @@
"""
WSGI config for server 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.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = get_wsgi_application()