Rework on personal notes. Fixed #3262.
This commit is contained in:
parent
7947f2ed34
commit
91d365e386
22
CHANGELOG
22
CHANGELOG
@ -14,36 +14,36 @@ Agenda:
|
|||||||
|
|
||||||
Motions:
|
Motions:
|
||||||
- New export dialog [#3185].
|
- New export dialog [#3185].
|
||||||
- New feature: Personal notes for motions [#3190].
|
- New feature: Personal notes for motions [#3190, #3267].
|
||||||
- Fixed issue when creating/deleting motion comment fields in the
|
- Fixed issue when creating/deleting motion comment fields in the
|
||||||
settings [#3187].
|
settings [#3187].
|
||||||
- Fixed empty motion comment field in motion update form [#3194].
|
- Fixed empty motion comment field in motion update form [#3194].
|
||||||
- Removed server side image to base64 transformation and
|
- Removed server side image to base64 transformation and
|
||||||
added local transformation [#3181]
|
added local transformation [#3181]
|
||||||
- Added support for export motions in a zip archive [#3189].
|
- Added support for export motions in a ZIP archive [#3189].
|
||||||
- Performance improvement for zip creation [#3251]
|
- Performance improvement for ZIP creation [#3251].
|
||||||
- Bugfix: changing motion line length did not invalidate cache [#3202]
|
- Bugfix: Changing motion line length did not invalidate cache [#3202].
|
||||||
- Bugfix: Added more distance in motion PDF for DEL-tags in new lines [#3211].
|
- Bugfix: Added more distance in motion PDF for DEL-tags in new lines [#3211].
|
||||||
- Added warning message if an edit dialog was already opened by another
|
- Added warning message if an edit dialog was already opened by another
|
||||||
client [#3212].
|
client [#3212].
|
||||||
- Reworked DOCX export parser and added comments to DOCX [#3258].
|
- Reworked DOCX export parser and added comments to DOCX [#3258].
|
||||||
- New pdf export for personal note and comments [#3239].
|
- New PDF export for personal note and comments [#3239].
|
||||||
|
|
||||||
Users:
|
Users:
|
||||||
- User without permission to see users can now see agenda item speakers,
|
- User without permission to see users can now see agenda item speakers,
|
||||||
motion submitters and supporters, assignment candidates, mediafile
|
motion submitters and supporters, assignment candidates, mediafile
|
||||||
uploader and chat message users if they have the respective
|
uploader and chat message users if they have the respective
|
||||||
permissions [#3191].
|
permissions [#3191, #3233].
|
||||||
- Added support for password validation using Django or custom validators
|
- Added support for password validation using Django or custom validators
|
||||||
e. g. for minimum password length [#3200].
|
e. g. for minimum password length [#3200].
|
||||||
- Fixed compare of duplicated users while csv user import [#3201].
|
- Fixed compare of duplicated users while CSV user import [#3201].
|
||||||
- Added fast mass import for users [#3290].
|
- Added fast mass import for users [#3290].
|
||||||
|
|
||||||
Core:
|
Core:
|
||||||
- No reload on logoff. OpenSlides is now a full single page
|
- No reload on logoff. OpenSlides is now a full single page
|
||||||
application [#3172].
|
application [#3172].
|
||||||
- Adding support for choosing image files as logos for projector and
|
- Adding support for choosing image files as logos for projector and
|
||||||
pdf [#3184, #3207, #3208].
|
PDF [#3184, #3207, #3208].
|
||||||
- Fixing error when clearing empty chat [#3199].
|
- Fixing error when clearing empty chat [#3199].
|
||||||
- Added notify system [#3212].
|
- Added notify system [#3212].
|
||||||
- Enhanced performance esp. for server restart and first connection of all
|
- Enhanced performance esp. for server restart and first connection of all
|
||||||
@ -51,14 +51,14 @@ Core:
|
|||||||
- Fixes autoupdate bug for a user without user.can_see_name permission [#3233].
|
- Fixes autoupdate bug for a user without user.can_see_name permission [#3233].
|
||||||
|
|
||||||
Mediafiles:
|
Mediafiles:
|
||||||
- Fixed reloading of PDF on page change [#3274]
|
- Fixed reloading of PDF on page change [#3274].
|
||||||
|
|
||||||
General:
|
General:
|
||||||
- Switched from npm to Yarn [#3188].
|
- Switched from npm to Yarn [#3188].
|
||||||
- Several bugfixes and minor improvements.
|
- Several bugfixes and minor improvements.
|
||||||
- Improved performance for pdf generation significantly (by upgrading
|
- Improved performance for PDF generation significantly (by upgrading
|
||||||
to pdfmake 0.1.30) [#3278, #3285].
|
to pdfmake 0.1.30) [#3278, #3285].
|
||||||
- Bugfixes for PDF creation [#3227, #3251, #3279, #3286]
|
- Bugfixes for PDF creation [#3227, #3251, #3279, #3286].
|
||||||
|
|
||||||
|
|
||||||
Version 2.1.1 (2017-04-05)
|
Version 2.1.1 (2017-04-05)
|
||||||
|
@ -92,8 +92,6 @@ STATICFILES_DIRS = [
|
|||||||
|
|
||||||
AUTH_USER_MODEL = 'users.User'
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
AUTH_PERSONAL_NOTE_MODEL = 'users.PersonalNote'
|
|
||||||
|
|
||||||
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
SESSION_COOKIE_NAME = 'OpenSlidesSessionID'
|
||||||
|
|
||||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
|
@ -71,16 +71,6 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
# No data in range. Just do nothing.
|
# No data in range. Just do nothing.
|
||||||
pass
|
pass
|
||||||
motion = full_copy
|
motion = full_copy
|
||||||
|
|
||||||
# Now filter personal notes.
|
|
||||||
motion = motion.copy()
|
|
||||||
motion['personal_notes'] = []
|
|
||||||
if user is not None:
|
|
||||||
for personal_note in full.get('personal_notes', []):
|
|
||||||
if personal_note.get('user_id') == user.id:
|
|
||||||
motion['personal_notes'].append(personal_note)
|
|
||||||
break
|
|
||||||
|
|
||||||
data.append(motion)
|
data.append(motion)
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
@ -50,8 +50,7 @@ class MotionManager(models.Manager):
|
|||||||
'attachments',
|
'attachments',
|
||||||
'tags',
|
'tags',
|
||||||
'submitters',
|
'submitters',
|
||||||
'supporters',
|
'supporters'))
|
||||||
'personal_notes'))
|
|
||||||
|
|
||||||
|
|
||||||
class Motion(RESTModelMixin, models.Model):
|
class Motion(RESTModelMixin, models.Model):
|
||||||
@ -171,8 +170,6 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
Configurable fields for comments. Contains a list of strings.
|
Configurable fields for comments. Contains a list of strings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
personal_notes = GenericRelation(settings.AUTH_PERSONAL_NOTE_MODEL, related_name='motions')
|
|
||||||
|
|
||||||
# In theory there could be one then more agenda_item. But we support only
|
# In theory there could be one then more agenda_item. But we support only
|
||||||
# one. See the property agenda_item.
|
# one. See the property agenda_item.
|
||||||
agenda_items = GenericRelation(Item, related_name='motions')
|
agenda_items = GenericRelation(Item, related_name='motions')
|
||||||
@ -642,14 +639,6 @@ class Motion(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.agenda_item.pk
|
return self.agenda_item.pk
|
||||||
|
|
||||||
def set_personal_note(self, user, note=None, star=None, skip_autoupdate=False):
|
|
||||||
"""
|
|
||||||
Saves or overrides a personal note to this motion for a given user.
|
|
||||||
"""
|
|
||||||
user.set_personal_note(self, note, star)
|
|
||||||
if not skip_autoupdate:
|
|
||||||
inform_changed_data(self)
|
|
||||||
|
|
||||||
def write_log(self, message_list, person=None, skip_autoupdate=False):
|
def write_log(self, message_list, person=None, skip_autoupdate=False):
|
||||||
"""
|
"""
|
||||||
Write a log message.
|
Write a log message.
|
||||||
|
@ -269,7 +269,6 @@ class MotionSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
active_version = PrimaryKeyRelatedField(read_only=True)
|
active_version = PrimaryKeyRelatedField(read_only=True)
|
||||||
comments = MotionCommentsJSONSerializerField(required=False)
|
comments = MotionCommentsJSONSerializerField(required=False)
|
||||||
personal_notes = SerializerMethodField()
|
|
||||||
log_messages = MotionLogSerializer(many=True, read_only=True)
|
log_messages = MotionLogSerializer(many=True, read_only=True)
|
||||||
polls = MotionPollSerializer(many=True, read_only=True)
|
polls = MotionPollSerializer(many=True, read_only=True)
|
||||||
reason = CharField(allow_blank=True, required=False, write_only=True)
|
reason = CharField(allow_blank=True, required=False, write_only=True)
|
||||||
@ -300,7 +299,6 @@ class MotionSerializer(ModelSerializer):
|
|||||||
'submitters',
|
'submitters',
|
||||||
'supporters',
|
'supporters',
|
||||||
'comments',
|
'comments',
|
||||||
'personal_notes',
|
|
||||||
'state',
|
'state',
|
||||||
'state_required_permission_to_see',
|
'state_required_permission_to_see',
|
||||||
'workflow_id',
|
'workflow_id',
|
||||||
@ -387,12 +385,6 @@ class MotionSerializer(ModelSerializer):
|
|||||||
|
|
||||||
return motion
|
return motion
|
||||||
|
|
||||||
def get_personal_notes(self, motion):
|
|
||||||
"""
|
|
||||||
Returns the personal notes of all users.
|
|
||||||
"""
|
|
||||||
return [personal_note.get_data() for personal_note in motion.personal_notes.all()]
|
|
||||||
|
|
||||||
def get_state_required_permission_to_see(self, motion):
|
def get_state_required_permission_to_see(self, motion):
|
||||||
"""
|
"""
|
||||||
Returns the permission (as string) that is required for non
|
Returns the permission (as string) that is required for non
|
||||||
|
@ -63,7 +63,7 @@ class MotionViewSet(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 ('metadata', 'partial_update', 'update', 'set_personal_note'):
|
elif self.action in ('metadata', 'partial_update', 'update'):
|
||||||
result = has_perm(self.request.user, 'motions.can_see')
|
result = has_perm(self.request.user, 'motions.can_see')
|
||||||
# For partial_update and update requests the rest of the check is
|
# For partial_update and update requests the rest of the check is
|
||||||
# done in the update method. See below.
|
# done in the update method. See below.
|
||||||
@ -372,20 +372,6 @@ class MotionViewSet(ModelViewSet):
|
|||||||
person=request.user)
|
person=request.user)
|
||||||
return Response({'detail': message})
|
return Response({'detail': message})
|
||||||
|
|
||||||
@detail_route(methods=['put'])
|
|
||||||
def set_personal_note(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
Special view endpoint to save a personal note to a motion.
|
|
||||||
|
|
||||||
Send PUT with {'note': <note>, 'star': True|False}.
|
|
||||||
"""
|
|
||||||
motion = self.get_object()
|
|
||||||
if not request.user.is_authenticated():
|
|
||||||
raise ValidationError({'detail': _('Anonymous users are not able to set personal notes.')})
|
|
||||||
motion.set_personal_note(request.user, request.data.get('note'), bool(request.data.get('star')))
|
|
||||||
message = _('Your personal note was successfully saved.')
|
|
||||||
return Response({'detail': message})
|
|
||||||
|
|
||||||
@detail_route(methods=['post'])
|
@detail_route(methods=['post'])
|
||||||
def create_poll(self, request, pk=None):
|
def create_poll(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
|
@ -136,3 +136,51 @@ class GroupAccessPermissions(BaseAccessPermissions):
|
|||||||
from .serializers import GroupSerializer
|
from .serializers import GroupSerializer
|
||||||
|
|
||||||
return GroupSerializer
|
return GroupSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNoteAccessPermissions(BaseAccessPermissions):
|
||||||
|
"""
|
||||||
|
Access permissions container for personal notes. Every authenticated user
|
||||||
|
can handle personal notes.
|
||||||
|
"""
|
||||||
|
def check_permissions(self, user):
|
||||||
|
"""
|
||||||
|
Returns True if the user has read access model instances.
|
||||||
|
"""
|
||||||
|
# Every authenticated user can retrieve personal notes.
|
||||||
|
return not isinstance(user, AnonymousUser)
|
||||||
|
|
||||||
|
def get_serializer_class(self, user=None):
|
||||||
|
"""
|
||||||
|
Returns serializer class.
|
||||||
|
"""
|
||||||
|
from .serializers import PersonalNoteSerializer
|
||||||
|
|
||||||
|
return PersonalNoteSerializer
|
||||||
|
|
||||||
|
def get_restricted_data(self, container, user):
|
||||||
|
"""
|
||||||
|
Returns the restricted serialized data for the instance prepared
|
||||||
|
for the user. Everybody gets only his own personal notes.
|
||||||
|
"""
|
||||||
|
# Expand full_data to a list if it is not one.
|
||||||
|
full_data = container.get_full_data() if isinstance(container, Collection) else [container.get_full_data()]
|
||||||
|
|
||||||
|
# Parse data.
|
||||||
|
for full in full_data:
|
||||||
|
if full['user_id'] == user.id:
|
||||||
|
data = [full]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
data = []
|
||||||
|
|
||||||
|
# Reduce result to a single item or None if it was not a collection at
|
||||||
|
# the beginning of the method.
|
||||||
|
if isinstance(container, Collection):
|
||||||
|
restricted_data = data
|
||||||
|
elif data:
|
||||||
|
restricted_data = data[0]
|
||||||
|
else:
|
||||||
|
restricted_data = None
|
||||||
|
|
||||||
|
return restricted_data
|
||||||
|
@ -20,7 +20,7 @@ class UsersAppConfig(AppConfig):
|
|||||||
from ..utils.rest_api import router
|
from ..utils.rest_api import router
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
from .signals import create_builtin_groups_and_admin, get_permission_change_data
|
from .signals import create_builtin_groups_and_admin, get_permission_change_data
|
||||||
from .views import GroupViewSet, UserViewSet
|
from .views import GroupViewSet, PersonalNoteViewSet, UserViewSet
|
||||||
|
|
||||||
# Define config variables
|
# Define config variables
|
||||||
config.update_config_variables(get_config_variables())
|
config.update_config_variables(get_config_variables())
|
||||||
@ -36,11 +36,12 @@ class UsersAppConfig(AppConfig):
|
|||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('User').get_collection_string(), UserViewSet)
|
router.register(self.get_model('User').get_collection_string(), UserViewSet)
|
||||||
router.register(self.get_model('Group').get_collection_string(), GroupViewSet)
|
router.register(self.get_model('Group').get_collection_string(), GroupViewSet)
|
||||||
|
router.register(self.get_model('PersonalNote').get_collection_string(), PersonalNoteViewSet)
|
||||||
|
|
||||||
def get_startup_elements(self):
|
def get_startup_elements(self):
|
||||||
"""
|
"""
|
||||||
Yields all collections required on startup i. e. opening the websocket
|
Yields all collections required on startup i. e. opening the websocket
|
||||||
connection.
|
connection.
|
||||||
"""
|
"""
|
||||||
for model in ('User', 'Group'):
|
for model in ('User', 'Group', 'PersonalNote'):
|
||||||
yield Collection(self.get_model(model).get_collection_string())
|
yield Collection(self.get_model(model).get_collection_string())
|
||||||
|
43
openslides/users/migrations/0005_personalnote_rework.py
Normal file
43
openslides/users/migrations/0005_personalnote_rework.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-05-23 11:25
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import jsonfield.fields
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0004_personalnote'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='personalnote',
|
||||||
|
name='content_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='personalnote',
|
||||||
|
name='user',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='PersonalNote',
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PersonalNote',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('notes', jsonfield.fields.JSONField()),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'default_permissions': (),
|
||||||
|
},
|
||||||
|
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||||
|
),
|
||||||
|
]
|
@ -9,47 +9,17 @@ from django.contrib.auth.models import (
|
|||||||
Permission,
|
Permission,
|
||||||
PermissionsMixin,
|
PermissionsMixin,
|
||||||
)
|
)
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Prefetch, Q
|
from django.db.models import Prefetch, Q
|
||||||
|
from jsonfield import JSONField
|
||||||
|
|
||||||
from ..utils.collection import CollectionElement
|
from ..utils.collection import CollectionElement
|
||||||
from ..utils.models import RESTModelMixin
|
from ..utils.models import RESTModelMixin
|
||||||
from .access_permissions import GroupAccessPermissions, UserAccessPermissions
|
from .access_permissions import (
|
||||||
|
GroupAccessPermissions,
|
||||||
|
PersonalNoteAccessPermissions,
|
||||||
class PersonalNote(models.Model):
|
UserAccessPermissions,
|
||||||
"""
|
)
|
||||||
Model for personal notes and likes (stars) of a user concerning different
|
|
||||||
openslides models like motions.
|
|
||||||
|
|
||||||
To use this in your app simply run e. g.
|
|
||||||
|
|
||||||
user.set_personal_note(motion, note, star)
|
|
||||||
|
|
||||||
in a setter view and add a SerializerMethodField to your serializer that
|
|
||||||
calls get_data for all users.
|
|
||||||
"""
|
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name='personal_notes')
|
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
||||||
object_id = models.PositiveIntegerField()
|
|
||||||
content_object = GenericForeignKey()
|
|
||||||
note = models.TextField(blank=True)
|
|
||||||
star = models.BooleanField(default=False, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
default_permissions = ()
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
"""
|
|
||||||
Returns note and star to be serialized in content object serializers.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'user_id': self.user_id,
|
|
||||||
'note': self.note,
|
|
||||||
'star': self.star,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
@ -246,29 +216,6 @@ class User(RESTModelMixin, PermissionsMixin, AbstractBaseUser):
|
|||||||
"""
|
"""
|
||||||
raise RuntimeError('Do not use user.has_perm() but use openslides.utils.auth.has_perm')
|
raise RuntimeError('Do not use user.has_perm() but use openslides.utils.auth.has_perm')
|
||||||
|
|
||||||
def set_personal_note(self, content_object, note=None, star=None):
|
|
||||||
"""
|
|
||||||
Saves or overrides a personal note for this user for a given object
|
|
||||||
like motion.
|
|
||||||
"""
|
|
||||||
changes = {}
|
|
||||||
if note is not None:
|
|
||||||
changes['note'] = note
|
|
||||||
if star is not None:
|
|
||||||
changes['star'] = star
|
|
||||||
if changes:
|
|
||||||
# TODO: This is prone to race-conditions in rare cases. Fix it.
|
|
||||||
personal_note, created = PersonalNote.objects.update_or_create(
|
|
||||||
user=self,
|
|
||||||
content_type=ContentType.objects.get_for_model(content_object),
|
|
||||||
object_id=content_object.id,
|
|
||||||
user_id=self.id,
|
|
||||||
defaults=changes,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
personal_note = None
|
|
||||||
return personal_note
|
|
||||||
|
|
||||||
|
|
||||||
class GroupManager(GroupManager):
|
class GroupManager(GroupManager):
|
||||||
"""
|
"""
|
||||||
@ -295,3 +242,33 @@ class Group(RESTModelMixin, DjangoGroup):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNoteManager(models.Manager):
|
||||||
|
"""
|
||||||
|
Customized model manager to support our get_full_queryset method.
|
||||||
|
"""
|
||||||
|
def get_full_queryset(self):
|
||||||
|
"""
|
||||||
|
Returns the normal queryset with all personal notes. In the background all
|
||||||
|
users are prefetched from the database.
|
||||||
|
"""
|
||||||
|
return self.get_queryset().select_related('user')
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNote(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for personal notes (e. g. likes/stars) of a user concerning different
|
||||||
|
openslides objects like motions.
|
||||||
|
"""
|
||||||
|
access_permissions = PersonalNoteAccessPermissions()
|
||||||
|
|
||||||
|
objects = PersonalNoteManager()
|
||||||
|
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
notes = JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
@ -6,11 +6,12 @@ from django.utils.translation import ugettext_lazy
|
|||||||
from ..utils.autoupdate import inform_changed_data
|
from ..utils.autoupdate import inform_changed_data
|
||||||
from ..utils.rest_api import (
|
from ..utils.rest_api import (
|
||||||
IdPrimaryKeyRelatedField,
|
IdPrimaryKeyRelatedField,
|
||||||
|
JSONField,
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
RelatedField,
|
RelatedField,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
)
|
)
|
||||||
from .models import Group, User
|
from .models import Group, PersonalNote, User
|
||||||
|
|
||||||
USERCANSEESERIALIZER_FIELDS = (
|
USERCANSEESERIALIZER_FIELDS = (
|
||||||
'id',
|
'id',
|
||||||
@ -147,3 +148,15 @@ class GroupSerializer(ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
instance = super().update(*args, **kwargs)
|
instance = super().update(*args, **kwargs)
|
||||||
return Group.objects.get(pk=instance.pk)
|
return Group.objects.get(pk=instance.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNoteSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for users.models.PersonalNote objects.
|
||||||
|
"""
|
||||||
|
notes = JSONField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PersonalNote
|
||||||
|
fields = ('id', 'user', 'notes', )
|
||||||
|
read_only_fields = ('user', )
|
||||||
|
@ -26,8 +26,12 @@ from ..utils.rest_api import (
|
|||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
from ..utils.views import APIView
|
from ..utils.views import APIView
|
||||||
from .access_permissions import GroupAccessPermissions, UserAccessPermissions
|
from .access_permissions import (
|
||||||
from .models import Group, User
|
GroupAccessPermissions,
|
||||||
|
PersonalNoteAccessPermissions,
|
||||||
|
UserAccessPermissions,
|
||||||
|
)
|
||||||
|
from .models import Group, PersonalNote, User
|
||||||
from .serializers import GroupSerializer, PermissionRelatedField
|
from .serializers import GroupSerializer, PermissionRelatedField
|
||||||
|
|
||||||
|
|
||||||
@ -268,6 +272,57 @@ class GroupViewSet(ModelViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNoteViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for personal notes.
|
||||||
|
|
||||||
|
There are the following views: metadata, list, retrieve, create,
|
||||||
|
partial_update, update, and destroy.
|
||||||
|
"""
|
||||||
|
access_permissions = PersonalNoteAccessPermissions()
|
||||||
|
queryset = PersonalNote.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
|
elif self.action in ('metadata', 'create', 'partial_update', 'update', 'destroy'):
|
||||||
|
# Every authenticated user can see metadata and create personal
|
||||||
|
# notes for himself and can manipulate only his own personal notes.
|
||||||
|
# See self.perform_create(), self.update() and self.destroy().
|
||||||
|
result = self.request.user.is_authenticated()
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
"""
|
||||||
|
Customized method to inject the request.user into serializer's save
|
||||||
|
method so that the request.user can be saved into the model field.
|
||||||
|
"""
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Customized method to ensure that every user can change only his own
|
||||||
|
personal notes.
|
||||||
|
"""
|
||||||
|
if self.get_object().user != self.request.user:
|
||||||
|
self.permission_denied(request)
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Customized method to ensure that every user can delete only his own
|
||||||
|
personal notes.
|
||||||
|
"""
|
||||||
|
if self.get_object().user != self.request.user:
|
||||||
|
self.permission_denied(request)
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Special API views
|
# Special API views
|
||||||
|
|
||||||
class UserLoginView(APIView):
|
class UserLoginView(APIView):
|
||||||
|
@ -17,6 +17,7 @@ from rest_framework.serializers import ( # noqa
|
|||||||
Field,
|
Field,
|
||||||
FileField,
|
FileField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
|
JSONField,
|
||||||
ListField,
|
ListField,
|
||||||
ListSerializer,
|
ListSerializer,
|
||||||
ManyRelatedField,
|
ManyRelatedField,
|
||||||
|
@ -42,11 +42,10 @@ class TestMotionDBQueries(TestCase):
|
|||||||
* 1 request to get the polls,
|
* 1 request to get the polls,
|
||||||
* 1 request to get the attachments,
|
* 1 request to get the attachments,
|
||||||
* 1 request to get the tags,
|
* 1 request to get the tags,
|
||||||
* 2 requests to get the submitters and supporters,
|
* 2 requests to get the submitters and supporters.
|
||||||
* 1 requests to get the personal notes.
|
|
||||||
"""
|
"""
|
||||||
self.client.force_login(get_user_model().objects.get(pk=1))
|
self.client.force_login(get_user_model().objects.get(pk=1))
|
||||||
with self.assertNumQueries(15):
|
with self.assertNumQueries(14):
|
||||||
self.client.get(reverse('motion-list'))
|
self.client.get(reverse('motion-list'))
|
||||||
|
|
||||||
@use_cache()
|
@use_cache()
|
||||||
@ -61,10 +60,9 @@ class TestMotionDBQueries(TestCase):
|
|||||||
* 1 request to get the polls,
|
* 1 request to get the polls,
|
||||||
* 1 request to get the attachments,
|
* 1 request to get the attachments,
|
||||||
* 1 request to get the tags,
|
* 1 request to get the tags,
|
||||||
* 2 requests to get the submitters and supporters,
|
* 2 requests to get the submitters and supporters.
|
||||||
* 1 request to get the personal notes.
|
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(14):
|
with self.assertNumQueries(13):
|
||||||
self.client.get(reverse('motion-list'))
|
self.client.get(reverse('motion-list'))
|
||||||
|
|
||||||
|
|
||||||
@ -400,12 +398,10 @@ class RetrieveMotion(TestCase):
|
|||||||
* 3 request to get the polls (1 of them is possibly a bug),
|
* 3 request to get the polls (1 of them is possibly a bug),
|
||||||
* 1 request to get the attachments,
|
* 1 request to get the attachments,
|
||||||
* 1 request to get the tags,
|
* 1 request to get the tags,
|
||||||
* 2 requests to get the submitters and supporters,
|
* 2 requests to get the submitters and supporters.
|
||||||
* 1 request to get personal notes.
|
|
||||||
|
|
||||||
TODO: Fix all bugs.
|
TODO: Fix all bugs.
|
||||||
"""
|
"""
|
||||||
with self.assertNumQueries(19):
|
with self.assertNumQueries(18):
|
||||||
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
|
self.client.get(reverse('motion-detail', args=[self.motion.pk]))
|
||||||
|
|
||||||
def test_guest_state_with_required_permission_to_see(self):
|
def test_guest_state_with_required_permission_to_see(self):
|
||||||
@ -458,14 +454,6 @@ class RetrieveMotion(TestCase):
|
|||||||
response_3 = guest_client.get(reverse('user-detail', args=[extra_user.pk]))
|
response_3 = guest_client.get(reverse('user-detail', args=[extra_user.pk]))
|
||||||
self.assertEqual(response_3.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response_3.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
def test_anonymous_without_personal_notes(self):
|
|
||||||
self.motion.set_personal_note(get_user_model().objects.get(pk=1), note='admin_personal_note_OoGh8choro0oosh0roob')
|
|
||||||
config['general_system_enable_anonymous'] = True
|
|
||||||
guest_client = APIClient()
|
|
||||||
response = guest_client.get(reverse('motion-detail', args=[self.motion.pk]))
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
||||||
self.assertNotContains(response, 'admin_personal_note_OoGh8choro0oosh0roob')
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateMotion(TestCase):
|
class UpdateMotion(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -3,7 +3,7 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.users.models import Group, User
|
from openslides.users.models import Group, PersonalNote, User
|
||||||
from openslides.users.serializers import UserFullSerializer
|
from openslides.users.serializers import UserFullSerializer
|
||||||
from openslides.utils.test import TestCase, use_cache
|
from openslides.utils.test import TestCase, use_cache
|
||||||
|
|
||||||
@ -540,3 +540,35 @@ class GroupDelete(TestCase):
|
|||||||
|
|
||||||
response = admin_client.delete(reverse('group-detail', args=[group_pk]))
|
response = admin_client.delete(reverse('group-detail', args=[group_pk]))
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalNoteTest(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for PersonalNote model.
|
||||||
|
"""
|
||||||
|
def test_anonymous_without_personal_notes(self):
|
||||||
|
admin = User.objects.get(pk=1)
|
||||||
|
personal_note = PersonalNote.objects.create(user=admin, notes='["admin_personal_note_OoGh8choro0oosh0roob"]')
|
||||||
|
config['general_system_enable_anonymous'] = True
|
||||||
|
guest_client = APIClient()
|
||||||
|
response = guest_client.get(reverse('personalnote-detail', args=[personal_note.pk]))
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_admin_send_JSON(self):
|
||||||
|
admin_client = APIClient()
|
||||||
|
admin_client.login(username='admin', password='admin')
|
||||||
|
response = admin_client.post(
|
||||||
|
reverse('personalnote-list'),
|
||||||
|
{
|
||||||
|
"notes": {
|
||||||
|
"example-model": {
|
||||||
|
"1": {
|
||||||
|
"note": "note for the example.model with id 1 Oohae1JeuSedooyeeviH",
|
||||||
|
"star": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
Loading…
Reference in New Issue
Block a user