Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: store user ui preferences #522

Closed
wants to merge 4 commits into from
Closed

Conversation

acellard
Copy link
Contributor

@acellard acellard commented Oct 6, 2022

Description

This PR is a result of October 27th "flash hackathon" involving @Milouu and @acellard. The goal was to figure out how to properly manage user ui preferences in the backend, and provide api to get, retrieve and update them.
For now, such preferences involve columns display list and favorites compute plans. With likely more preferences in the future.
To keep it flexible and leave the responsibility to the frontend, the preferences are stored in a json field.

This PR also does some refactoring to use serializer to validate data.

@acellard acellard force-pushed the feat/user_preferences branch 2 times, most recently from b0df88b to cd4abe8 Compare October 7, 2022 12:29
return ui_preferences

def update(self, instance, validated_data):
instance.set_password(validated_data.get("password"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should only be done if there is a new password?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed, thanks! Changed.

@@ -51,7 +52,7 @@ def test_user_create_duplicate(self):
response = AuthenticatedClient(role=UserChannel.Role.ADMIN, channel=self.channel).post(self.url, data=data)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data.get("message") == "Username already exists"
assert response.data.get("username")[0] == "A user with that username already exists."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API change needs to be reflected in the frontend: the message was directly displayed to the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as last commit, I leveraged django validation handling:
HTTP 400 Bad Request response with data format : {field: [error list on this field]}.
Is this convenient for clients ? Or should we catch validation exception and reformat response message to have something like {message: 'error string'}.
I feel it's convenient to use what django already does but I don't have a strong opinion on this.

@parameterized.expand(
[
({"role": UserChannel.Role.USER},),
({"password": "newpas$w0rdtestofdrea6S43"},),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel updating the password should be done in a separate test with a check that the password is indeed updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate test added

@@ -16,3 +16,4 @@ class Role(models.TextChoices):
settings.AUTH_USER_MODEL, parent_link=True, on_delete=models.CASCADE, related_name="channel"
)
channel_name = models.CharField(max_length=100)
ui_preferences = models.JSONField(null=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to not have null values but a default at {} instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hesitated between null or {} default. I initially chose null to avoid migration but from frontend pov I agree, it's better to avoid null values. Modified and migration added.

from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as djangoValidationError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing capital "D" for DjangoValidationError


def validate_ui_preferences(self, value):
if type(value) is not dict:
raise serializers.ValidationError({"UI preferences should be a dict"})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't you missing the ui_preferences key in {"ui_preferences": "UI preferences should be a dict"}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep consistency yes, key added.

[
({"role": UserChannel.Role.USER},),
({"password": "newpas$w0rdtestofdrea6S43"},),
({"ui_preferences": {"columns": ["col1", "col2"]}},),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test on the fact that ui_preferences is partially updated and not fully replaced? That is keys that were not in the request payload are left untouched.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added.

Comment on lines 144 to 145
username = kwargs.get("username")
instance = self.user_model.objects.get(username=username)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test this with a username like "John Doe"? self.get_object() is able to decode the urlencodedentities but kwargs only contains raw values that need to be decoded manually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the test and it seems to work yes. But still reverting to get_object(). For context, in certain cases there was an uncatch exception in get_object(). But this was fixed here https://github.com/Substra/substra-backend/pull/522/files#diff-fa54e25c9fbe9c2560d0fbef5cb51cb8f37b0c7793d322f409194417453082bbR62 so it will be catch before.

Signed-off-by: Alison Cellard <alison.cellard@owkin.com>
Signed-off-by: Alison Cellard <alison.cellard@owkin.com>
Signed-off-by: Alison Cellard <alison.cellard@owkin.com>
Signed-off-by: Alison Cellard <alison.cellard@owkin.com>
@guilhem-barthes
Copy link
Contributor

Thank you for your contribution! Unfortunately, due to organisational and roadmap changes, this item has been deprioritised. We currently have no bandwidth to cope with the code drift between the current code state in the last release and this PR, therefore we are closing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants