Skip to content
Snippets Groups Projects
Verified Commit d18f98e0 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

See #248: can now sign up using invitation code

parent 789bef38
No related branches found
No related tags found
No related merge requests found
...@@ -7,12 +7,12 @@ from django.contrib.auth.admin import UserAdmin as AuthUserAdmin ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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"]
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 ...@@ -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 ...@@ -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): ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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' ...@@ -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',
components: {
PasswordInput
},
props: { props: {
invitation: {type: String, required: false, default: null},
next: {type: String, default: '/'} next: {type: String, default: '/'}
}, },
components: {
PasswordInput
},
data () { data () {
return { return {
username: '', username: '',
...@@ -85,7 +98,8 @@ export default { ...@@ -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({ ...@@ -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',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment