Merge pull request #182 from cthulahoops/site_settings

Add instance settings.
This commit is contained in:
Mouse Reeve 2020-06-16 17:35:49 -07:00 committed by GitHub
commit a0cc88d316
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 327 additions and 4 deletions

View file

@ -1,5 +1,8 @@
''' usin django model forms ''' ''' usin django model forms '''
from django.forms import ModelForm, PasswordInput import datetime
from django.core.exceptions import ValidationError
from django.forms import ModelForm, PasswordInput, widgets
from django import forms from django import forms
from fedireads import models from fedireads import models
@ -21,7 +24,7 @@ class RegisterForm(ModelForm):
fields = ['username', 'email', 'password'] fields = ['username', 'email', 'password']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
widgets = { widgets = {
'password': PasswordInput(), 'password': PasswordInput()
} }
@ -115,3 +118,35 @@ class EditionForm(ModelForm):
class ImportForm(forms.Form): class ImportForm(forms.Form):
csv_file = forms.FileField() csv_file = forms.FileField()
class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name):
selected_string = super().value_from_datadict(data, files, name)
if selected_string == 'day':
interval = datetime.timedelta(days=1)
elif selected_string == 'week':
interval = datetime.timedelta(days=7)
elif selected_string == 'month':
interval = datetime.timedelta(days=31) # Close enough?
elif selected_string == 'forever':
return None
else:
return selected_string # "This will raise
return datetime.datetime.now() + interval
class CreateInviteForm(ModelForm):
class Meta:
model = models.SiteInvite
exclude = ['code', 'user', 'times_used']
widgets = {
'expiry': ExpiryWidget(choices=[
('day', 'One Day'),
('week', 'One Week'),
('month', 'One Month'),
('forever', 'Does Not Expire')]),
'use_limit': widgets.Select(
choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]]
+ [(None, 'Unlimited')])
}

View file

