dflango 0.3.2__tar.gz → 0.3.4__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.
- {dflango-0.3.2/dflango.egg-info → dflango-0.3.4}/PKG-INFO +1 -1
- {dflango-0.3.2 → dflango-0.3.4}/dflango/__init__.py +1 -1
- {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/schemas.py +37 -1
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/base.py +4 -3
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/registry.py +29 -1
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/schema_factory.py +67 -2
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/views.py +43 -0
- {dflango-0.3.2 → dflango-0.3.4/dflango.egg-info}/PKG-INFO +1 -1
- {dflango-0.3.2 → dflango-0.3.4}/pyproject.toml +1 -1
- {dflango-0.3.2 → dflango-0.3.4}/LICENSE +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/MANIFEST.in +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/README.md +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/command_registration.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/load_fixtures.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/commands/start_app.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/config.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/core.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/db.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/encoders.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/enum.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/fields.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/exports/csv/registry.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/logging.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/mixin/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/mixin/permission_mixin.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/permissions.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/providers.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/routes/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/routes/route_registry.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/fields.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/model_schemas.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/schemas/schemas.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/services/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/services/auth.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/commands.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/supermodel/exceptions.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/config.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/constants.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/models/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/models/models.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/schemas/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/schemas/schemas.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/urls.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/views/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/templates/app_template/views/views.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/__init__.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/base_export_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/base_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/config_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/detail_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/general_list_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/internal_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/list_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/search_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango/views/statistic_view.py +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/SOURCES.txt +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/dependency_links.txt +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/requires.txt +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/dflango.egg-info/top_level.txt +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/setup.cfg +0 -0
- {dflango-0.3.2 → dflango-0.3.4}/setup.py +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Built-in
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
|
|
4
|
+
# Third-party
|
|
5
|
+
from sqlalchemy import String
|
|
6
|
+
|
|
4
7
|
# Local
|
|
5
8
|
from dflango.exports.csv.fields import sanitize_csv_value
|
|
6
9
|
|
|
@@ -29,9 +32,42 @@ class CSVExportSchema:
|
|
|
29
32
|
delimiter: str = ","
|
|
30
33
|
encoding: str = "utf-8"
|
|
31
34
|
include_bom: bool = False
|
|
35
|
+
allowed_filters: dict = {}
|
|
32
36
|
|
|
33
37
|
def validate_filters(self, filters: dict) -> dict:
|
|
34
|
-
|
|
38
|
+
if not self.allowed_filters:
|
|
39
|
+
return filters
|
|
40
|
+
validated = {}
|
|
41
|
+
for field, lookups in self.allowed_filters.items():
|
|
42
|
+
for lookup in lookups:
|
|
43
|
+
key = f"{field}__{lookup}"
|
|
44
|
+
if key in filters:
|
|
45
|
+
validated[key] = filters[key]
|
|
46
|
+
return validated
|
|
47
|
+
|
|
48
|
+
def apply_filters(self, query, model_cls, filters: dict):
|
|
49
|
+
for key, value in filters.items():
|
|
50
|
+
if "__" not in key:
|
|
51
|
+
continue
|
|
52
|
+
field_name, lookup = key.rsplit("__", 1)
|
|
53
|
+
column = getattr(model_cls, field_name, None)
|
|
54
|
+
if column is None:
|
|
55
|
+
continue
|
|
56
|
+
if lookup == "icontains":
|
|
57
|
+
query = query.filter(column.cast(String).ilike(f"%{value}%"))
|
|
58
|
+
elif lookup == "exact":
|
|
59
|
+
try:
|
|
60
|
+
if column.property.columns[0].type.__class__.__name__ == "ARRAY":
|
|
61
|
+
query = query.filter(column.any(value))
|
|
62
|
+
else:
|
|
63
|
+
query = query.filter(column == value)
|
|
64
|
+
except Exception:
|
|
65
|
+
query = query.filter(column == value)
|
|
66
|
+
elif lookup == "gte":
|
|
67
|
+
query = query.filter(column >= value)
|
|
68
|
+
elif lookup == "lte":
|
|
69
|
+
query = query.filter(column <= value)
|
|
70
|
+
return query
|
|
35
71
|
|
|
36
72
|
def check_permissions(self, context: ExportContext) -> None:
|
|
37
73
|
pass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
|
|
3
|
-
from sqlalchemy import Column, DateTime
|
|
3
|
+
from sqlalchemy import Boolean, Column, DateTime
|
|
4
4
|
from sqlalchemy.dialects.postgresql import UUID
|
|
5
5
|
from sqlalchemy.sql import func
|
|
6
6
|
|
|
@@ -11,14 +11,15 @@ class SuperModel:
|
|
|
11
11
|
Provides built-in common fields and introspection utilities.
|
|
12
12
|
|
|
13
13
|
Usage:
|
|
14
|
-
class
|
|
15
|
-
__tablename__ = "
|
|
14
|
+
class Category(SuperModel, db.Model):
|
|
15
|
+
__tablename__ = "categories"
|
|
16
16
|
name = db.Column(db.String(255), nullable=False)
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
__abstract__ = True
|
|
20
20
|
|
|
21
21
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
22
|
+
is_active = Column(Boolean, nullable=False, default=True, server_default="true")
|
|
22
23
|
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
|
23
24
|
updated_at = Column(
|
|
24
25
|
DateTime, nullable=False, server_default=func.now(), onupdate=func.now()
|
|
@@ -41,6 +41,8 @@ class SuperModelConfig:
|
|
|
41
41
|
schema_class=None,
|
|
42
42
|
viewset_class=None,
|
|
43
43
|
filters=None,
|
|
44
|
+
value_field="id",
|
|
45
|
+
label_field="name",
|
|
44
46
|
):
|
|
45
47
|
self.model_class = model_class
|
|
46
48
|
self.slug = slug
|
|
@@ -50,6 +52,8 @@ class SuperModelConfig:
|
|
|
50
52
|
self.permissions = permissions
|
|
51
53
|
self.schema_class = schema_class
|
|
52
54
|
self.viewset_class = viewset_class
|
|
55
|
+
self.value_field = value_field
|
|
56
|
+
self.label_field = label_field
|
|
53
57
|
# If filters is True or not specified, auto-generate from columns.
|
|
54
58
|
# If filters is a dict, use it directly. If False/empty, no filtering.
|
|
55
59
|
if filters is True or filters is None:
|
|
@@ -87,6 +91,8 @@ class SuperModelRegistry:
|
|
|
87
91
|
schema_class=None,
|
|
88
92
|
viewset_class=None,
|
|
89
93
|
filters=None,
|
|
94
|
+
value_field="id",
|
|
95
|
+
label_field="name",
|
|
90
96
|
):
|
|
91
97
|
"""Decorator to register a SuperModel model.
|
|
92
98
|
|
|
@@ -97,6 +103,8 @@ class SuperModelRegistry:
|
|
|
97
103
|
None/True = auto-generate from columns.
|
|
98
104
|
False = disable filtering.
|
|
99
105
|
Dict = use as-is.
|
|
106
|
+
value_field: Field to use as 'value' in the minimal endpoint. Default: 'id'.
|
|
107
|
+
label_field: Field to use as 'label' in the minimal endpoint. Default: 'name'.
|
|
100
108
|
"""
|
|
101
109
|
|
|
102
110
|
def decorator(model_class):
|
|
@@ -118,6 +126,8 @@ class SuperModelRegistry:
|
|
|
118
126
|
schema_class=schema_class,
|
|
119
127
|
viewset_class=viewset_class,
|
|
120
128
|
filters=filters,
|
|
129
|
+
value_field=value_field,
|
|
130
|
+
label_field=label_field,
|
|
121
131
|
)
|
|
122
132
|
|
|
123
133
|
self._items[resolved_slug] = config
|
|
@@ -201,6 +211,7 @@ class SuperModelRegistry:
|
|
|
201
211
|
"detail": f"GET {config.endpoint_prefix}/<id>",
|
|
202
212
|
"update": f"PUT {config.endpoint_prefix}/<id>",
|
|
203
213
|
"delete": f"DELETE {config.endpoint_prefix}/<id>",
|
|
214
|
+
"minimal": f"GET {config.endpoint_prefix}/@minimal",
|
|
204
215
|
},
|
|
205
216
|
"fields": fields,
|
|
206
217
|
"readonly_fields": list(model_class.READONLY_FIELDS),
|
|
@@ -233,7 +244,11 @@ class SuperModelRegistry:
|
|
|
233
244
|
...
|
|
234
245
|
] + support_registry.get_urlpatterns()
|
|
235
246
|
"""
|
|
236
|
-
from dflango.supermodel.views import
|
|
247
|
+
from dflango.supermodel.views import (
|
|
248
|
+
create_list_create_view,
|
|
249
|
+
create_detail_view,
|
|
250
|
+
create_minimal_view,
|
|
251
|
+
)
|
|
237
252
|
|
|
238
253
|
urlpatterns = []
|
|
239
254
|
|
|
@@ -256,4 +271,17 @@ class SuperModelRegistry:
|
|
|
256
271
|
detail_view.as_view(detail_view_name),
|
|
257
272
|
))
|
|
258
273
|
|
|
274
|
+
# Minimal view (value/label for dropdowns)
|
|
275
|
+
minimal_schema = SuperModelSchemaFactory.create_minimal_schema(
|
|
276
|
+
config.model_class,
|
|
277
|
+
value_field=config.value_field,
|
|
278
|
+
label_field=config.label_field,
|
|
279
|
+
)
|
|
280
|
+
minimal_view = create_minimal_view(config, minimal_schema)
|
|
281
|
+
minimal_view_name = f"{config.slug}_minimal"
|
|
282
|
+
urlpatterns.append((
|
|
283
|
+
f"{config.endpoint_prefix}/@minimal",
|
|
284
|
+
minimal_view.as_view(minimal_view_name),
|
|
285
|
+
))
|
|
286
|
+
|
|
259
287
|
return urlpatterns
|
|
@@ -74,13 +74,14 @@ class SuperModelSchemaFactory:
|
|
|
74
74
|
|
|
75
75
|
for column in model_class.get_public_columns():
|
|
76
76
|
# Skip built-in fields, we add them explicitly below
|
|
77
|
-
if column.name in ("id", "created_at", "updated_at", "deleted_at"):
|
|
77
|
+
if column.name in ("id", "is_active", "created_at", "updated_at", "deleted_at"):
|
|
78
78
|
continue
|
|
79
79
|
schema_fields[column.name] = _resolve_marshmallow_field(column)
|
|
80
80
|
schema_fields[column.name].dump_only = True
|
|
81
81
|
|
|
82
82
|
# Add built-in fields explicitly
|
|
83
83
|
schema_fields["id"] = ma_fields.String(dump_only=True)
|
|
84
|
+
schema_fields["is_active"] = ma_fields.Boolean(dump_only=True)
|
|
84
85
|
schema_fields["created_at"] = ma_fields.DateTime(dump_only=True)
|
|
85
86
|
schema_fields["updated_at"] = ma_fields.DateTime(dump_only=True)
|
|
86
87
|
schema_fields["deleted_at"] = ma_fields.DateTime(dump_only=True, allow_none=True)
|
|
@@ -91,7 +92,8 @@ class SuperModelSchemaFactory:
|
|
|
91
92
|
@classmethod
|
|
92
93
|
def _create_write_schema(cls, model_class):
|
|
93
94
|
"""Schema for deserialization (POST/PUT body).
|
|
94
|
-
Extends ModelSchema to support validate() + save().
|
|
95
|
+
Extends ModelSchema to support validate() + save().
|
|
96
|
+
When an instance is set (PUT), validates in partial mode."""
|
|
95
97
|
from dflango.schemas import ModelSchema
|
|
96
98
|
|
|
97
99
|
schema_fields = {}
|
|
@@ -105,4 +107,67 @@ class SuperModelSchemaFactory:
|
|
|
105
107
|
meta = type("Meta", (), {"model": model_class})
|
|
106
108
|
schema_fields["Meta"] = meta
|
|
107
109
|
|
|
110
|
+
def validate_with_partial(self, data, *args, **kwargs):
|
|
111
|
+
"""Override validate to use partial=True when updating (instance is set)."""
|
|
112
|
+
from flask import abort, make_response, jsonify
|
|
113
|
+
from marshmallow import ValidationError
|
|
114
|
+
from dflango.core import get_db
|
|
115
|
+
|
|
116
|
+
partial = self.instance is not None
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
loaded = self.load(data, partial=partial, unknown=EXCLUDE)
|
|
120
|
+
|
|
121
|
+
model_cls = self.Meta.model
|
|
122
|
+
errors = {}
|
|
123
|
+
|
|
124
|
+
for column in model_cls.__table__.columns:
|
|
125
|
+
if column.unique or column.primary_key:
|
|
126
|
+
column_name = column.name
|
|
127
|
+
if column_name in loaded:
|
|
128
|
+
value = loaded[column_name]
|
|
129
|
+
if value is None:
|
|
130
|
+
continue
|
|
131
|
+
db = get_db()
|
|
132
|
+
query = db.session.query(model_cls).filter(
|
|
133
|
+
getattr(model_cls, column_name) == value
|
|
134
|
+
)
|
|
135
|
+
if self.instance and hasattr(self.instance, "id"):
|
|
136
|
+
query = query.filter(model_cls.id != self.instance.id)
|
|
137
|
+
if query.first() is not None:
|
|
138
|
+
label = column_name.replace("_", " ").capitalize()
|
|
139
|
+
msg = f"{label} '{value}' is already in use."
|
|
140
|
+
errors[column_name] = [msg]
|
|
141
|
+
|
|
142
|
+
if errors:
|
|
143
|
+
abort(make_response(jsonify(errors), 422))
|
|
144
|
+
|
|
145
|
+
self.validated_data = loaded
|
|
146
|
+
|
|
147
|
+
except ValidationError as err:
|
|
148
|
+
abort(make_response(jsonify(err.messages), 422))
|
|
149
|
+
|
|
150
|
+
schema_fields["validate"] = validate_with_partial
|
|
151
|
+
|
|
108
152
|
return type(schema_name, (ModelSchema,), schema_fields)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def create_minimal_schema(cls, model_class, value_field="id", label_field="name"):
|
|
156
|
+
"""Generate a minimal schema that returns only value and label fields.
|
|
157
|
+
Used for dropdown/select endpoints."""
|
|
158
|
+
|
|
159
|
+
schema_fields = {
|
|
160
|
+
"value": ma_fields.String(dump_only=True),
|
|
161
|
+
"label": ma_fields.String(dump_only=True),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def dump_fn(self, obj, **kwargs):
|
|
165
|
+
return {
|
|
166
|
+
"value": str(getattr(obj, value_field, None) or ""),
|
|
167
|
+
"label": str(getattr(obj, label_field, None) or ""),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
schema_fields["dump"] = dump_fn
|
|
171
|
+
|
|
172
|
+
schema_name = f"{model_class.__name__}MinimalSchema"
|
|
173
|
+
return type(schema_name, (_SuperModelReadSchema,), schema_fields)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dflango.views.list_view import ListView
|
|
2
2
|
from dflango.views.detail_view import DetailView
|
|
3
|
+
from dflango.views.base_view import BaseView
|
|
3
4
|
|
|
4
5
|
import uuid as _uuid
|
|
5
6
|
|
|
@@ -104,3 +105,45 @@ def create_detail_view(config, schema_class):
|
|
|
104
105
|
view_class.get_instance = get_instance
|
|
105
106
|
|
|
106
107
|
return view_class
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def create_minimal_view(config, minimal_schema):
|
|
111
|
+
"""
|
|
112
|
+
Dynamically generate a minimal view for the registered SuperModel.
|
|
113
|
+
|
|
114
|
+
Returns a flat list of {value, label} objects for dropdown/select usage.
|
|
115
|
+
Only returns active, non-deleted records.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
model = config.model_class
|
|
119
|
+
|
|
120
|
+
class MinimalView(BaseView):
|
|
121
|
+
accepted_methods = ["GET"]
|
|
122
|
+
|
|
123
|
+
MinimalView.model = model
|
|
124
|
+
|
|
125
|
+
if config.permissions:
|
|
126
|
+
MinimalView.get_permissions = config.permissions
|
|
127
|
+
|
|
128
|
+
def get(self, *args, **kwargs):
|
|
129
|
+
from flask import jsonify
|
|
130
|
+
from sqlalchemy import func as sa_func
|
|
131
|
+
|
|
132
|
+
query = model.query.filter(model.deleted_at.is_(None))
|
|
133
|
+
|
|
134
|
+
# Filter only active records
|
|
135
|
+
if hasattr(model, "is_active"):
|
|
136
|
+
query = query.filter(model.is_active.is_(True))
|
|
137
|
+
|
|
138
|
+
instances = query.all()
|
|
139
|
+
schema = minimal_schema()
|
|
140
|
+
result = [schema.dump(instance) for instance in instances]
|
|
141
|
+
return jsonify(result), 200
|
|
142
|
+
|
|
143
|
+
MinimalView.get = get
|
|
144
|
+
|
|
145
|
+
view_name = f"{model.__name__}MinimalView"
|
|
146
|
+
MinimalView.__name__ = view_name
|
|
147
|
+
MinimalView.__qualname__ = view_name
|
|
148
|
+
|
|
149
|
+
return MinimalView
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|