commit
3d12a5bfd4
|
@ -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"
|
||||||
|
|
|
@ -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 = '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 = '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')
|
||||||
|
|
|
@ -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 :(')
|
||||||
|
|
||||||
|
|
|
@ -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 = Townie.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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -42,13 +42,37 @@ 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='')
|
plans = TextField(blank=True, null=False, default='')
|
||||||
socials = TextField(blank=True, null=False, default='')
|
socials = TextField(blank=True, null=False, default='')
|
||||||
referral = CharField(max_length=100, null=True, blank=True)
|
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):
|
||||||
|
@ -85,7 +109,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',
|
||||||
|
@ -207,7 +231,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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -219,13 +243,17 @@ 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))
|
||||||
instance.create_on_disk()
|
instance.create_on_disk()
|
||||||
instance.send_welcome_email()
|
instance.send_welcome_email()
|
||||||
instance.write_authorized_keys()
|
instance.write_authorized_keys()
|
||||||
return
|
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
|
# See if this user needs a rename on disk
|
||||||
logger.info('checking for rename {} vs {}'.format(
|
logger.info('checking for rename {} vs {}'.format(
|
||||||
|
@ -251,15 +279,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
|
|
||||||
|
|
Loading…
Reference in New Issue