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.

Files changed (39) hide show
  1. udata/api/fields.py +8 -0
  2. udata/commands/fixtures.py +7 -1
  3. udata/core/discussions/api.py +113 -10
  4. udata/core/discussions/csv.py +2 -0
  5. udata/core/discussions/forms.py +14 -1
  6. udata/core/discussions/models.py +57 -3
  7. udata/core/discussions/permissions.py +31 -9
  8. udata/core/discussions/tasks.py +3 -3
  9. udata/core/spam/models.py +4 -0
  10. udata/forms/fields.py +10 -8
  11. udata/harvest/backends/maaf.py +234 -0
  12. udata/harvest/backends/maaf.xsd +362 -0
  13. udata/harvest/tests/person.jsonld +72 -0
  14. udata/static/chunks/{11.b6f741fcc366abfad9c4.js → 11.8a2f7828175824bcd74b.js} +3 -3
  15. udata/static/chunks/{11.b6f741fcc366abfad9c4.js.map → 11.8a2f7828175824bcd74b.js.map} +1 -1
  16. udata/static/chunks/{13.2d06442dd9a05d9777b5.js → 13.39e106d56f794ebd06a0.js} +2 -2
  17. udata/static/chunks/{13.2d06442dd9a05d9777b5.js.map → 13.39e106d56f794ebd06a0.js.map} +1 -1
  18. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js → 17.70cbb4a91b002338007e.js} +2 -2
  19. udata/static/chunks/{17.e8e4caaad5cb0cc0bacc.js.map → 17.70cbb4a91b002338007e.js.map} +1 -1
  20. udata/static/chunks/{19.f03a102365af4315f9db.js → 19.df16abde17a42033a7f8.js} +3 -3
  21. udata/static/chunks/{19.f03a102365af4315f9db.js.map → 19.df16abde17a42033a7f8.js.map} +1 -1
  22. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js → 5.5660483641193b7f8295.js} +3 -3
  23. udata/static/chunks/{5.0fa1408dae4e76b87b2e.js.map → 5.5660483641193b7f8295.js.map} +1 -1
  24. udata/static/chunks/{6.d663709d877baa44a71e.js → 6.30dce49d17db07600b06.js} +3 -3
  25. udata/static/chunks/{6.d663709d877baa44a71e.js.map → 6.30dce49d17db07600b06.js.map} +1 -1
  26. udata/static/chunks/{8.778091d55cd8ea39af6b.js → 8.54e44b102164ae5e7a67.js} +2 -2
  27. udata/static/chunks/{8.778091d55cd8ea39af6b.js.map → 8.54e44b102164ae5e7a67.js.map} +1 -1
  28. udata/static/common.js +1 -1
  29. udata/static/common.js.map +1 -1
  30. udata/templates/mail/discussion_closed.html +4 -3
  31. udata/templates/mail/discussion_closed.txt +1 -1
  32. udata/templates/mail/new_discussion_comment.html +2 -2
  33. udata/tests/test_discussions.py +281 -3
  34. {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/METADATA +3 -1
  35. {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/RECORD +39 -36
  36. {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/entry_points.txt +1 -0
  37. {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/LICENSE +0 -0
  38. {udata-10.3.1.dev34819.dist-info → udata-10.3.1.dev34849.dist-info}/WHEEL +0 -0
  39. {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):
@@ -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(
@@ -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 DiscussionCommentForm, DiscussionCreateForm
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
- message = Message(content=form.comment.data, posted_by=current_user.id)
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
- CloseDiscussionPermission(discussion).test()
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.secure(admin_permission)
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.secure(admin_permission)
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(content=form.comment.data, posted_by=current_user.id)
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()
@@ -19,4 +19,6 @@ class DiscussionCsvAdapter(csv.Adapter):
19
19
  "closed",
20
20
  "closed_by",
21
21
  ("closed_by_id", "closed_by.id"),
22
+ "closed_by_organization",
23
+ ("closed_by_organization_id", "closed_by_organization.id"),
22
24
  )
@@ -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)
@@ -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.posted_by:
26
- message += f" de [{self.posted_by.fullname}]({self.posted_by.external_url})"
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
- class CloseDiscussionPermission(Permission):
9
- def __init__(self, discussion):
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 getattr(subject, "organization"):
14
- needs.append(OrganizationAdminNeed(subject.organization.id))
15
- needs.append(OrganizationEditorNeed(subject.organization.id))
16
- elif subject.owner:
17
- needs.append(UserNeed(subject.owner.fs_uniquifier))
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(CloseDiscussionPermission, self).__init__(*needs)
41
+ super(DiscussionMessagePermission, self).__init__(*needs)
@@ -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.posted_by.fullname)
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 != message.posted_by}.values())
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
- # Ensure either owner field or this field value is unset
786
- owner_field = form._fields[self.owner_field]
787
- if self.raw_data:
788
- owner_field.data = None
789
- elif getattr(form._obj, self.short_name) and not owner_field.data:
790
- pass
791
- else:
792
- self.data = None
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