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.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"]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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',
|
||||
props: {
|
||||
invitation: {type: String, required: false, default: null},
|
||||
next: {type: String, default: '/'}
|
||||
},
|
||||
components: {
|
||||
PasswordInput
|
||||
},
|
||||
props: {
|
||||
next: {type: String, default: '/'}
|
||||
},
|
||||
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',
|
||||
|
|
Loading…
Reference in New Issue