See #248: can now sign up using invitation code
This commit is contained in:
parent
789bef38cb
commit
d18f98e0f8
|
@ -7,12 +7,12 @@ from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
|
||||||
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .models import User
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class MyUserChangeForm(UserChangeForm):
|
class MyUserChangeForm(UserChangeForm):
|
||||||
class Meta(UserChangeForm.Meta):
|
class Meta(UserChangeForm.Meta):
|
||||||
model = User
|
model = models.User
|
||||||
|
|
||||||
|
|
||||||
class MyUserCreationForm(UserCreationForm):
|
class MyUserCreationForm(UserCreationForm):
|
||||||
|
@ -22,7 +22,7 @@ class MyUserCreationForm(UserCreationForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(UserCreationForm.Meta):
|
class Meta(UserCreationForm.Meta):
|
||||||
model = User
|
model = models.User
|
||||||
|
|
||||||
def clean_username(self):
|
def clean_username(self):
|
||||||
username = self.cleaned_data["username"]
|
username = self.cleaned_data["username"]
|
||||||
|
@ -33,7 +33,7 @@ class MyUserCreationForm(UserCreationForm):
|
||||||
raise forms.ValidationError(self.error_messages["duplicate_username"])
|
raise forms.ValidationError(self.error_messages["duplicate_username"])
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User)
|
@admin.register(models.User)
|
||||||
class UserAdmin(AuthUserAdmin):
|
class UserAdmin(AuthUserAdmin):
|
||||||
form = MyUserChangeForm
|
form = MyUserChangeForm
|
||||||
add_form = MyUserCreationForm
|
add_form = MyUserCreationForm
|
||||||
|
@ -74,3 +74,11 @@ class UserAdmin(AuthUserAdmin):
|
||||||
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
|
||||||
(_("Useless fields"), {"fields": ("user_permissions", "groups")}),
|
(_("Useless fields"), {"fields": ("user_permissions", "groups")}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.Invitation)
|
||||||
|
class InvitationAdmin(admin.ModelAdmin):
|
||||||
|
list_select_related = True
|
||||||
|
list_display = ["owner", "code", "creation_date", "expiration_date"]
|
||||||
|
search_fields = ["owner__username", "code"]
|
||||||
|
readonly_fields = ["expiration_date", "code"]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_auth.serializers import PasswordResetSerializer as PRS
|
from rest_auth.serializers import PasswordResetSerializer as PRS
|
||||||
|
from rest_auth.registration.serializers import RegisterSerializer as RS
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from funkwhale_api.activity import serializers as activity_serializers
|
from funkwhale_api.activity import serializers as activity_serializers
|
||||||
|
@ -7,6 +8,28 @@ from funkwhale_api.activity import serializers as activity_serializers
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterSerializer(RS):
|
||||||
|
invitation = serializers.CharField(
|
||||||
|
required=False, allow_null=True, allow_blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_invitation(self, value):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
return models.Invitation.objects.open().get(code=value.lower())
|
||||||
|
except models.Invitation.DoesNotExist:
|
||||||
|
raise serializers.ValidationError("Invalid invitation code")
|
||||||
|
|
||||||
|
def save(self, request):
|
||||||
|
user = super().save(request)
|
||||||
|
if self.validated_data.get("invitation"):
|
||||||
|
user.invitation = self.validated_data.get("invitation")
|
||||||
|
user.save(update_fields=["invitation"])
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
class UserActivitySerializer(activity_serializers.ModelSerializer):
|
class UserActivitySerializer(activity_serializers.ModelSerializer):
|
||||||
type = serializers.SerializerMethodField()
|
type = serializers.SerializerMethodField()
|
||||||
name = serializers.CharField(source="username")
|
name = serializers.CharField(source="username")
|
||||||
|
|
|
@ -10,8 +10,11 @@ from . import models, serializers
|
||||||
|
|
||||||
|
|
||||||
class RegisterView(BaseRegisterView):
|
class RegisterView(BaseRegisterView):
|
||||||
|
serializer_class = serializers.RegisterSerializer
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
if not self.is_open_for_signup(request):
|
invitation_code = request.data.get("invitation")
|
||||||
|
if not invitation_code and not self.is_open_for_signup(request):
|
||||||
r = {"detail": "Registration has been disabled"}
|
r = {"detail": "Registration has been disabled"}
|
||||||
return Response(r, status=403)
|
return Response(r, status=403)
|
||||||
return super().create(request, *args, **kwargs)
|
return super().create(request, *args, **kwargs)
|
||||||
|
|
|
@ -50,6 +50,39 @@ def test_can_disable_registration_view(preferences, api_client, db):
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_signup_with_invitation(preferences, factories, api_client):
|
||||||
|
url = reverse("rest_register")
|
||||||
|
invitation = factories["users.Invitation"](code="hello")
|
||||||
|
data = {
|
||||||
|
"username": "test1",
|
||||||
|
"email": "test1@test.com",
|
||||||
|
"password1": "testtest",
|
||||||
|
"password2": "testtest",
|
||||||
|
"invitation": "hello",
|
||||||
|
}
|
||||||
|
preferences["users__registration_enabled"] = False
|
||||||
|
response = api_client.post(url, data)
|
||||||
|
assert response.status_code == 201
|
||||||
|
u = User.objects.get(email="test1@test.com")
|
||||||
|
assert u.username == "test1"
|
||||||
|
assert u.invitation == invitation
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_signup_with_invitation_invalid(preferences, factories, api_client):
|
||||||
|
url = reverse("rest_register")
|
||||||
|
invitation = factories["users.Invitation"](code="hello")
|
||||||
|
data = {
|
||||||
|
"username": "test1",
|
||||||
|
"email": "test1@test.com",
|
||||||
|
"password1": "testtest",
|
||||||
|
"password2": "testtest",
|
||||||
|
"invitation": "nope",
|
||||||
|
}
|
||||||
|
response = api_client.post(url, data)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert "invitation" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_can_fetch_data_from_api(api_client, factories):
|
def test_can_fetch_data_from_api(api_client, factories):
|
||||||
url = reverse("api:v1:users:users-me")
|
url = reverse("api:v1:users:users-me")
|
||||||
response = api_client.get(url)
|
response = api_client.get(url)
|
||||||
|
|
|
@ -2,19 +2,22 @@
|
||||||
<div class="main pusher" v-title="'Sign Up'">
|
<div class="main pusher" v-title="'Sign Up'">
|
||||||
<div class="ui vertical stripe segment">
|
<div class="ui vertical stripe segment">
|
||||||
<div class="ui small text container">
|
<div class="ui small text container">
|
||||||
<h2><i18next path="Create a funkwhale account"/></h2>
|
<h2>{{ $t("Create a funkwhale account") }}</h2>
|
||||||
<form
|
<form
|
||||||
v-if="$store.state.instance.settings.users.registration_enabled.value"
|
|
||||||
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
|
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
|
||||||
@submit.prevent="submit()">
|
@submit.prevent="submit()">
|
||||||
|
<p class="ui message" v-if="!$store.state.instance.settings.users.registration_enabled.value">
|
||||||
|
{{ $t('Registration are closed on this instance, you will need an invitation code to signup.') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div v-if="errors.length > 0" class="ui negative message">
|
<div v-if="errors.length > 0" class="ui negative message">
|
||||||
<div class="header"><i18next path="We cannot create your account"/></div>
|
<div class="header">{{ $t("We cannot create your account") }}</div>
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
<li v-for="error in errors">{{ error }}</li>
|
<li v-for="error in errors">{{ error }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<i18next tag="label" path="Username"/>
|
<label>{{ $t("Username") }}</label>
|
||||||
<input
|
<input
|
||||||
ref="username"
|
ref="username"
|
||||||
required
|
required
|
||||||
|
@ -24,7 +27,7 @@
|
||||||
v-model="username">
|
v-model="username">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<i18next tag="label" path="Email"/>
|
<label>{{ $t("Email") }}</label>
|
||||||
<input
|
<input
|
||||||
ref="email"
|
ref="email"
|
||||||
required
|
required
|
||||||
|
@ -33,12 +36,22 @@
|
||||||
v-model="email">
|
v-model="email">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<i18next tag="label" path="Password"/>
|
<label>{{ $t("Password") }}</label>
|
||||||
<password-input v-model="password" />
|
<password-input v-model="password" />
|
||||||
</div>
|
</div>
|
||||||
<button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit"><i18next path="Create my account"/></button>
|
<div class="field">
|
||||||
|
<label v-if="!$store.state.instance.settings.users.registration_enabled.value">{{ $t("Invitation code") }}</label>
|
||||||
|
<label v-else>{{ $t("Invitation code (optional)") }}</label>
|
||||||
|
<input
|
||||||
|
:required="!$store.state.instance.settings.users.registration_enabled.value"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('Enter your invitation code (case insensitive)')"
|
||||||
|
v-model="invitation">
|
||||||
|
</div>
|
||||||
|
<button :class="['ui', 'green', {'loading': isLoading}, 'button']" type="submit">
|
||||||
|
{{ $t("Create my account") }}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<i18next v-else tag="p" path="Registration is currently disabled on this instance, please try again later."/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,13 +64,13 @@ import logger from '@/logging'
|
||||||
import PasswordInput from '@/components/forms/PasswordInput'
|
import PasswordInput from '@/components/forms/PasswordInput'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
props: {
|
||||||
|
invitation: {type: String, required: false, default: null},
|
||||||
|
next: {type: String, default: '/'}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
PasswordInput
|
PasswordInput
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
next: {type: String, default: '/'}
|
|
||||||
},
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
username: '',
|
username: '',
|
||||||
|
@ -85,7 +98,8 @@ export default {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password1: this.password,
|
password1: this.password,
|
||||||
password2: this.password,
|
password2: this.password,
|
||||||
email: this.email
|
email: this.email,
|
||||||
|
invitation: this.invitation
|
||||||
}
|
}
|
||||||
return axios.post('auth/registration/', payload).then(response => {
|
return axios.post('auth/registration/', payload).then(response => {
|
||||||
logger.default.info('Successfully created account')
|
logger.default.info('Successfully created account')
|
||||||
|
|
|
@ -96,7 +96,10 @@ export default new Router({
|
||||||
{
|
{
|
||||||
path: '/signup',
|
path: '/signup',
|
||||||
name: 'signup',
|
name: 'signup',
|
||||||
component: Signup
|
component: Signup,
|
||||||
|
props: (route) => ({
|
||||||
|
invitation: route.query.invitation
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/logout',
|
path: '/logout',
|
||||||
|
|
Loading…
Reference in New Issue