commit
b429a9c678
197 changed files with 6226 additions and 0 deletions
@ -0,0 +1,161 @@ |
|||
### Django ### |
|||
*.log |
|||
*.pot |
|||
*.pyc |
|||
__pycache__/ |
|||
local_settings.py |
|||
db.sqlite3 |
|||
db.sqlite3-journal |
|||
media |
|||
|
|||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ |
|||
# in your Git repository. Update and uncomment the following line accordingly. |
|||
# <django-project-name>/staticfiles/ |
|||
|
|||
### Django.Python Stack ### |
|||
# Byte-compiled / optimized / DLL files |
|||
*.py[cod] |
|||
*$py.class |
|||
|
|||
# C extensions |
|||
*.so |
|||
|
|||
# Distribution / packaging |
|||
.Python |
|||
build/ |
|||
develop-eggs/ |
|||
dist/ |
|||
downloads/ |
|||
eggs/ |
|||
.eggs/ |
|||
parts/ |
|||
sdist/ |
|||
var/ |
|||
wheels/ |
|||
pip-wheel-metadata/ |
|||
share/python-wheels/ |
|||
*.egg-info/ |
|||
.installed.cfg |
|||
*.egg |
|||
MANIFEST |
|||
|
|||
# PyInstaller |
|||
# Usually these files are written by a python script from a template |
|||
# before PyInstaller builds the exe, so as to inject date/other infos into it. |
|||
*.manifest |
|||
*.spec |
|||
|
|||
# Installer logs |
|||
pip-log.txt |
|||
pip-delete-this-directory.txt |
|||
|
|||
# Unit test / coverage reports |
|||
htmlcov/ |
|||
.tox/ |
|||
.nox/ |
|||
.coverage |
|||
.coverage.* |
|||
.cache |
|||
nosetests.xml |
|||
coverage.xml |
|||
*.cover |
|||
*.py,cover |
|||
.hypothesis/ |
|||
.pytest_cache/ |
|||
pytestdebug.log |
|||
|
|||
# Translations |
|||
*.mo |
|||
|
|||
# Django stuff: |
|||
|
|||
# Flask stuff: |
|||
instance/ |
|||
.webassets-cache |
|||
|
|||
# Scrapy stuff: |
|||
.scrapy |
|||
|
|||
# Sphinx documentation |
|||
docs/_build/ |
|||
doc/_build/ |
|||
|
|||
# PyBuilder |
|||
target/ |
|||
|
|||
# Jupyter Notebook |
|||
.ipynb_checkpoints |
|||
|
|||
# IPython |
|||
profile_default/ |
|||
ipython_config.py |
|||
|
|||
# pyenv |
|||
.python-version |
|||
|
|||
# pipenv |
|||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. |
|||
# However, in case of collaboration, if having platform-specific dependencies or dependencies |
|||
# having no cross-platform support, pipenv may install dependencies that don't work, or not |
|||
# install all needed dependencies. |
|||
#Pipfile.lock |
|||
|
|||
# poetry |
|||
#poetry.lock |
|||
|
|||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow |
|||
__pypackages__/ |
|||
|
|||
# Celery stuff |
|||
celerybeat-schedule |
|||
celerybeat.pid |
|||
|
|||
# SageMath parsed files |
|||
*.sage.py |
|||
|
|||
# Environments |
|||
# .env |
|||
.env/ |
|||
.venv/ |
|||
env/ |
|||
venv/ |
|||
ENV/ |
|||
env.bak/ |
|||
venv.bak/ |
|||
pythonenv* |
|||
|
|||
# Spyder project settings |
|||
.spyderproject |
|||
.spyproject |
|||
|
|||
# Rope project settings |
|||
.ropeproject |
|||
|
|||
# mkdocs documentation |
|||
/site |
|||
|
|||
# mypy |
|||
.mypy_cache/ |
|||
.dmypy.json |
|||
dmypy.json |
|||
|
|||
# Pyre type checker |
|||
.pyre/ |
|||
|
|||
# pytype static type analyzer |
|||
.pytype/ |
|||
|
|||
# operating system-related files |
|||
# file properties cache/storage on macOS |
|||
*.DS_Store |
|||
# thumbnail cache on Windows |
|||
Thumbs.db |
|||
|
|||
# profiling data |
|||
.prof |
|||
|
|||
# pycharm |
|||
.idea/ |
|||
|
|||
# entropy specific stuff |
|||
.secret |
@ -0,0 +1,3 @@ |
|||
[submodule "src/static/forkawesome"] |
|||
path = src/static/forkawesome |
|||
url = https://github.com/ForkAwesome/Fork-Awesome.git |
@ -0,0 +1,7 @@ |
|||
celery |
|||
datedelta |
|||
django |
|||
django-polymorphic |
|||
psycopg2 |
|||
redis |
|||
requests |
@ -0,0 +1,51 @@ |
|||
from django.apps import AppConfig |
|||
from django.conf import settings |
|||
from django.urls import reverse_lazy |
|||
|
|||
|
|||
class BaseConfig(AppConfig): |
|||
name = 'base' |
|||
|
|||
def ready(self): |
|||
from base.context import nav, different_if_logged_in |
|||
|
|||
def user_settings(r): |
|||
return { |
|||
'url': reverse_lazy('user'), |
|||
'display_text': r.user.displayname, |
|||
'icon': 'user', |
|||
'prio': 9 |
|||
} |
|||
|
|||
login = { |
|||
'url': reverse_lazy('auth.login'), |
|||
'display_text': 'log in', |
|||
'icon': 'sign-in', |
|||
'prio': 9 |
|||
} |
|||
|
|||
logout = { |
|||
'url': reverse_lazy('auth.logout'), |
|||
'display_text': 'log off', |
|||
'icon': 'sign-out', |
|||
'prio': 10 |
|||
} |
|||
|
|||
def signup(r): |
|||
if settings.ENTROPY_OPEN_SIGNUPS: |
|||
return { |
|||
'url': reverse_lazy('auth.signup'), |
|||
'display_text': 'create account', |
|||
'icon': 'user-plus', |
|||
'prio': 10 |
|||
} |
|||
else: |
|||
return { |
|||
'url': reverse_lazy('contact'), |
|||
'display_text': 'want an account?', |
|||
'icon': 'user-plus', |
|||
'prio': 10, |
|||
} |
|||
|
|||
nav.register(different_if_logged_in(user_settings, login)) |
|||
nav.register(different_if_logged_in(logout, signup)) |
@ -0,0 +1,48 @@ |
|||
class _Nav: |
|||
def __init__(self): |
|||
self.entries = [] |
|||
|
|||
def register(self, entry): |
|||
self.entries.append(entry) |
|||
|
|||
def __call__(self, request): |
|||
entries = [] |
|||
for e in self.entries: |
|||
if callable(e): |
|||
if (r := e(request)) is not None: |
|||
entries.append(r) |
|||
else: |
|||
entries.append(e) |
|||
|
|||
return {'nav_entries': entries} |
|||
|
|||
|
|||
nav = _Nav() |
|||
|
|||
|
|||
def login_required_or_none(f): |
|||
""" |
|||
:param f: callable or static value |
|||
:return: wrapped function that calls the callable or returns the static value if user is logged in |
|||
""" |
|||
def wrapper(r): |
|||
if r.user.is_authenticated: |
|||
return f(r) if callable(f) else f |
|||
|
|||
return wrapper |
|||
|
|||
|
|||
def different_if_logged_in(logged_in, logged_out): |
|||
""" |
|||
:param logged_in: callable or static value |
|||
:param logged_out: callable or static value |
|||
:return: function that returns logged_in or logged_out |
|||
""" |
|||
|
|||
def wrapper(r): |
|||
if r.user.is_authenticated: |
|||
return logged_in(r) if callable(logged_in) else logged_in |
|||
else: |
|||
return logged_out(r) if callable(logged_out) else logged_out |
|||
|
|||
return wrapper |
@ -0,0 +1,18 @@ |
|||
from django import forms |
|||
from django.contrib.auth.forms import UserCreationForm |
|||
|
|||
from base.models import EntropyUser |
|||
|
|||
|
|||
class EntropyUserCreateForm(UserCreationForm): |
|||
ignore_validators = forms.BooleanField(required=False, label_suffix=' listed above:') |
|||
|
|||
class Meta: |
|||
model = EntropyUser |
|||
fields = ['username', 'password1', 'ignore_validators', 'password2'] |
|||
|
|||
def _post_clean(self): |
|||
if self.cleaned_data.get('ignore_validators'): # skips all password validation except matching pass1 and pass2 |
|||
super(UserCreationForm, self)._post_clean() |
|||
else: |
|||
super()._post_clean() |
@ -0,0 +1,56 @@ |
|||
# Generated by Django 3.2 on 2021-05-15 15:02 |
|||
|
|||
import django.contrib.auth.models |
|||
import django.contrib.auth.validators |
|||
from django.db import migrations, models |
|||
import django.db.models.deletion |
|||
import django.utils.timezone |
|||
|
|||
|
|||
class Migration(migrations.Migration): |
|||
|
|||
initial = True |
|||
|
|||
dependencies = [ |
|||
('contenttypes', '0002_remove_content_type_name'), |
|||
('auth', '0012_alter_user_first_name_max_length'), |
|||
] |
|||
|
|||
operations = [ |
|||
migrations.CreateModel( |
|||
name='Tag', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('tag', models.CharField(max_length=50)), |
|||
('object_id', models.PositiveIntegerField()), |
|||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), |
|||
], |
|||
), |
|||
migrations.CreateModel( |
|||
name='EntropyUser', |
|||
fields=[ |
|||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|||
('password', models.CharField(max_length=128, verbose_name='password')), |
|||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), |
|||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), |
|||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), |
|||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), |
|||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), |
|||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), |
|||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), |
|||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), |
|||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), |
|||
('slug', models.CharField(max_length=6, unique=True)), |
|||
('displayname', models.CharField(max_length=150)), |
|||
('upload_limit', models.IntegerField(default=20971520)), |
|||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), |
|||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), |
|||
], |
|||
options={ |
|||
'abstract': False, |
|||
}, |
|||
managers=[ |
|||
('objects', django.contrib.auth.models.UserManager()), |
|||
], |
|||
), |
|||
] |
@ -0,0 +1,4 @@ |
|||
from .users import EntropyUser |
|||
from .tags import Tag |
|||
|
|||
__all__ = ['EntropyUser', 'Tag'] |
@ -0,0 +1,13 @@ |
|||
from django.contrib.contenttypes.fields import GenericForeignKey |
|||
from django.contrib.contenttypes.models import ContentType |
|||
from django.db import models |
|||
|
|||
|
|||
class Tag(models.Model): |
|||
tag = models.CharField(max_length=50) |
|||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) |
|||
object_id = models.PositiveIntegerField() |
|||
content_object = GenericForeignKey() |
|||
|
|||
def __str__(self): |
|||
return self.tag |
@ -0,0 +1,15 @@ |
|||
from django.contrib.auth.models import AbstractUser |
|||
from django.db import models |
|||
|
|||
from base.models.utils import RandomSlugMixin |
|||
|
|||
|
|||
class EntropyUser(RandomSlugMixin, AbstractUser): |
|||
displayname = models.CharField(max_length=150) |
|||
upload_limit = models.IntegerField(default=20*1024*1024) |
|||
|
|||
def save(self, *args, **kwargs): |
|||
if not self.displayname: |
|||
self.displayname = self.get_username() |
|||
|
|||
super().save(*args, **kwargs) |
@ -0,0 +1,134 @@ |
|||
from django.contrib import messages |
|||
from django.db import models |
|||
from django.forms import ModelForm |
|||
from django.http import Http404 |
|||
from django.shortcuts import redirect |
|||
from django.utils.crypto import get_random_string |
|||
from django.views.generic import RedirectView |
|||
|
|||
base32_chars = 'abcdefghijkmnpqrstuwxyz123467890' |
|||
|
|||
|
|||
class RandomSlugMixin(models.Model): |
|||
class Meta: |
|||
abstract = True |
|||
|
|||
slug = models.CharField(max_length=6, unique=True) |
|||
|
|||
def save(self, **kwargs): |
|||
self.generate_slug() |
|||
super().save(**kwargs) |
|||
|
|||
def generate_slug(self): |
|||
if not self.slug: |
|||
self.slug = self.generate_new_slug() |
|||
|
|||
def generate_new_slug(self): |
|||
slug = get_random_string(self.slug_length, base32_chars) |
|||
if self.__class__.objects.filter(slug=slug).exists(): |
|||
return self.generate_new_slug() |
|||
return slug |
|||
|
|||
@property |
|||
def slugs_available(self): |
|||
return self.slug_count_total - self.__class__.objects.count() |
|||
|
|||
@property |
|||
def slug_count_total(self): |
|||
return len(base32_chars) ** self.slug_length |
|||
|
|||
@property |
|||
def slug_length(self): |
|||
return self.__class__._meta.get_field('slug').max_length |
|||
|
|||
|
|||
class UserSpecificMixin(models.Model): |
|||
class Meta: |
|||
abstract = True |
|||
|
|||
# i was today years old when i learned that it's possible to explicitly specify the package the model comes from |
|||
# ( 2021-04-27 ) |
|||
user = models.ForeignKey('base.EntropyUser', on_delete=models.CASCADE) |
|||
|
|||
|
|||
class UserQSMixin: |
|||
def get_queryset(self): |
|||
qs = super().get_queryset() |
|||
return qs.filter(user=self.request.user) |
|||
|
|||
|
|||
class SharedUserSpecificMixin(UserSpecificMixin): |
|||
class Meta: |
|||
abstract = True |
|||
|
|||
allowed_access_users = models.ManyToManyField('base.EntropyUser', related_name='allowed_users') |
|||
|
|||
def save(self, **kwargs): |
|||
if self.user not in self.allowed_access_users: |
|||
self.allowed_access_users.add(self.user) |
|||
super().save(**kwargs) |
|||
|
|||
|
|||
class DatesTrackedMixin(models.Model): |
|||
class Meta: |
|||
abstract = True |
|||
|
|||
creation_date = models.DateTimeField(auto_now_add=True) |
|||
last_modified = models.DateTimeField(auto_now=True) |
|||
|
|||
|
|||
class RequestFormMixin: |
|||
"""provides the request object inside the form class""" |
|||
def __init__(self, *args, **kwargs): |
|||
self.request = kwargs.pop('request') |
|||
super().__init__(*args, **kwargs) |
|||
|
|||
|
|||
class UserModelFormMixin(RequestFormMixin): |
|||
"""sets self.instance.user to the user in the request""" |
|||
def save(self, commit=True): |
|||
self.instance.user = self.request.user |
|||
return super().save(commit) |
|||
|
|||
|
|||
class UserModelForm(UserModelFormMixin, ModelForm): |
|||
... |
|||
|
|||
|
|||
class RequestFormViewMixin: |
|||
"""passes request object into the form, to be used with RequestFormMixin or UserModelFormMixin""" |
|||
def get_form_kwargs(self): |
|||
kw = super().get_form_kwargs() |
|||
kw['request'] = self.request |
|||
return kw |
|||
|
|||
|
|||
class EmptyListRedirectMixin: |
|||
allow_empty = False |
|||
plural_object_name = 'objects' |
|||
message = 'you were redirected because you have not created any {} yet' |
|||
to = None |
|||
|
|||
def get(self, request, *args, **kwargs): |
|||
try: |
|||
r = super().get(request, *args, **kwargs) |
|||
except Http404: |
|||
messages.info(request, self.get_message()) |
|||
return redirect(self.to) |
|||
return r |
|||
|
|||
def get_message(self): |
|||
if '{}' in self.message: |
|||
return self.message.format(self.plural_object_name) |
|||
else: |
|||
return self.message |
|||
|
|||
|
|||
class RedirectToGetParamView(RedirectView): |
|||
param_name = 'to' |
|||
|
|||
def get(self, request, *args, **kwargs): |
|||
to = self.request.GET.get(self.param_name, None) |
|||
if to is not None: |
|||
self.url = to |
|||
return super().get(request, *args, **kwargs) |
@ -0,0 +1,459 @@ |
|||
:root { |
|||
--global-font-size: 15px; |
|||
--global-line-height: 1.4em; |
|||
--global-space: 10px; |
|||
--font-stack: Fira Mono, Menlo, Monaco, Lucida Console, Liberation Mono, |
|||
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, |
|||
serif; |
|||
--mono-font-stack: Fira Mono, Menlo, Monaco, Lucida Console, Liberation Mono, |
|||
DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, |
|||
serif; |
|||
--page-width: 100%; |
|||
--input-style: solid; |
|||
--display-h1-decoration: none; |
|||
--background-color: #000000; |
|||
--font-color: #e8e9ed; |
|||
--invert-font-color: #000000; |
|||
--secondary-color: #a3abba; |
|||
--code-bg-color: #111118; |
|||
--primary-color: #cc0cfc; |
|||
--error-color: #ce0208; |
|||
--progress-bar-background: #3f3f44; |
|||
--progress-bar-fill: #cc0cfc; |
|||
|
|||
--warning-color: #eac204; |
|||
--success-color: #32b507; |
|||
--menu-space: 2em; |
|||
} |
|||
|
|||
|
|||
/* own shit */ |
|||
|
|||
html { |
|||
height: 100vh; |
|||
} |
|||
|
|||
body { |
|||
min-height: 100%; |
|||
display: flex; |
|||
flex-flow: column nowrap; |
|||
align-items: stretch; |
|||
} |
|||
|
|||
.main { |
|||
width: 100%; |
|||
flex-grow: 1; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: stretch; |
|||
} |
|||
|
|||
.main > aside { |
|||
width: -moz-fit-content; |
|||
width: fit-content; |
|||
resize: horizontal; |
|||
overflow: auto; |
|||
padding-right: var(--global-space); |
|||
border-right: 1px solid var(--secondary-color); |
|||
margin-right: calc(var(--global-space) * 2); |
|||
min-height: 100%; |
|||
} |
|||
|
|||
.main > main { |
|||
flex-grow: 1; |
|||
margin-top: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.entropy-top-nav { |
|||
display: flex; |
|||
flex-flow: row nowrap; |
|||
border-bottom: 2px solid var(--secondary-color); |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.entropy-top-nav ul { |
|||
flex-flow: column nowrap; |
|||
align-items: end; |
|||
} |
|||
|
|||
.entropy-top-nav .logo { |
|||
margin-right: var(--menu-space); |
|||
} |
|||
|
|||
.entropy-top-nav * { |
|||
margin-top: 0; |
|||
padding-top: 0; |
|||
margin-bottom: 0; |
|||
padding-bottom: 0; |
|||
} |
|||
|
|||
@media only screen and (min-width: 30em) { |
|||
.entropy-top-nav ul { |
|||
flex-flow: row wrap; |
|||
margin-top: 0; |
|||
} |
|||
} |
|||
|
|||
.box { |
|||
border: 1px solid var(--secondary-color); |
|||
padding: var(--global-space); |
|||
} |
|||
|
|||
.center { |
|||
margin-right: auto; |
|||
margin-left: auto; |
|||
} |
|||
|
|||
.not-too-wide { |
|||
max-width: 60em; |
|||
} |
|||
|
|||
/* message container */ |
|||
.message-container { |
|||
border: 1px solid var(--secondary-color); |
|||
padding: var(--global-space); |
|||
color: var(--font-color); |
|||
} |
|||
|
|||
.message-container > .success > i { |
|||
color: var(--success-color); |
|||
} |
|||
|
|||
.message-container > .warning > i { |
|||
color: var(--warning-color); |
|||
} |
|||
|
|||
.message-container > .error > i { |
|||
color: var(--error-color); |
|||
} |
|||
|
|||
/* plain links */ |
|||
a.plain-link { |
|||
color: var(--font-color); |
|||
} |
|||
|
|||
a.stealth-link { |
|||
color: inherit; |
|||
background-color: inherit; |
|||
} |
|||
|
|||
/* side by side style content */ |
|||
.side-by-side { |
|||
display: flex; |
|||
flex-flow: row wrap; |
|||
gap: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.side-by-side.border > * { |
|||
padding: var(--global-space); |
|||
border: 1px solid var(--secondary-color); |
|||
} |
|||
|
|||
.side-by-side.space-between { |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.side-by-side.nowrap { |
|||
flex-wrap: nowrap; |
|||
} |
|||
|
|||
.column { |
|||
display: flex; |
|||
flex-flow: column nowrap; |
|||
} |
|||
|
|||
/* pull */ |
|||
.pull-left { |
|||
float: left; |
|||
} |
|||
|
|||
.pull-right { |
|||
float: right; |
|||
} |
|||
|
|||
/* center text */ |
|||
.text-center-h { |
|||
text-align: center; |
|||
} |
|||
|
|||
/* form styling */ |
|||
form ul { |
|||
border: 1px var(--input-style) var(--font-color); |
|||
width: 100%; |
|||
padding: 0.7em 0.5em; |
|||
font-size: 1em; |
|||
font-family: var(--font-stack); |
|||
border-radius: 0; |
|||
color: var(--font-color); |
|||
background-color: var(--background-color); |
|||
margin: 0; |
|||
vertical-align: center; |
|||
} |
|||
|
|||
form ul > li { |
|||
padding-left: 0; |
|||
} |
|||
|
|||
form ul > li:after { |
|||
content: ''; |
|||
} |
|||
|
|||
/* form error list */ |
|||
form ul.errorlist { |
|||
border-color: var(--error-color); |
|||
} |
|||
|
|||
form ul.errorlist > li:before { |
|||
font: normal normal normal 14px/1 ForkAwesome; |
|||
font-size: inherit; |
|||
content: '\f06a'; |
|||
color: var(--error-color); |
|||
margin-right: var(--global-space); |
|||
} |
|||
|
|||
/* separator classes */ |
|||
.sep-bottom { |
|||
padding-bottom: var(--global-space); |
|||
border-bottom: 1px solid var(--secondary-color); |
|||
margin-bottom: var(--global-space); |
|||
} |
|||
|
|||
.sep-top { |
|||
margin-top: var(--global-space); |
|||
border-top: 1px solid var(--secondary-color); |
|||
padding-top: var(--global-space); |
|||
} |
|||
|
|||
.sep-right { |
|||
padding-right: var(--global-space); |
|||
border-right: 1px solid var(--secondary-color); |
|||
margin-right: var(--global-space); |
|||
} |
|||
|
|||
.sep-left { |
|||
margin-left: var(--global-space); |
|||
border-left: 1px solid var(--secondary-color); |
|||
padding-left: var(--global-space); |
|||
} |
|||
|
|||
.space-bottom { |
|||
margin-bottom: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.space-top { |
|||
margin-top: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.space-right { |
|||
margin-right: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.space-left { |
|||
margin-left: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.tiny-space-around { |
|||
margin: var(--global-space); |
|||
} |
|||
.tiny-space-h { |
|||
margin: var(--global-space) 0; |
|||
} |
|||
.tiny-space-v { |
|||
margin: 0 var(--global-space); |
|||
} |
|||
.tiny-space-top { |
|||
margin-top: var(--global-space); |
|||
} |
|||
.tiny-space-bottom { |
|||
margin-bottom: var(--global-space); |
|||
} |
|||
.tiny-space-left { |
|||
margin-left: var(--global-space); |
|||
} |
|||
.tiny-space-right { |
|||
margin-right: var(--global-space); |
|||
} |
|||
|
|||
.flush-bottom { |
|||
margin-bottom: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.flush-top { |
|||
margin-top: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.flush-right { |
|||
margin-right: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.flush-left { |
|||
margin-left: calc(var(--global-space) * 2); |
|||
} |
|||
|
|||
.last-p-nospace > p:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
|
|||
/* terminal.css additions and tweaks */ |
|||
|
|||
/* form select */ |
|||
/* this was so fucked up that i gave up after trying for 3 hours. |
|||
make note of any further time you waste below here: |
|||
|
|||
*/ |
|||
select { |
|||
border: 1px var(--input-style) var(--font-color); |
|||
width: 100%; |
|||
padding: 0.7em 0.5em; |
|||
font-size: 1em; |
|||
font-family: var(--font-stack); |
|||
border-radius: 0; |
|||
color: var(--font-color); |
|||
background-color: var(--background-color); |
|||
} |
|||
|
|||
select:not(:placeholder-shown):invalid { |
|||
border-color: var(--error-color); |
|||
} |
|||
|
|||
select[multiple] option:before { |
|||
content: '[ ]'; |
|||
} |
|||
|
|||
select[multiple] option[selected]:before { |
|||
content: '[x]'; |
|||
} |
|||
|
|||
.terminal-timeline > .terminal-card:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
|
|||
/* fix form fields */ |
|||
input[type="url"], |
|||
input[type="file"] { |
|||
border: 1px var(--input-style) var(--font-color); |
|||
width: 100%; |
|||
padding: 0.7em 0.5em; |
|||
font-size: 1em; |
|||
font-family: var(--font-stack); |
|||
-webkit-appearance: none; |
|||
border-radius: 0; |
|||
} |
|||
|
|||
input[type="url"]:focus, |
|||
input[type="file"]:focus { |
|||
outline: none; |
|||
-webkit-appearance: none; |
|||
border: 1px solid var(--font-color); |
|||
} |
|||
|
|||
input[type="url"]:not(:placeholder-shown):invalid, |
|||
input[type="file"]:not(:placeholder-shown):invalid { |
|||
border-color: var(--error-color); |
|||
} |
|||
|
|||
/* fix spacings when reordering */ |
|||
.terminal-menu ul { |
|||
column-gap: var(--menu-space); |
|||
} |
|||
|
|||
.terminal-menu li { |
|||
margin: 0; |
|||
} |
|||
|
|||
/* fix weird tables, move to extra class */ |
|||
table tbody td:first-child { |
|||
font-weight: inherit; |
|||
color: inherit; |
|||
} |
|||
|
|||
table.first-col-special tbody td:first-child { |
|||
font-weight: 700; |
|||
color: var(--secondary-color); |
|||
} |
|||
|
|||
/* color text classes */ |
|||
.color-primary { |
|||
color: var(--primary-color); |
|||
} |
|||
|
|||
.color-font { |
|||
color: var(--font-color); |
|||
} |
|||
|
|||
.color-secondary { |
|||
color: var(--secondary-color); |
|||
} |
|||
|
|||
.color-warning { |
|||
color: var(--warning-color); |
|||
} |
|||
|
|||
.color-error { |
|||
color: var(--error-color); |
|||
} |
|||
|
|||
.color-success { |
|||
color: var(--success-color); |
|||
} |
|||
|
|||
/* add success classes */ |
|||
.terminal-alert-success { |
|||
color: var(--success-color); |
|||
border-color: var(--success-color); |
|||
} |
|||
|
|||
.btn-success { |
|||
color: var(--invert-font-color); |
|||
background-color: var(--success-color); |
|||
border: 1px solid var(--success-color); |
|||
} |
|||
|
|||
.btn-success:hover, |
|||
.btn-success:focus:not(.btn-ghost) { |
|||
background-color: var(--success-color); |
|||
border-color: var(--success-color); |
|||
} |
|||
|
|||
.btn-success.btn-ghost { |
|||
border-color: var(--success-color); |
|||
color: var(--success-color); |
|||
} |
|||
|
|||
.btn-success.btn-ghost:focus, |
|||
.btn-success.btn-ghost:hover { |
|||
border-color: var(--success-color); |
|||
color: var(--success-color); |
|||
z-index: 2; |
|||
} |
|||
|
|||
/* add warning classes */ |
|||
.terminal-alert-warning { |
|||
color: var(--warning-color); |
|||
border-color: var(--warning-color); |
|||
} |
|||
|
|||
.btn-warning { |
|||
color: var(--invert-font-color); |
|||
background-color: var(--warning-color); |
|||
border: 1px solid var(--warning-color); |
|||
} |
|||
|
|||
.btn-warning:hover, |
|||
.btn-warning:focus:not(.btn-ghost) { |
|||
background-color: var(--warning-color); |
|||
border-color: var(--warning-color); |
|||
} |
|||
|
|||
.btn-warning.btn-ghost { |
|||
border-color: var(--warning-color); |
|||
color: var(--warning-color); |
|||
} |
|||
|
|||
.btn-warning.btn-ghost:focus, |
|||
.btn-warning.btn-ghost:hover { |
|||
border-color: var(--warning-color); |
|||
color: var(--warning-color); |
|||
z-index: 2; |
|||
} |
@ -0,0 +1,8 @@ |
|||
from django.core.mail import send_mail |
|||
|
|||
from entropy.celery import app |
|||
|
|||
|
|||
@app.task |
|||
def mail(subject, body, sender, recipients): |
|||
send_mail(subject, body, sender, recipients) |
@ -0,0 +1,63 @@ |
|||
{% load static %} |
|||
{% load utils %} |
|||
|
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1"> |
|||
<title>{% block title %}Entropy{% endblock %}</title> |
|||
<link rel="stylesheet" href="{% static 'css/normalize.css' %}"> |
|||
<link rel="stylesheet" href="{% static 'forkawesome/css/fork-awesome.css' %}"> |
|||
<link rel="stylesheet" href="{% static 'css/terminal.css' %}"> |
|||
<link rel="stylesheet" href="{% static 'entropybase/base.css' %}"> |
|||
|
|||
{% block meta %}{% endblock %} |
|||
{% block css %}{% endblock %} |
|||
</head> |
|||
<body class="terminal"> |
|||
<div class="terminal-nav entropy-top-nav"> |
|||
<div class="terminal-logo"> |
|||
<div class="logo"> |
|||
<a href="{% url 'home' %}">Entropy</a> |
|||
</div> |
|||
</div> |
|||
<nav class="terminal-menu"> |
|||
<ul> |
|||
{% for nav_entry in nav_entries %} |
|||
<li{% if nav_entry.prio %} style="order: {{ nav_entry.prio }};"{% endif %}> |
|||
{% if nav_entry.url %} |
|||
<a href="{{ nav_entry.url }}">{% if nav_entry.icon %}<i class="fa fa-{{ nav_entry.icon }}"></i> |
|||
{% endif %} {{ nav_entry.display_text }}</a> |
|||
{% else %} |
|||
{% if nav_entry.icon %} |
|||
<i class="fa fa-{{ nav_entry.icon }}"></i> |
|||
{% endif %} {{ nav_entry.display_text }} |
|||
{% endif %} |
|||
</li> |
|||
{% endfor %} |
|||
</ul> |
|||
</nav> |
|||
</div> |
|||
<div class="container main"> |
|||
{% block main %} |
|||
<aside> |
|||
{% block sidebar %}{% endblock %} |
|||
</aside> |
|||
<main> |
|||
{% if messages %} |
|||
<div class="message-container"> |
|||
{% for m in messages %} |
|||
<div class="{{ m.tags }}"> |
|||
<header>{% message_icon m %} {{ m }}</header> |
|||
</div> |
|||
{% endfor %} |
|||
</div> |
|||
{% endif %} |
|||
{% block content %}{% endblock %} |
|||
</main> |
|||
{% endblock %} |
|||
</div> |
|||
{% block js %}{% endblock %} |
|||
</body> |
|||
</html> |
@ -0,0 +1,17 @@ |
|||
{% extends 'entropybase/base.html' %} |
|||
{% load utils %} |
|||
|
|||
{% block main %} |
|||
<main> |
|||
{% if messages %} |
|||
<div class="message-container"> |
|||
{% for m in messages %} |
|||
<div class="{{ m.tags }}"> |
|||
<header>{% message_icon m %} {{ m }}</header> |
|||
</div> |
|||
{% endfor %} |
|||
</div> |
|||
{% endif %} |
|||
{% block content %}{% endblock %} |
|||
</main> |
|||
{% endblock %} |
@ -0,0 +1,22 @@ |
|||
{% load static %} |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width,initial-scale=1"> |
|||
<title>{% block title %}Entropy{% endblock %}</title> |
|||
<link rel="stylesheet" href="{% static 'css/normalize.css' %}"> |
|||
<link rel="stylesheet" href="{% static 'forkawesome/css/fork-awesome.css' %}"> |
|||
<link rel="stylesheet" href="{% static 'css/terminal.css' %}"> |
|||
<link rel="stylesheet" href="{% static 'entropybase/base.css' %}"> |
|||
|
|||
{% block meta %}{% endblock %} |
|||
{% block css %}{% endblock %} |
|||
</head> |
|||
<body> |
|||
<div class="container"> |
|||
{% block content %}{% endblock %} |
|||
</div> |
|||
{% block js %}{% endblock %} |
|||
</body> |
|||
</html> |
@ -0,0 +1,19 @@ |
|||
{% extends 'entropybase/base_nosidebar.html' %} |
|||
{% load utils %} |
|||
|
|||
{% block main %} |
|||
<main class="center not-too-wide"> |
|||
{% if messages %} |
|||
<div class="message-container"> |
|||
{% for m in messages %} |
|||
<div class="{{ m.tags }}"> |
|||
<header>{% message_icon m %} {{ m }}</header> |
|||
</div> |
|||
{% endfor %} |
|||
</div> |
|||
{% endif %} |
|||
<div class="box space-top"> |
|||
{{ contactinfo|safe }} |
|||
</div> |
|||
</main> |
|||
{% endblock %} |
@ -0,0 +1,10 @@ |
|||
{% extends 'entropybase/base_plain.html' %} |
|||
|
|||
{% block content %} |
|||
<form method="post"> |
|||
{% csrf_token %} |
|||
{{ form.as_p }} |
|||
<input type="submit" value="Log in" class="btn btn-success"> |
|||
<p class="sep-top">the signup form is <a href="{% url 'auth.signup' %}">over here</a></p> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,10 @@ |
|||
{% extends 'entropybase/base_plain.html' %} |
|||
|
|||
{% block content %} |
|||
<form method="post"> |
|||
{% csrf_token %} |
|||
{{ form.as_p }} |
|||
<input type="submit" value="Sign up" class="btn btn-success"> |
|||
<p class="sep-top">the login form is <a href="{% url 'auth.login' %}">over here</a></p> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,9 @@ |
|||
{% extends 'entropybase/base.html' %} |
|||
|
|||
{% block content %} |
|||
<form method="post"> |
|||
{% csrf_token %} |
|||
<p>are you sure you want to delete your entire user profile, {{ object.username }}?</p> |
|||
<input type="submit" value="Delete it"> |
|||
</form> |
|||
{% endblock %} |
@ -0,0 +1,10 @@ |
|||
{% extends 'entropybase/base.html' %} |
|||
|
|||
{% block content %} |
|||
<div> |
|||
<p> |
|||
user: {{ object.username }} <br> |
|||
display: {{ object.displayname }} |
|||
</p> |
|||
</div> |
|||
{% endblock %} |
@ -0,0 +1,8 @@ |
|||
<form method="post"> |
|||
{% csrf_token %} |
|||
{% if heading %} |
|||
<p class="sep-bottom">{{ heading }}</p> |
|||
{% endif %} |
|||
{{ form.as_p }} |
|||
<input type="submit" value="{{ button_text }}" class="btn btn-{{ button_class }}"> |
|||
</form> |
@ -0,0 +1,36 @@ |
|||
from django import template |
|||
from django.utils.safestring import mark_safe |
|||
|
|||
register = template.Library() |
|||
|
|||
|
|||
@register.simple_tag |
|||
def urlparams(*args, **kwargs): |
|||
kwargs.update({k: None for k in args}) |
|||
if kwargs: |
|||
# urlparse casts None to string, which is not the behavior wanted here; |
|||
# keys without vaue are supported in the url spec |
|||
return '?' + '&'.join([k + ('=' + str(v) if v is not None else '') for k, v in kwargs.items()]) |
|||
|
|||
|
|||
@register.simple_tag |
|||
def message_icon(message): |
|||
icon_codes = { |
|||
10: 'code', |
|||
20: 'info-circle', |
|||
25: 'check-circle', |
|||
30: 'exclamation-triangle', |
|||
40: 'exclamation-circle' |
|||
} |
|||
|
|||
return mark_safe(f'<i class="fa fa-{icon_codes[message.level]}"></i>') |
|||
|
|||
|
|||
@register.inclusion_tag('entropybase/utils/basic_form.html') |
|||
def basic_form(form, heading=None, button_text='submit', button_class='success'): |
|||
return { |
|||
'form': form, |
|||
'heading': heading, |
|||
'button_text': button_text, |
|||
'button_class': button_class, |
|||
} |
@ -0,0 +1,3 @@ |
|||
from django.test import TestCase |
|||
|
|||
# Create your tests here. |
@ -0,0 +1,14 @@ |
|||
from django.urls import path |
|||
|
|||
from .views import ( |
|||
EntropyLoginView, HomeView, EntropyLogoutView, EntropySignupView, EntropyUserDetailView, EntropyAdminContactView |
|||
) |
|||
|
|||
urlpatterns = [ |
|||
path('', HomeView.as_view(), name='home'), |
|||
path('auth/login', EntropyLoginView.as_view(), name='auth.login'), |
|||
path('auth/logout', EntropyLogoutView.as_view(), name='auth.logout'), |
|||
path('auth/signup', EntropySignupView.as_view(), name='auth.signup'), |
|||
path('contact', EntropyAdminContactView.as_view(), name='contact'), |
|||
path('user', EntropyUserDetailView.as_view(), name='user'), |
|||
] |
@ -0,0 +1,76 @@ |
|||
from django.conf import settings |
|||
from django.contrib import messages |
|||
from django.contrib.auth import logout, login |
|||
from django.contrib.auth.mixins import LoginRequiredMixin |
|||
from django.contrib.auth.views import LoginView, LogoutView |
|||
from django.core.exceptions import PermissionDenied |
|||
from django.shortcuts import redirect |
|||
from django.urls import reverse_lazy |
|||
from django.views.generic import TemplateView, CreateView, DetailView, DeleteView |
|||
|
|||
from base.forms import EntropyUserCreateForm |
|||
from base.models import EntropyUser |
|||
|
|||
|
|||
class EntropyLoginView(LoginView): |
|||
template_name = 'entropybase/login.html' |
|||
|
|||
|
|||
class EntropyLogoutView(LogoutView): |
|||
next_page = reverse_lazy('home') |
|||
|
|||
|
|||
class HomeView(TemplateView): |
|||
template_name = 'entropybase/base_nosidebar.html' |
|||
|
|||
|
|||
class EntropySignupView(CreateView): |
|||
model = EntropyUser |
|||
form_class = EntropyUserCreateForm |
|||
template_name = 'entropybase/user/create.html' |
|||
success_url = reverse_lazy('home') |
|||
|
|||
def get(self, request, *args, **kwargs): |
|||
if settings.ENTROPY_OPEN_SIGNUPS: # this check makes it impossible to get a csrf token -> secure |
|||
return super().get(request, *args, **kwargs) |
|||
else: |
|||
return redirect(reverse_lazy('contact')) |
|||
|
|||
def form_valid(self, form): |
|||
r = super().form_valid(form) |
|||
login(self.request, self.object) |
|||
return r |
|||
|
|||
|
|||
class UserDeleteView(LoginRequiredMixin, DeleteView): |
|||
model = EntropyUser |
|||
template_name = 'entropybase/user/delete.html' |
|||
success_url = reverse_lazy('home') |
|||
|
|||
def post(self, request, *args, **kwargs): |
|||
self.object.active = False |
|||
self.object.save() |
|||
logout(request) |
|||
return redirect(self.success_url) |
|||
|
|||
|
|||
class EntropyUserDetailView(LoginRequiredMixin, DetailView): |
|||
model = EntropyUser |
|||
template_name = 'entropybase/user/detail.html' |
|||
|
|||
def get_object(self, queryset=None): |
|||
return self.request.user |
|||
|
|||
|
|||
class EntropyAdminContactView(TemplateView): |
|||
template_name = 'entropybase/contact.html' |
|||
|
|||
def get(self, request, *args, **kwargs): |
|||
messages.info(request, |
|||
'this instance does not have open registrations; the admin has provided the following info:') |
|||
return super().get(request, *args, **kwargs) |
|||
|
|||
def get_context_data(self, **kwargs): |
|||
ctx = super().get_context_data(**kwargs) |
|||
ctx.setdefault('contactinfo', settings.ENTROPY_ADMIN_CONTACT_STRING.strip()) |
|||
return ctx |
@ -0,0 +1,3 @@ |
|||
from .celery import app as celery_app |
|||
|
|||
__all__ = ('celery_app',) |
@ -0,0 +1,16 @@ |
|||
""" |
|||
ASGI config for entropy project. |
|||
|
|||
It exposes the ASGI callable as a module-level variable named ``application``. |
|||
|
|||
For more information on this file, see |
|||
https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ |
|||
""" |
|||
|
|||
import os |
|||
|
|||
from django.core.asgi import get_asgi_application |
|||
|
|||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'entropy.settings') |
|||
|
|||
application = get_asgi_application() |
@ -0,0 +1,29 @@ |
|||
import os |
|||
|
|||
from celery import Celery |
|||
|
|||
# set the default Django settings module for the 'celery' program. |
|||
from django.conf import settings |
|||
|
|||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'entropy.settings') |
|||
|
|||
app = Celery('entropy') |
|||
|
|||
|
|||
# Using a string here means the worker doesn't have to serialize |
|||
# the configuration object to child processes. |
|||
# - namespace='CELERY' means all celery-related configuration keys |
|||
# should have a `CELERY_` prefix. |
|||
app.config_from_object('django.conf:settings', namespace='CELERY') |
|||
|
|||
# Load task modules from all registered Django app configs. |
|||
app.autodiscover_tasks() |
|||
|
|||
|
|||
app.conf.beat_schedule = { |
|||
'send_notifs': { |
|||
'task': 'notifs.send_notifs', |
|||
'schedule': settings.ENTROPY_NOTIF_INTERVAL, |
|||
}, |
|||
} |
|||
app.conf.timezone = 'UTC' |
@ -0,0 +1,194 @@ |
|||
""" |
|||
Django settings for entropy project. |
|||
|
|||
Generated by 'django-admin startproject' using Django 3.1.7. |
|||
|
|||
For more information on this file, see |
|||
https://docs.djangoproject.com/en/3.1/topics/settings/ |
|||
|
|||
For the full list of settings and their values, see |
|||
https://docs.djangoproject.com/en/3.1/ref/settings/ |
|||
""" |
|||
from os.path import isfile |
|||
from pathlib import Path |
|||
|
|||
# Build paths inside the project like this: BASE_DIR / 'subdir'. |
|||
from django.urls import reverse, reverse_lazy |
|||
|
|||
BASE_DIR = Path(__file__).resolve().parent.parent |
|||
|
|||
# not production ready. |
|||
|
|||
# SECURITY WARNING: keep the secret key used in production secret! |
|||
|
|||
|
|||
def load_or_generate_secret(): |
|||
if isfile(BASE_DIR / '.secret'): |
|||
with open(BASE_DIR / '.secret') as f: |
|||
return f.read() |
|||
else: |
|||
from django.core.management.utils import get_random_secret_key |
|||
with open(BASE_DIR / '.secret', 'w') as f: |
|||
secret = get_random_secret_key() |
|||
f.write(secret) |
|||
return secret |
|||
|
|||
|
|||
SECRET_KEY = load_or_generate_secret() |
|||
|
|||
|
|||
# SECURITY WARNING: don't run with debug turned on in production! |
|||
DEBUG = True |
|||
|
|||
ALLOWED_HOSTS = ['*'] |
|||
|
|||
|
|||
# Application definition |
|||
|
|||
INSTALLED_APPS = [ |
|||
'django.contrib.auth', |
|||
'django.contrib.contenttypes', |
|||
'django.contrib.sessions', |
|||
'django.contrib.messages', |
|||
'django.contrib.staticfiles', |
|||
'base', |
|||
'files', |
|||
'goals', |
|||
# 'knowledge', # maybe some day... prioritizing other stuff for now. |
|||
'people', |
|||
'timelines', |
|||
'shortlinks', |
|||
'notifs', |
|||
] |
|||
|
|||
MIDDLEWARE = [ |
|||
'django.middleware.security.SecurityMiddleware', |
|||
'django.contrib.sessions.middleware.SessionMiddleware', |
|||
'django.middleware.common.CommonMiddleware', |
|||
'django.middleware.csrf.CsrfViewMiddleware', |
|||
'django.contrib.auth.middleware.AuthenticationMiddleware', |
|||
'django.contrib.messages.middleware.MessageMiddleware', |
|||
'django.middleware.clickjacking.XFrameOptionsMiddleware', |
|||
] |
|||
|
|||
ROOT_URLCONF = 'entropy.urls' |
|||
|
|||
TEMPLATES = [ |
|||
{ |
|||
'BACKEND': 'django.template.backends.django.DjangoTemplates', |
|||
'DIRS': [BASE_DIR / 'templates'] |
|||
, |
|||
'APP_DIRS': True, |
|||
'OPTIONS': { |
|||
'context_processors': [ |
|||
'django.template.context_processors.debug', |
|||
'django.template.context_processors.request', |
|||
'django.contrib.auth.context_processors.auth', |
|||
'django.contrib.messages.context_processors.messages', |
|||
'base.context.nav' |
|||
], |
|||
}, |
|||
}, |
|||
] |
|||
|
|||
WSGI_APPLICATION = 'entropy.wsgi.application' |
|||
|
|||
|
|||
# Database |
|||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases |
|||
|
|||
# note: the notifs package uses postgresql's ArrayField. all databases except postgres are unsupported |
|||
|
|||
DATABASES = { |
|||
# 'default': { |
|||
# 'ENGINE': 'django.db.backends.sqlite3', |
|||
# 'NAME': BASE_DIR / 'db.sqlite3', |
|||
# }, |
|||
'default': { |
|||
'ENGINE': 'django.db.backends.postgresql', |
|||
'HOST': 'localhost', |
|||
'PORT': '5432', |
|||
'USER': 'entropy', |
|||
'NAME': 'entropy', |
|||
} |
|||
} |
|||
|
|||
|
|||
# Password validation |
|||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators |
|||
|
|||
AUTH_PASSWORD_VALIDATORS = [ |
|||
{ |
|||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', |
|||
}, |
|||
{ |
|||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', |
|||
}, |
|||
{ |
|||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', |
|||
}, |
|||
{ |
|||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', |
|||
}, |
|||
] |
|||
|
|||
AUTH_USER_MODEL = 'base.EntropyUser' |
|||
|
|||
|
|||
# Internationalization |
|||
# https://docs.djangoproject.com/en/3.1/topics/i18n/ |
|||
|
|||
LANGUAGE_CODE = 'en-us' |
|||
|
|||
TIME_ZONE = 'UTC' |
|||
|
|||
USE_I18N = False # todo (maybe): internationalization |
|||
|
|||
USE_L10N = False # enabling this will cause local formats to be used instead of the formats defined below |
|||
|
|||
USE_TZ = True |
|||
|
|||
|
|||
# Static files (CSS, JavaScript, Images) |
|||
# https://docs.djangoproject.com/en/3.1/howto/static-files/ |
|||
|
|||
STATIC_URL = '/static/' |
|||
STATICFILES_DIRS = [ |
|||
BASE_DIR / 'static' |
|||
] |
|||
|
|||
# heckin django 3.2+ new default |
|||
|
|||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' |
|||
|
|||
# heckin login redirect make login view go brr |
|||
|
|||
LOGIN_REDIRECT_URL = reverse_lazy('home') |
|||
LOGIN_URL = reverse_lazy('auth.login') |
|||
|
|||
# heckin media root so that file uploads work |
|||
|
|||
MEDIA_ROOT = BASE_DIR / 'media' |
|||
|
|||
# heckin date/time formats so that no shitty american formatting |
|||
|
|||
DATE_FORMAT = 'Y N jS' |
|||
TIME_FORMAT = 'H:i' |
|||
DATETIME_FORMAT = f'{DATE_FORMAT}, {TIME_FORMAT}' |
|||
SHORT_DATE_FORMAT = 'Y-m-d' |
|||
SHORT_DATETIME_FORMAT = f'{SHORT_DATE_FORMAT}\\TH:i e' |
|||
|
|||
# heckin entropy specific settings |
|||
|
|||
ENTROPY_OPEN_SIGNUPS = True |
|||
|
|||
# can be any html |
|||
# users will look at this if they want an account |
|||
ENTROPY_ADMIN_CONTACT_STRING = """ |
|||