Merge pull request #4992 from FinnStutzenstein/reworkPersonalNotes

Single create and update request for personal notes
This commit is contained in:
Emanuel Schütze 2019-09-10 13:40:46 +02:00 committed by GitHub
commit 6c7db17641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 102 additions and 83 deletions

View File

@ -7,6 +7,12 @@ import { HttpService } from '../core-services/http.service';
import { OperatorService } from '../core-services/operator.service'; import { OperatorService } from '../core-services/operator.service';
import { PersonalNote, PersonalNoteContent, PersonalNoteObject } from '../../shared/models/users/personal-note'; import { PersonalNote, PersonalNoteContent, PersonalNoteObject } from '../../shared/models/users/personal-note';
type PersonalNoteRequestData = {
collection: string;
id: number;
content: object;
}[];
/** /**
* Handles saving personal notes. * Handles saving personal notes.
*/ */
@ -17,7 +23,7 @@ export class PersonalNoteService {
/** /**
* The personal note object for the operator * The personal note object for the operator
*/ */
private personalNoteObject: PersonalNoteObject; private personalNoteObject: PersonalNoteObject | null;
/** /**
* Watches for changes in the personal note model and the operator. * Watches for changes in the personal note model and the operator.
@ -34,6 +40,7 @@ export class PersonalNoteService {
*/ */
private updatePersonalNoteObject(): void { private updatePersonalNoteObject(): void {
if (this.operator.isAnonymous) { if (this.operator.isAnonymous) {
this.personalNoteObject = null;
return; return;
} }
@ -49,16 +56,13 @@ export class PersonalNoteService {
* @param content The new content. * @param content The new content.
*/ */
public async savePersonalNote(model: BaseModel | BaseViewModel, content: PersonalNoteContent): Promise<void> { public async savePersonalNote(model: BaseModel | BaseViewModel, content: PersonalNoteContent): Promise<void> {
const pnObject: Partial<PersonalNoteObject> = this.personalNoteObject || {}; await this.savePersonalNoteObject([
if (!pnObject.notes) { {
pnObject.notes = {}; collection: model.collectionString,
id: model.id,
content: content
} }
if (!pnObject.notes[model.collectionString]) { ]);
pnObject.notes[model.collectionString] = {};
}
pnObject.notes[model.collectionString][model.id] = content;
await this.savePersonalNoteObject(pnObject);
} }
/** /**
@ -75,7 +79,7 @@ export class PersonalNoteService {
if (!pnObject.notes) { if (!pnObject.notes) {
pnObject.notes = {}; pnObject.notes = {};
} }
for (const model of models) { const requestData: PersonalNoteRequestData = models.map(model => {
if (!pnObject.notes[model.collectionString]) { if (!pnObject.notes[model.collectionString]) {
pnObject.notes[model.collectionString] = {}; pnObject.notes[model.collectionString] = {};
} }
@ -84,20 +88,21 @@ export class PersonalNoteService {
} else { } else {
pnObject.notes[model.collectionString][model.id] = { star: star, note: '' }; pnObject.notes[model.collectionString][model.id] = { star: star, note: '' };
} }
} return {
await this.savePersonalNoteObject(pnObject); collection: model.collectionString,
id: model.id,
content: pnObject.notes[model.collectionString][model.id]
};
});
await this.savePersonalNoteObject(requestData);
} }
/** /**
* Sends an updated personal note to the server * Sends an updated personal note to the server
* *
* @param pnObject a partial (if new) or complete personal note object * @param requestData The data to send to the server
*/ */
private async savePersonalNoteObject(pnObject: Partial<PersonalNoteObject>): Promise<void> { private async savePersonalNoteObject(requestData: PersonalNoteRequestData): Promise<void> {
if (!pnObject.id) { await this.http.post(`/rest/users/personal-note/create_or_update/`, requestData);
await this.http.post('/rest/users/personal-note/', pnObject);
} else {
await this.http.put(`/rest/users/personal-note/${pnObject.id}/`, pnObject);
}
} }
} }

View File

@ -17,7 +17,6 @@ from django.contrib.sites.shortcuts import get_current_site
from django.core import mail from django.core import mail
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import transaction from django.db import transaction
from django.db.utils import IntegrityError
from django.http.request import QueryDict from django.http.request import QueryDict
from django.utils.encoding import force_bytes, force_text from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
@ -608,13 +607,7 @@ class PersonalNoteViewSet(ModelViewSet):
""" """
if self.action in ("list", "retrieve"): if self.action in ("list", "retrieve"):
result = self.get_access_permissions().check_permissions(self.request.user) result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ( elif self.action in ("create_or_update", "destroy"):
"metadata",
"create",
"partial_update",
"update",
"destroy",
):
# Every authenticated user can see metadata and create personal # Every authenticated user can see metadata and create personal
# notes for himself and can manipulate only his own personal notes. # notes for himself and can manipulate only his own personal notes.
# See self.perform_create(), self.update() and self.destroy(). # See self.perform_create(), self.update() and self.destroy().
@ -623,29 +616,44 @@ class PersonalNoteViewSet(ModelViewSet):
result = False result = False
return result return result
def perform_create(self, serializer): @list_route(methods=["post"])
""" @transaction.atomic
Customized method to inject the request.user into serializer's save def create_or_update(self, request, *args, **kwargs):
method so that the request.user can be saved into the model field.
"""
try:
serializer.save(user=self.request.user)
except IntegrityError:
raise ValidationError(
{
"detail": "The personal note for user {0} does already exist",
"args": [self.request.user.id],
}
)
def update(self, request, *args, **kwargs):
""" """
Customized method to ensure that every user can change only his own Customized method to ensure that every user can change only his own
personal notes. personal notes.
[{
collection: <collection>,
id: <id>,
content: <Any>,
}, ...]
""" """
if self.get_object().user != self.request.user: # verify data:
self.permission_denied(request) if not isinstance(request.data, list):
return super().update(request, *args, **kwargs) raise ValidationError({"detail": "Data must be a list"})
for data in request.data:
if not isinstance(data, dict):
raise ValidationError({"detail": "Every entry must be a dict"})
if not isinstance(data.get("collection"), str):
raise ValidationError({"detail": "The collection must be a string"})
if not isinstance(data.get("id"), int):
raise ValidationError({"detail": "The id must be an integer"})
# get note
personal_note, _ = PersonalNote.objects.get_or_create(user=request.user)
# set defaults
if not personal_note.notes:
personal_note.notes = {}
for data in request.data:
if data["collection"] not in personal_note.notes:
personal_note.notes[data["collection"]] = {}
personal_note.notes[data["collection"]][data["id"]] = data["content"]
personal_note.save()
return Response()
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
""" """

View File

@ -853,69 +853,75 @@ class PersonalNoteTest(TestCase):
def test_create(self): def test_create(self):
admin_client = APIClient() admin_client = APIClient()
admin_client.login(username="admin", password="admin") admin_client.login(username="admin", password="admin")
response = admin_client.post( content1 = {
reverse("personalnote-list"),
{
"notes": {
"example-model": {
"1": {
"note": "note for the example.model with id 1 Oohae1JeuSedooyeeviH", "note": "note for the example.model with id 1 Oohae1JeuSedooyeeviH",
"star": True, "star": True,
} }
content2 = {
"note": "note for the example.model with id 2 gegjhjynjiohnhioaaiu",
"star": False,
} }
} response = admin_client.post(
}, reverse("personalnote-create-or-update"),
[
{"collection": "example-model", "id": 1, "content": content1},
{"collection": "example-model", "id": 2, "content": content2},
],
format="json", format="json",
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(PersonalNote.objects.exists())
personal_note = PersonalNote.objects.get()
self.assertTrue("example-model" in personal_note.notes)
self.assertTrue("1" in personal_note.notes["example-model"])
self.assertTrue("2" in personal_note.notes["example-model"])
self.assertEqual(personal_note.notes["example-model"]["1"], content1)
self.assertEqual(personal_note.notes["example-model"]["2"], content2)
def test_anonymous_create(self): def test_anonymous_create(self):
guest_client = APIClient() guest_client = APIClient()
response = guest_client.post( response = guest_client.post(
reverse("personalnote-list"), {"notes": {}}, format="json" reverse("personalnote-create-or-update"), [], format="json"
) )
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertFalse(PersonalNote.objects.exists()) self.assertFalse(PersonalNote.objects.exists())
def test_create_twice(self):
admin_client = APIClient()
admin_client.login(username="admin", password="admin")
response = admin_client.post(
reverse("personalnote-list"), {"notes": {}}, format="json"
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = admin_client.post(
reverse("personalnote-list"), {"notes": {}}, format="json"
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_update(self): def test_update(self):
admin_client = APIClient() admin_client = APIClient()
admin_client.login(username="admin", password="admin") admin_client.login(username="admin", password="admin")
personal_note = PersonalNote.objects.create( personal_note = PersonalNote.objects.create(
user=self.admin, notes="test_note_ld3mo1xjcnKNC(836qWe" user=self.admin,
notes={"test_collection": {2: "test_note_ld3mo1xjcnKNC(836qWe"}},
) )
response = admin_client.put( response = admin_client.post(
reverse("personalnote-detail", args=[personal_note.pk]), reverse("personalnote-create-or-update"),
{"notes": "test_note_do2ncoi7ci2fm93LjwlO"}, [
{
"collection": "test_collection",
"id": 2,
"content": "test_note_do2ncoi7ci2fm93LjwlO",
}
],
format="json", format="json",
) )
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
personal_note = PersonalNote.objects.get()
self.assertTrue("test_collection" in personal_note.notes)
self.assertTrue("2" in personal_note.notes["test_collection"])
self.assertEqual( self.assertEqual(
PersonalNote.objects.get().notes, "test_note_do2ncoi7ci2fm93LjwlO" personal_note.notes["test_collection"]["2"],
"test_note_do2ncoi7ci2fm93LjwlO",
) )
def test_update_other_user(self): def test_delete_other_user(self):
user = User.objects.create(username="user") user = User.objects.create(username="user")
admin_client = APIClient() admin_client = APIClient()
admin_client.login(username="admin", password="admin") admin_client.login(username="admin", password="admin")
personal_note = PersonalNote.objects.create( personal_note = PersonalNote.objects.create(
user=user, notes="test_note_fof3joqmcufh32fn(/2f" user=user, notes="test_note_fof3joqmcufh32fn(/2f"
) )
response = admin_client.put( response = admin_client.delete(
reverse("personalnote-detail", args=[personal_note.pk]), reverse("personalnote-detail", args=[personal_note.pk])
{"notes": "test_note_1qowuddm3d8mF8h29fwI"},
format="json",
) )
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual( self.assertEqual(