Compare commits
39 Commits
feature/en
...
master
Author | SHA1 | Date |
---|---|---|
vilmibm | afc5d30ed8 | |
vilmibm | 8d04787277 | |
vilmibm | be2975ad62 | |
vilmibm | 0f078f86fd | |
vilmibm | 90dca0274a | |
vilmibm | 158bdcff5f | |
vilmibm | 77d728a153 | |
vilmibm | 2385446e74 | |
vilmibm | 5220d9bfad | |
vilmibm | 4e3977edd2 | |
vilmibm | 2354695401 | |
vilmibm | de07b42082 | |
Nate Smith | 3d12a5bfd4 | |
Nate Smith | 32e640b875 | |
Nate Smith | 02a50e1d11 | |
Nate Smith | dfd843064d | |
Nate Smith | 31adb3cc3a | |
Nate Smith | e59bd893fd | |
Nate Smith | a2af7c5c96 | |
Nate Smith | 9373589c52 | |
Nate Smith | 2c71afebc2 | |
Nate Smith | e9d1fea15e | |
vilmibm | ffeb999290 | |
vilmibm | 0881982614 | |
Nate Smith | 4eb72edf42 | |
vilmibm | 3cb39206c1 | |
Nate Smith | 1757ec85c8 | |
vilmibm | 627ccf6e55 | |
vilmibm | b33c244d7b | |
vilmibm | f56e12a053 | |
vilmibm | a8917a05c3 | |
Nate Smith | 57e18fb337 | |
vilmibm | 677f5ff2ee | |
vilmibm | 92177e7a66 | |
vilmibm | d316fef6b6 | |
vilmibm | 5271fdc8f5 | |
vilmibm | 330ab81d63 | |
vilmibm | 08ff82094d | |
Nate Smith | 3c4eabd42e |
|
@ -10,7 +10,7 @@ _Being an adminstrative tool written in Django for <https://tilde.town>_.
|
||||||
* [x] Helpdesk
|
* [x] Helpdesk
|
||||||
* [x] User account management
|
* [x] User account management
|
||||||
* [ ] Start/stop services
|
* [ ] Start/stop services
|
||||||
* [ ] Cost reporting from AWS
|
* [ ] Cost reporting from DO
|
||||||
* [ ] Status monitoring
|
* [ ] Status monitoring
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
|
@ -9,4 +9,4 @@ VENV=/town/venvs/ttadmin
|
||||||
source $VENV/bin/activate
|
source $VENV/bin/activate
|
||||||
export DJANGO_SETTINGS_MODULE=settings_live
|
export DJANGO_SETTINGS_MODULE=settings_live
|
||||||
cd $APP_ROOT
|
cd $APP_ROOT
|
||||||
gunicorn -b0.0.0.0:$PORT ttadmin.wsgi
|
gunicorn -t120 -b0.0.0.0:$PORT ttadmin.wsgi
|
||||||
|
|
9
setup.py
9
setup.py
|
@ -15,12 +15,11 @@ setup(
|
||||||
'License :: OSI Approved :: Affero GNU General Public License v3 (AGPLv3)',
|
'License :: OSI Approved :: Affero GNU General Public License v3 (AGPLv3)',
|
||||||
],
|
],
|
||||||
packages=['ttadmin'],
|
packages=['ttadmin'],
|
||||||
install_requires = ['Django==1.10.2',
|
install_requires = ['Django==1.11.29',
|
||||||
'sshpubkeys==2.2.0',
|
'sshpubkeys==2.2.0',
|
||||||
'psycopg2==2.7.6.1',
|
'psycopg2-binary==2.8.5',
|
||||||
'requests==2.12.5',
|
|
||||||
'gunicorn==19.6.0',
|
'gunicorn==19.6.0',
|
||||||
'Mastodon.py==1.1.1',
|
'Mastodon.py==1.4.5',
|
||||||
'tweepy==3.5.0'],
|
'tweepy==3.7.0'],
|
||||||
include_package_data = True,
|
include_package_data = True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,15 +4,15 @@ from random import shuffle
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms import ChoiceField
|
from django.forms import ChoiceField
|
||||||
|
|
||||||
CAPTCHA_CHOICES = [('two', 'zorp borp'),
|
CAPTCHA_CHOICES = [('number', 'zorp borp'),
|
||||||
('three', 'quop bop'),
|
('hey', 'quop bop'),
|
||||||
('four', 'NO, I AM NOT A ROBOT'),
|
('never', 'NO, I AM NOT A ROBOT'),
|
||||||
('five', 'crackle zop'),
|
('eleven', 'crackle zop'),
|
||||||
('six', '*rusty screech*'),
|
('twelve', '*rusty screech*'),
|
||||||
('seven', 'mother, give me legs')]
|
('eighty', 'mother, give me legs')]
|
||||||
shuffle(CAPTCHA_CHOICES)
|
shuffle(CAPTCHA_CHOICES)
|
||||||
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
|
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
|
||||||
NOT_A_ROBOT = 'four'
|
NOT_A_ROBOT = 'never'
|
||||||
|
|
||||||
def validate_captcha(captcha):
|
def validate_captcha(captcha):
|
||||||
if captcha != NOT_A_ROBOT:
|
if captcha != NOT_A_ROBOT:
|
||||||
|
|
|
@ -1,35 +1,26 @@
|
||||||
import logging
|
import logging
|
||||||
|
from smtplib import SMTP_SSL, SMTPException
|
||||||
import requests
|
from email.message import EmailMessage
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
ADMIN_NAME='vilmibm'
|
|
||||||
EXTERNAL_FROM='root@tilde.town'
|
|
||||||
REPLY_TO='tildetown@protonmail.ch'
|
|
||||||
|
|
||||||
def send_email(to, body, subject='a message from tilde.town'):
|
def send_email(to, body, subject='a message from tilde.town'):
|
||||||
"""Sends an email using mailgun. Logs on failure."""
|
"""Sends an email using external SMTP. Logs on failure."""
|
||||||
response = requests.post(
|
em = EmailMessage()
|
||||||
settings.MAILGUN_URL,
|
em['Subject'] = subject
|
||||||
auth=('api', settings.MAILGUN_KEY),
|
em['From'] = 'root@tilde.town'
|
||||||
data={
|
em['To'] = to
|
||||||
'from': EXTERNAL_FROM,
|
em.set_content(body)
|
||||||
'h:Reply-To': REPLY_TO,
|
try:
|
||||||
'to': to,
|
with SMTP_SSL(port=settings.SMTP_PORT, host=settings.SMTP_HOST) as smtp:
|
||||||
'subject': subject,
|
smtp.login('root@tilde.town', settings.SMTP_PASSWORD)
|
||||||
'text': body
|
smtp.send_message(em)
|
||||||
}
|
smtp.quit()
|
||||||
)
|
except SMTPException as e:
|
||||||
|
logger.error(f'failed to send email "{subject}" to {to}: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
success = response.status_code == 200
|
return True
|
||||||
|
|
||||||
if not success:
|
|
||||||
logger.error('{}: failed to send email "{}" to {}'.format(
|
|
||||||
response.status_code,
|
|
||||||
subject,
|
|
||||||
to))
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>tilde.town guestbook</h1>
|
<h1>tilde.town guestbook</h1>
|
||||||
|
<p><em>don't try to post urls. it won't work.</em></p>
|
||||||
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
|
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
|
||||||
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
|
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
@ -5,6 +7,8 @@ from django.views.generic.edit import FormView
|
||||||
from .forms import GuestbookForm
|
from .forms import GuestbookForm
|
||||||
from .models import GuestbookMessage
|
from .models import GuestbookMessage
|
||||||
|
|
||||||
|
SUSPICIOUS_RE = re.compile(r'https?://')
|
||||||
|
|
||||||
|
|
||||||
class GuestbookView(FormView):
|
class GuestbookView(FormView):
|
||||||
form_class = GuestbookForm
|
form_class = GuestbookForm
|
||||||
|
@ -17,5 +21,7 @@ class GuestbookView(FormView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
del form.cleaned_data['captcha']
|
del form.cleaned_data['captcha']
|
||||||
|
if SUSPICIOUS_RE.search(form.cleaned_data['msg']) != None:
|
||||||
|
return redirect('guestbook:guestbook')
|
||||||
t = GuestbookMessage.objects.create(**form.cleaned_data)
|
t = GuestbookMessage.objects.create(**form.cleaned_data)
|
||||||
return redirect('guestbook:guestbook')
|
return redirect('guestbook:guestbook')
|
||||||
|
|
|
@ -1,9 +1,44 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Ticket
|
from django.forms import ModelForm
|
||||||
|
from .models import Ticket, Note
|
||||||
|
|
||||||
|
|
||||||
|
class ImmutableNoteInline(admin.TabularInline):
|
||||||
|
model = Note
|
||||||
|
extra = 1
|
||||||
|
max_num = 0
|
||||||
|
fields = ('author', 'created', 'body')
|
||||||
|
readonly_fields = ('author', 'created', 'body')
|
||||||
|
can_delete = False
|
||||||
|
ordering = ('created',)
|
||||||
|
|
||||||
|
|
||||||
|
class NewNoteInline(admin.StackedInline):
|
||||||
|
model = Note
|
||||||
|
extra = 0
|
||||||
|
fields = ('body',)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
queryset = super().get_queryset(request)
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Ticket)
|
@admin.register(Ticket)
|
||||||
class TicketAdmin(admin.ModelAdmin):
|
class TicketAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ('submitted',)
|
inlines = [ImmutableNoteInline, NewNoteInline]
|
||||||
list_display = ('submitted', 'issue_status', 'issue_type', 'name', 'email')
|
readonly_fields = ('submitted', 'issue_type')
|
||||||
list_filter = ('issue_status', 'issue_type')
|
list_display = ('submitted', 'issue_status', 'assigned', 'issue_type', 'name', 'email',)
|
||||||
fields = ('submitted', 'name', 'email', 'issue_status', 'issue_type', 'issue_text')
|
list_filter = ('issue_status', 'issue_type', 'assigned')
|
||||||
|
fields = ('submitted', 'name', 'email', 'assigned', 'issue_status', 'issue_type', 'issue_text')
|
||||||
|
|
||||||
|
def save_related(self, request, form, formsets, change):
|
||||||
|
# THIS IS EXTREMELY BOOTLEG AND MAY BREAK IF MORE INLINES ARE ADDED TO THIS ADMIN.
|
||||||
|
for formset in formsets:
|
||||||
|
if len(formset.forms) == 1:
|
||||||
|
# It's probably the add new note form (i hope).
|
||||||
|
note_form = formset.forms[0]
|
||||||
|
note_form.instance.author = request.user
|
||||||
|
note_form.instance.save()
|
||||||
|
note_form.save(commit=False)
|
||||||
|
note_form.save_m2m()
|
||||||
|
return super().save_related(request, form, formsets, change)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-16 02:43
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0015_remove_townie_reviewed'),
|
||||||
|
('help', '0004_ticket_submitted'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Note',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('body', models.TextField()),
|
||||||
|
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Townie')),
|
||||||
|
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='help.Ticket')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-16 02:58
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('help', '0005_note'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='note',
|
||||||
|
name='author',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-18 22:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('help', '0006_auto_20190716_0258'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='assigned',
|
||||||
|
field=models.ForeignKey(help_text='Assign this ticket to an admin or unassign it.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-18 22:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('help', '0007_ticket_assigned'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='assigned',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Assign this ticket to an admin or unassign it.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,5 @@
|
||||||
from django.db.models import Model, TextField, EmailField, CharField, DateTimeField
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Model, TextField, EmailField, CharField, DateTimeField, ForeignKey
|
||||||
|
|
||||||
ISSUE_TYPE_CHOICES = (
|
ISSUE_TYPE_CHOICES = (
|
||||||
('logging_in', 'help logging in'),
|
('logging_in', 'help logging in'),
|
||||||
|
@ -31,6 +32,17 @@ class Ticket(Model):
|
||||||
null=False,
|
null=False,
|
||||||
max_length=50,
|
max_length=50,
|
||||||
default=ISSUE_STATUS_CHOICES[0][0])
|
default=ISSUE_STATUS_CHOICES[0][0])
|
||||||
|
assigned = ForeignKey(User, blank=True, null=True, help_text="Assign this ticket to an admin or unassign it.")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{} from {}'.format(self.issue_type, self.name)
|
return '{} from {}'.format(self.issue_type, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class Note(Model):
|
||||||
|
created = DateTimeField(auto_now_add=True)
|
||||||
|
body = TextField(blank=False, null=False)
|
||||||
|
author = ForeignKey(User)
|
||||||
|
ticket = ForeignKey(Ticket)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "admin note"
|
||||||
|
|
|
@ -6,7 +6,7 @@ To run this For Real, you'll want to:
|
||||||
* set a different SECRET_KEY
|
* set a different SECRET_KEY
|
||||||
* change the password for the database or delete the password and use ident
|
* change the password for the database or delete the password and use ident
|
||||||
* change DEBUG to False
|
* change DEBUG to False
|
||||||
* set mailgun api info
|
* set smtp password
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -101,8 +101,9 @@ STATIC_URL = '/static/'
|
||||||
# Not used during local development, but used in staging+live environments
|
# Not used during local development, but used in staging+live environments
|
||||||
STATIC_ROOT = 'static'
|
STATIC_ROOT = 'static'
|
||||||
|
|
||||||
MAILGUN_URL = "OVERWRITE THIS"
|
SMTP_PORT=465
|
||||||
MAILGUN_KEY = "OVERWRITE THIS"
|
SMTP_HOST="smtp.zoho.com"
|
||||||
|
SMTP_PASSWORD="OVERWRITE THIS"
|
||||||
|
|
||||||
# Mastodon credentials
|
# Mastodon credentials
|
||||||
MASTO_CLIENT_ID = "OVERWRITE THIS"
|
MASTO_CLIENT_ID = "OVERWRITE THIS"
|
||||||
|
|
|
@ -2,8 +2,8 @@ from django.conf.urls import url, include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^help/', include('help.urls')),
|
# url(r'^help/', include('help.urls')),
|
||||||
url(r'^users/', include('users.urls')),
|
url(r'^users/', include('users.urls')),
|
||||||
url(r'^guestbook/', include('guestbook.urls')),
|
# url(r'^guestbook/', include('guestbook.urls')),
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,19 +9,27 @@ class PubkeyInline(admin.TabularInline):
|
||||||
model = Pubkey
|
model = Pubkey
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
def bulk_review(madmin, req, qs):
|
def bulk_accept(madmin, req, qs):
|
||||||
for townie in qs:
|
for townie in qs:
|
||||||
townie.reviewed = True
|
townie.state = Townie.ACCEPTED
|
||||||
townie.save()
|
townie.save()
|
||||||
post_users_to_social(qs)
|
post_users_to_social(qs)
|
||||||
|
|
||||||
bulk_review.short_description = 'mark selected townies as reviewed'
|
bulk_accept.short_description = 'mark selected townies as accepted'
|
||||||
|
|
||||||
|
def bulk_reject(madmin, req, qs):
|
||||||
|
for townie in qs:
|
||||||
|
townie.state = Townie.REJECTED
|
||||||
|
townie.save()
|
||||||
|
|
||||||
|
bulk_reject.short_description = 'mark selected townies as rejected'
|
||||||
|
|
||||||
@admin.register(Townie)
|
@admin.register(Townie)
|
||||||
class TownieAdmin(admin.ModelAdmin):
|
class TownieAdmin(admin.ModelAdmin):
|
||||||
inlines = [PubkeyInline]
|
inlines = [PubkeyInline]
|
||||||
list_display = ('username', 'reviewed', 'email')
|
list_display = ('username', 'state', 'email')
|
||||||
ordering = ('reviewed',)
|
readonly_fields = ('reasons', 'plans', 'socials')
|
||||||
|
ordering = ('state',)
|
||||||
exclude = ('first_name', 'last_name', 'password', 'groups', 'user_permissions', 'last_login', 'is_staff', 'is_active', 'is_superuser')
|
exclude = ('first_name', 'last_name', 'password', 'groups', 'user_permissions', 'last_login', 'is_staff', 'is_active', 'is_superuser')
|
||||||
actions = (bulk_review,)
|
actions = (bulk_accept, bulk_reject,)
|
||||||
search_fields = ('username', 'email', 'displayname')
|
search_fields = ('username', 'email', 'displayname')
|
||||||
|
|
|
@ -12,9 +12,9 @@ submission_throttle = {}
|
||||||
throttle_submission = throttler(submission_throttle)
|
throttle_submission = throttler(submission_throttle)
|
||||||
|
|
||||||
|
|
||||||
USERNAME_RE = re.compile(r'[a-z][a-z0-9_]+')
|
USERNAME_RE = re.compile(r'^[a-z][a-z0-9_]+$')
|
||||||
USERNAME_MIN_LENGTH = 3
|
USERNAME_MIN_LENGTH = 2
|
||||||
DISPLAY_NAME_RE = re.compile(r"[a-zA-Z0-9_\-']+")
|
DISPLAY_NAME_RE = re.compile(r"^[a-zA-Z0-9_\-']+$")
|
||||||
DISPLAY_MIN_LENGTH = 2
|
DISPLAY_MIN_LENGTH = 2
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ def validate_username(username):
|
||||||
raise ValidationError('Username too short.')
|
raise ValidationError('Username too short.')
|
||||||
if not USERNAME_RE.match(username):
|
if not USERNAME_RE.match(username):
|
||||||
raise ValidationError('Username must be all lowercase, start with a letter, and only use the _ special character')
|
raise ValidationError('Username must be all lowercase, start with a letter, and only use the _ special character')
|
||||||
duplicate = Townie.objects.filter(username=username).count()
|
duplicate = Townie.objects.filter(username=username).exclude(state=Townie.REJECTED).count()
|
||||||
if duplicate > 0:
|
if duplicate > 0:
|
||||||
raise ValidationError('Username already in use :(')
|
raise ValidationError('Username already in use :(')
|
||||||
|
|
||||||
|
@ -53,31 +53,62 @@ class TownieForm(Form):
|
||||||
validators=(validate_username,),
|
validators=(validate_username,),
|
||||||
help_text='lowercase and no spaces. underscore ok',
|
help_text='lowercase and no spaces. underscore ok',
|
||||||
label='username')
|
label='username')
|
||||||
|
|
||||||
email = EmailField(
|
email = EmailField(
|
||||||
help_text='only used to message you about your account and nothing else.',
|
help_text='only used to message you about your account and nothing else.',
|
||||||
label='e-mail')
|
label='e-mail')
|
||||||
|
|
||||||
displayname = CharField(
|
displayname = CharField(
|
||||||
validators=(validate_displayname,),
|
validators=(validate_displayname,),
|
||||||
help_text='100% optional. pseudonyms welcome.',
|
help_text='100% optional. pseudonyms welcome.',
|
||||||
label='display name',
|
label='display name',
|
||||||
required=False)
|
required=False)
|
||||||
|
|
||||||
|
referral = CharField(
|
||||||
|
required=False,
|
||||||
|
label='did a townie refer you? put their handle here.',
|
||||||
|
help_text="this is optional and just helps us when reviewing your application.")
|
||||||
|
|
||||||
reasons = CharField(
|
reasons = CharField(
|
||||||
widget=Textarea,
|
widget=Textarea,
|
||||||
required=False,
|
required=True,
|
||||||
label='what interests you about tilde.town?',
|
label='what interests you about tilde.town?',
|
||||||
help_text='This is a totally optional place for you to tell us what excites you about getting a ~ account. This is mainly just so we can all feel warm fuzzies.')
|
help_text="""
|
||||||
|
What about this intentional community intrigues you and makes you want to be a part of it?
|
||||||
|
""".strip())
|
||||||
|
|
||||||
|
plans = CharField(
|
||||||
|
widget=Textarea,
|
||||||
|
required=True,
|
||||||
|
label='what sort of things do you want to do on tilde.town?',
|
||||||
|
help_text="""
|
||||||
|
Do you want to socialize? Make something? Learn stuff?
|
||||||
|
""".strip())
|
||||||
|
|
||||||
|
socials = CharField(
|
||||||
|
widget=Textarea,
|
||||||
|
required=False,
|
||||||
|
label='where else are you online?',
|
||||||
|
help_text="""Optional, but if you're comfortable sharing with us some links to other online
|
||||||
|
spaces you're in (like twitter, mastodon, neocities, or whatever) we'd love to get to know
|
||||||
|
you when reviewing your application.
|
||||||
|
""".strip())
|
||||||
|
|
||||||
captcha = CaptchaField()
|
captcha = CaptchaField()
|
||||||
|
|
||||||
pubkey = CharField(
|
pubkey = CharField(
|
||||||
widget=Textarea,
|
widget=Textarea,
|
||||||
validators=(validate_pubkey,),
|
validators=(validate_pubkey,),
|
||||||
label='SSH public key',
|
label='SSH public key',
|
||||||
help_text='if this is not a thing you are familiar with, that\'s okay! you can make one <a href="/users/keymachine">here</a> or read <a href="https://tilde.town/wiki/getting-started/ssh.html">our guide</a> to learn more.')
|
help_text='if this is not a thing you are familiar with, that\'s okay! you can make one <a href="/users/keymachine">here</a> or read <a href="https://tilde.town/wiki/getting-started/ssh.html">our guide</a> to learn more.')
|
||||||
|
|
||||||
pubkey_type = ChoiceField(
|
pubkey_type = ChoiceField(
|
||||||
choices=SSH_TYPE_CHOICES,
|
choices=SSH_TYPE_CHOICES,
|
||||||
label='SSH public key type',
|
label='SSH public key type',
|
||||||
help_text="unless you know what you're doing you can leave this be.")
|
help_text="unless you know what you're doing you can leave this be.")
|
||||||
|
|
||||||
aup = BooleanField(
|
aup = BooleanField(
|
||||||
label='i super agree to our acceptable use policy',
|
label="i agree to the town's acceptable use policy",
|
||||||
help_text='please read our <a href="https://tilde.town/wiki/conduct.html">code of conduct</a> and click this box if you agree.')
|
help_text='please read our <a href="https://tilde.town/wiki/conduct.html">code of conduct</a> and click this box if you agree.')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-04-16 01:26
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0010_auto_20190119_1828'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='townie',
|
||||||
|
name='plans',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='townie',
|
||||||
|
name='referral',
|
||||||
|
field=models.CharField(max_length=100, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='townie',
|
||||||
|
name='socials',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-10 16:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0011_auto_20190416_0126'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='townie',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='townie',
|
||||||
|
name='referral',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-16 01:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0012_auto_20190710_1622'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='townie',
|
||||||
|
name='state',
|
||||||
|
field=models.CharField(choices=[('3_rejected', 'Rejected'), ('2_accepted', 'Accepted'), ('0_unreviewed', 'Unreviewed'), ('4_permaban', 'Permanently Banned'), ('1_tempban', 'Temporarily Banned')], default='0_unreviewed', max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='townie',
|
||||||
|
name='notes',
|
||||||
|
field=models.TextField(blank=True, help_text='Use this field to share information about this user (reviewed or not) for other admins to see', null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-16 01:51
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
def set_state(apps, _):
|
||||||
|
Townie = apps.get_model('users', 'Townie')
|
||||||
|
for townie in Townie.objects.all():
|
||||||
|
if townie.reviewed:
|
||||||
|
townie.state = '2_accepted'
|
||||||
|
|
||||||
|
townie.save()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0013_auto_20190716_0148'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_state)
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.2 on 2019-07-16 02:10
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0014_auto_20190716_0151'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='townie',
|
||||||
|
name='reviewed',
|
||||||
|
),
|
||||||
|
]
|
|
@ -10,7 +10,7 @@ from django.contrib.auth.models import User
|
||||||
from django.db.models import TextField, BooleanField, CharField, ForeignKey
|
from django.db.models import TextField, BooleanField, CharField, ForeignKey
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
|
|
||||||
from common.mailing import send_email, ADMIN_NAME
|
from common.mailing import send_email
|
||||||
from help.models import Ticket
|
from help.models import Ticket
|
||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
@ -19,6 +19,7 @@ SSH_TYPE_CHOICES = (
|
||||||
('ssh-rsa', 'ssh-rsa',),
|
('ssh-rsa', 'ssh-rsa',),
|
||||||
('ssh-dss', 'ssh-dss',),
|
('ssh-dss', 'ssh-dss',),
|
||||||
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
|
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
|
||||||
|
('ssh-ed25519', 'ssh-ed25519'),
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
|
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
|
||||||
|
@ -42,20 +43,54 @@ class Townie(User):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = 'Townie'
|
verbose_name = 'Townie'
|
||||||
verbose_name_plural = 'Townies'
|
verbose_name_plural = 'Townies'
|
||||||
|
|
||||||
|
# the actual values here have a leading int for sorting :(
|
||||||
|
UNREVIEWED = '0_unreviewed'
|
||||||
|
TEMPBAN = '1_tempban'
|
||||||
|
ACCEPTED = '2_accepted'
|
||||||
|
REJECTED = '3_rejected'
|
||||||
|
PERMABAN = '4_permaban'
|
||||||
|
STATE_CHOICES = (
|
||||||
|
(REJECTED, 'Rejected'),
|
||||||
|
(ACCEPTED, 'Accepted'),
|
||||||
|
(UNREVIEWED, 'Unreviewed'),
|
||||||
|
(PERMABAN, 'Permanently Banned'),
|
||||||
|
(TEMPBAN, 'Temporarily Banned'),
|
||||||
|
)
|
||||||
shell = CharField(max_length=50, default="/bin/bash")
|
shell = CharField(max_length=50, default="/bin/bash")
|
||||||
reviewed = BooleanField(default=False)
|
state = CharField(max_length=20, choices=STATE_CHOICES, default=UNREVIEWED)
|
||||||
reasons = TextField(blank=True, null=False, default='')
|
reasons = TextField(blank=True, null=False, default='')
|
||||||
|
plans = TextField(blank=True, null=False, default='')
|
||||||
|
socials = TextField(blank=True, null=False, default='')
|
||||||
|
referral = CharField(max_length=100, null=True, blank=True)
|
||||||
displayname = CharField(max_length=100, blank=False, null=False)
|
displayname = CharField(max_length=100, blank=False, null=False)
|
||||||
|
notes = TextField(blank=True, null=True,
|
||||||
|
help_text='Use this field to share information about this user (reviewed or not) for other admins to see')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accepted(self):
|
||||||
|
return self.ACCEPTED == self.state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unreviewed(self):
|
||||||
|
return self.UNREVIEWED == self.state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def home(self):
|
def home(self):
|
||||||
return os.path.join('/home', self.username)
|
return os.path.join('/home', self.username)
|
||||||
|
|
||||||
|
def generate_gift(self):
|
||||||
|
command = '/town/bin/generate_welcome_present.sh'
|
||||||
|
error = _guarded_run(['sudo', command, self.username])
|
||||||
|
if error:
|
||||||
|
logger.error(error)
|
||||||
|
return
|
||||||
|
|
||||||
def send_welcome_email(self):
|
def send_welcome_email(self):
|
||||||
welcome_tmpl = get_template('users/welcome_email.txt')
|
welcome_tmpl = get_template('users/welcome_email.txt')
|
||||||
context = {
|
context = {
|
||||||
'username': self.username,
|
'username': self.username,
|
||||||
'admin_name': ADMIN_NAME,
|
'admin_name': 'vilmibm',
|
||||||
}
|
}
|
||||||
text = welcome_tmpl.render(context)
|
text = welcome_tmpl.render(context)
|
||||||
success = send_email(self.email, text, subject='tilde.town!')
|
success = send_email(self.email, text, subject='tilde.town!')
|
||||||
|
@ -82,7 +117,7 @@ class Townie(User):
|
||||||
"""A VERY NOT IDEMPOTENT create function. Originally, I had ambitions
|
"""A VERY NOT IDEMPOTENT create function. Originally, I had ambitions
|
||||||
to have this be idempotent and able to incrementally update a user as
|
to have this be idempotent and able to incrementally update a user as
|
||||||
needed, but decided that was overkill for now."""
|
needed, but decided that was overkill for now."""
|
||||||
assert(self.reviewed)
|
assert(self.accepted)
|
||||||
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
||||||
|
|
||||||
error = _guarded_run(['sudo',
|
error = _guarded_run(['sudo',
|
||||||
|
@ -204,7 +239,7 @@ def on_pubkey_post_save(sender, instance, **kwargs):
|
||||||
|
|
||||||
townie = townie[0]
|
townie = townie[0]
|
||||||
|
|
||||||
if townie.reviewed:
|
if townie.accepted:
|
||||||
townie.write_authorized_keys()
|
townie.write_authorized_keys()
|
||||||
|
|
||||||
|
|
||||||
|
@ -216,12 +251,22 @@ def on_townie_pre_save(sender, instance, **kwargs):
|
||||||
|
|
||||||
existing = Townie.objects.get(id=instance.id)
|
existing = Townie.objects.get(id=instance.id)
|
||||||
|
|
||||||
# See if we need to create this user on disk.
|
# See if we need to create the user on disk.
|
||||||
if not existing.reviewed and instance.reviewed is True:
|
if existing.unreviewed and instance.accepted:
|
||||||
logger.info('Creating user {} on disk.'.format(instance.username))
|
logger.info('Creating user {} on disk.'.format(instance.username))
|
||||||
|
try:
|
||||||
instance.create_on_disk()
|
instance.create_on_disk()
|
||||||
instance.send_welcome_email()
|
|
||||||
instance.write_authorized_keys()
|
instance.write_authorized_keys()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Failed syncing user {} to disk: {}'.format(instance.username, e))
|
||||||
|
else:
|
||||||
|
instance.send_welcome_email()
|
||||||
|
instance.generate_gift()
|
||||||
|
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# This user state transition is currently undefined. In the future, we can check for things
|
||||||
|
# like bans/unbans and then take the appropriate action.
|
||||||
return
|
return
|
||||||
|
|
||||||
# See if this user needs a rename on disk
|
# See if this user needs a rename on disk
|
||||||
|
@ -248,15 +293,3 @@ def _guarded_run(cmd_args, **run_args):
|
||||||
issue_text='error while running {}: {}'.format(
|
issue_text='error while running {}: {}'.format(
|
||||||
cmd_args, e))
|
cmd_args, e))
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
# things to consider:
|
|
||||||
# * what happens when a user is marked as not reviewed?
|
|
||||||
# * does this signal user deletion? Or does literal Townie deletion signal
|
|
||||||
# "needs to be removed from disk"? I think it makes the most sense for the
|
|
||||||
# latter to imply full user deletion.
|
|
||||||
# * I honestly can't even think of a reason to revert a user to "not reviewed"
|
|
||||||
# and perhaps it's best to just not make that possible. for now, though, I
|
|
||||||
# think I can ignore it.
|
|
||||||
# * what happens when a user needs to be banned?
|
|
||||||
# * the Townie should be deleted via post_delete signal
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
|
||||||
|
|
||||||
|
{% if signups_enabled %}
|
||||||
<form class="tilde" action="{% url 'users:signup' %}" method="post">
|
<form class="tilde" action="{% url 'users:signup' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table id="signup">
|
<table id="signup">
|
||||||
|
@ -40,5 +41,11 @@
|
||||||
<input type="submit" value="sign up <3" />
|
<input type="submit" value="sign up <3" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
Town signups are currently disabled, but will resume in the future. If you can't wait for an account, you
|
||||||
|
can check out some <a href="https://tildeverse.org/">similar servers</a>.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
<li><a href="https://tilde.town/~kc/blackout/">black out</a></li>
|
<li><a href="https://tilde.town/~kc/blackout/">black out</a></li>
|
||||||
<li><a href="https://tilde.town/~subtransience/machinecode/index.html">the machine room</a></li>
|
<li><a href="https://tilde.town/~subtransience/machinecode/index.html">the machine room</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>or a <a href="https://tilde.town/cgi/random">random page</a></h2>
|
<h2>or a <a href="https://cgi.tilde.town/users/random">random page</a></h2>
|
||||||
</td>
|
</td>
|
||||||
<td style="padding:1em">
|
<td style="padding:1em">
|
||||||
<a href="http://giphy.com/gifs/cyndipop-golden-girls-bea-arthur-ToMjGpK80QLT7KLWPLO">
|
<a href="http://giphy.com/gifs/cyndipop-golden-girls-bea-arthur-ToMjGpK80QLT7KLWPLO">
|
||||||
|
|
|
@ -12,10 +12,18 @@ from django.views.generic.edit import FormView
|
||||||
from .forms import TownieForm
|
from .forms import TownieForm
|
||||||
from .models import Townie, Pubkey
|
from .models import Townie, Pubkey
|
||||||
|
|
||||||
|
SIGNUPS_ENABLED = True
|
||||||
|
|
||||||
class SignupView(FormView):
|
class SignupView(FormView):
|
||||||
form_class = TownieForm
|
form_class = TownieForm
|
||||||
template_name = 'users/signup.html'
|
template_name = 'users/signup.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
ctx['signups_enabled'] = SIGNUPS_ENABLED
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
del form.cleaned_data['captcha']
|
del form.cleaned_data['captcha']
|
||||||
|
|
Loading…
Reference in New Issue