Browse Source

api for editing user details

Rafał Pitoń 8 years ago
parent
commit
1da2394430

+ 71 - 1
misago/users/api/userendpoints/changedetails.py

@@ -1,5 +1,75 @@
 from rest_framework.response import Response
 
+from django import forms
+
+
+from misago.users.profilefields import profilefields, serialize_profilefields_data
+
 
 def change_details_endpoint(request, user):
-    return Response({'detail': 'TODO'})
+    if request.method == 'GET':
+        return get_form_description(request, user)
+
+    return submit_form(request, user)
+
+
+def get_form_description(request, user):
+    groups = []
+    for group in profilefields.get_fields_groups():
+        group_fields = []
+        for field in group['fields']:
+            if field.can_edit(request, user):
+                group_fields.append(field.get_edit_field_json(request, user))
+        if group_fields:
+            groups.append({
+                'name': group['name'],
+                'fields': group_fields
+            })
+
+    return Response(groups)
+
+
+def submit_form(request, user):
+    fields = []
+    for field in profilefields.get_fields():
+        if field.can_edit(request, user):
+            fields.append(field)
+
+    form = DetailsForm(
+        request.data,
+        request=request,
+        user=user,
+        profilefields=fields,
+    )
+
+    if form.is_valid():
+        user.profile_fields = form.cleaned_data
+        user.save(update_fields=['profile_fields'])
+
+        return Response(serialize_profilefields_data(request, profilefields, user))
+
+    return Response(form.errors, status=400)
+
+
+class DetailsForm(forms.Form):
+    def __init__(self, *args, **kwargs):
+        self.request = kwargs.pop('request')
+        self.user = kwargs.pop('user')
+        self.profilefields = kwargs.pop('profilefields')
+
+        super(DetailsForm, self).__init__(*args, **kwargs)
+
+        for field in self.profilefields:
+            self.fields[field.fieldname] = field.get_field_for_validation(
+                self.request, self.user)
+
+    def clean(self):
+        data = super(DetailsForm, self).clean()
+        for field in self.profilefields:
+            if field.fieldname in data:
+                try:
+                    data[field.fieldname] = field.clean_field(
+                        self.request, self.user, data[field.fieldname])
+                except forms.ValidationError as e:
+                    self.add_error(field.fieldname, e)
+        return data

+ 5 - 0
misago/users/profilefields/__init__.py

@@ -98,6 +98,11 @@ class ProfileFields(object):
 
         return queryset
 
+    def get_fields(self):
+        if not self.is_loaded:
+            self.load()
+        return self.fields_dict.values()
+
     def get_fields_groups(self):
         if not self.is_loaded:
             self.load()

+ 51 - 1
misago/users/profilefields/basefields.py

@@ -73,6 +73,26 @@ class ProfileField(object):
     def can_edit(self, request, user):
         return not self.readonly
 
+    def get_edit_field_json(self, request, user):
+        return {
+            'fieldname': self.fieldname,
+            'label': self.get_label(user),
+            'help_text': self.get_help_text(user),
+            'initial': user.profile_fields.get(self.fieldname),
+            'input': self.get_edit_field_input_attrs(request, user)
+        }
+
+    def get_edit_field_input_attrs(self, request, user):
+        return {
+            'type': 'text',
+        }
+
+    def get_field_for_validation(self, request, user):
+        return forms.CharField(max_length=250, required=False)
+
+    def clean_field(self, request, user, data):
+        return data
+
 
 class ChoiceProfileField(ProfileField):
     choices = None
@@ -117,6 +137,22 @@ class ChoiceProfileField(ProfileField):
                 }
         return None
 
+    def get_edit_field_input_attrs(self, request, user):
+        choices = []
+        for key, choice in self.get_choices():
+            choices.append({
+                'value': key,
+                'label': choice,
+            })
+
+        return {
+            'type': 'select',
+            'choices': choices,
+        }
+
+    def get_field_for_validation(self, request, user):
+        return forms.ChoiceField(choices=self.get_choices(user), required=False)
+
 
 class TextProfileField(ProfileField):
     def get_admin_field(self, user):
@@ -136,7 +172,7 @@ class TextareaProfileField(ProfileField):
             label=self.get_label(user),
             help_text=self.get_help_text(user),
             initial=user.profile_fields.get(self.fieldname),
-            max_length=250,
+            max_length=500,
             widget=forms.Textarea(
                 attrs={'rows': 4},
             ),
@@ -149,6 +185,14 @@ class TextareaProfileField(ProfileField):
             'html': html.linebreaks(html.escape(data)),
         }
 
+    def get_edit_field_input_attrs(self, request, user):
+        return {
+            'type': 'texarea',
+        }
+
+    def get_field_for_validation(self, request, user):
+        return forms.CharField(max_length=500, required=False)
+
 
 class UrlifiedTextareaProfileField(TextareaProfileField):
     def get_display_data(self, request, user, data):
@@ -174,6 +218,9 @@ class SlugProfileField(ProfileField):
             'url': data,
         }
 
+    def get_field_for_validation(self, request, user):
+        return forms.SlugField(max_length=250, required=False)
+
 
 class UrlProfileField(ProfileField):
     def get_admin_field(self, user):
@@ -191,3 +238,6 @@ class UrlProfileField(ProfileField):
             'text': data,
             'url': data,
         }
+
+    def get_field_for_validation(self, request, user):
+        return forms.URLField(max_length=250, required=False)

+ 5 - 2
misago/users/profilefields/default.py

