udata 14.0.0__py3-none-any.whl → 14.4.1.dev7__py3-none-any.whl
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.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- udata/api_fields.py +35 -4
- udata/app.py +18 -20
- udata/auth/__init__.py +29 -6
- udata/auth/forms.py +2 -2
- udata/auth/views.py +6 -3
- udata/commands/serve.py +3 -11
- udata/commands/tests/test_fixtures.py +9 -9
- udata/core/access_type/api.py +1 -1
- udata/core/access_type/constants.py +12 -8
- udata/core/activity/api.py +5 -6
- udata/core/badges/tests/test_commands.py +6 -6
- udata/core/csv.py +5 -0
- udata/core/dataservices/models.py +1 -1
- udata/core/dataservices/tasks.py +7 -0
- udata/core/dataset/api.py +2 -0
- udata/core/dataset/models.py +2 -2
- udata/core/dataset/permissions.py +31 -0
- udata/core/dataset/tasks.py +17 -5
- udata/core/discussions/models.py +1 -0
- udata/core/organization/api.py +8 -5
- udata/core/organization/mails.py +1 -1
- udata/core/organization/models.py +9 -1
- udata/core/organization/notifications.py +84 -0
- udata/core/organization/permissions.py +1 -1
- udata/core/organization/tasks.py +3 -0
- udata/core/pages/tests/test_api.py +32 -0
- udata/core/post/api.py +24 -69
- udata/core/post/models.py +84 -16
- udata/core/post/tests/test_api.py +24 -1
- udata/core/reports/api.py +18 -0
- udata/core/reports/models.py +42 -2
- udata/core/reuse/models.py +1 -1
- udata/core/reuse/tasks.py +7 -0
- udata/core/spatial/forms.py +2 -2
- udata/core/user/models.py +5 -1
- udata/features/notifications/api.py +7 -18
- udata/features/notifications/models.py +56 -0
- udata/features/notifications/tasks.py +25 -0
- udata/flask_mongoengine/engine.py +0 -4
- udata/frontend/markdown.py +2 -1
- udata/harvest/actions.py +21 -1
- udata/harvest/api.py +25 -8
- udata/harvest/backends/base.py +27 -1
- udata/harvest/backends/ckan/harvesters.py +11 -2
- udata/harvest/commands.py +33 -0
- udata/harvest/filters.py +17 -6
- udata/harvest/models.py +16 -0
- udata/harvest/permissions.py +27 -0
- udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
- udata/harvest/tests/test_actions.py +58 -5
- udata/harvest/tests/test_api.py +276 -122
- udata/harvest/tests/test_base_backend.py +86 -1
- udata/harvest/tests/test_dcat_backend.py +57 -10
- udata/harvest/tests/test_filters.py +6 -0
- udata/i18n.py +1 -4
- udata/mail.py +5 -1
- udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
- udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
- udata/mongo/slug_fields.py +1 -1
- udata/rdf.py +45 -6
- udata/routing.py +2 -2
- udata/settings.py +7 -0
- udata/tasks.py +1 -0
- udata/templates/mail/message.html +5 -31
- udata/tests/__init__.py +27 -2
- udata/tests/api/__init__.py +108 -21
- udata/tests/api/test_activities_api.py +36 -0
- udata/tests/api/test_auth_api.py +121 -95
- udata/tests/api/test_base_api.py +7 -4
- udata/tests/api/test_datasets_api.py +44 -19
- udata/tests/api/test_organizations_api.py +192 -197
- udata/tests/api/test_reports_api.py +157 -0
- udata/tests/api/test_reuses_api.py +147 -147
- udata/tests/api/test_security_api.py +12 -12
- udata/tests/api/test_swagger.py +4 -4
- udata/tests/api/test_tags_api.py +8 -8
- udata/tests/api/test_user_api.py +1 -1
- udata/tests/apiv2/test_swagger.py +4 -4
- udata/tests/cli/test_cli_base.py +8 -9
- udata/tests/dataset/test_dataset_commands.py +4 -4
- udata/tests/dataset/test_dataset_model.py +66 -26
- udata/tests/dataset/test_dataset_rdf.py +99 -5
- udata/tests/frontend/test_auth.py +24 -1
- udata/tests/frontend/test_csv.py +0 -3
- udata/tests/helpers.py +25 -27
- udata/tests/organization/test_notifications.py +67 -2
- udata/tests/plugin.py +6 -261
- udata/tests/site/test_site_csv_exports.py +22 -10
- udata/tests/test_activity.py +9 -9
- udata/tests/test_dcat_commands.py +2 -2
- udata/tests/test_discussions.py +5 -5
- udata/tests/test_migrations.py +21 -21
- udata/tests/test_notifications.py +15 -57
- udata/tests/test_notifications_task.py +43 -0
- udata/tests/test_owned.py +81 -1
- udata/tests/test_storages.py +25 -19
- udata/tests/test_topics.py +77 -61
- udata/tests/test_uris.py +33 -0
- udata/tests/workers/test_jobs_commands.py +23 -23
- udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
- udata/translations/ar/LC_MESSAGES/udata.po +187 -108
- udata/translations/de/LC_MESSAGES/udata.mo +0 -0
- udata/translations/de/LC_MESSAGES/udata.po +187 -108
- udata/translations/es/LC_MESSAGES/udata.mo +0 -0
- udata/translations/es/LC_MESSAGES/udata.po +187 -108
- udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/fr/LC_MESSAGES/udata.po +188 -109
- udata/translations/it/LC_MESSAGES/udata.mo +0 -0
- udata/translations/it/LC_MESSAGES/udata.po +187 -108
- udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
- udata/translations/pt/LC_MESSAGES/udata.po +187 -108
- udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
- udata/translations/sr/LC_MESSAGES/udata.po +187 -108
- udata/translations/udata.pot +215 -106
- udata/uris.py +0 -2
- udata-14.4.1.dev7.dist-info/METADATA +109 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +121 -123
- udata/core/post/forms.py +0 -30
- udata/flask_mongoengine/json.py +0 -38
- udata/templates/mail/base.html +0 -105
- udata/templates/mail/base.txt +0 -6
- udata/templates/mail/button.html +0 -3
- udata/templates/mail/layouts/1-column.html +0 -19
- udata/templates/mail/layouts/2-columns.html +0 -20
- udata/templates/mail/layouts/center-panel.html +0 -16
- udata-14.0.0.dist-info/METADATA +0 -132
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +0 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
- {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
udata/tests/test_migrations.py
CHANGED
|
@@ -43,23 +43,23 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
43
43
|
if migration_path.exists():
|
|
44
44
|
migration_path.unlink()
|
|
45
45
|
|
|
46
|
-
def test_list_available_migrations(self
|
|
46
|
+
def test_list_available_migrations(self):
|
|
47
47
|
"""Test that we can list available migrations"""
|
|
48
|
-
result = cli("db status")
|
|
48
|
+
result = self.cli("db status")
|
|
49
49
|
assert result.exit_code == 0
|
|
50
50
|
# Should contain at least some output (may be empty if no migrations)
|
|
51
51
|
|
|
52
|
-
def test_migration_workflow(self,
|
|
52
|
+
def test_migration_workflow(self, db, migration_file):
|
|
53
53
|
"""Test complete migration workflow: info, migrate, status, unrecord"""
|
|
54
54
|
|
|
55
55
|
# 1. Test info command on non-executed migration
|
|
56
|
-
result = cli(f"db info {migration_file}")
|
|
56
|
+
result = self.cli(f"db info {migration_file}")
|
|
57
57
|
assert result.exit_code == 0
|
|
58
58
|
assert "Test migration for integration testing" in result.output
|
|
59
59
|
assert "Not applied" in result.output
|
|
60
60
|
|
|
61
61
|
# 2. Test migrate command
|
|
62
|
-
result = cli("db migrate")
|
|
62
|
+
result = self.cli("db migrate")
|
|
63
63
|
assert result.exit_code == 0
|
|
64
64
|
|
|
65
65
|
# Verify migration was executed
|
|
@@ -76,17 +76,17 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
76
76
|
assert record["ops"][0]["success"] is True
|
|
77
77
|
|
|
78
78
|
# 3. Test status command after migration
|
|
79
|
-
result = cli("db status")
|
|
79
|
+
result = self.cli("db status")
|
|
80
80
|
assert result.exit_code == 0
|
|
81
81
|
assert migration_file.replace(".py", "") in result.output
|
|
82
82
|
|
|
83
83
|
# 4. Test info command on executed migration
|
|
84
|
-
result = cli(f"db info {migration_file}")
|
|
84
|
+
result = self.cli(f"db info {migration_file}")
|
|
85
85
|
assert result.exit_code == 0
|
|
86
86
|
assert "Test migration for integration testing" in result.output
|
|
87
87
|
|
|
88
88
|
# 5. Test unrecord command
|
|
89
|
-
result = cli(f"db unrecord {migration_file}")
|
|
89
|
+
result = self.cli(f"db unrecord {migration_file}")
|
|
90
90
|
assert result.exit_code == 0
|
|
91
91
|
|
|
92
92
|
# Verify migration record was removed
|
|
@@ -101,10 +101,10 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
101
101
|
# Cleanup test data
|
|
102
102
|
db.test_collection.delete_many({})
|
|
103
103
|
|
|
104
|
-
def test_migrate_recordonly(self,
|
|
104
|
+
def test_migrate_recordonly(self, db, migration_file):
|
|
105
105
|
"""Test migrate with --record flag"""
|
|
106
106
|
|
|
107
|
-
result = cli("db migrate --record")
|
|
107
|
+
result = self.cli("db migrate --record")
|
|
108
108
|
assert result.exit_code == 0
|
|
109
109
|
|
|
110
110
|
# Migration should be recorded
|
|
@@ -119,10 +119,10 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
119
119
|
# Cleanup
|
|
120
120
|
db.migrations.delete_one({"filename": migration_file})
|
|
121
121
|
|
|
122
|
-
def test_migrate_dry_run(self,
|
|
122
|
+
def test_migrate_dry_run(self, db, migration_file):
|
|
123
123
|
"""Test migrate with --dry-run flag"""
|
|
124
124
|
|
|
125
|
-
result = cli("db migrate --dry-run")
|
|
125
|
+
result = self.cli("db migrate --dry-run")
|
|
126
126
|
assert result.exit_code == 0
|
|
127
127
|
|
|
128
128
|
# Migration should NOT be recorded
|
|
@@ -133,18 +133,18 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
133
133
|
inserted = db.test_collection.find_one()
|
|
134
134
|
assert inserted is None
|
|
135
135
|
|
|
136
|
-
def test_migrate_already_applied(self,
|
|
136
|
+
def test_migrate_already_applied(self, db, migration_file):
|
|
137
137
|
"""Test that already applied migrations are skipped"""
|
|
138
138
|
|
|
139
139
|
# First migration
|
|
140
|
-
result = cli("db migrate")
|
|
140
|
+
result = self.cli("db migrate")
|
|
141
141
|
assert result.exit_code == 0
|
|
142
142
|
|
|
143
143
|
# Count records
|
|
144
144
|
count_before = db.test_collection.count_documents({})
|
|
145
145
|
|
|
146
146
|
# Second migration attempt
|
|
147
|
-
result = cli("db migrate")
|
|
147
|
+
result = self.cli("db migrate")
|
|
148
148
|
assert result.exit_code == 0
|
|
149
149
|
assert "Skipped" in result.output
|
|
150
150
|
|
|
@@ -156,7 +156,7 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
156
156
|
db.test_collection.delete_many({})
|
|
157
157
|
db.migrations.delete_one({"filename": migration_file})
|
|
158
158
|
|
|
159
|
-
def test_unrecord_with_complete_filename(self,
|
|
159
|
+
def test_unrecord_with_complete_filename(self, db):
|
|
160
160
|
"""Should unrecord migration with complete filename"""
|
|
161
161
|
db.migrations.insert_one(
|
|
162
162
|
{
|
|
@@ -172,11 +172,11 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
172
172
|
],
|
|
173
173
|
}
|
|
174
174
|
)
|
|
175
|
-
result = cli("db unrecord test.py")
|
|
175
|
+
result = self.cli("db unrecord test.py")
|
|
176
176
|
assert result.exit_code == 0
|
|
177
177
|
assert db.migrations.count_documents({}) == 0
|
|
178
178
|
|
|
179
|
-
def test_unrecord_without_parameters(self,
|
|
179
|
+
def test_unrecord_without_parameters(self, db):
|
|
180
180
|
"""Should fail when no filename is provided"""
|
|
181
181
|
db.migrations.insert_one(
|
|
182
182
|
{
|
|
@@ -192,17 +192,17 @@ class MigrationsCommandsTest(PytestOnlyDBTestCase):
|
|
|
192
192
|
],
|
|
193
193
|
}
|
|
194
194
|
)
|
|
195
|
-
result = cli("db unrecord",
|
|
195
|
+
result = self.cli("db unrecord", expect_error=True)
|
|
196
196
|
assert result.exit_code != 0
|
|
197
197
|
assert db.migrations.count_documents({}) == 1
|
|
198
198
|
|
|
199
|
-
def test_all_existing_migrations_can_run(self,
|
|
199
|
+
def test_all_existing_migrations_can_run(self, db):
|
|
200
200
|
"""Test that all existing migrations can be executed without errors on a clean database"""
|
|
201
201
|
# Get all available migrations
|
|
202
202
|
all_migrations = migrations.list_available()
|
|
203
203
|
|
|
204
204
|
# Run migrations
|
|
205
|
-
result = cli("db migrate")
|
|
205
|
+
result = self.cli("db migrate")
|
|
206
206
|
assert result.exit_code == 0
|
|
207
207
|
|
|
208
208
|
# Verify all migrations were recorded successfully
|
|
@@ -1,75 +1,33 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
|
|
3
|
-
import pytz
|
|
4
1
|
from flask import url_for
|
|
5
2
|
|
|
6
|
-
from udata.core.
|
|
7
|
-
from udata.
|
|
8
|
-
|
|
9
|
-
from .api import APITestCase, DBTestCase
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class NotificationsMixin(object):
|
|
13
|
-
def setUp(self):
|
|
14
|
-
actions._providers = {}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class NotificationsActionsTest(NotificationsMixin, DBTestCase):
|
|
18
|
-
def test_registered_provider_is_listed(self):
|
|
19
|
-
def fake_provider(user):
|
|
20
|
-
return []
|
|
21
|
-
|
|
22
|
-
actions.register_provider("fake", fake_provider)
|
|
23
|
-
|
|
24
|
-
self.assertIn("fake", actions.list_providers())
|
|
3
|
+
from udata.core.organization.factories import OrganizationFactory
|
|
4
|
+
from udata.core.organization.models import Member
|
|
25
5
|
|
|
26
|
-
|
|
27
|
-
@actions.notifier("fake")
|
|
28
|
-
def fake_provider(user):
|
|
29
|
-
return []
|
|
6
|
+
from .api import APITestCase
|
|
30
7
|
|
|
31
|
-
self.assertIn("fake", actions.list_providers())
|
|
32
8
|
|
|
33
|
-
|
|
34
|
-
dt = datetime.utcnow()
|
|
35
|
-
|
|
36
|
-
def fake_provider(user):
|
|
37
|
-
return [(dt, {"some": "value"})]
|
|
38
|
-
|
|
39
|
-
actions.register_provider("fake", fake_provider)
|
|
40
|
-
|
|
41
|
-
user = UserFactory()
|
|
42
|
-
notifs = actions.get_notifications(user)
|
|
43
|
-
|
|
44
|
-
self.assertEqual(len(notifs), 1)
|
|
45
|
-
self.assertEqual(notifs[0]["type"], "fake")
|
|
46
|
-
self.assertEqual(notifs[0]["details"], {"some": "value"})
|
|
47
|
-
self.assertEqualDates(notifs[0]["created_on"], dt)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class NotificationsAPITest(NotificationsMixin, APITestCase):
|
|
9
|
+
class NotificationsAPITest(APITestCase):
|
|
51
10
|
def test_no_notifications(self):
|
|
52
11
|
self.login()
|
|
53
12
|
response = self.get(url_for("api.notifications"))
|
|
54
13
|
self.assert200(response)
|
|
55
14
|
|
|
56
|
-
self.assertEqual(
|
|
15
|
+
self.assertEqual(response.json["total"], 0)
|
|
57
16
|
|
|
58
17
|
def test_has_notifications(self):
|
|
18
|
+
admin = self.login()
|
|
59
19
|
self.login()
|
|
60
|
-
|
|
20
|
+
organization = OrganizationFactory(members=[Member(user=admin, role="admin")])
|
|
21
|
+
data = {"comment": "a comment"}
|
|
61
22
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return [(dt, {"some": "value"}), (dt, {"another": "value"})]
|
|
23
|
+
response = self.post(url_for("api.request_membership", org=organization), data)
|
|
24
|
+
self.assert201(response)
|
|
65
25
|
|
|
26
|
+
self.login(admin)
|
|
66
27
|
response = self.get(url_for("api.notifications"))
|
|
67
28
|
self.assert200(response)
|
|
68
29
|
|
|
69
|
-
self.assertEqual(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
self.assertEqual(notification["type"], "fake")
|
|
74
|
-
self.assertEqual(response.json[0]["details"], {"some": "value"})
|
|
75
|
-
self.assertEqual(response.json[1]["details"], {"another": "value"})
|
|
30
|
+
self.assertEqual(response.json["total"], 1)
|
|
31
|
+
self.assertEqual(
|
|
32
|
+
response.json["data"][0]["details"]["request_organization"]["id"], str(organization.id)
|
|
33
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from flask import current_app
|
|
5
|
+
|
|
6
|
+
from udata.core.organization.models import Member, MembershipRequest, Organization
|
|
7
|
+
from udata.core.user.factories import UserFactory
|
|
8
|
+
from udata.features.notifications import tasks
|
|
9
|
+
from udata.features.notifications.models import Notification
|
|
10
|
+
from udata.tests.api import APITestCase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UserTasksTest(APITestCase):
|
|
14
|
+
@pytest.mark.options(DAYS_AFTER_NOTIFICATION_EXPIRED=3)
|
|
15
|
+
def test_notify_inactive_users(self):
|
|
16
|
+
self.login()
|
|
17
|
+
member = Member(user=self.user, role="admin")
|
|
18
|
+
org = Organization.objects.create(
|
|
19
|
+
name="with transfert", description="XXX", members=[member]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
notification_handled_date = (
|
|
23
|
+
datetime.utcnow()
|
|
24
|
+
- timedelta(days=current_app.config["DAYS_AFTER_NOTIFICATION_EXPIRED"])
|
|
25
|
+
- timedelta(days=1) # add margin
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
applicant = UserFactory()
|
|
29
|
+
|
|
30
|
+
request = MembershipRequest(user=applicant, comment="test")
|
|
31
|
+
org.add_membership_request(request)
|
|
32
|
+
|
|
33
|
+
assert Notification.objects.count() == 1
|
|
34
|
+
|
|
35
|
+
request.status = "accepted"
|
|
36
|
+
request.handled_by = self.user
|
|
37
|
+
request.handled_on = notification_handled_date
|
|
38
|
+
org.save()
|
|
39
|
+
MembershipRequest.after_handle.send(request, org=org)
|
|
40
|
+
|
|
41
|
+
tasks.delete_expired_notifications()
|
|
42
|
+
|
|
43
|
+
assert Notification.objects.count() == 0
|
udata/tests/test_owned.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from mongoengine import post_save
|
|
2
2
|
|
|
3
3
|
import udata.core.owned as owned
|
|
4
|
+
from udata.core.dataset.permissions import OwnableReadPermission
|
|
4
5
|
from udata.core.organization.factories import OrganizationFactory
|
|
5
6
|
from udata.core.organization.models import Organization
|
|
6
7
|
from udata.core.user.factories import AdminFactory, UserFactory
|
|
7
8
|
from udata.core.user.models import User
|
|
8
9
|
from udata.models import Member
|
|
9
10
|
from udata.mongo import db
|
|
10
|
-
from udata.tests.api import DBTestCase
|
|
11
|
+
from udata.tests.api import APITestCase, DBTestCase
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class CustomQuerySet(owned.OwnedQuerySet):
|
|
@@ -265,3 +266,82 @@ class OwnedQuerysetTest(DBTestCase):
|
|
|
265
266
|
name="private_owned_by_other_user"
|
|
266
267
|
)
|
|
267
268
|
self.assertEqual(len(result), 0)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class OwnableReadPermissionTest(APITestCase):
|
|
272
|
+
def setUp(self):
|
|
273
|
+
super().setUp()
|
|
274
|
+
from flask import g
|
|
275
|
+
from flask_principal import AnonymousIdentity
|
|
276
|
+
|
|
277
|
+
g.identity = AnonymousIdentity()
|
|
278
|
+
|
|
279
|
+
def test_public_object_visible_by_anonymous(self):
|
|
280
|
+
"""Public objects should be visible by anonymous users."""
|
|
281
|
+
obj = Owned.objects.create(owner=UserFactory(), private=False)
|
|
282
|
+
assert OwnableReadPermission(obj).can() is True
|
|
283
|
+
|
|
284
|
+
def test_public_object_visible_by_authenticated(self):
|
|
285
|
+
"""Public objects should be visible by authenticated users."""
|
|
286
|
+
obj = Owned.objects.create(owner=UserFactory(), private=False)
|
|
287
|
+
self.login()
|
|
288
|
+
assert OwnableReadPermission(obj).can() is True
|
|
289
|
+
|
|
290
|
+
def test_private_object_not_visible_by_anonymous(self):
|
|
291
|
+
"""Private objects should not be visible by anonymous users."""
|
|
292
|
+
obj = Owned.objects.create(owner=UserFactory(), private=True)
|
|
293
|
+
assert OwnableReadPermission(obj).can() is False
|
|
294
|
+
|
|
295
|
+
def test_private_object_not_visible_by_other_user(self):
|
|
296
|
+
"""Private objects should not be visible by other users."""
|
|
297
|
+
obj = Owned.objects.create(owner=UserFactory(), private=True)
|
|
298
|
+
self.login()
|
|
299
|
+
assert OwnableReadPermission(obj).can() is False
|
|
300
|
+
|
|
301
|
+
def test_private_object_visible_by_owner(self):
|
|
302
|
+
"""Private objects should be visible by their owner."""
|
|
303
|
+
owner = UserFactory()
|
|
304
|
+
obj = Owned.objects.create(owner=owner, private=True)
|
|
305
|
+
self.login(owner)
|
|
306
|
+
assert OwnableReadPermission(obj).can() is True
|
|
307
|
+
|
|
308
|
+
def test_private_object_visible_by_org_admin(self):
|
|
309
|
+
"""Private objects should be visible by organization admins."""
|
|
310
|
+
admin = UserFactory()
|
|
311
|
+
org = OrganizationFactory(members=[Member(user=admin, role="admin")])
|
|
312
|
+
obj = Owned.objects.create(organization=org, private=True)
|
|
313
|
+
self.login(admin)
|
|
314
|
+
assert OwnableReadPermission(obj).can() is True
|
|
315
|
+
|
|
316
|
+
def test_private_object_visible_by_org_editor(self):
|
|
317
|
+
"""Private objects should be visible by organization editors."""
|
|
318
|
+
editor = UserFactory()
|
|
319
|
+
org = OrganizationFactory(members=[Member(user=editor, role="editor")])
|
|
320
|
+
obj = Owned.objects.create(organization=org, private=True)
|
|
321
|
+
self.login(editor)
|
|
322
|
+
assert OwnableReadPermission(obj).can() is True
|
|
323
|
+
|
|
324
|
+
def test_private_object_not_visible_by_other_org_member(self):
|
|
325
|
+
"""Private objects should not be visible by members of other organizations."""
|
|
326
|
+
member = UserFactory()
|
|
327
|
+
OrganizationFactory(members=[Member(user=member, role="admin")])
|
|
328
|
+
org = OrganizationFactory()
|
|
329
|
+
obj = Owned.objects.create(organization=org, private=True)
|
|
330
|
+
self.login(member)
|
|
331
|
+
assert OwnableReadPermission(obj).can() is False
|
|
332
|
+
|
|
333
|
+
def test_private_object_visible_by_admin(self):
|
|
334
|
+
"""Private objects should be visible by sysadmins."""
|
|
335
|
+
admin = AdminFactory()
|
|
336
|
+
obj = Owned.objects.create(owner=UserFactory(), private=True)
|
|
337
|
+
self.login(admin)
|
|
338
|
+
assert OwnableReadPermission(obj).can() is True
|
|
339
|
+
|
|
340
|
+
def test_object_without_private_attribute(self):
|
|
341
|
+
"""Objects without private attribute should be visible by everyone."""
|
|
342
|
+
|
|
343
|
+
class OwnedWithoutPrivate(owned.Owned, db.Document):
|
|
344
|
+
name = db.StringField()
|
|
345
|
+
|
|
346
|
+
obj = OwnedWithoutPrivate.objects.create(owner=UserFactory())
|
|
347
|
+
assert OwnableReadPermission(obj).can() is True
|
udata/tests/test_storages.py
CHANGED
|
@@ -13,7 +13,7 @@ from udata.core.storages import utils
|
|
|
13
13
|
from udata.core.storages.api import META, chunk_filename
|
|
14
14
|
from udata.core.storages.tasks import purge_chunks
|
|
15
15
|
from udata.tests import PytestOnlyTestCase
|
|
16
|
-
from udata.tests.api import
|
|
16
|
+
from udata.tests.api import PytestOnlyAPITestCase
|
|
17
17
|
from udata.utils import faker
|
|
18
18
|
|
|
19
19
|
from .helpers import assert200, assert400
|
|
@@ -108,12 +108,13 @@ class ConfigurableAllowedExtensionsTest(PytestOnlyTestCase):
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
@pytest.mark.usefixtures("instance_path")
|
|
111
|
-
class StorageUploadViewTest(
|
|
112
|
-
def test_standard_upload(self
|
|
113
|
-
|
|
114
|
-
response =
|
|
115
|
-
url_for("
|
|
111
|
+
class StorageUploadViewTest(PytestOnlyAPITestCase):
|
|
112
|
+
def test_standard_upload(self):
|
|
113
|
+
self.login()
|
|
114
|
+
response = self.post(
|
|
115
|
+
url_for("storage.upload", name="resources"),
|
|
116
116
|
{"file": (BytesIO(b"aaa"), "Test with spaces.TXT")},
|
|
117
|
+
json=False,
|
|
117
118
|
)
|
|
118
119
|
|
|
119
120
|
assert200(response)
|
|
@@ -128,14 +129,14 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
128
129
|
assert response.json["url"] == expected
|
|
129
130
|
assert response.json["mime"] == "text/plain"
|
|
130
131
|
|
|
131
|
-
def test_chunked_upload(self
|
|
132
|
-
|
|
133
|
-
url = url_for("
|
|
132
|
+
def test_chunked_upload(self):
|
|
133
|
+
self.login()
|
|
134
|
+
url = url_for("storage.upload", name="tmp")
|
|
134
135
|
uuid = str(uuid4())
|
|
135
136
|
parts = 4
|
|
136
137
|
|
|
137
138
|
for i in range(parts):
|
|
138
|
-
response =
|
|
139
|
+
response = self.post(
|
|
139
140
|
url,
|
|
140
141
|
{
|
|
141
142
|
"file": (BytesIO(b"a"), "blob"),
|
|
@@ -147,6 +148,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
147
148
|
"totalparts": parts,
|
|
148
149
|
"chunksize": 1,
|
|
149
150
|
},
|
|
151
|
+
json=False,
|
|
150
152
|
)
|
|
151
153
|
|
|
152
154
|
assert200(response)
|
|
@@ -157,7 +159,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
157
159
|
assert "sha1" not in response.json
|
|
158
160
|
assert "url" not in response.json
|
|
159
161
|
|
|
160
|
-
response =
|
|
162
|
+
response = self.post(
|
|
161
163
|
url,
|
|
162
164
|
{
|
|
163
165
|
"uuid": uuid,
|
|
@@ -165,6 +167,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
165
167
|
"totalfilesize": parts,
|
|
166
168
|
"totalparts": parts,
|
|
167
169
|
},
|
|
170
|
+
json=False,
|
|
168
171
|
)
|
|
169
172
|
assert "filename" in response.json
|
|
170
173
|
assert "url" in response.json
|
|
@@ -180,13 +183,13 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
180
183
|
assert storages.tmp.read(filename) == b"aaaa"
|
|
181
184
|
assert list(storages.chunks.list_files()) == []
|
|
182
185
|
|
|
183
|
-
def test_chunked_upload_bad_chunk(self
|
|
184
|
-
|
|
185
|
-
url = url_for("
|
|
186
|
+
def test_chunked_upload_bad_chunk(self):
|
|
187
|
+
self.login()
|
|
188
|
+
url = url_for("storage.upload", name="tmp")
|
|
186
189
|
uuid = str(uuid4())
|
|
187
190
|
parts = 4
|
|
188
191
|
|
|
189
|
-
response =
|
|
192
|
+
response = self.post(
|
|
190
193
|
url,
|
|
191
194
|
{
|
|
192
195
|
"file": (BytesIO(b"a"), "blob"),
|
|
@@ -198,6 +201,7 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
198
201
|
"totalparts": parts,
|
|
199
202
|
"chunksize": 10, # Does not match
|
|
200
203
|
},
|
|
204
|
+
json=False,
|
|
201
205
|
)
|
|
202
206
|
|
|
203
207
|
assert400(response)
|
|
@@ -210,10 +214,12 @@ class StorageUploadViewTest(PytestOnlyDBTestCase):
|
|
|
210
214
|
|
|
211
215
|
assert list(storages.chunks.list_files()) == []
|
|
212
216
|
|
|
213
|
-
def test_upload_resource_bad_request(self
|
|
214
|
-
|
|
215
|
-
response =
|
|
216
|
-
url_for("
|
|
217
|
+
def test_upload_resource_bad_request(self):
|
|
218
|
+
self.login()
|
|
219
|
+
response = self.post(
|
|
220
|
+
url_for("storage.upload", name="tmp"),
|
|
221
|
+
{"bad": (BytesIO(b"aaa"), "test.txt")},
|
|
222
|
+
json=False,
|
|
217
223
|
)
|
|
218
224
|
|
|
219
225
|
assert400(response)
|
udata/tests/test_topics.py
CHANGED
|
@@ -16,6 +16,7 @@ from udata.core.topic.factories import (
|
|
|
16
16
|
TopicWithElementsFactory,
|
|
17
17
|
)
|
|
18
18
|
from udata.core.topic.models import Topic, TopicElement
|
|
19
|
+
from udata.core.user.factories import UserFactory
|
|
19
20
|
from udata.search import reindex
|
|
20
21
|
from udata.tests.api import PytestOnlyDBTestCase
|
|
21
22
|
from udata.tests.helpers import assert_emit
|
|
@@ -55,26 +56,32 @@ class TopicModelTest(PytestOnlyDBTestCase):
|
|
|
55
56
|
TopicWithElementsFactory()
|
|
56
57
|
job_reindex.assert_called()
|
|
57
58
|
|
|
58
|
-
def test_topic_activities(self,
|
|
59
|
+
def test_topic_activities(self, app, mocker):
|
|
59
60
|
# A user must be authenticated for activities to be emitted
|
|
60
|
-
|
|
61
|
+
from flask_login import login_user
|
|
62
|
+
|
|
63
|
+
user = UserFactory()
|
|
61
64
|
|
|
62
65
|
mock_created = mocker.patch.object(UserCreatedTopic, "emit")
|
|
63
66
|
mock_updated = mocker.patch.object(UserUpdatedTopic, "emit")
|
|
64
67
|
|
|
65
|
-
with
|
|
66
|
-
|
|
67
|
-
mock_created.assert_called()
|
|
68
|
+
with app.test_request_context():
|
|
69
|
+
login_user(user)
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
mock_updated.assert_called()
|
|
71
|
+
with assert_emit(Topic.on_create):
|
|
72
|
+
topic = TopicFactory(owner=user)
|
|
73
|
+
mock_created.assert_called()
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
with assert_emit(Topic.on_update):
|
|
76
|
+
topic.name = "new name"
|
|
77
|
+
topic.save()
|
|
78
|
+
mock_updated.assert_called()
|
|
79
|
+
|
|
80
|
+
def test_topic_element_activities(self, app, mocker):
|
|
75
81
|
# A user must be authenticated for activities to be emitted
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
from flask_login import login_user
|
|
83
|
+
|
|
84
|
+
user = UserFactory()
|
|
78
85
|
|
|
79
86
|
mock_topic_created = mocker.patch.object(UserCreatedTopic, "emit")
|
|
80
87
|
mock_topic_updated = mocker.patch.object(UserUpdatedTopic, "emit")
|
|
@@ -82,55 +89,64 @@ class TopicModelTest(PytestOnlyDBTestCase):
|
|
|
82
89
|
mock_element_updated = mocker.patch.object(UserUpdatedTopicElement, "emit")
|
|
83
90
|
mock_element_deleted = mocker.patch.object(UserDeletedTopicElement, "emit")
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
92
|
+
with app.test_request_context():
|
|
93
|
+
login_user(user)
|
|
94
|
+
|
|
95
|
+
topic = TopicFactory(owner=user)
|
|
96
|
+
|
|
97
|
+
# Reset mocks after topic creation since it emits activities
|
|
98
|
+
mock_topic_created.reset_mock()
|
|
99
|
+
mock_topic_updated.reset_mock()
|
|
100
|
+
|
|
101
|
+
# Test TopicElement creation
|
|
102
|
+
element = TopicElementDatasetFactory(topic=topic)
|
|
103
|
+
mock_element_created.assert_called_once()
|
|
104
|
+
mock_topic_created.assert_not_called()
|
|
105
|
+
mock_topic_updated.assert_not_called()
|
|
106
|
+
mock_element_updated.assert_not_called()
|
|
107
|
+
mock_element_deleted.assert_not_called()
|
|
108
|
+
|
|
109
|
+
call_args = mock_element_created.call_args
|
|
110
|
+
assert call_args[0][0] == topic # related_to
|
|
111
|
+
assert call_args[0][1] == topic.organization # organization
|
|
112
|
+
assert call_args[1]["extras"]["element_id"] == str(element.id)
|
|
113
|
+
|
|
114
|
+
mock_element_created.reset_mock()
|
|
115
|
+
|
|
116
|
+
# Test TopicElement update
|
|
117
|
+
element.title = "Updated title"
|
|
118
|
+
element.extras = {"key": "value"}
|
|
119
|
+
element.save()
|
|
120
|
+
mock_element_updated.assert_called_once()
|
|
121
|
+
mock_topic_created.assert_not_called()
|
|
122
|
+
mock_topic_updated.assert_not_called()
|
|
123
|
+
mock_element_created.assert_not_called()
|
|
124
|
+
mock_element_deleted.assert_not_called()
|
|
125
|
+
|
|
126
|
+
call_args = mock_element_updated.call_args
|
|
127
|
+
assert call_args[0][0] == topic # related_to
|
|
128
|
+
assert call_args[0][1] == topic.organization # organization
|
|
129
|
+
assert call_args[0][2] == ["title", "extras"] # changed_fields
|
|
130
|
+
assert call_args[1]["extras"]["element_id"] == str(element.id)
|
|
131
|
+
|
|
132
|
+
mock_element_updated.reset_mock()
|
|
133
|
+
|
|
134
|
+
# Test TopicElement deletion
|
|
135
|
+
element_id = element.id
|
|
136
|
+
element.delete()
|
|
137
|
+
|
|
138
|
+
# Deletion should only trigger delete activity
|
|
139
|
+
mock_element_deleted.assert_called_once()
|
|
140
|
+
mock_element_updated.assert_not_called()
|
|
141
|
+
mock_topic_created.assert_not_called()
|
|
142
|
+
mock_topic_updated.assert_not_called()
|
|
143
|
+
mock_element_created.assert_not_called()
|
|
144
|
+
|
|
145
|
+
# Verify delete activity arguments
|
|
146
|
+
delete_call_args = mock_element_deleted.call_args
|
|
147
|
+
assert delete_call_args[0][0] == topic # related_to
|
|
148
|
+
assert delete_call_args[0][1] == topic.organization # organization
|
|
149
|
+
assert delete_call_args[1]["extras"]["element_id"] == str(element_id)
|
|
134
150
|
|
|
135
151
|
def test_topic_element_wrong_class(self):
|
|
136
152
|
# use a model instance that is not supported
|