Browse Source

initial commit

pull/4/head
Julia Luna 1 year ago
commit
b429a9c678
Signed by: julia GPG Key ID: 6A0C04FA9A7D7582
  1. 161
      .gitignore
  2. 3
      .gitmodules
  3. 7
      requirements.txt
  4. 0
      src/base/__init__.py
  5. 51
      src/base/apps.py
  6. 48
      src/base/context.py
  7. 18
      src/base/forms.py
  8. 56
      src/base/migrations/0001_initial.py
  9. 0
      src/base/migrations/__init__.py
  10. 4
      src/base/models/__init__.py
  11. 13
      src/base/models/tags.py
  12. 15
      src/base/models/users.py
  13. 134
      src/base/models/utils.py
  14. 459
      src/base/static/entropybase/base.css
  15. 8
      src/base/tasks.py
  16. 63
      src/base/templates/entropybase/base.html
  17. 17
      src/base/templates/entropybase/base_nosidebar.html
  18. 22
      src/base/templates/entropybase/base_plain.html
  19. 19
      src/base/templates/entropybase/contact.html
  20. 10
      src/base/templates/entropybase/login.html
  21. 10
      src/base/templates/entropybase/user/create.html
  22. 9
      src/base/templates/entropybase/user/delete.html
  23. 10
      src/base/templates/entropybase/user/detail.html
  24. 8
      src/base/templates/entropybase/utils/basic_form.html
  25. 0
      src/base/templatetags/__init__.py
  26. 36
      src/base/templatetags/utils.py
  27. 3
      src/base/tests.py
  28. 14
      src/base/urls.py
  29. 76
      src/base/views.py
  30. 3
      src/entropy/__init__.py
  31. 16
      src/entropy/asgi.py
  32. 29
      src/entropy/celery.py
  33. 194
      src/entropy/settings.py
  34. 0
      src/entropy/tasks.py
  35. 31
      src/entropy/urls.py
  36. 16
      src/entropy/wsgi.py
  37. 0
      src/files/__init__.py
  38. 17
      src/files/apps.py
  39. 7
      src/files/dev_urls.py
  40. 14
      src/files/forms.py
  41. 31
      src/files/migrations/0001_initial.py
  42. 0
      src/files/migrations/__init__.py
  43. 18
      src/files/models.py
  44. 17
      src/files/templates/files/base.html
  45. 9
      src/files/templates/files/delete.html
  46. 8
      src/files/templates/files/file_card.html
  47. 10
      src/files/templates/files/list.html
  48. 9
      src/files/templates/files/update.html
  49. 9
      src/files/templates/files/upload.html
  50. 0
      src/files/templatetags/__init__.py
  51. 20
      src/files/templatetags/files.py
  52. 3
      src/files/tests.py
  53. 10
      src/files/urls.py
  54. 56
      src/files/views.py
  55. 0
      src/goals/__init__.py
  56. 17
      src/goals/apps.py
  57. 54
      src/goals/forms.py
  58. 33
      src/goals/migrations/0001_initial.py
  59. 0
      src/goals/migrations/__init__.py
  60. 18
      src/goals/models.py
  61. 77
      src/goals/static/goals/goals.css
  62. 24
      src/goals/templates/goals/base.html
  63. 9
      src/goals/templates/goals/create.html
  64. 33
      src/goals/templates/goals/debug_list.html
  65. 9
      src/goals/templates/goals/delete.html
  66. 10
      src/goals/templates/goals/overview.html
  67. 58
      src/goals/templates/goals/tree.html
  68. 29
      src/goals/templates/goals/update.html
  69. 0
      src/goals/templatetags/__init__.py
  70. 28
      src/goals/templatetags/goals.py
  71. 3
      src/goals/tests.py
  72. 14
      src/goals/urls.py
  73. 64
      src/goals/views.py
  74. 3
      src/gotify/__init__.py
  75. 115
      src/gotify/gotify.py
  76. 0
      src/knowledge/__init__.py
  77. 16
      src/knowledge/apps.py
  78. 8
      src/knowledge/forms.py
  79. 0
      src/knowledge/migrations/__init__.py
  80. 20
      src/knowledge/models.py
  81. 0
      src/knowledge/templates/knowledge/tree.html
  82. 0
      src/knowledge/templatetags/__init__.py
  83. 8
      src/knowledge/templatetags/knowledge.py
  84. 3
      src/knowledge/tests.py
  85. 7
      src/knowledge/urls.py
  86. 6
      src/knowledge/views.py
  87. 22
      src/manage.py
  88. 0
      src/notifs/__init__.py
  89. 3
      src/notifs/admin.py
  90. 16
      src/notifs/apps.py
  91. 0
      src/notifs/forms/__init__.py
  92. 33
      src/notifs/forms/base.py
  93. 8
      src/notifs/forms/email.py
  94. 8
      src/notifs/forms/gotify.py
  95. 0
      src/notifs/management/__init__.py
  96. 0
      src/notifs/management/commands/__init__.py
  97. 8
      src/notifs/management/commands/send_notifs.py
  98. 102
      src/notifs/migrations/0001_initial.py
  99. 0
      src/notifs/migrations/__init__.py
  100. 0
      src/notifs/models/__init__.py

161
.gitignore

@ -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

3
.gitmodules

@ -0,0 +1,3 @@
[submodule "src/static/forkawesome"]
path = src/static/forkawesome
url = https://github.com/ForkAwesome/Fork-Awesome.git

7
requirements.txt

@ -0,0 +1,7 @@
celery
datedelta
django
django-polymorphic
psycopg2
redis
requests

0
src/base/__init__.py

51
src/base/apps.py

@ -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))

48
src/base/context.py

@ -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

18
src/base/forms.py

@ -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()

56
src/base/migrations/0001_initial.py

@ -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
src/base/migrations/__init__.py

4
src/base/models/__init__.py

@ -0,0 +1,4 @@
from .users import EntropyUser
from .tags import Tag
__all__ = ['EntropyUser', 'Tag']

13
src/base/models/tags.py

@ -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

15
src/base/models/users.py

@ -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)

134
src/base/models/utils.py

@ -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)

459
src/base/static/entropybase/base.css

@ -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;
}

8
src/base/tasks.py

@ -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)

63
src/base/templates/entropybase/base.html

@ -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>

17
src/base/templates/entropybase/base_nosidebar.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 %}

22
src/base/templates/entropybase/base_plain.html

@ -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>

19
src/base/templates/entropybase/contact.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 %}

10
src/base/templates/entropybase/login.html

@ -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 %}

10
src/base/templates/entropybase/user/create.html

@ -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 %}

9
src/base/templates/entropybase/user/delete.html

@ -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 %}

10
src/base/templates/entropybase/user/detail.html

@ -0,0 +1,10 @@
{% extends 'entropybase/base.html' %}
{% block content %}
<div>
<p>
user: {{ object.username }} <br>
display: {{ object.displayname }}
</p>
</div>
{% endblock %}

8
src/base/templates/entropybase/utils/basic_form.html

@ -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
src/base/templatetags/__init__.py

36
src/base/templatetags/utils.py

@ -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,
}

3
src/base/tests.py

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

14
src/base/urls.py

@ -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'),
]

76
src/base/views.py

@ -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

3
src/entropy/__init__.py

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

16
src/entropy/asgi.py

@ -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()

29
src/entropy/celery.py

@ -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'

194
src/entropy/settings.py

@ -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 = """