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] User account management
|
||||
* [ ] Start/stop services
|
||||
* [ ] Cost reporting from AWS
|
||||
* [ ] Cost reporting from DO
|
||||
* [ ] Status monitoring
|
||||
|
||||
## Requirements
|
||||
|
|
|
@ -9,4 +9,4 @@ VENV=/town/venvs/ttadmin
|
|||
source $VENV/bin/activate
|
||||
export DJANGO_SETTINGS_MODULE=settings_live
|
||||
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)',
|
||||
],
|
||||
packages=['ttadmin'],
|
||||
install_requires = ['Django==1.10.2',
|
||||
install_requires = ['Django==1.11.29',
|
||||
'sshpubkeys==2.2.0',
|
||||
'psycopg2==2.7.6.1',
|
||||
'requests==2.12.5',
|
||||
'psycopg2-binary==2.8.5',
|
||||
'gunicorn==19.6.0',
|
||||
'Mastodon.py==1.1.1',
|
||||
'tweepy==3.5.0'],
|
||||
'Mastodon.py==1.4.5',
|
||||
'tweepy==3.7.0'],
|
||||
include_package_data = True,
|
||||
)
|
||||
|
|
|
@ -4,15 +4,15 @@ from random import shuffle
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.forms import ChoiceField
|
||||
|
||||
CAPTCHA_CHOICES = [('two', 'zorp borp'),
|
||||
('three', 'quop bop'),
|
||||
('four', 'NO, I AM NOT A ROBOT'),
|
||||
('five', 'crackle zop'),
|
||||
('six', '*rusty screech*'),
|
||||
('seven', 'mother, give me legs')]
|
||||
CAPTCHA_CHOICES = [('number', 'zorp borp'),
|
||||
('hey', 'quop bop'),
|
||||
('never', 'NO, I AM NOT A ROBOT'),
|
||||
('eleven', 'crackle zop'),
|
||||
('twelve', '*rusty screech*'),
|
||||
('eighty', 'mother, give me legs')]
|
||||
shuffle(CAPTCHA_CHOICES)
|
||||
CAPTCHA_CHOICES.insert(0, ('one', 'beep boop'),)
|
||||
NOT_A_ROBOT = 'four'
|
||||
NOT_A_ROBOT = 'never'
|
||||
|
||||
def validate_captcha(captcha):
|
||||
if captcha != NOT_A_ROBOT:
|
||||
|
|
|
@ -1,35 +1,26 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
from smtplib import SMTP_SSL, SMTPException
|
||||
from email.message import EmailMessage
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
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'):
|
||||
"""Sends an email using mailgun. Logs on failure."""
|
||||
response = requests.post(
|
||||
settings.MAILGUN_URL,
|
||||
auth=('api', settings.MAILGUN_KEY),
|
||||
data={
|
||||
'from': EXTERNAL_FROM,
|
||||
'h:Reply-To': REPLY_TO,
|
||||
'to': to,
|
||||
'subject': subject,
|
||||
'text': body
|
||||
}
|
||||
)
|
||||
"""Sends an email using external SMTP. Logs on failure."""
|
||||
em = EmailMessage()
|
||||
em['Subject'] = subject
|
||||
em['From'] = 'root@tilde.town'
|
||||
em['To'] = to
|
||||
em.set_content(body)
|
||||
try:
|
||||
with SMTP_SSL(port=settings.SMTP_PORT, host=settings.SMTP_HOST) as smtp:
|
||||
smtp.login('root@tilde.town', settings.SMTP_PASSWORD)
|
||||
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
|
||||
|
||||
if not success:
|
||||
logger.error('{}: failed to send email "{}" to {}'.format(
|
||||
response.status_code,
|
||||
subject,
|
||||
to))
|
||||
|
||||
return success
|
||||
return True
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>tilde.town guestbook</h1>
|
||||
<p><em>don't try to post urls. it won't work.</em></p>
|
||||
<marquee>~*~*~*~*say hello*~*~*~*~</marquee>
|
||||
<form class="tilde" action="{% url 'guestbook:guestbook' %}" method="post">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import re
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.edit import FormView
|
||||
|
@ -5,6 +7,8 @@ from django.views.generic.edit import FormView
|
|||
from .forms import GuestbookForm
|
||||
from .models import GuestbookMessage
|
||||
|
||||
SUSPICIOUS_RE = re.compile(r'https?://')
|
||||
|
||||
|
||||
class GuestbookView(FormView):
|
||||
form_class = GuestbookForm
|
||||
|
@ -17,5 +21,7 @@ class GuestbookView(FormView):
|
|||
|
||||
def form_valid(self, form):
|
||||
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)
|
||||
return redirect('guestbook:guestbook')
|
||||
|
|
|
@ -1,9 +1,44 @@
|
|||
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)
|
||||
class TicketAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('submitted',)
|
||||
list_display = ('submitted', 'issue_status', 'issue_type', 'name', 'email')
|
||||
list_filter = ('issue_status', 'issue_type')
|
||||
fields = ('submitted', 'name', 'email', 'issue_status', 'issue_type', 'issue_text')
|
||||
inlines = [ImmutableNoteInline, NewNoteInline]
|
||||
readonly_fields = ('submitted', 'issue_type')
|
||||
list_display = ('submitted', 'issue_status', 'assigned', 'issue_type', 'name', 'email',)
|
||||
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 = (
|
||||
('logging_in', 'help logging in'),
|
||||
|
@ -31,6 +32,17 @@ class Ticket(Model):
|
|||
null=False,
|
||||
max_length=50,
|
||||
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):
|
||||
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
|
||||
* change the password for the database or delete the password and use ident
|
||||
* change DEBUG to False
|
||||
* set mailgun api info
|
||||
* set smtp password
|
||||
"""
|
||||
import os
|
||||
|
||||
|
@ -101,8 +101,9 @@ STATIC_URL = '/static/'
|
|||
# Not used during local development, but used in staging+live environments
|
||||
STATIC_ROOT = 'static'
|
||||
|
||||
MAILGUN_URL = "OVERWRITE THIS"
|
||||
MAILGUN_KEY = "OVERWRITE THIS"
|
||||
SMTP_PORT=465
|
||||
SMTP_HOST="smtp.zoho.com"
|
||||
SMTP_PASSWORD="OVERWRITE THIS"
|
||||
|
||||
# Mastodon credentials
|
||||
MASTO_CLIENT_ID = "OVERWRITE THIS"
|
||||
|
|
|
@ -2,8 +2,8 @@ from django.conf.urls import url, include
|
|||
from django.contrib import admin
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^help/', include('help.urls')),
|
||||
# url(r'^help/', include('help.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),
|
||||
]
|
||||
|
|
|
@ -9,19 +9,27 @@ class PubkeyInline(admin.TabularInline):
|
|||
model = Pubkey
|
||||
extra = 1
|
||||
|
||||
def bulk_review(madmin, req, qs):
|
||||
def bulk_accept(madmin, req, qs):
|
||||
for townie in qs:
|
||||
townie.reviewed = True
|
||||
townie.state = Townie.ACCEPTED
|
||||
townie.save()
|
||||
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)
|
||||
class TownieAdmin(admin.ModelAdmin):
|
||||
inlines = [PubkeyInline]
|
||||
list_display = ('username', 'reviewed', 'email')
|
||||
ordering = ('reviewed',)
|
||||
list_display = ('username', 'state', 'email')
|
||||
readonly_fields = ('reasons', 'plans', 'socials')
|
||||
ordering = ('state',)
|
||||
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')
|
||||
|
|
|
@ -12,9 +12,9 @@ submission_throttle = {}
|
|||
throttle_submission = throttler(submission_throttle)
|
||||
|
||||
|
||||
USERNAME_RE = re.compile(r'[a-z][a-z0-9_]+')
|
||||
USERNAME_MIN_LENGTH = 3
|
||||
DISPLAY_NAME_RE = re.compile(r"[a-zA-Z0-9_\-']+")
|
||||
USERNAME_RE = re.compile(r'^[a-z][a-z0-9_]+$')
|
||||
USERNAME_MIN_LENGTH = 2
|
||||
DISPLAY_NAME_RE = re.compile(r"^[a-zA-Z0-9_\-']+$")
|
||||
DISPLAY_MIN_LENGTH = 2
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ def validate_username(username):
|
|||
raise ValidationError('Username too short.')
|
||||
if not USERNAME_RE.match(username):
|
||||
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:
|
||||
raise ValidationError('Username already in use :(')
|
||||
|
||||
|
@ -53,31 +53,62 @@ class TownieForm(Form):
|
|||
validators=(validate_username,),
|
||||
help_text='lowercase and no spaces. underscore ok',
|
||||
label='username')
|
||||
|
||||
email = EmailField(
|
||||
help_text='only used to message you about your account and nothing else.',
|
||||
label='e-mail')
|
||||
|
||||
displayname = CharField(
|
||||
validators=(validate_displayname,),
|
||||
help_text='100% optional. pseudonyms welcome.',
|
||||
label='display name',
|
||||
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(
|
||||
widget=Textarea,
|
||||
required=False,
|
||||
required=True,
|
||||
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()
|
||||
|
||||
pubkey = CharField(
|
||||
widget=Textarea,
|
||||
validators=(validate_pubkey,),
|
||||
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.')
|
||||
|
||||
pubkey_type = ChoiceField(
|
||||
choices=SSH_TYPE_CHOICES,
|
||||
label='SSH public key type',
|
||||
help_text="unless you know what you're doing you can leave this be.")
|
||||
|
||||
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.')
|
||||
|
||||
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.template.loader import get_template
|
||||
|
||||
from common.mailing import send_email, ADMIN_NAME
|
||||
from common.mailing import send_email
|
||||
from help.models import Ticket
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
@ -19,6 +19,7 @@ SSH_TYPE_CHOICES = (
|
|||
('ssh-rsa', 'ssh-rsa',),
|
||||
('ssh-dss', 'ssh-dss',),
|
||||
('ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256'),
|
||||
('ssh-ed25519', 'ssh-ed25519'),
|
||||
)
|
||||
|
||||
DEFAULT_INDEX_PATH = '/etc/skel/public_html/index.html'
|
||||
|
@ -42,20 +43,54 @@ class Townie(User):
|
|||
class Meta:
|
||||
verbose_name = 'Townie'
|
||||
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")
|
||||
reviewed = BooleanField(default=False)
|
||||
state = CharField(max_length=20, choices=STATE_CHOICES, default=UNREVIEWED)
|
||||
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)
|
||||
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
|
||||
def home(self):
|
||||
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):
|
||||
welcome_tmpl = get_template('users/welcome_email.txt')
|
||||
context = {
|
||||
'username': self.username,
|
||||
'admin_name': ADMIN_NAME,
|
||||
'admin_name': 'vilmibm',
|
||||
}
|
||||
text = welcome_tmpl.render(context)
|
||||
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
|
||||
to have this be idempotent and able to incrementally update a user as
|
||||
needed, but decided that was overkill for now."""
|
||||
assert(self.reviewed)
|
||||
assert(self.accepted)
|
||||
dot_ssh_path = '/home/{}/.ssh'.format(self.username)
|
||||
|
||||
error = _guarded_run(['sudo',
|
||||
|
@ -204,7 +239,7 @@ def on_pubkey_post_save(sender, instance, **kwargs):
|
|||
|
||||
townie = townie[0]
|
||||
|
||||
if townie.reviewed:
|
||||
if townie.accepted:
|
||||
townie.write_authorized_keys()
|
||||
|
||||
|
||||
|
@ -216,12 +251,22 @@ def on_townie_pre_save(sender, instance, **kwargs):
|
|||
|
||||
existing = Townie.objects.get(id=instance.id)
|
||||
|
||||
# See if we need to create this user on disk.
|
||||
if not existing.reviewed and instance.reviewed is True:
|
||||
# See if we need to create the user on disk.
|
||||
if existing.unreviewed and instance.accepted:
|
||||
logger.info('Creating user {} on disk.'.format(instance.username))
|
||||
instance.create_on_disk()
|
||||
instance.send_welcome_email()
|
||||
instance.write_authorized_keys()
|
||||
try:
|
||||
instance.create_on_disk()
|
||||
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
|
||||
|
||||
# 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(
|
||||
cmd_args, 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 signups_enabled %}
|
||||
<form class="tilde" action="{% url 'users:signup' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<table id="signup">
|
||||
|
@ -40,5 +41,11 @@
|
|||
<input type="submit" value="sign up <3" />
|
||||
</div>
|
||||
</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>
|
||||
</html>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<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>
|
||||
</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 style="padding:1em">
|
||||
<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 .models import Townie, Pubkey
|
||||
|
||||
SIGNUPS_ENABLED = True
|
||||
|
||||
class SignupView(FormView):
|
||||
form_class = TownieForm
|
||||
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
|
||||
def form_valid(self, form):
|
||||
del form.cleaned_data['captcha']
|
||||
|
|
Loading…
Reference in New Issue