mirror of
https://github.com/Balshgit/public.git
synced 2026-02-04 10:00:39 +03:00
initial commit
This commit is contained in:
0
github-stars/server/__init__.py
Normal file
0
github-stars/server/__init__.py
Normal file
0
github-stars/server/apps/__init__.py
Normal file
0
github-stars/server/apps/__init__.py
Normal file
0
github-stars/server/apps/main/__init__.py
Normal file
0
github-stars/server/apps/main/__init__.py
Normal file
8
github-stars/server/apps/main/admin.py
Normal file
8
github-stars/server/apps/main/admin.py
Normal 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."""
|
||||
28
github-stars/server/apps/main/celery_config.py
Normal file
28
github-stars/server/apps/main/celery_config.py
Normal 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()
|
||||
102
github-stars/server/apps/main/commands.py
Normal file
102
github-stars/server/apps/main/commands.py
Normal 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'
|
||||
8
github-stars/server/apps/main/forms.py
Normal file
8
github-stars/server/apps/main/forms.py
Normal 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',)
|
||||
11
github-stars/server/apps/main/logic/__init__.py
Normal file
11
github-stars/server/apps/main/logic/__init__.py
Normal 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!
|
||||
"""
|
||||
@@ -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')
|
||||
35
github-stars/server/apps/main/migrations/0001_initial.py
Normal file
35
github-stars/server/apps/main/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
33
github-stars/server/apps/main/models.py
Normal file
33
github-stars/server/apps/main/models.py
Normal 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]
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
29
github-stars/server/apps/main/static/main/css/index.css
Normal file
29
github-stars/server/apps/main/static/main/css/index.css
Normal 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 |
31
github-stars/server/apps/main/templates/base.html
Normal file
31
github-stars/server/apps/main/templates/base.html
Normal 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>
|
||||
@@ -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 %}
|
||||
40
github-stars/server/apps/main/templates/download.html
Normal file
40
github-stars/server/apps/main/templates/download.html
Normal 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 %}
|
||||
25
github-stars/server/apps/main/templates/main/github.html
Normal file
25
github-stars/server/apps/main/templates/main/github.html
Normal 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>
|
||||
@@ -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>
|
||||
46
github-stars/server/apps/main/templates/main/index.html
Normal file
46
github-stars/server/apps/main/templates/main/index.html
Normal file
File diff suppressed because one or more lines are too long
26
github-stars/server/apps/main/templates/progress.html
Normal file
26
github-stars/server/apps/main/templates/progress.html
Normal 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"> </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 %}
|
||||
|
||||
|
||||
14
github-stars/server/apps/main/templates/txt/humans.txt
Normal file
14
github-stars/server/apps/main/templates/txt/humans.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
# The humans responsible & technology colophon
|
||||
# http://humanstxt.org/
|
||||
|
||||
|
||||
## balsh
|
||||
|
||||
Team:
|
||||
|
||||
|
||||
## Technologies
|
||||
|
||||
Language: English
|
||||
Doctype: HTML5
|
||||
Technologies: Python, Django
|
||||
2
github-stars/server/apps/main/templates/txt/robots.txt
Normal file
2
github-stars/server/apps/main/templates/txt/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
9
github-stars/server/apps/main/urls.py
Normal file
9
github-stars/server/apps/main/urls.py
Normal 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'),
|
||||
]
|
||||
107
github-stars/server/apps/main/views.py
Normal file
107
github-stars/server/apps/main/views.py
Normal 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': {}})
|
||||
40
github-stars/server/settings/__init__.py
Normal file
40
github-stars/server/settings/__init__.py
Normal 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)
|
||||
12
github-stars/server/settings/components/__init__.py
Normal file
12
github-stars/server/settings/components/__init__.py
Normal 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'))
|
||||
16
github-stars/server/settings/components/caches.py
Normal file
16
github-stars/server/settings/components/caches.py
Normal 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'
|
||||
211
github-stars/server/settings/components/common.py
Normal file
211
github-stars/server/settings/components/common.py
Normal 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
|
||||
15
github-stars/server/settings/components/csp.py
Normal file
15
github-stars/server/settings/components/csp.py
Normal 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'",)
|
||||
19
github-stars/server/settings/components/email.py
Normal file
19
github-stars/server/settings/components/email.py
Normal 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
|
||||
77
github-stars/server/settings/components/logging.py
Normal file
77
github-stars/server/settings/components/logging.py
Normal 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,
|
||||
)
|
||||
2
github-stars/server/settings/environments/__init__.py
Normal file
2
github-stars/server/settings/environments/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
"""Overriding settings based on the environment."""
|
||||
150
github-stars/server/settings/environments/development.py
Normal file
150
github-stars/server/settings/environments/development.py
Normal 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
|
||||
@@ -0,0 +1 @@
|
||||
"""Override any custom settings here."""
|
||||
75
github-stars/server/settings/environments/production.py
Normal file
75
github-stars/server/settings/environments/production.py
Normal 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
|
||||
14
github-stars/server/templates/txt/humans.txt
Normal file
14
github-stars/server/templates/txt/humans.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
# The humans responsible & technology colophon
|
||||
# http://humanstxt.org/
|
||||
|
||||
|
||||
## balsh
|
||||
|
||||
Team:
|
||||
|
||||
|
||||
## Technologies
|
||||
|
||||
Language: English
|
||||
Doctype: HTML5
|
||||
Technologies: Python, Django
|
||||
2
github-stars/server/templates/txt/robots.txt
Normal file
2
github-stars/server/templates/txt/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
72
github-stars/server/urls.py
Normal file
72
github-stars/server/urls.py
Normal 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,
|
||||
)
|
||||
15
github-stars/server/wsgi.py
Normal file
15
github-stars/server/wsgi.py
Normal 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()
|
||||
Reference in New Issue
Block a user