udata 10.3.1.dev34819__py2.py3-none-any.whl → 10.3.1.dev34849__py2.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 +8 -0
- udata/commands/fixtures.py +7 -1
- udata/core/discussions/api.py +113 -10
- udata/core/discussions/csv.py +2 -0
- udata/core/discussions/forms.py +14 -1
- udata/core/discussions/models.py +57 -3
- udata/core/discussions/permissions.py +31 -9
- udata/core/discussions/tasks.py +3 -3
- udata/core/spam/models.py +4 -0
- udata/forms/fields.py +10 -8
- udata/harvest/backends/maaf.py +234 -0
- udata/harvest/backends/maaf.xsd +362 -0
- udata/harvest/tests/person.jsonld +72 -0
- udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.8a2f7828175824bcd74b.js} +3 -3
- udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.8a2f7828175824bcd74b.js.map} +1 -1
- udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.39e106d56f794ebd06a0.js} +2 -2
- udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.39e106d56f794ebd06a0.js.map} +1 -1
- udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.70cbb4a91b002338007e.js} +2 -2
- udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.70cbb4a91b002338007e.js.map} +1 -1
- udata/static/chunks/{19.f03a102365af4315f9db.js → 19.df16abde17a42033a7f8.js} +3 -3
- udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.df16abde17a42033a7f8.js.map} +1 -1
- udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.5660483641193b7f8295.js} +3 -3
- udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.5660483641193b7f8295.js.map} +1 -1
- udata/static/chunks/{6.d663709d877baa44a71e.js → 6.30dce49d17db07600b06.js} +3 -3
- udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.30dce49d17db07600b06.js.map} +1 -1
- udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.54e44b102164ae5e7a67.js} +2 -2
- udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.54e44b102164ae5e7a67.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- udata/templates/mail/discussion_closed.html +4 -3
- udata/templates/mail/discussion_closed.txt +1 -1
- udata/templates/mail/new_discussion_comment.html +2 -2
- udata/tests/test_discussions.py +281 -3
- {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/METADATA +3 -1
- {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/RECORD +39 -36
- {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/entry_points.txt +1 -0
- {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/LICENSE +0 -0
- {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/WHEEL +0 -0
- {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/top_level.txt +0 -0
udata/api/fields.py
CHANGED
|
@@ -68,6 +68,14 @@ class UrlFor(String):
|
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
class Permission(Boolean):
|
|
72
|
+
def __init__(self, mapper=None, **kwargs):
|
|
73
|
+
super(Permission, self).__init__(**kwargs)
|
|
74
|
+
|
|
75
|
+
def format(self, field):
|
|
76
|
+
return field.can()
|
|
77
|
+
|
|
78
|
+
|
|
71
79
|
class NextPageUrl(String):
|
|
72
80
|
def output(self, key, obj, **kwargs):
|
|
73
81
|
if not getattr(obj, "has_next", None):
|
udata/commands/fixtures.py
CHANGED
|
@@ -65,7 +65,8 @@ UNWANTED_KEYS: dict[str, list[str]] = {
|
|
|
65
65
|
"last_modified",
|
|
66
66
|
"preview_url",
|
|
67
67
|
],
|
|
68
|
-
"discussion": ["subject", "url", "class"],
|
|
68
|
+
"discussion": ["subject", "url", "class", "permissions"],
|
|
69
|
+
"discussion_message": ["permissions"],
|
|
69
70
|
"user": ["uri", "page", "class", "avatar_thumbnail", "email"],
|
|
70
71
|
"posted_by": ["uri", "page", "class", "avatar_thumbnail", "email"],
|
|
71
72
|
"dataservice": [
|
|
@@ -154,6 +155,11 @@ def generate_fixtures_file(data_source: str, results_filename: str) -> None:
|
|
|
154
155
|
).json()["data"]
|
|
155
156
|
for discussion in json_discussion:
|
|
156
157
|
discussion = remove_unwanted_keys(discussion, "discussion")
|
|
158
|
+
for index, message in enumerate(discussion["discussion"]):
|
|
159
|
+
discussion["discussion"][index] = remove_unwanted_keys(
|
|
160
|
+
message, "discussion_message"
|
|
161
|
+
)
|
|
162
|
+
|
|
157
163
|
json_fixture["discussions"] = json_discussion
|
|
158
164
|
|
|
159
165
|
json_dataservices = requests.get(
|
udata/core/discussions/api.py
CHANGED
|
@@ -5,9 +5,9 @@ from flask_restx.inputs import boolean
|
|
|
5
5
|
from flask_security import current_user
|
|
6
6
|
|
|
7
7
|
from udata.api import API, api, fields
|
|
8
|
-
from udata.auth import admin_permission
|
|
9
8
|
from udata.core.dataservices.models import Dataservice
|
|
10
9
|
from udata.core.dataset.models import Dataset
|
|
10
|
+
from udata.core.organization.api_fields import org_ref_fields
|
|
11
11
|
from udata.core.organization.models import Organization
|
|
12
12
|
from udata.core.reuse.models import Reuse
|
|
13
13
|
from udata.core.spam.api import SpamAPIMixin
|
|
@@ -15,23 +15,43 @@ from udata.core.spam.fields import spam_fields
|
|
|
15
15
|
from udata.core.user.api_fields import user_ref_fields
|
|
16
16
|
from udata.utils import id_or_404
|
|
17
17
|
|
|
18
|
-
from .forms import
|
|
18
|
+
from .forms import (
|
|
19
|
+
DiscussionCommentForm,
|
|
20
|
+
DiscussionCreateForm,
|
|
21
|
+
DiscussionEditCommentForm,
|
|
22
|
+
DiscussionEditForm,
|
|
23
|
+
)
|
|
19
24
|
from .models import Discussion, Message
|
|
20
|
-
from .permissions import CloseDiscussionPermission
|
|
21
25
|
from .signals import on_discussion_deleted
|
|
22
26
|
|
|
23
27
|
ns = api.namespace("discussions", "Discussion related operations")
|
|
24
28
|
|
|
29
|
+
|
|
30
|
+
message_permissions_fields = api.model(
|
|
31
|
+
"DiscussionMessagePermissions",
|
|
32
|
+
{"delete": fields.Permission(), "edit": fields.Permission()},
|
|
33
|
+
)
|
|
34
|
+
|
|
25
35
|
message_fields = api.model(
|
|
26
36
|
"DiscussionMessage",
|
|
27
37
|
{
|
|
28
38
|
"content": fields.String(description="The message body"),
|
|
29
39
|
"posted_by": fields.Nested(user_ref_fields, description="The message author"),
|
|
40
|
+
"posted_by_organization": fields.Nested(
|
|
41
|
+
org_ref_fields, description="The organization to show to users", allow_null=True
|
|
42
|
+
),
|
|
30
43
|
"posted_on": fields.ISODateTime(description="The message posting date"),
|
|
44
|
+
"last_modified_at": fields.ISODateTime(description="The message last edit date"),
|
|
31
45
|
"spam": fields.Nested(spam_fields),
|
|
46
|
+
"permissions": fields.Nested(message_permissions_fields),
|
|
32
47
|
},
|
|
33
48
|
)
|
|
34
49
|
|
|
50
|
+
discussion_permissions_fields = api.model(
|
|
51
|
+
"DiscussionPermissions",
|
|
52
|
+
{"delete": fields.Permission(), "edit": fields.Permission(), "close": fields.Permission()},
|
|
53
|
+
)
|
|
54
|
+
|
|
35
55
|
discussion_fields = api.model(
|
|
36
56
|
"Discussion",
|
|
37
57
|
{
|
|
@@ -40,15 +60,24 @@ discussion_fields = api.model(
|
|
|
40
60
|
"class": fields.ClassName(description="The object class", discriminator=True),
|
|
41
61
|
"title": fields.String(description="The discussion title"),
|
|
42
62
|
"user": fields.Nested(user_ref_fields, description="The discussion author"),
|
|
63
|
+
"organization": fields.Nested(
|
|
64
|
+
org_ref_fields, description="The discussion author", allow_null=True
|
|
65
|
+
),
|
|
43
66
|
"created": fields.ISODateTime(description="The discussion creation date"),
|
|
44
67
|
"closed": fields.ISODateTime(description="The discussion closing date"),
|
|
45
68
|
"closed_by": fields.Nested(
|
|
46
69
|
user_ref_fields, allow_null=True, description="The user who closed the discussion"
|
|
47
70
|
),
|
|
71
|
+
"closed_by_organization": fields.Nested(
|
|
72
|
+
org_ref_fields,
|
|
73
|
+
allow_null=True,
|
|
74
|
+
description="The organization who closed the discussion",
|
|
75
|
+
),
|
|
48
76
|
"discussion": fields.Nested(message_fields),
|
|
49
77
|
"url": fields.UrlFor("api.discussion", description="The discussion API URI"),
|
|
50
78
|
"extras": fields.Raw(description="Extra attributes as key-value pairs"),
|
|
51
79
|
"spam": fields.Nested(spam_fields),
|
|
80
|
+
"permissions": fields.Nested(discussion_permissions_fields),
|
|
52
81
|
},
|
|
53
82
|
)
|
|
54
83
|
|
|
@@ -60,6 +89,9 @@ start_discussion_fields = api.model(
|
|
|
60
89
|
"subject": fields.Nested(
|
|
61
90
|
api.model_reference, description="The discussion target object", required=True
|
|
62
91
|
),
|
|
92
|
+
"organization": fields.Nested(
|
|
93
|
+
org_ref_fields, allow_null=True, description="Publish in the name of this organization"
|
|
94
|
+
),
|
|
63
95
|
"extras": fields.Raw(description="Extras attributes as key-value pairs"),
|
|
64
96
|
},
|
|
65
97
|
)
|
|
@@ -74,6 +106,20 @@ comment_discussion_fields = api.model(
|
|
|
74
106
|
},
|
|
75
107
|
)
|
|
76
108
|
|
|
109
|
+
edit_comment_discussion_fields = api.model(
|
|
110
|
+
"DiscussionEditComment",
|
|
111
|
+
{
|
|
112
|
+
"comment": fields.String(description="The new comment", required=True),
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
edit_discussion_fields = api.model(
|
|
117
|
+
"DiscussionEdit",
|
|
118
|
+
{
|
|
119
|
+
"title": fields.String(description="The new title", required=True),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
77
123
|
discussion_page_fields = api.model("DiscussionPage", fields.pager(discussion_fields))
|
|
78
124
|
|
|
79
125
|
parser = api.parser()
|
|
@@ -138,14 +184,30 @@ class DiscussionAPI(API):
|
|
|
138
184
|
if discussion.closed:
|
|
139
185
|
api.abort(403, "Can't add comments on a closed discussion")
|
|
140
186
|
form = api.validate(DiscussionCommentForm)
|
|
141
|
-
|
|
142
|
-
discussion.discussion.append(message)
|
|
143
|
-
message_idx = len(discussion.discussion) - 1
|
|
187
|
+
|
|
144
188
|
close = form.close.data
|
|
189
|
+
if not close and not form.comment.data:
|
|
190
|
+
api.abort(
|
|
191
|
+
400, "Can only close without message. Please provide either `close` or a `comment`."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if form.comment.data:
|
|
195
|
+
message = Message(
|
|
196
|
+
content=form.comment.data,
|
|
197
|
+
posted_by=current_user.id,
|
|
198
|
+
posted_by_organization=form.organization.data,
|
|
199
|
+
)
|
|
200
|
+
discussion.discussion.append(message)
|
|
201
|
+
message_idx = len(discussion.discussion) - 1
|
|
202
|
+
else:
|
|
203
|
+
message_idx = None
|
|
204
|
+
|
|
145
205
|
if close:
|
|
146
|
-
|
|
206
|
+
discussion.permissions["close"].test()
|
|
147
207
|
discussion.closed_by = current_user._get_current_object()
|
|
208
|
+
discussion.closed_by_organization = form.organization.data
|
|
148
209
|
discussion.closed = datetime.utcnow()
|
|
210
|
+
|
|
149
211
|
discussion.save()
|
|
150
212
|
if close:
|
|
151
213
|
discussion.signal_close(message=message_idx)
|
|
@@ -153,12 +215,27 @@ class DiscussionAPI(API):
|
|
|
153
215
|
discussion.signal_comment(message=message_idx)
|
|
154
216
|
return discussion
|
|
155
217
|
|
|
156
|
-
@api.
|
|
218
|
+
@api.doc("update_discussion")
|
|
219
|
+
@api.response(403, "Not allowed to update this discussion")
|
|
220
|
+
@api.expect(edit_comment_discussion_fields)
|
|
221
|
+
@api.marshal_with(discussion_fields)
|
|
222
|
+
def put(self, id):
|
|
223
|
+
"""Update a discussion given its ID"""
|
|
224
|
+
discussion = Discussion.objects.get_or_404(id=id_or_404(id))
|
|
225
|
+
discussion.permissions["edit"].test()
|
|
226
|
+
|
|
227
|
+
form = api.validate(DiscussionEditForm, discussion)
|
|
228
|
+
form.save()
|
|
229
|
+
|
|
230
|
+
return discussion
|
|
231
|
+
|
|
157
232
|
@api.doc("delete_discussion")
|
|
158
233
|
@api.response(403, "Not allowed to delete this discussion")
|
|
159
234
|
def delete(self, id):
|
|
160
235
|
"""Delete a discussion given its ID"""
|
|
161
236
|
discussion = Discussion.objects.get_or_404(id=id_or_404(id))
|
|
237
|
+
discussion.permissions["delete"].test()
|
|
238
|
+
|
|
162
239
|
discussion.delete()
|
|
163
240
|
on_discussion_deleted.send(discussion)
|
|
164
241
|
return "", 204
|
|
@@ -182,7 +259,26 @@ class DiscussionCommentAPI(API):
|
|
|
182
259
|
Base class for a comment in a discussion thread.
|
|
183
260
|
"""
|
|
184
261
|
|
|
185
|
-
@api.
|
|
262
|
+
@api.doc("edit_discussion_comment")
|
|
263
|
+
@api.response(403, "Not allowed to edit this comment")
|
|
264
|
+
@api.expect(edit_comment_discussion_fields)
|
|
265
|
+
@api.marshal_with(discussion_fields)
|
|
266
|
+
def put(self, id, cidx):
|
|
267
|
+
"""Edit a comment given its index"""
|
|
268
|
+
discussion = Discussion.objects.get_or_404(id=id_or_404(id))
|
|
269
|
+
if len(discussion.discussion) <= cidx:
|
|
270
|
+
api.abort(404, "Comment does not exist")
|
|
271
|
+
|
|
272
|
+
message = discussion.discussion[cidx]
|
|
273
|
+
message.permissions["edit"].test()
|
|
274
|
+
|
|
275
|
+
form = api.validate(DiscussionEditCommentForm)
|
|
276
|
+
|
|
277
|
+
discussion.discussion[cidx].content = form.comment.data
|
|
278
|
+
discussion.discussion[cidx].last_modified_at = datetime.utcnow()
|
|
279
|
+
discussion.save()
|
|
280
|
+
return discussion
|
|
281
|
+
|
|
186
282
|
@api.doc("delete_discussion_comment")
|
|
187
283
|
@api.response(403, "Not allowed to delete this comment")
|
|
188
284
|
def delete(self, id, cidx):
|
|
@@ -192,6 +288,9 @@ class DiscussionCommentAPI(API):
|
|
|
192
288
|
api.abort(404, "Comment does not exist")
|
|
193
289
|
elif cidx == 0:
|
|
194
290
|
api.abort(400, "You cannot delete the first comment of a discussion")
|
|
291
|
+
|
|
292
|
+
discussion.discussion[cidx].permissions["delete"].test()
|
|
293
|
+
|
|
195
294
|
discussion.discussion.pop(cidx)
|
|
196
295
|
discussion.save()
|
|
197
296
|
return "", 204
|
|
@@ -238,7 +337,11 @@ class DiscussionsAPI(API):
|
|
|
238
337
|
"""Create a new Discussion"""
|
|
239
338
|
form = api.validate(DiscussionCreateForm)
|
|
240
339
|
|
|
241
|
-
message = Message(
|
|
340
|
+
message = Message(
|
|
341
|
+
content=form.comment.data,
|
|
342
|
+
posted_by=current_user.id,
|
|
343
|
+
posted_by_organization=form.organization.data,
|
|
344
|
+
)
|
|
242
345
|
discussion = Discussion(user=current_user.id, discussion=[message])
|
|
243
346
|
form.populate_obj(discussion)
|
|
244
347
|
discussion.save()
|
udata/core/discussions/csv.py
CHANGED
udata/core/discussions/forms.py
CHANGED
|
@@ -10,6 +10,7 @@ __all__ = ("DiscussionCreateForm", "DiscussionCommentForm")
|
|
|
10
10
|
class DiscussionCreateForm(ModelForm):
|
|
11
11
|
model_class = Discussion
|
|
12
12
|
|
|
13
|
+
organization = fields.PublishAsField(_("Publish as"), owner_field=None)
|
|
13
14
|
title = fields.StringField(_("Title"), [validators.DataRequired()])
|
|
14
15
|
comment = fields.StringField(
|
|
15
16
|
_("Comment"), [validators.DataRequired(), validators.Length(max=COMMENT_SIZE_LIMIT)]
|
|
@@ -18,8 +19,20 @@ class DiscussionCreateForm(ModelForm):
|
|
|
18
19
|
extras = fields.ExtrasField()
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
class DiscussionEditForm(ModelForm):
|
|
23
|
+
model_class = Discussion
|
|
24
|
+
|
|
25
|
+
title = fields.StringField(_("Title"), [validators.DataRequired()])
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
class DiscussionCommentForm(Form):
|
|
29
|
+
organization = fields.PublishAsField(_("Publish as"), owner_field=None)
|
|
30
|
+
|
|
31
|
+
comment = fields.StringField(_("Comment"), [validators.Length(max=COMMENT_SIZE_LIMIT)])
|
|
32
|
+
close = fields.BooleanField(default=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DiscussionEditCommentForm(Form):
|
|
22
36
|
comment = fields.StringField(
|
|
23
37
|
_("Comment"), [validators.DataRequired(), validators.Length(max=COMMENT_SIZE_LIMIT)]
|
|
24
38
|
)
|
|
25
|
-
close = fields.BooleanField(default=False)
|
udata/core/discussions/models.py
CHANGED
|
@@ -16,14 +16,37 @@ class Message(SpamMixin, db.EmbeddedDocument):
|
|
|
16
16
|
content = db.StringField(required=True)
|
|
17
17
|
posted_on = db.DateTimeField(default=datetime.utcnow, required=True)
|
|
18
18
|
posted_by = db.ReferenceField("User")
|
|
19
|
+
posted_by_organization = db.ReferenceField("Organization")
|
|
20
|
+
last_modified_at = db.DateTimeField()
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def permissions(self):
|
|
24
|
+
from .permissions import DiscussionMessagePermission
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"delete": DiscussionMessagePermission(self),
|
|
28
|
+
"edit": DiscussionMessagePermission(self),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def posted_by_name(self):
|
|
33
|
+
return (
|
|
34
|
+
self.posted_by_organization.name
|
|
35
|
+
if self.posted_by_organization
|
|
36
|
+
else self.posted_by.fullname
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def posted_by_org_or_user(self):
|
|
41
|
+
return self.posted_by_organization or self.posted_by
|
|
19
42
|
|
|
20
43
|
def texts_to_check_for_spam(self):
|
|
21
44
|
return [self.content]
|
|
22
45
|
|
|
23
46
|
def spam_report_message(self, breadcrumb):
|
|
24
47
|
message = "Spam potentiel dans le message"
|
|
25
|
-
if self.
|
|
26
|
-
message += f" de [{self.
|
|
48
|
+
if self.posted_by_org_or_user:
|
|
49
|
+
message += f" de [{self.posted_by_name}]({self.posted_by_org_or_user.external_url})"
|
|
27
50
|
|
|
28
51
|
if len(breadcrumb) != 2:
|
|
29
52
|
log.warning(
|
|
@@ -46,12 +69,15 @@ class Message(SpamMixin, db.EmbeddedDocument):
|
|
|
46
69
|
|
|
47
70
|
class Discussion(SpamMixin, db.Document):
|
|
48
71
|
user = db.ReferenceField("User")
|
|
72
|
+
organization = db.ReferenceField("Organization")
|
|
73
|
+
|
|
49
74
|
subject = db.GenericReferenceField()
|
|
50
75
|
title = db.StringField(required=True)
|
|
51
76
|
discussion = db.ListField(db.EmbeddedDocumentField(Message))
|
|
52
77
|
created = db.DateTimeField(default=datetime.utcnow, required=True)
|
|
53
78
|
closed = db.DateTimeField()
|
|
54
79
|
closed_by = db.ReferenceField("User")
|
|
80
|
+
closed_by_organization = db.ReferenceField("Organization")
|
|
55
81
|
extras = db.ExtrasField()
|
|
56
82
|
|
|
57
83
|
meta = {
|
|
@@ -59,6 +85,34 @@ class Discussion(SpamMixin, db.Document):
|
|
|
59
85
|
"ordering": ["-created"],
|
|
60
86
|
}
|
|
61
87
|
|
|
88
|
+
@property
|
|
89
|
+
def permissions(self):
|
|
90
|
+
from udata.core.discussions.permissions import (
|
|
91
|
+
DiscussionAuthorOrSubjectOwnerPermission,
|
|
92
|
+
DiscussionAuthorPermission,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"delete": DiscussionAuthorPermission(self),
|
|
97
|
+
# To edit the title of a discussion we need to be the owner of the first message
|
|
98
|
+
"edit": DiscussionAuthorPermission(self),
|
|
99
|
+
"close": DiscussionAuthorOrSubjectOwnerPermission(self),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def closed_by_name(self):
|
|
104
|
+
if self.closed_by_organization:
|
|
105
|
+
return self.closed_by_organization.name
|
|
106
|
+
|
|
107
|
+
if self.closed_by:
|
|
108
|
+
return self.closed_by.fullname
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def closed_by_org_or_user(self):
|
|
114
|
+
return self.closed_by_organization or self.closed_by
|
|
115
|
+
|
|
62
116
|
def person_involved(self, person):
|
|
63
117
|
"""Return True if the given person has been involved in the
|
|
64
118
|
|
|
@@ -113,7 +167,7 @@ class Discussion(SpamMixin, db.Document):
|
|
|
113
167
|
def signal_new(self):
|
|
114
168
|
on_new_discussion.send(self)
|
|
115
169
|
|
|
116
|
-
@spam_protected(lambda discussion, message: discussion.discussion[message])
|
|
170
|
+
@spam_protected(lambda discussion, message: discussion.discussion[message] if message else None)
|
|
117
171
|
def signal_close(self, message):
|
|
118
172
|
on_discussion_closed.send(self, message=message)
|
|
119
173
|
|
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
from udata.auth import Permission, UserNeed
|
|
2
|
+
from udata.core.dataset.permissions import OwnablePermission
|
|
2
3
|
from udata.core.organization.permissions import (
|
|
3
4
|
OrganizationAdminNeed,
|
|
4
5
|
OrganizationEditorNeed,
|
|
5
6
|
)
|
|
6
7
|
|
|
8
|
+
from .models import Discussion, Message
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
# This is a hack to because double inheritance doesn't work really well with permissions.
|
|
12
|
+
# I simulate a class constructor with a function to keep the same API than other permissions
|
|
13
|
+
# but use the `.union()` of two permission under the hood.
|
|
14
|
+
def DiscussionAuthorOrSubjectOwnerPermission(discussion: Discussion):
|
|
15
|
+
return OwnablePermission(discussion.subject).union(DiscussionAuthorPermission(discussion))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DiscussionAuthorPermission(Permission):
|
|
19
|
+
def __init__(self, discussion: Discussion):
|
|
20
|
+
needs = []
|
|
21
|
+
|
|
22
|
+
if discussion.organization:
|
|
23
|
+
needs.append(OrganizationAdminNeed(discussion.organization.id))
|
|
24
|
+
needs.append(OrganizationEditorNeed(discussion.organization.id))
|
|
25
|
+
else:
|
|
26
|
+
needs.append(UserNeed(discussion.user.fs_uniquifier))
|
|
27
|
+
|
|
28
|
+
super(DiscussionAuthorPermission, self).__init__(*needs)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DiscussionMessagePermission(Permission):
|
|
32
|
+
def __init__(self, message: Message):
|
|
10
33
|
needs = []
|
|
11
|
-
subject = discussion.subject
|
|
12
34
|
|
|
13
|
-
if
|
|
14
|
-
needs.append(OrganizationAdminNeed(
|
|
15
|
-
needs.append(OrganizationEditorNeed(
|
|
16
|
-
|
|
17
|
-
needs.append(UserNeed(
|
|
35
|
+
if message.posted_by_organization:
|
|
36
|
+
needs.append(OrganizationAdminNeed(message.posted_by_organization.id))
|
|
37
|
+
needs.append(OrganizationEditorNeed(message.posted_by_organization.id))
|
|
38
|
+
else:
|
|
39
|
+
needs.append(UserNeed(message.posted_by.fs_uniquifier))
|
|
18
40
|
|
|
19
|
-
super(
|
|
41
|
+
super(DiscussionMessagePermission, self).__init__(*needs)
|
udata/core/discussions/tasks.py
CHANGED
|
@@ -42,7 +42,7 @@ def notify_new_discussion_comment(discussion_id, message=None):
|
|
|
42
42
|
if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
|
|
43
43
|
recipients = owner_recipients(discussion) + [m.posted_by for m in discussion.discussion]
|
|
44
44
|
recipients = list({u.id: u for u in recipients if u != message.posted_by}.values())
|
|
45
|
-
subject = _("%(user)s commented your discussion", user=message.
|
|
45
|
+
subject = _("%(user)s commented your discussion", user=message.posted_by_name)
|
|
46
46
|
|
|
47
47
|
mail.send(
|
|
48
48
|
subject, recipients, "new_discussion_comment", discussion=discussion, message=message
|
|
@@ -54,10 +54,10 @@ def notify_new_discussion_comment(discussion_id, message=None):
|
|
|
54
54
|
@connect(on_discussion_closed, by_id=True)
|
|
55
55
|
def notify_discussion_closed(discussion_id, message=None):
|
|
56
56
|
discussion = Discussion.objects.get(pk=discussion_id)
|
|
57
|
-
message = discussion.discussion[message]
|
|
57
|
+
message = discussion.discussion[message] if message else None
|
|
58
58
|
if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
|
|
59
59
|
recipients = owner_recipients(discussion) + [m.posted_by for m in discussion.discussion]
|
|
60
|
-
recipients = list({u.id: u for u in recipients if u !=
|
|
60
|
+
recipients = list({u.id: u for u in recipients if u != discussion.closed_by}.values())
|
|
61
61
|
subject = _("A discussion has been closed")
|
|
62
62
|
mail.send(subject, recipients, "discussion_closed", discussion=discussion, message=message)
|
|
63
63
|
else:
|
udata/core/spam/models.py
CHANGED
|
@@ -193,6 +193,10 @@ def spam_protected(get_model_to_check=None):
|
|
|
193
193
|
base_model = args[0]
|
|
194
194
|
if get_model_to_check:
|
|
195
195
|
model_to_check = get_model_to_check(*args, **kwargs)
|
|
196
|
+
|
|
197
|
+
if not model_to_check:
|
|
198
|
+
f(*args, **kwargs)
|
|
199
|
+
return
|
|
196
200
|
else:
|
|
197
201
|
model_to_check = base_model
|
|
198
202
|
|
udata/forms/fields.py
CHANGED
|
@@ -782,14 +782,16 @@ class PublishAsField(ModelFieldMixin, Field):
|
|
|
782
782
|
raise validators.ValidationError(_("You must be authenticated"))
|
|
783
783
|
elif not OrganizationPrivatePermission(self.data).can():
|
|
784
784
|
raise validators.ValidationError(_("Permission denied for this organization"))
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
owner_field
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
785
|
+
|
|
786
|
+
if self.owner_field:
|
|
787
|
+
# Ensure either owner field or this field value is unset
|
|
788
|
+
owner_field = form._fields[self.owner_field]
|
|
789
|
+
if self.raw_data:
|
|
790
|
+
owner_field.data = None
|
|
791
|
+
elif getattr(form._obj, self.short_name) and not owner_field.data:
|
|
792
|
+
pass
|
|
793
|
+
else:
|
|
794
|
+
self.data = None
|
|
793
795
|
return True
|
|
794
796
|
|
|
795
797
|
|