django-rest-spreadsheets 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_rest_spreadsheets-1.0.0/LICENCE +7 -0
- django_rest_spreadsheets-1.0.0/MANIFEST.in +2 -0
- django_rest_spreadsheets-1.0.0/PKG-INFO +33 -0
- django_rest_spreadsheets-1.0.0/README.md +14 -0
- django_rest_spreadsheets-1.0.0/django_rest_spreadsheets.egg-info/PKG-INFO +33 -0
- django_rest_spreadsheets-1.0.0/django_rest_spreadsheets.egg-info/SOURCES.txt +38 -0
- django_rest_spreadsheets-1.0.0/django_rest_spreadsheets.egg-info/dependency_links.txt +1 -0
- django_rest_spreadsheets-1.0.0/django_rest_spreadsheets.egg-info/top_level.txt +1 -0
- django_rest_spreadsheets-1.0.0/pyproject.toml +19 -0
- django_rest_spreadsheets-1.0.0/setup.cfg +4 -0
- django_rest_spreadsheets-1.0.0/setup.py +24 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/__init__.py +0 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/fields.py +196 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/migrations/__init__.py +0 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/schema.py +116 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/serializers.py +142 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/jquery-ui.css +1312 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/jquery-ui.js +18706 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/jquery.datetimepicker.css +568 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/jquery.datetimepicker.full.js +2887 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/jquery.datetimepicker.js +2656 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/jquery.datetimepicker.min.css +1 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/deps/moment.js +4506 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/css/menu.css +46 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/css/reset.css +5 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/css/screen.css +40 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/css/spreadsheet.css +246 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/img/cancel.svg +5 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/img/create.svg +3 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/img/delete.svg +3 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/img/icon.png +0 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/img/update.svg +3 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/js/RestApi6.js +61 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/js/ScreenStack.js +25 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/js/SpreadSheet.js +543 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/static/spreadsheet/js/menu.js +12 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/templates/index.html +77 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/urls.py +10 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/util.py +16 -0
- django_rest_spreadsheets-1.0.0/spreadsheet/views.py +137 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Gerben van Eerten
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-rest-spreadsheets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Spreadsheets based on django rest framework
|
|
5
|
+
Home-page: https://github.com/g3rb3n/django-rest-spreadsheets/
|
|
6
|
+
Author: gerben van Eerten
|
|
7
|
+
Author-email: Gerben van Eerten <info@gerbenvaneerten.nl>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/g3rb3n/django-rest-spreadsheets
|
|
10
|
+
Project-URL: Issues, https://github.com/g3rb3n/django-rest-spreadsheets/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENCE
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Introduction
|
|
21
|
+
Spreadsheets based on django models and django rest framework.
|
|
22
|
+
|
|
23
|
+
# Setup
|
|
24
|
+
`make setup`
|
|
25
|
+
|
|
26
|
+
# Run
|
|
27
|
+
`make run`
|
|
28
|
+
|
|
29
|
+
# Open browser
|
|
30
|
+
`make open-browser`
|
|
31
|
+
|
|
32
|
+
# Testing
|
|
33
|
+
`make test`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-rest-spreadsheets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Spreadsheets based on django rest framework
|
|
5
|
+
Home-page: https://github.com/g3rb3n/django-rest-spreadsheets/
|
|
6
|
+
Author: gerben van Eerten
|
|
7
|
+
Author-email: Gerben van Eerten <info@gerbenvaneerten.nl>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/g3rb3n/django-rest-spreadsheets
|
|
10
|
+
Project-URL: Issues, https://github.com/g3rb3n/django-rest-spreadsheets/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENCE
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Introduction
|
|
21
|
+
Spreadsheets based on django models and django rest framework.
|
|
22
|
+
|
|
23
|
+
# Setup
|
|
24
|
+
`make setup`
|
|
25
|
+
|
|
26
|
+
# Run
|
|
27
|
+
`make run`
|
|
28
|
+
|
|
29
|
+
# Open browser
|
|
30
|
+
`make open-browser`
|
|
31
|
+
|
|
32
|
+
# Testing
|
|
33
|
+
`make test`
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
LICENCE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.py
|
|
6
|
+
django_rest_spreadsheets.egg-info/PKG-INFO
|
|
7
|
+
django_rest_spreadsheets.egg-info/SOURCES.txt
|
|
8
|
+
django_rest_spreadsheets.egg-info/dependency_links.txt
|
|
9
|
+
django_rest_spreadsheets.egg-info/top_level.txt
|
|
10
|
+
spreadsheet/__init__.py
|
|
11
|
+
spreadsheet/fields.py
|
|
12
|
+
spreadsheet/schema.py
|
|
13
|
+
spreadsheet/serializers.py
|
|
14
|
+
spreadsheet/urls.py
|
|
15
|
+
spreadsheet/util.py
|
|
16
|
+
spreadsheet/views.py
|
|
17
|
+
spreadsheet/migrations/__init__.py
|
|
18
|
+
spreadsheet/static/deps/jquery-ui.css
|
|
19
|
+
spreadsheet/static/deps/jquery-ui.js
|
|
20
|
+
spreadsheet/static/deps/jquery.datetimepicker.css
|
|
21
|
+
spreadsheet/static/deps/jquery.datetimepicker.full.js
|
|
22
|
+
spreadsheet/static/deps/jquery.datetimepicker.js
|
|
23
|
+
spreadsheet/static/deps/jquery.datetimepicker.min.css
|
|
24
|
+
spreadsheet/static/deps/moment.js
|
|
25
|
+
spreadsheet/static/spreadsheet/css/menu.css
|
|
26
|
+
spreadsheet/static/spreadsheet/css/reset.css
|
|
27
|
+
spreadsheet/static/spreadsheet/css/screen.css
|
|
28
|
+
spreadsheet/static/spreadsheet/css/spreadsheet.css
|
|
29
|
+
spreadsheet/static/spreadsheet/img/cancel.svg
|
|
30
|
+
spreadsheet/static/spreadsheet/img/create.svg
|
|
31
|
+
spreadsheet/static/spreadsheet/img/delete.svg
|
|
32
|
+
spreadsheet/static/spreadsheet/img/icon.png
|
|
33
|
+
spreadsheet/static/spreadsheet/img/update.svg
|
|
34
|
+
spreadsheet/static/spreadsheet/js/RestApi6.js
|
|
35
|
+
spreadsheet/static/spreadsheet/js/ScreenStack.js
|
|
36
|
+
spreadsheet/static/spreadsheet/js/SpreadSheet.js
|
|
37
|
+
spreadsheet/static/spreadsheet/js/menu.js
|
|
38
|
+
spreadsheet/templates/index.html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spreadsheet
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-rest-spreadsheets"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Gerben van Eerten", email="info@gerbenvaneerten.nl" },
|
|
6
|
+
]
|
|
7
|
+
description = "Spreadsheets based on django rest framework"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENCE*"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/g3rb3n/django-rest-spreadsheets"
|
|
19
|
+
Issues = "https://github.com/g3rb3n/django-rest-spreadsheets/issues"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import setuptools
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setuptools.setup(
|
|
7
|
+
name="django-rest-spreadsheets",
|
|
8
|
+
version="1.0.0",
|
|
9
|
+
author="gerben van Eerten",
|
|
10
|
+
author_email="info@gerbenvaneerten.nl",
|
|
11
|
+
description="Spreadsheets based on django rest framework",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/g3rb3n/django-rest-spreadsheets/",
|
|
15
|
+
packages=setuptools.find_packages(exclude=['testproject*']),
|
|
16
|
+
package_data={'django-rest-spreadsheets': ['spreadsheet/static', 'spreadsheet/templates', 'spreadsheet/migrations']},
|
|
17
|
+
include_package_data=True,
|
|
18
|
+
|
|
19
|
+
classifiers=[
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
],
|
|
24
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve
|
|
5
|
+
from django.utils.encoding import uri_to_iri
|
|
6
|
+
from rest_framework import relations
|
|
7
|
+
from rest_framework.reverse import reverse
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Ref(str):
|
|
15
|
+
"""
|
|
16
|
+
A string like object that additionally has an associated name.
|
|
17
|
+
We use this for hyperlinked URLs that may render as a named link
|
|
18
|
+
in some contexts, or render as a plain URL in others.
|
|
19
|
+
"""
|
|
20
|
+
def __new__(self, url, obj):
|
|
21
|
+
ret = super().__new__(self, url)
|
|
22
|
+
ret.obj = obj
|
|
23
|
+
return ret
|
|
24
|
+
|
|
25
|
+
def __getnewargs__(self):
|
|
26
|
+
return(str(self), self.name,)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def name(self):
|
|
30
|
+
# This ensures that we only called `__str__` lazily,
|
|
31
|
+
# as in some cases calling __str__ on a model instances *might*
|
|
32
|
+
# involve a database lookup.
|
|
33
|
+
return str(self.obj)
|
|
34
|
+
|
|
35
|
+
is_hyperlink = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RefRelatedField(relations.RelatedField):
|
|
39
|
+
lookup_field = 'pk'
|
|
40
|
+
view_name = None
|
|
41
|
+
|
|
42
|
+
default_error_messages = {
|
|
43
|
+
'required': _('This field is required.'),
|
|
44
|
+
'no_match': _('Invalid hyperlink - No URL match.'),
|
|
45
|
+
'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
|
|
46
|
+
'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
|
|
47
|
+
'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def __init__(self, view_name=None, **kwargs):
|
|
51
|
+
assert view_name is not None, 'The `view_name` argument is required.'
|
|
52
|
+
self.view_name = view_name
|
|
53
|
+
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
|
|
54
|
+
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
|
|
55
|
+
self.format = kwargs.pop('format', None)
|
|
56
|
+
|
|
57
|
+
# We include this simply for dependency injection in tests.
|
|
58
|
+
# We can't add it as a class attributes or it would expect an
|
|
59
|
+
# implicit `self` argument to be passed.
|
|
60
|
+
self.reverse = reverse
|
|
61
|
+
|
|
62
|
+
super(RefRelatedField, self).__init__(**kwargs)
|
|
63
|
+
|
|
64
|
+
def use_pk_only_optimization(self):
|
|
65
|
+
return False
|
|
66
|
+
#return self.lookup_field == 'pk'
|
|
67
|
+
|
|
68
|
+
def get_object(self, view_name, view_args, view_kwargs):
|
|
69
|
+
"""
|
|
70
|
+
Return the object corresponding to a matched URL.
|
|
71
|
+
|
|
72
|
+
Takes the matched URL conf arguments, and should return an
|
|
73
|
+
object instance, or raise an `ObjectDoesNotExist` exception.
|
|
74
|
+
"""
|
|
75
|
+
lookup_value = view_kwargs[self.lookup_url_kwarg]
|
|
76
|
+
lookup_kwargs = {self.lookup_field: lookup_value}
|
|
77
|
+
return self.get_queryset().get(**lookup_kwargs)
|
|
78
|
+
|
|
79
|
+
def get_url(self, obj, view_name, request, format):
|
|
80
|
+
"""
|
|
81
|
+
Given an object, return the URL that hyperlinks to the object.
|
|
82
|
+
|
|
83
|
+
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
|
|
84
|
+
attributes are not configured to correctly match the URL conf.
|
|
85
|
+
"""
|
|
86
|
+
# Unsaved objects will not yet have a valid URL.
|
|
87
|
+
if hasattr(obj, 'pk') and obj.pk in (None, ''):
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
lookup_value = getattr(obj, self.lookup_field)
|
|
91
|
+
kwargs = {self.lookup_url_kwarg: lookup_value}
|
|
92
|
+
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
|
|
93
|
+
|
|
94
|
+
def to_internal_value(self, data):
|
|
95
|
+
request = self.context.get('request', None)
|
|
96
|
+
try:
|
|
97
|
+
http_prefix = data.startswith(('http:', 'https:'))
|
|
98
|
+
except AttributeError:
|
|
99
|
+
self.fail('incorrect_type', data_type=type(data).__name__)
|
|
100
|
+
|
|
101
|
+
if http_prefix:
|
|
102
|
+
# If needed convert absolute URLs to relative path
|
|
103
|
+
data = urlparse(data).path
|
|
104
|
+
prefix = get_script_prefix()
|
|
105
|
+
if data.startswith(prefix):
|
|
106
|
+
data = '/' + data[len(prefix):]
|
|
107
|
+
|
|
108
|
+
data = uri_to_iri(data)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
match = resolve(data)
|
|
112
|
+
except Resolver404:
|
|
113
|
+
self.fail('no_match')
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
expected_viewname = request.versioning_scheme.get_versioned_viewname(
|
|
117
|
+
self.view_name, request
|
|
118
|
+
)
|
|
119
|
+
except AttributeError:
|
|
120
|
+
expected_viewname = self.view_name
|
|
121
|
+
|
|
122
|
+
if match.view_name != expected_viewname:
|
|
123
|
+
self.fail('incorrect_match')
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
return self.get_object(match.view_name, match.args, match.kwargs)
|
|
127
|
+
except (ObjectDoesNotExist, TypeError, ValueError):
|
|
128
|
+
self.fail('does_not_exist')
|
|
129
|
+
|
|
130
|
+
def to_representation(self, value):
|
|
131
|
+
assert 'request' in self.context, (
|
|
132
|
+
"`%s` requires the request in the serializer"
|
|
133
|
+
" context. Add `context={'request': request}` when instantiating "
|
|
134
|
+
"the serializer." % self.__class__.__name__
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
request = self.context['request']
|
|
138
|
+
format = self.context.get('format', None)
|
|
139
|
+
|
|
140
|
+
# By default use whatever format is given for the current context
|
|
141
|
+
# unless the target is a different type to the source.
|
|
142
|
+
#
|
|
143
|
+
# Eg. Consider a HyperlinkedIdentityField pointing from a json
|
|
144
|
+
# representation to an html property of that representation...
|
|
145
|
+
#
|
|
146
|
+
# '/snippets/1/' should link to '/snippets/1/highlight/'
|
|
147
|
+
# ...but...
|
|
148
|
+
# '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
|
|
149
|
+
if format and self.format and self.format != format:
|
|
150
|
+
format = self.format
|
|
151
|
+
|
|
152
|
+
# Return the hyperlink, or error if incorrectly configured.
|
|
153
|
+
try:
|
|
154
|
+
url = self.get_url(value, self.view_name, request, format)
|
|
155
|
+
logger.debug(url)
|
|
156
|
+
logger.debug(value)
|
|
157
|
+
except NoReverseMatch:
|
|
158
|
+
msg = (
|
|
159
|
+
'Could not resolve URL for hyperlinked relationship using '
|
|
160
|
+
'view name "%s". You may have failed to include the related '
|
|
161
|
+
'model in your API, or incorrectly configured the '
|
|
162
|
+
'`lookup_field` attribute on this field.'
|
|
163
|
+
)
|
|
164
|
+
if value in ('', None):
|
|
165
|
+
value_string = {'': 'the empty string', None: 'None'}[value]
|
|
166
|
+
msg += (
|
|
167
|
+
" WARNING: The value of the field on the model instance "
|
|
168
|
+
"was %s, which may be why it didn't match any "
|
|
169
|
+
"entries in your URL conf." % value_string
|
|
170
|
+
)
|
|
171
|
+
raise ImproperlyConfigured(msg % self.view_name)
|
|
172
|
+
|
|
173
|
+
if url is None:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
return Ref(url, value)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class RefIdentityField(RefRelatedField):
|
|
180
|
+
"""
|
|
181
|
+
A read-only field that represents the identity URL for an object, itself.
|
|
182
|
+
|
|
183
|
+
This is in contrast to `HyperlinkedRelatedField` which represents the
|
|
184
|
+
URL of relationships to other objects.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
def __init__(self, view_name=None, **kwargs):
|
|
188
|
+
assert view_name is not None, 'The `view_name` argument is required.'
|
|
189
|
+
kwargs['read_only'] = True
|
|
190
|
+
kwargs['source'] = '*'
|
|
191
|
+
super().__init__(view_name, **kwargs)
|
|
192
|
+
|
|
193
|
+
def use_pk_only_optimization(self):
|
|
194
|
+
# We have the complete object instance already. We don't need
|
|
195
|
+
# to run the 'only get the pk for this relationship' code.
|
|
196
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import django
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from rest_framework import serializers
|
|
8
|
+
from rest_framework import fields as rffields
|
|
9
|
+
|
|
10
|
+
from . import util
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_field_schema(field):
|
|
17
|
+
data_type = get_type(field)
|
|
18
|
+
schema = {
|
|
19
|
+
'name': field.name,
|
|
20
|
+
'type': data_type
|
|
21
|
+
}
|
|
22
|
+
if field.name == 'id':
|
|
23
|
+
schema['isKey'] = True
|
|
24
|
+
if isinstance(field, django.db.models.fields.Field):
|
|
25
|
+
schema['title'] = field.verbose_name
|
|
26
|
+
if isinstance(field, django.db.models.fields.related.ForeignKey):
|
|
27
|
+
schema['$ref'] = field.related_model.__name__
|
|
28
|
+
schema['count'] = field.related_model.objects.count()
|
|
29
|
+
schema['multiple'] = False
|
|
30
|
+
if isinstance(field, django.db.models.fields.related.ManyToManyField):
|
|
31
|
+
schema['$ref'] = field.related_model.__name__
|
|
32
|
+
schema['multiple'] = True
|
|
33
|
+
if isinstance(field, django.db.models.fields.reverse_related.ForeignObjectRel):
|
|
34
|
+
if (field.remote_field.name[-4:] == '_ptr'):
|
|
35
|
+
return None
|
|
36
|
+
schema['$ref'] = field.related_model.__name__
|
|
37
|
+
schema['multiple'] = field.multiple
|
|
38
|
+
schema['title'] = field.related_model._meta.verbose_name_plural
|
|
39
|
+
schema['lookup'] = field.remote_field.name
|
|
40
|
+
if hasattr(field, 'blank'):
|
|
41
|
+
schema['required'] = not field.blank
|
|
42
|
+
if hasattr(field, 'choices') and field.choices:
|
|
43
|
+
sorted_choices = sorted(field.get_choices(), key=lambda tup: tup[1])
|
|
44
|
+
schema['type'] = 'oneOf'
|
|
45
|
+
schema['oneOf'] = OrderedDict(sorted_choices)
|
|
46
|
+
if data_type == 'string':
|
|
47
|
+
schema['length'] = field.max_length
|
|
48
|
+
return schema
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def generate_model_schema(model_class):
|
|
52
|
+
name = model_class.__name__
|
|
53
|
+
model_schema = {
|
|
54
|
+
'$schema': 'http://json-schema.org/draft-06/schema#',
|
|
55
|
+
'title': name,
|
|
56
|
+
'description': name,
|
|
57
|
+
'type': 'object',
|
|
58
|
+
'properties': {}
|
|
59
|
+
}
|
|
60
|
+
fields = model_class._meta.get_fields()
|
|
61
|
+
required = []
|
|
62
|
+
order = []
|
|
63
|
+
for field in fields:
|
|
64
|
+
if field.name[-4:] == '_ptr':
|
|
65
|
+
continue
|
|
66
|
+
field_schema = generate_field_schema(field)
|
|
67
|
+
if not field_schema:
|
|
68
|
+
continue
|
|
69
|
+
order.append(field.name)
|
|
70
|
+
model_schema['properties'][field.name] = field_schema
|
|
71
|
+
model_schema['fields'] = order
|
|
72
|
+
return model_schema
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_type(field):
|
|
76
|
+
t = field.get_internal_type()
|
|
77
|
+
if t == 'CharField': return 'string'
|
|
78
|
+
if t == 'BooleanField': return 'boolean'
|
|
79
|
+
if t == 'IntegerField': return 'integer'
|
|
80
|
+
if t == 'DecimalField': return 'number'
|
|
81
|
+
if t == 'DateTimeField': return 'datetime'
|
|
82
|
+
if t == 'DateField': return 'date'
|
|
83
|
+
if t == 'TimeField': return 'time'
|
|
84
|
+
if t == 'AutoField': return 'integer'
|
|
85
|
+
if t == 'ForeignKey': return 'object'
|
|
86
|
+
if t == 'OneToOneField': return 'object'
|
|
87
|
+
if t == 'ManyToManyField': return 'many'
|
|
88
|
+
if t == 'OneToOneRel': return 'object'
|
|
89
|
+
if t == 'OneToManyRel': return 'object'
|
|
90
|
+
if t == 'ManyToOneRel': return 'object'
|
|
91
|
+
if t == 'ManyToManyRel': return 'object'
|
|
92
|
+
logger.error('Unsupported field in get_type %s for %s', t, field)
|
|
93
|
+
return t
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_django_models(module):
|
|
97
|
+
classes = inspect.getmembers(module, inspect.isclass)
|
|
98
|
+
models = {}
|
|
99
|
+
for name, cls in classes:
|
|
100
|
+
mro = inspect.getmro(cls)
|
|
101
|
+
if django.db.models.base.Model in mro:
|
|
102
|
+
models[name] = cls
|
|
103
|
+
return models
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def generate(models_module):
|
|
107
|
+
schema = {}
|
|
108
|
+
schema['$schema'] = 'http://json-schema.org/draft-06/schema#'
|
|
109
|
+
title = ''
|
|
110
|
+
model_classes = get_django_models(models_module)
|
|
111
|
+
for name, model_class in model_classes.items():
|
|
112
|
+
if util.model_is_abstract(model_class):
|
|
113
|
+
continue
|
|
114
|
+
logger.info('Schema: generate %s' % name)
|
|
115
|
+
schema[name] = generate_model_schema(model_class)
|
|
116
|
+
return schema
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import sys, inspect
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
from rest_framework import serializers
|
|
6
|
+
from rest_framework import fields as rffields
|
|
7
|
+
from rest_framework.reverse import reverse
|
|
8
|
+
from rest_framework.utils.field_mapping import (
|
|
9
|
+
get_nested_relation_kwargs,
|
|
10
|
+
get_relation_kwargs, get_url_kwargs
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .fields import *
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RefModelSerializer(serializers.ModelSerializer):
|
|
20
|
+
"""
|
|
21
|
+
A type of `ModelSerializer` that uses hyperlinked relationships instead
|
|
22
|
+
of primary key relationships. Specifically:
|
|
23
|
+
|
|
24
|
+
* A 'url' field is included instead of the 'id' field.
|
|
25
|
+
* Relationships to other instances are hyperlinks, instead of primary keys.
|
|
26
|
+
"""
|
|
27
|
+
#setattr(serializers.ModelSerializer, 'serializer_url_field', RefRelatedField)
|
|
28
|
+
serializer_related_field = RefRelatedField
|
|
29
|
+
meta_serializer_field = RefRelatedField
|
|
30
|
+
meta_serializer_identity_field = RefIdentityField
|
|
31
|
+
meta_field_name = '_'
|
|
32
|
+
|
|
33
|
+
def build_meta_field(self, field_name, model_class):
|
|
34
|
+
"""
|
|
35
|
+
Create a field representing the object's own URL.
|
|
36
|
+
"""
|
|
37
|
+
#field_class = self.serializer_url_field
|
|
38
|
+
field_class = self.meta_serializer_identity_field
|
|
39
|
+
field_kwargs = get_url_kwargs(model_class)
|
|
40
|
+
|
|
41
|
+
return field_class, field_kwargs
|
|
42
|
+
|
|
43
|
+
def build_relational_field(self, field_name, relation_info):
|
|
44
|
+
"""
|
|
45
|
+
Create fields for forward and reverse relationships.
|
|
46
|
+
"""
|
|
47
|
+
field_class = self.serializer_related_field
|
|
48
|
+
field_kwargs = get_relation_kwargs(field_name, relation_info)
|
|
49
|
+
|
|
50
|
+
to_field = field_kwargs.pop('to_field', None)
|
|
51
|
+
if to_field and not relation_info.reverse and not relation_info.related_model._meta.get_field(to_field).primary_key:
|
|
52
|
+
field_kwargs['slug_field'] = to_field
|
|
53
|
+
field_class = self.serializer_related_to_field
|
|
54
|
+
|
|
55
|
+
# `view_name` is only valid for hyperlinked relationships.
|
|
56
|
+
if not issubclass(field_class, RefRelatedField):
|
|
57
|
+
field_kwargs.pop('view_name', None)
|
|
58
|
+
|
|
59
|
+
return field_class, field_kwargs
|
|
60
|
+
|
|
61
|
+
def to_representation(self, instance):
|
|
62
|
+
representation = super().to_representation(instance);
|
|
63
|
+
representation['_str'] = str(instance)
|
|
64
|
+
#representation['_'] = {
|
|
65
|
+
# 'url': '',
|
|
66
|
+
# 'id': instance.pk,
|
|
67
|
+
# 'string': str(instance)
|
|
68
|
+
#}
|
|
69
|
+
#RefRelatedField().to_representation(instance)
|
|
70
|
+
return representation
|
|
71
|
+
|
|
72
|
+
def build_field(self, field_name, info, model_class, nested_depth):
|
|
73
|
+
"""
|
|
74
|
+
Return a two tuple of (cls, kwargs) to build a serializer field with.
|
|
75
|
+
"""
|
|
76
|
+
if field_name == '_':
|
|
77
|
+
return self.build_meta_field(field_name, model_class)
|
|
78
|
+
if field_name == '_str':
|
|
79
|
+
return rffields.CharField, {'read_only': True}
|
|
80
|
+
return super().build_field(field_name, info, model_class, nested_depth)
|
|
81
|
+
|
|
82
|
+
def get_fields(self):
|
|
83
|
+
fields = super().get_fields()
|
|
84
|
+
logger.debug('get_fields')
|
|
85
|
+
logger.debug(fields)
|
|
86
|
+
return fields
|
|
87
|
+
|
|
88
|
+
def get_default_field_names(self, declared_fields, model_info):
|
|
89
|
+
"""
|
|
90
|
+
Return the default list of field names that will be used if the
|
|
91
|
+
`Meta.fields` option is not specified.
|
|
92
|
+
"""
|
|
93
|
+
logger.debug('get_default_field_names')
|
|
94
|
+
logger.debug(model_info)
|
|
95
|
+
return (
|
|
96
|
+
['_'] +
|
|
97
|
+
['_str'] +
|
|
98
|
+
list(declared_fields) +
|
|
99
|
+
list(model_info.fields) +
|
|
100
|
+
list(model_info.forward_relations) +
|
|
101
|
+
list(model_info.reverse_relations)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def build_nested_field(self, field_name, relation_info, nested_depth):
|
|
105
|
+
"""
|
|
106
|
+
Create nested fields for forward and reverse relationships.
|
|
107
|
+
"""
|
|
108
|
+
class NestedSerializer(RefModelSerializer):
|
|
109
|
+
class Meta:
|
|
110
|
+
model = relation_info.related_model
|
|
111
|
+
depth = nested_depth - 1
|
|
112
|
+
fields = '__all__'
|
|
113
|
+
|
|
114
|
+
field_class = NestedSerializer
|
|
115
|
+
field_kwargs = get_nested_relation_kwargs(relation_info)
|
|
116
|
+
|
|
117
|
+
return field_class, field_kwargs
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def s(m):
|
|
121
|
+
class Serializer(RefModelSerializer):
|
|
122
|
+
class Meta:
|
|
123
|
+
model = m
|
|
124
|
+
exclude = ()
|
|
125
|
+
|
|
126
|
+
return Serializer
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def generate(models_module, serializers_module):
|
|
130
|
+
generated = {}
|
|
131
|
+
model_classes = inspect.getmembers(models_module, inspect.isclass)
|
|
132
|
+
#serializer_module = sys.modules[__name__]
|
|
133
|
+
serializer_classes = dir(serializers_module)
|
|
134
|
+
for name, cls in model_classes:
|
|
135
|
+
logger.debug('Serializers: generate %s' % name)
|
|
136
|
+
if name + 'Serializer' in serializer_classes:
|
|
137
|
+
logger.info('already defined %s' % name)
|
|
138
|
+
generated[name] = getattr(serializers_module, name + 'Serializer')
|
|
139
|
+
else:
|
|
140
|
+
logger.info('generate %s' % name)
|
|
141
|
+
generated[name] = s(cls)
|
|
142
|
+
return generated
|