@ -0,0 +1,23 @@
# Generated by Django 3.0.3 on 2020-06-01 18:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0041_user_remote_id'),
]
operations = [
migrations.CreateModel(
name='SiteSettings',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='wyrms.cthulahoops.org', max_length=100)),
('instance_description', models.TextField(default='This instance has no description.')),
('code_of_conduct', models.TextField(default='Add a code of conduct here.')),
('allow_registration', models.BooleanField(default=True)),
],
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.0.3 on 2020-06-01 21:31
from django.db import migrations, models
import fedireads.models.site
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0042_sitesettings'),
]
operations = [
migrations.CreateModel(
name='SiteInvite',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(default=fedireads.models.site.new_invite_code, max_length=32)),
('expiry', models.DateTimeField(blank=True, null=True)),
('use_limit', models.IntegerField(blank=True, null=True)),
('times_used', models.IntegerField(default=0)),
],
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.0.3 on 2020-06-02 15:46
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0043_siteinvite'),
]
operations = [
migrations.AddField(
model_name='siteinvite',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View file

@ -6,3 +6,4 @@ from .status import Favorite, Boost, Tag, Notification, ReadThrough
from .user import User, UserFollows, UserFollowRequest, UserBlocks from .user import User, UserFollows, UserFollowRequest, UserBlocks
from .user import FederatedServer from .user import FederatedServer
from .import_job import ImportJob, ImportItem from .import_job import ImportJob, ImportItem
from .site import SiteSettings, SiteInvite

44
fedireads/models/site.py Normal file
View file

@ -0,0 +1,44 @@
import base64
from Crypto import Random
from django.db import models
from django.utils import timezone
from fedireads.settings import DOMAIN
from .user import User
class SiteSettings(models.Model):
name = models.CharField(default=DOMAIN, max_length=100)
instance_description = models.TextField(
default="This instance has no description.")
code_of_conduct = models.TextField(
default="Add a code of conduct here.")
allow_registration = models.BooleanField(default=True)
@classmethod
def get(cls):
try:
return cls.objects.get(id=1)
except cls.DoesNotExist:
default_settings = SiteSettings(id=1)
default_settings.save()
return default_settings
def new_invite_code():
return base64.b32encode(Random.get_random_bytes(5)).decode('ascii')
class SiteInvite(models.Model):
code = models.CharField(max_length=32, default=new_invite_code)
expiry = models.DateTimeField(blank=True, null=True)
use_limit = models.IntegerField(blank=True, null=True)
times_used = models.IntegerField(default=0)
user = models.ForeignKey(User, on_delete=models.CASCADE)
def valid(self):
return (
(self.expiry is None or self.expiry > timezone.now()) and
(self.use_limit is None or self.times_used < self.use_limit))
@property
def link(self):
return "https://{}/invite/{}".format(DOMAIN, self.code)

View file

@ -0,0 +1,21 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>About {{ site_settings.name }}</h2>
<p>
{{ site_settings.instance_description }}
</p>
<p>
<small>
<a href="/login/">Login or Create an Account</a>
</small>
</p>
<h2>Code of Conduct</h2>
<p>
{{ site_settings.code_of_conduct }}
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,33 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>About {{ site_settings.name }}</h2>
<p>
{{ site_settings.instance_description }}
</p>
<p>
<small>
<a href="/about/">More about this site</a>
</small>
</p>
</div>
<div class="content-container login">
<h2>Create an Account</h2>
<p><small>
With a BookWyrm account, you can track and share your reading activity with
friends here and on any other federated server, like Mastodon and PixelFed.
</small></p>
<div>
<form name="register" method="post" action="/register">
{% csrf_token %}
{{ register_form.as_p }}
<input type=hidden name="invite_code" value="{{ invite.code }}">
<button type="submit">Create account</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -62,7 +62,8 @@
<ul class="pulldown"> <ul class="pulldown">
<li><a href="/user/{{ request.user }}">Your profile</a></li> <li><a href="/user/{{ request.user }}">Your profile</a></li>
<li><a href="/user-edit/">Settings</a></li> <li><a href="/user-edit/">Settings</a></li>
<li><a href="/import">Import Books</a><li> <li><a href="/import">Import Books</a></li>
<li><a href="/manage_invites/">Invites</a></li>
<li><a href="/logout/">Log out</a></li> <li><a href="/logout/">Log out</a></li>
</ul> </ul>
</p> </p>

View file

@ -1,7 +1,20 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="row">
<div class="content-container">
<h2>About {{ site_settings.name }}</h2>
<p>
{{ site_settings.instance_description }}
</p>
<p>
<small>
<a href="/about/">More about this site</a>
</small>
</p>
</div>
<div class="row">
<div class="content-container login"> <div class="content-container login">
<h2>Create an Account</h2> <h2>Create an Account</h2>
<p><small> <p><small>
@ -9,6 +22,7 @@
friends here and on any other federated server, like Mastodon and PixelFed. friends here and on any other federated server, like Mastodon and PixelFed.
</small></p> </small></p>
{% if site_settings.allow_registration %}
<div> <div>
<form name="register" method="post" action="/register"> <form name="register" method="post" action="/register">
{% csrf_token %} {% csrf_token %}
@ -16,6 +30,12 @@
<button type="submit">Create account</button> <button type="submit">Create account</button>
</form> </form>
</div> </div>
{% else %}
<small>
This instance is not open for registration.
</small>
{% endif %}
</div> </div>
<div class="content-container login"> <div class="content-container login">

View file

@ -0,0 +1,32 @@
{% extends 'layout.html' %}
{% load humanize %}
{% block content %}
<div class="content-container">
<div class="manage-invites">
<h2>Invites</h2>
<table>
<tr>
<th>Link</th>
<th>Expires</th>
<th>Max uses</th>
<th>Times used</th>
</tr>
{% for invite in invites %}
<tr>
<td><a href="{{ invite.link }}">{{ invite.link }}</td>
<td>{{ invite.expiry|naturaltime }}</td>
<td>{{ invite.use_limit }}</td>
<td>{{ invite.times_used }}</td>
</tr>
{% endfor %}
</table>
<h2>Generate New Invite</h2>
<form name="avatar" action="/create_invite/" method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Create Invite</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -34,6 +34,9 @@ urlpatterns = [
# ui views # ui views
re_path(r'^login/?$', views.login_page), re_path(r'^login/?$', views.login_page),
re_path(r'^about/?$', views.about_page),
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.invite_page),
re_path(r'^manage_invites/?$', views.manage_invites),
path('', views.home), path('', views.home),
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab), re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
@ -102,4 +105,6 @@ urlpatterns = [
re_path(r'^clear-notifications/?$', actions.clear_notifications), re_path(r'^clear-notifications/?$', actions.clear_notifications),
re_path(r'^create_invite/?$', actions.create_invite),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -8,6 +8,7 @@ from django.core.files.base import ContentFile
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.core.exceptions import PermissionDenied
from fedireads import books_manager from fedireads import books_manager
from fedireads import forms, models, outgoing from fedireads import forms, models, outgoing
@ -49,6 +50,19 @@ def register(request):
if request.method == 'GET': if request.method == 'GET':
return redirect('/login') return redirect('/login')
if not models.SiteSettings.get().allow_registration:
invite_code = request.POST.get('invite_code')
if not invite_code:
raise PermissionDenied
try:
invite = models.SiteInvite.objects.get(code=invite_code)
except models.SiteInvite.DoesNotExist:
raise PermissionDenied
else:
invite = None
form = forms.RegisterForm(request.POST) form = forms.RegisterForm(request.POST)
if not form.is_valid(): if not form.is_valid():
return redirect('/register/') return redirect('/register/')
@ -58,6 +72,10 @@ def register(request):
password = form.data['password'] password = form.data['password']
user = models.User.objects.create_user(username, email, password) user = models.User.objects.create_user(username, email, password)
if invite:
invite.times_used += 1
invite.save()
login(request, user) login(request, user)
return redirect('/') return redirect('/')
@ -413,3 +431,15 @@ def import_data(request):
goodreads_import.start_import(job) goodreads_import.start_import(job)
return redirect('/import_status/%d' % (job.id,)) return redirect('/import_status/%d' % (job.id,))
return HttpResponseBadRequest() return HttpResponseBadRequest()
@login_required
def create_invite(request):
form = forms.CreateInviteForm(request.POST)
if not form.is_valid():
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
invite = form.save(commit=False)
invite.user = request.user
invite.save()
return redirect('/manage_invites')

View file

@ -208,12 +208,45 @@ def login_page(request):
''' authentication ''' ''' authentication '''
# send user to the login page # send user to the login page
data = { data = {
'site_settings': models.SiteSettings.get(),
'login_form': forms.LoginForm(), 'login_form': forms.LoginForm(),
'register_form': forms.RegisterForm(), 'register_form': forms.RegisterForm(),
} }
return TemplateResponse(request, 'login.html', data) return TemplateResponse(request, 'login.html', data)
def about_page(request):
''' more information about the instance '''
data = {
'site_settings': models.SiteSettings.get(),
}
return TemplateResponse(request, 'about.html', data)
def invite_page(request, code):
''' Handle invites. '''
try:
invite = models.SiteInvite.objects.get(code=code)
if not invite.valid():
raise PermissionDenied
except models.SiteInvite.DoesNotExist:
raise PermissionDenied
data = {
'site_settings': models.SiteSettings.get(),
'register_form': forms.RegisterForm(),
'invite': invite,
}
return TemplateResponse(request, 'invite.html', data)
@login_required
def manage_invites(request):
data = {
'invites': models.SiteInvite.objects.filter(user=request.user),
'form': forms.CreateInviteForm(),
}
return TemplateResponse(request, 'manage_invites.html', data)
@login_required @login_required
def notifications_page(request): def notifications_page(request):
''' list notitications ''' ''' list notitications '''