forked from mirrors/bookwyrm
Merge pull request #182 from cthulahoops/site_settings
Add instance settings.
This commit is contained in:
commit
a0cc88d316
14 changed files with 327 additions and 4 deletions
|
@ -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')])
|
||||||
|
}
|
||||||
|
|
23
fedireads/migrations/0042_sitesettings.py
Normal file
23
fedireads/migrations/0042_sitesettings.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
24
fedireads/migrations/0043_siteinvite.py
Normal file
24
fedireads/migrations/0043_siteinvite.py
Normal 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)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
21
fedireads/migrations/0044_siteinvite_user.py
Normal file
21
fedireads/migrations/0044_siteinvite_user.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
44
fedireads/models/site.py
Normal 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)
|
21
fedireads/templates/about.html
Normal file
21
fedireads/templates/about.html
Normal 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 %}
|
33
fedireads/templates/invite.html
Normal file
33
fedireads/templates/invite.html
Normal 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 %}
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
32
fedireads/templates/manage_invites.html
Normal file
32
fedireads/templates/manage_invites.html
Normal 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 %}
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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 '''
|
||||||
|
|
Loading…
Reference in a new issue