Merge pull request #1442 from normanjaeckel/TornadoAutoupdate
Rework of tornado autoupdate functionality
This commit is contained in:
commit
e92666d473
@ -1,11 +1,11 @@
|
||||
from rest_framework.reverse import reverse
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from openslides.utils.rest_api import serializers
|
||||
from openslides.utils.rest_api import get_collection_and_id_from_url, serializers
|
||||
|
||||
from .models import Item, Speaker
|
||||
|
||||
|
||||
class SpeakerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class SpeakerSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for agenda.models.Speaker objects.
|
||||
"""
|
||||
@ -21,22 +21,20 @@ class SpeakerSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class RelatedItemRelatedField(serializers.RelatedField):
|
||||
"""
|
||||
A custom field to use for the `content_object` generic relationship.
|
||||
A custom field to use for the content_object generic relationship.
|
||||
"""
|
||||
def to_representation(self, value):
|
||||
"""
|
||||
Returns the url to the related object.
|
||||
Returns info concerning the related object extracted from the api URL
|
||||
of this object.
|
||||
"""
|
||||
request = self.context.get('request', None)
|
||||
assert request is not None, (
|
||||
"`%s` requires the request in the serializer"
|
||||
" context. Add `context={'request': request}` when instantiating "
|
||||
"the serializer." % self.__class__.__name__)
|
||||
view_name = '%s-detail' % type(value)._meta.object_name.lower()
|
||||
return reverse(view_name, kwargs={'pk': value.pk}, request=request)
|
||||
url = reverse(view_name, kwargs={'pk': value.pk})
|
||||
collection, obj_id = get_collection_and_id_from_url(url)
|
||||
return {'collection': collection, 'id': obj_id}
|
||||
|
||||
|
||||
class ItemSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class ItemSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for agenda.models.Item objects.
|
||||
"""
|
||||
@ -49,7 +47,7 @@ class ItemSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = (
|
||||
'url',
|
||||
'id',
|
||||
'item_number',
|
||||
'item_no',
|
||||
'title',
|
||||
|
@ -9,7 +9,7 @@ from .models import (
|
||||
AssignmentVote)
|
||||
|
||||
|
||||
class AssignmentCandidateSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AssignmentCandidateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentCandidate objects.
|
||||
"""
|
||||
@ -19,21 +19,19 @@ class AssignmentCandidateSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'id',
|
||||
'person',
|
||||
'elected',
|
||||
'blocked')
|
||||
'blocked',)
|
||||
|
||||
|
||||
class AssignmentVoteSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AssignmentVoteSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentVote objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = AssignmentVote
|
||||
fields = (
|
||||
'weight',
|
||||
'value')
|
||||
fields = ('weight', 'value',)
|
||||
|
||||
|
||||
class AssignmentOptionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AssignmentOptionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentOption objects.
|
||||
"""
|
||||
@ -41,9 +39,7 @@ class AssignmentOptionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AssignmentOption
|
||||
fields = (
|
||||
'candidate',
|
||||
'assignmentvote_set')
|
||||
fields = ('candidate', 'assignmentvote_set',)
|
||||
|
||||
|
||||
class FilterPollListSerializer(serializers.ListSerializer):
|
||||
@ -62,7 +58,7 @@ class FilterPollListSerializer(serializers.ListSerializer):
|
||||
return [self.child.to_representation(item) for item in iterable]
|
||||
|
||||
|
||||
class AssignmentAllPollSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AssignmentAllPollSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.AssignmentPoll objects.
|
||||
|
||||
@ -80,7 +76,7 @@ class AssignmentAllPollSerializer(serializers.HyperlinkedModelSerializer):
|
||||
'assignmentoption_set',
|
||||
'votesvalid',
|
||||
'votesinvalid',
|
||||
'votescast')
|
||||
'votescast',)
|
||||
|
||||
|
||||
class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
|
||||
@ -100,10 +96,10 @@ class AssignmentShortPollSerializer(AssignmentAllPollSerializer):
|
||||
'assignmentoption_set',
|
||||
'votesvalid',
|
||||
'votesinvalid',
|
||||
'votescast')
|
||||
'votescast',)
|
||||
|
||||
|
||||
class AssignmentFullSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class AssignmentFullSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for assignment.models.Assignment objects. With all polls.
|
||||
"""
|
||||
@ -113,7 +109,7 @@ class AssignmentFullSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Assignment
|
||||
fields = (
|
||||
'url',
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'posts',
|
||||
@ -133,7 +129,7 @@ class AssignmentShortSerializer(AssignmentFullSerializer):
|
||||
class Meta:
|
||||
model = Assignment
|
||||
fields = (
|
||||
'url',
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'posts',
|
||||
|
@ -3,19 +3,19 @@ from openslides.utils.rest_api import serializers
|
||||
from .models import CustomSlide, Tag
|
||||
|
||||
|
||||
class CustomSlideSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class CustomSlideSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.CustomSlide objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = CustomSlide
|
||||
fields = ('url', 'title', 'text', 'weight',)
|
||||
fields = ('id', 'title', 'text', 'weight',)
|
||||
|
||||
|
||||
class TagSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class TagSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for core.models.Tag objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ('url', 'name',)
|
||||
fields = ('id', 'name',)
|
||||
|
@ -170,10 +170,13 @@ CKEDITOR_CONFIGS = {
|
||||
}
|
||||
|
||||
|
||||
# Use small alternative with tornado as frontend or big alternative with a
|
||||
# webserver as wsgi server.
|
||||
# Set this True to use tornado as single wsgi server. Set this False to use
|
||||
# other webserver like Apache or Nginx as wsgi server.
|
||||
USE_TORNADO_AS_WSGI_SERVER = True
|
||||
|
||||
OPENSLIDES_WSGI_NETWORK_LOCATION = ''
|
||||
|
||||
|
||||
TEST_RUNNER = 'openslides.utils.test.OpenSlidesDiscoverRunner'
|
||||
|
||||
# Config for the REST Framework
|
||||
|
@ -3,7 +3,7 @@ from openslides.utils.rest_api import serializers
|
||||
from .models import Mediafile
|
||||
|
||||
|
||||
class MediafileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class MediafileSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for mediafile.models.Mediafile objects.
|
||||
"""
|
||||
@ -11,6 +11,15 @@ class MediafileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Mediafile
|
||||
fields = (
|
||||
'id',
|
||||
'title',
|
||||
'mediafile',
|
||||
'uploader',
|
||||
'filesize',
|
||||
'filetype',
|
||||
'timestamp',
|
||||
'is_presentable',)
|
||||
|
||||
def get_filesize(self, mediafile):
|
||||
return mediafile.get_filesize()
|
||||
|
@ -1,5 +1,3 @@
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from openslides.utils.rest_api import serializers
|
||||
|
||||
from .models import (
|
||||
@ -16,13 +14,13 @@ from .models import (
|
||||
Workflow,)
|
||||
|
||||
|
||||
class CategorySerializer(serializers.HyperlinkedModelSerializer):
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Category objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = ('url', 'name', 'prefix',)
|
||||
fields = ('id', 'name', 'prefix',)
|
||||
|
||||
|
||||
class StateSerializer(serializers.ModelSerializer):
|
||||
@ -46,7 +44,7 @@ class StateSerializer(serializers.ModelSerializer):
|
||||
'next_states',)
|
||||
|
||||
|
||||
class WorkflowSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class WorkflowSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Workflow objects.
|
||||
"""
|
||||
@ -55,10 +53,10 @@ class WorkflowSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ('url', 'name', 'state_set', 'first_state',)
|
||||
fields = ('id', 'name', 'state_set', 'first_state',)
|
||||
|
||||
|
||||
class MotionSubmitterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class MotionSubmitterSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionSubmitter objects.
|
||||
"""
|
||||
@ -67,7 +65,7 @@ class MotionSubmitterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
fields = ('person',) # TODO: Rename this to 'user', see #1348
|
||||
|
||||
|
||||
class MotionSupporterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class MotionSupporterSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionSupporter objects.
|
||||
"""
|
||||
@ -76,7 +74,7 @@ class MotionSupporterSerializer(serializers.HyperlinkedModelSerializer):
|
||||
fields = ('person',) # TODO: Rename this to 'user', see #1348
|
||||
|
||||
|
||||
class MotionLogSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class MotionLogSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionLog objects.
|
||||
"""
|
||||
@ -136,7 +134,7 @@ class MotionVersionSerializer(serializers.ModelSerializer):
|
||||
'reason',)
|
||||
|
||||
|
||||
class MotionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class MotionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Motion objects.
|
||||
"""
|
||||
@ -152,7 +150,7 @@ class MotionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = Motion
|
||||
fields = (
|
||||
'url',
|
||||
'id',
|
||||
'identifier',
|
||||
'identifier_number',
|
||||
'parent',
|
||||
@ -170,11 +168,6 @@ class MotionSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
def get_workflow(self, motion):
|
||||
"""
|
||||
Returns the hyperlink to the workflow of the motion.
|
||||
Returns the id of the workflow of the motion.
|
||||
"""
|
||||
request = self.context.get('request', None)
|
||||
assert request is not None, (
|
||||
"`%s` requires the request in the serializer"
|
||||
" context. Add `context={'request': request}` when instantiating "
|
||||
"the serializer." % self.__class__.__name__)
|
||||
return reverse('workflow-detail', kwargs={'pk': motion.state.workflow.pk}, request=request)
|
||||
return motion.state.workflow.pk
|
||||
|
@ -16,7 +16,7 @@ class UsersAppConfig(AppConfig):
|
||||
from openslides.projector.api import register_slide_model
|
||||
from openslides.utils.rest_api import router
|
||||
from .signals import setup_users_config, user_post_save
|
||||
from .views import UserViewSet
|
||||
from .views import GroupViewSet, UserViewSet
|
||||
|
||||
# Load User model.
|
||||
User = self.get_model('User')
|
||||
@ -30,3 +30,4 @@ class UsersAppConfig(AppConfig):
|
||||
|
||||
# Register viewsets.
|
||||
router.register('users/user', UserViewSet)
|
||||
router.register('users/group', GroupViewSet)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from openslides.utils.rest_api import serializers
|
||||
|
||||
from .models import User
|
||||
from .models import Group, User # TODO: Don't import Group from models but from core.models.
|
||||
|
||||
|
||||
class UserShortSerializer(serializers.ModelSerializer):
|
||||
@ -12,12 +12,13 @@ class UserShortSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'url',
|
||||
'id',
|
||||
'username',
|
||||
'title',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'structure_level')
|
||||
'structure_level',
|
||||
'groups',)
|
||||
|
||||
|
||||
class UserFullSerializer(serializers.ModelSerializer):
|
||||
@ -29,7 +30,7 @@ class UserFullSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = (
|
||||
'url',
|
||||
'id',
|
||||
'is_present',
|
||||
'username',
|
||||
'title',
|
||||
@ -38,5 +39,32 @@ class UserFullSerializer(serializers.ModelSerializer):
|
||||
'structure_level',
|
||||
'about_me',
|
||||
'comment',
|
||||
'groups',
|
||||
'default_password',
|
||||
'is_active')
|
||||
'last_login',
|
||||
'is_active',)
|
||||
|
||||
|
||||
class PermissionRelatedField(serializers.RelatedField):
|
||||
"""
|
||||
A custom field to use for the permission relationship.
|
||||
"""
|
||||
def to_representation(self, value):
|
||||
"""
|
||||
Returns the permission name (app_label.codename).
|
||||
"""
|
||||
return '.'.join((value.content_type.app_label, value.codename,))
|
||||
|
||||
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer for django.contrib.auth.models.Group objects.
|
||||
"""
|
||||
permissions = PermissionRelatedField(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = (
|
||||
'id',
|
||||
'name',
|
||||
'permissions',)
|
||||
|
@ -20,7 +20,7 @@ from .forms import (GroupForm, UserCreateForm, UserMultipleCreateForm,
|
||||
UsersettingsForm, UserUpdateForm)
|
||||
from .models import Group, User
|
||||
from .pdf import users_to_pdf, users_passwords_to_pdf
|
||||
from .serializers import UserFullSerializer, UserShortSerializer
|
||||
from .serializers import GroupSerializer, UserFullSerializer, UserShortSerializer
|
||||
|
||||
|
||||
class UserListView(ListView):
|
||||
@ -263,7 +263,7 @@ class ResetPasswordView(SingleObjectMixin, QuestionView):
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint to list, retrive, create, update and delete users.
|
||||
API endpoint to list, retrieve, create, update and delete users.
|
||||
"""
|
||||
model = User
|
||||
queryset = User.objects.all()
|
||||
@ -291,6 +291,27 @@ class UserViewSet(viewsets.ModelViewSet):
|
||||
return serializer_class
|
||||
|
||||
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint to list, retrieve, create, update and delete groups.
|
||||
"""
|
||||
model = Group
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
|
||||
def check_permissions(self, request):
|
||||
"""
|
||||
Calls self.permission_denied() if the requesting user has not the
|
||||
permission to see users and in case of create, update or destroy
|
||||
requests the permission to see extra user data and to manage users.
|
||||
"""
|
||||
if (not request.user.has_perm('users.can_see_name') or
|
||||
(self.action in ('create', 'update', 'destroy') and not
|
||||
(request.user.has_perm('users.can_manage') and
|
||||
request.user.has_perm('users.can_see_extra_data')))):
|
||||
self.permission_denied(request)
|
||||
|
||||
|
||||
class GroupListView(ListView):
|
||||
"""
|
||||
Overview over all groups.
|
||||
|
@ -1,3 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
from urllib.parse import unquote
|
||||
@ -18,8 +19,7 @@ from tornado.web import (
|
||||
)
|
||||
from tornado.wsgi import WSGIContainer
|
||||
|
||||
REST_URL = 'http://localhost:8000'
|
||||
# TODO: this is propably in the config
|
||||
from .rest_api import get_collection_and_id_from_url
|
||||
|
||||
|
||||
class DjangoStaticFileHandler(StaticFileHandler):
|
||||
@ -58,56 +58,62 @@ class DjangoStaticFileHandler(StaticFileHandler):
|
||||
|
||||
class OpenSlidesSockJSConnection(SockJSConnection):
|
||||
"""
|
||||
Sockjs connections for OpenSlides.
|
||||
SockJS connection for OpenSlides.
|
||||
"""
|
||||
waiters = set()
|
||||
|
||||
def on_open(self, request_info):
|
||||
OpenSlidesSockJSConnection.waiters.add(self)
|
||||
self.request_info = request_info
|
||||
def on_open(self, info):
|
||||
self.waiters.add(self)
|
||||
self.connection_info = info
|
||||
|
||||
def on_close(self):
|
||||
OpenSlidesSockJSConnection.waiters.remove(self)
|
||||
|
||||
def handle_rest_request(self, response):
|
||||
def forward_rest_response(self, response):
|
||||
"""
|
||||
Handler that is called when the rest api responds.
|
||||
Sends data to the client of the connection instance.
|
||||
|
||||
Sends the response.body to the client.
|
||||
This method is called after succesful response of AsyncHTTPClient().
|
||||
See send_object().
|
||||
"""
|
||||
# TODO: update cookies
|
||||
if response.code == 200:
|
||||
self.send(response.body)
|
||||
|
||||
@classmethod
|
||||
def send_updates(cls, data):
|
||||
# TODO: use a bluk send
|
||||
for waiter in cls.waiters:
|
||||
waiter.send(data)
|
||||
collection, obj_id = get_collection_and_id_from_url(response.request.url)
|
||||
data = {
|
||||
'url': response.request.url,
|
||||
'status_code': response.code,
|
||||
'collection': collection,
|
||||
'id': obj_id,
|
||||
'data': json.loads(response.body.decode())}
|
||||
self.send(data)
|
||||
|
||||
@classmethod
|
||||
def send_object(cls, object_url):
|
||||
"""
|
||||
Send OpenSlides objects to all connected clients.
|
||||
Sends an OpenSlides object to all connected clients (waiters).
|
||||
|
||||
First, receive the object from the OpenSlides ReST API.
|
||||
First, retrieve the object from the OpenSlides REST api using the given
|
||||
object_url.
|
||||
"""
|
||||
for waiter in cls.waiters:
|
||||
# Get the object from the ReST API
|
||||
http_client = AsyncHTTPClient()
|
||||
headers = HTTPHeaders()
|
||||
# TODO: read to python Morselcookies and why "set-Cookie" does not work
|
||||
request_cookies = waiter.request_info.cookies.values()
|
||||
cookie_value = ';'.join("%s=%s" % (cookie.key, cookie.value)
|
||||
for cookie in request_cookies)
|
||||
headers.parse_line("Cookie: %s" % cookie_value)
|
||||
# Join network location with object URL.
|
||||
# TODO: Use host and port as given in the start script
|
||||
wsgi_network_location = settings.OPENSLIDES_WSGI_NETWORK_LOCATION or 'http://localhost:8000'
|
||||
url = ''.join((wsgi_network_location, object_url))
|
||||
|
||||
# Send out internal HTTP request to get data from the REST api.
|
||||
for waiter in cls.waiters:
|
||||
# Read waiter's former cookies and parse session cookie to new header object.
|
||||
session_cookie = waiter.connection_info.cookies[settings.SESSION_COOKIE_NAME]
|
||||
headers = HTTPHeaders()
|
||||
headers.add('Cookie', '%s=%s' % (settings.SESSION_COOKIE_NAME, session_cookie.value))
|
||||
# Setup uncompressed request.
|
||||
request = HTTPRequest(
|
||||
url=''.join((REST_URL, object_url)),
|
||||
url=url,
|
||||
headers=headers,
|
||||
decompress_response=False)
|
||||
# TODO: use proxy_host as header from waiter.request_info
|
||||
http_client.fetch(request, waiter.handle_rest_request)
|
||||
# Setup non-blocking HTTP client
|
||||
http_client = AsyncHTTPClient()
|
||||
# Executes the request, asynchronously returning an HTTPResponse
|
||||
# and calling waiter's forward_rest_response() method.
|
||||
http_client.fetch(request, waiter.forward_rest_response)
|
||||
|
||||
|
||||
def run_tornado(addr, port, *args, **kwargs):
|
||||
@ -150,7 +156,7 @@ def inform_changed_data(*args):
|
||||
try:
|
||||
rest_urls.add(instance.get_root_rest_url())
|
||||
except AttributeError:
|
||||
# instance has no method get_root_rest_url
|
||||
# Instance has no method get_root_rest_url. Just skip it.
|
||||
pass
|
||||
|
||||
if settings.USE_TORNADO_AS_WSGI_SERVER:
|
||||
@ -158,7 +164,7 @@ def inform_changed_data(*args):
|
||||
OpenSlidesSockJSConnection.send_object(url)
|
||||
else:
|
||||
pass
|
||||
# TODO: fix me
|
||||
# TODO: Implement big varainte with Apache or Nginx as wsgi webserver.
|
||||
|
||||
|
||||
def inform_changed_data_receiver(sender, instance, **kwargs):
|
||||
|
@ -1,6 +1,12 @@
|
||||
import re
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from rest_framework import response, routers, serializers, viewsets # noqa
|
||||
|
||||
from .exceptions import OpenSlidesError
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
|
||||
@ -26,3 +32,20 @@ class RESTModelMixin:
|
||||
root_instance = self.get_root_rest_element()
|
||||
rest_url = '%s-detail' % type(root_instance)._meta.object_name.lower()
|
||||
return reverse(rest_url, args=[str(root_instance.pk)])
|
||||
|
||||
|
||||
def get_collection_and_id_from_url(url):
|
||||
"""
|
||||
Helper function. Returns a tuple containing the collection name and the id
|
||||
extracted out of the given REST api URL.
|
||||
|
||||
For example get_collection_and_id_from_url('http://localhost/api/users/user/3/')
|
||||
returns ('users/user', '3').
|
||||
|
||||
Raises OpenSlidesError if the URL is invalid.
|
||||
"""
|
||||
path = urlparse(url).path
|
||||
match = re.match(r'^/api/(?P<name>[-\w]+/[-\w]+)/(?P<id>[-\w]+)/$', path)
|
||||
if not match:
|
||||
raise OpenSlidesError('Invalid REST api URL: %s' % url)
|
||||
return match.group('name'), match.group('id')
|
||||
|
Loading…
Reference in New Issue
Block a user