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
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.utils.translation import ugettext_lazy as _
from .models import User
from . import models
class MyUserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = User
model = models.User
class MyUserCreationForm(UserCreationForm):
......@@ -22,7 +22,7 @@ class MyUserCreationForm(UserCreationForm):
)
class Meta(UserCreationForm.Meta):
model = User
model = models.User
def clean_username(self):
username = self.cleaned_data["username"]
......@@ -33,7 +33,7 @@ class MyUserCreationForm(UserCreationForm):
raise forms.ValidationError(self.error_messages["duplicate_username"])
@admin.register(User)
@admin.register(models.User)
class UserAdmin(AuthUserAdmin):
form = MyUserChangeForm
add_form = MyUserCreationForm
......@@ -74,3 +74,11 @@ class UserAdmin(AuthUserAdmin):
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
(_("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 rest_auth.serializers import PasswordResetSerializer as PRS
from rest_auth.registration.serializers import RegisterSerializer as RS
from rest_framework import 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
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):
type = serializers.SerializerMethodField()
name = serializers.CharField(source="username")
......
......@@ -10,8 +10,11 @@ from . import models, serializers
class RegisterView(BaseRegisterView):
serializer_class = serializers.RegisterSerializer
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"}
return Response(r, status=403)
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
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):
url = reverse("api:v1:users:users-me")
response = api_client.get(url)
......
......@@ -2,19 +2,22 @@
<div class="main pusher" v-title="'Sign Up'">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<h2><i18next path="Create a funkwhale account"/></h2>
<h2>{{ $t("Create a funkwhale account") }}</h2>
<form
v-if="$store.state.instance.settings.users.registration_enabled.value"
:class="['ui', {'loading': isLoadingInstanceSetting}, 'form']"
@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 class="header"><i18next path="We cannot create your account"/></div>
<div class="header">{{ $t("We cannot create your account") }}</div>
<ul class="list">
<li v-for="error in errors">{{ error }}</li>
</ul>
</div>
<div class="field">
<i18next tag="label" path="Username"/>
<label>{{ $t("Username") }}</label>
<input
ref="username"
required
......@@ -24,7 +27,7 @@
v-model="username">
</div>
<div class="field">
<i18next tag="label" path="Email"/>
<label>{{ $t("Email") }}</label>
<input
ref="email"
required
......@@ -33,12 +36,22 @@
v-model="email">
</div>
<div class="field">
<i18next tag="label" path="Password"/>
<label>{{ $t("Password") }}</label>
<password-input v-model="password" />
</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>
<i18next v-else tag="p" path="Registration is currently disabled on this instance, please try again later."/>
</div>
</div>
</div>
......@@ -51,13 +64,13 @@ import logger from '@/logging'
import PasswordInput from '@/components/forms/PasswordInput'
export default {
name: 'login',
components: {
PasswordInput
},
props: {
invitation: {type: String, required: false, default: null},
next: {type: String, default: '/'}
},
components: {
PasswordInput
},
data () {
return {
username: '',
......@@ -85,7 +98,8 @@ export default {
username: this.username,
password1: this.password,
password2: this.password,
email: this.email
email: this.email,
invitation: this.invitation
}
return axios.post('auth/registration/', payload).then(response => {
logger.default.info('Successfully created account')
......
......@@ -96,7 +96,10 @@ export default new Router({
{
path: '/signup',
name: 'signup',
component: Signup
component: Signup,
props: (route) => ({
invitation: route.query.invitation
})
},
{
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