@@ -28,7 +28,7 @@ class GenderField(basefields.ChoiceProfileField):
         ('', _('Not specified')),
         ('secret', _('Not telling')),
         ('f', _('Female')),
-        ('f', _('Male')),
+        ('m', _('Male')),
     )
 
 
@@ -42,7 +42,7 @@ class SkypeHandleField(basefields.TextProfileField):
     label = _("Skype ID")
 
 
-class TwitterHandleField(basefields.SlugProfileField):
+class TwitterHandleField(basefields.TextProfileField):
     fieldname = 'twitter'
     label = _("Twitter handle")
     help_text = _('Without leading "@" sign.')
@@ -52,3 +52,6 @@ class TwitterHandleField(basefields.SlugProfileField):
             'text': '@{}'.format(data),
             'url': 'https://twitter.com/{}'.format(data),
         }
+
+    def clean_field(self, request, user, data):
+        return data.lstrip('@')

+ 1 - 0
misago/users/profilefields/serializers.py

@@ -3,6 +3,7 @@ from misago.users.permissions import can_edit_profile_details
 
 def serialize_profilefields_data(request, profilefields, user):
     data = {
+        'id': user.pk,
         'groups': [],
         'edit': False,
     }

+ 145 - 0
misago/users/tests/test_user_changedetails_api.py

@@ -0,0 +1,145 @@
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+from misago.acl.testutils import override_acl
+
+from misago.users.testutils import AuthenticatedUserTestCase
+
+
+UserModel = get_user_model()
+
+
+class UserChangeDetailsApiTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(UserChangeDetailsApiTests, self).setUp()
+
+        self.api_link = reverse(
+            'misago:api:user-change-details',
+            kwargs={
+                'pk': self.user.pk,
+            }
+        )
+
+    def get_profile_fields(self):
+        return UserModel.objects.get(pk=self.user.pk).profile_fields
+
+    def test_api_has_no_showstoppers(self):
+        """api outputs response for freshly created user"""
+        response = self.client.get(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_api_has_no_showstoppers_old_user(self):
+        """api outputs response for freshly created user"""
+        response = self.client.get(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+    def test_other_user(self):
+        """api handles scenario when its other user looking at profile"""
+        test_user = UserModel.objects.create_user('BobBoberson', 'bob@test.com', 'bob123456')
+
+        api_link = reverse(
+            'misago:api:user-change-details',
+            kwargs={
+                'pk': test_user.pk,
+            }
+        )
+
+        # moderator has permission to edit details
+        override_acl(self.user, {
+            'can_moderate_profile_details': True,
+        })
+
+        response = self.client.get(api_link)
+        self.assertEqual(response.status_code, 200)
+
+        # non-moderator has no permission to edit details
+        override_acl(self.user, {
+            'can_moderate_profile_details': False,
+        })
+
+        response = self.client.get(api_link)
+        self.assertEqual(response.status_code, 403)
+
+    def test_nonexistant_user(self):
+        """api handles nonexistant users"""
+        api_link = reverse(
+            'misago:api:user-change-details',
+            kwargs={
+                'pk': self.user.pk + 123,
+            }
+        )
+
+        response = self.client.get(api_link)
+        self.assertEqual(response.status_code, 404)
+
+    def test_api_updates_text_field(self):
+        """api updates text field"""
+        response = self.client.post(self.api_link, data={
+            'bio': 'I have some, as is tradition.'
+        })
+        self.assertEqual(response.status_code, 200)
+
+        profile_fields = self.get_profile_fields()
+        self.assertEqual(profile_fields['bio'], 'I have some, as is tradition.')
+
+        response_json = response.json()
+        self.assertEqual(response_json['id'], self.user.id)
+        self.assertTrue(response_json['edit'])
+        self.assertTrue(response_json['groups'])
+
+    def test_api_updates_select_field(self):
+        """api updates select field"""
+        response = self.client.post(self.api_link, data={
+            'gender': 'f',
+        })
+
+        self.assertEqual(response.status_code, 200)
+
+        profile_fields = self.get_profile_fields()
+        self.assertEqual(profile_fields['gender'], 'f')
+
+        response_json = response.json()
+        self.assertEqual(response_json['id'], self.user.id)
+        self.assertTrue(response_json['edit'])
+        self.assertTrue(response_json['groups'])
+
+    def test_api_validates_url_field(self):
+        """api runs basic validation against url fields"""
+        response = self.client.post(self.api_link, data={
+            'website': 'noturl',
+        })
+
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(response.json(), {'website': ['Enter a valid URL.']})
+
+    def test_api_cleans_url_field(self):
+        """api cleans url fields"""
+        response = self.client.post(self.api_link, data={
+            'website': 'onet.pl',
+        })
+
+        self.assertEqual(response.status_code, 200)
+
+        profile_fields = self.get_profile_fields()
+        self.assertEqual(profile_fields['website'], 'http://onet.pl')
+
+        response_json = response.json()
+        self.assertEqual(response_json['id'], self.user.id)
+        self.assertTrue(response_json['edit'])
+        self.assertTrue(response_json['groups'])
+
+    def test_api_custom_cleans_url_field(self):
+        """api calls fields clean method"""
+        response = self.client.post(self.api_link, data={
+            'twitter': '@Weebl',
+        })
+
+        self.assertEqual(response.status_code, 200)
+
+        profile_fields = self.get_profile_fields()
+        self.assertEqual(profile_fields['twitter'], 'Weebl')
+
+        response_json = response.json()
+        self.assertEqual(response_json['id'], self.user.id)
+        self.assertTrue(response_json['edit'])
+        self.assertTrue(response_json['groups'])