Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ac64669
refactor: use read permission instead of can for get endpoints
ThibaudDauce Feb 17, 2026
2b2f6bf
allow read deleted objects by partial editors
ThibaudDauce Feb 19, 2026
0ea56ae
feat(visualizations): first draft
nicolaskempf57 Feb 23, 2026
d9ec13c
feat(visualizations): wip
nicolaskempf57 Mar 4, 2026
7b27899
fix(visualizations): fix embedded list fields
nicolaskempf57 Mar 10, 2026
532cff5
test(visualizations): fix API test
nicolaskempf57 Mar 11, 2026
88f4ed0
Merge branch 'main' into feat_add_viz_poc_1
nicolaskempf57 Mar 13, 2026
f49baa0
feat(charts): add all filters
nicolaskempf57 Apr 2, 2026
d7942ca
Merge branch 'main' into feat_add_viz_poc_1
nicolaskempf57 Apr 10, 2026
113f8fc
fix: merge conflict
nicolaskempf57 Apr 10, 2026
7c6f9be
chore: lint
nicolaskempf57 Apr 10, 2026
617a26f
refac: remove deprecated utcnow calls
nicolaskempf57 Apr 13, 2026
2ed3606
fix: missing imports
nicolaskempf57 Apr 13, 2026
7a54c2e
fix: duplicate import
nicolaskempf57 Apr 13, 2026
d0362b1
fix: make delete_at readonly
nicolaskempf57 Apr 13, 2026
67d687e
test: add FloatField to test api_fields
nicolaskempf57 Apr 13, 2026
76a3836
fix: remove dummy content
nicolaskempf57 Apr 13, 2026
0db6490
refac: add queryset
nicolaskempf57 Apr 13, 2026
187dfdf
fix: missing import
nicolaskempf57 Apr 13, 2026
95eded4
chore: format
nicolaskempf57 Apr 13, 2026
f4305d6
chore: remove comment
nicolaskempf57 Apr 15, 2026
2fd6021
chore: simplify comment
nicolaskempf57 Apr 15, 2026
b012a57
chore: not required ?
nicolaskempf57 Apr 15, 2026
075b613
test: better coverage and required fields
nicolaskempf57 Apr 15, 2026
12cba50
test: remove print
nicolaskempf57 Apr 15, 2026
1fd9bcb
test: add filters tests
nicolaskempf57 Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions udata/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ def init_app(app):
import udata.core.dataset.apiv2 # noqa
import udata.core.dataservices.api # noqa
import udata.core.dataservices.apiv2 # noqa
import udata.core.visualizations.api # noqa
import udata.core.discussions.api # noqa
import udata.core.discussions.apiv2 # noqa
import udata.core.reuse.api # noqa
Expand Down
4 changes: 2 additions & 2 deletions udata/api_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ def convert_db_to_field(key, field, info) -> tuple[Callable | None, Callable | N
constructor = restx_fields.String
elif isinstance(field, mongo_fields.FloatField):
constructor = restx_fields.Float
params["min"] = field.min # TODO min_value?
params["max"] = field.max
params["min"] = field.min_value
params["max"] = field.max_value
elif isinstance(field, mongo_fields.IntField):
Comment thread
nicolaskempf57 marked this conversation as resolved.
constructor = restx_fields.Integer
params["min"] = field.min_value
Expand Down
2 changes: 1 addition & 1 deletion udata/core/organization/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ def post(self, org, id):

membership_request.status = "canceled"
membership_request.handled_by = current_user._get_current_object()
membership_request.handled_on = datetime.utcnow()
membership_request.handled_on = datetime.now(UTC)

org.save()
MembershipRequest.after_handle.send(membership_request, org=org)
Expand Down
4 changes: 2 additions & 2 deletions udata/core/organization/assignment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import UTC, datetime

from mongoengine import CASCADE
from mongoengine.signals import post_save
Expand All @@ -24,7 +24,7 @@ class Assignment(db.Document):
subject = field(
db.GenericReferenceField(choices=ASSIGNABLE_OBJECT_TYPES, required=True),
)
created_at = field(db.DateTimeField(default=datetime.utcnow), readonly=True)
created_at = field(db.DateTimeField(default=lambda: datetime.now(UTC)), readonly=True)

meta = {
"indexes": [
Expand Down
4 changes: 2 additions & 2 deletions udata/core/spam/tests/test_spam.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def test_dismissed_spam_in_embed_not_reflagged(self):
When spam is in an embedded document (Message) and the report is dismissed,
adding a new comment should NOT create a new report for the same embed.
"""
from datetime import datetime
from datetime import UTC, datetime

user = UserFactory()
dataset = DatasetFactory()
Expand All @@ -102,7 +102,7 @@ def test_dismissed_spam_in_embed_not_reflagged(self):
report = Report.objects(
subject=discussion, reason=REASON_AUTO_SPAM, subject_embed_id=spam_message.id
).first()
report.dismissed_at = datetime.utcnow()
report.dismissed_at = datetime.now(UTC)
report.save()

self.assertFalse(self.has_spam_report(discussion, spam_message.id))
Expand Down
6 changes: 3 additions & 3 deletions udata/core/user/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import UTC, datetime

from flask import request as flask_request
from flask_security import current_user, logout_user
Expand Down Expand Up @@ -296,7 +296,7 @@ def post(self, id):

req.status = "accepted"
req.handled_by = user
req.handled_on = datetime.utcnow()
req.handled_on = datetime.now(UTC)

member = Member(user=user, role=req.role)
org.members.append(member)
Expand Down Expand Up @@ -344,7 +344,7 @@ def post(self, id):

req.status = "refused"
req.handled_by = user
req.handled_on = datetime.utcnow()
req.handled_on = datetime.now(UTC)
org.save()
MembershipRequest.after_handle.send(req, org=org)
notify_membership_invitation_response.delay(str(org.id), str(req.id))
Expand Down
1 change: 1 addition & 0 deletions udata/core/visualizations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Visualizations module
Comment thread
nicolaskempf57 marked this conversation as resolved.
Outdated
84 changes: 84 additions & 0 deletions udata/core/visualizations/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from datetime import UTC, datetime

import mongoengine
from flask import request
from flask_login import current_user

from udata.api import API, api
from udata.api_fields import patch

from .models import Chart

ns = api.namespace("visualizations", "Visualizations related operations")

common_doc = {"params": {"visualization": "The visualization ID or slug"}}


@ns.route("/", endpoint="visualizations")
class VisualizationsAPI(API):
"""Visualizations collection endpoint"""

@api.doc("list_visualizations")
@api.expect(Chart.__index_parser__)
@api.marshal_with(Chart.__page_fields__)
def get(self):
"""List or search all visualizations"""
query = Chart.objects.visible_by_user(
current_user, mongoengine.Q(private__ne=True, deleted_at=None)
)

return Chart.apply_pagination(Chart.apply_sort_filters(query))

@api.secure
@api.doc("create_visualization", responses={400: "Validation error"})
@api.expect(Chart.__write_fields__)
@api.marshal_with(Chart.__read_fields__, code=201)
def post(self):
"""Create a new visualization"""
visualization = patch(Chart(), request)
if not visualization.owner and not visualization.organization:
visualization.owner = current_user._get_current_object()
visualization.save()
return visualization, 201


@ns.route("/<visualization:visualization>/", endpoint="visualization", doc=common_doc)
class VisualizationAPI(API):
@api.doc("get_visualization")
@api.marshal_with(Chart.__read_fields__)
def get(self, visualization):
"""Get a visualization given its identifier"""
if not visualization.permissions["read"].can():
if not visualization.private and visualization.deleted_at:
api.abort(410, "Visualization has been deleted")
api.abort(404)
return visualization

@api.secure
@api.doc("update_visualization", responses={400: "Validation error"})
@api.expect(Chart.__write_fields__)
@api.marshal_with(Chart.__read_fields__)
def patch(self, visualization):
"""Update a visualization given its identifier"""
if visualization.deleted_at:
api.abort(410, "Visualization has been deleted")

visualization.permissions["edit"].test()

patch(visualization, request)
visualization.save()
return visualization

@api.secure
@api.doc("delete_visualization")
@api.response(204, "Visualization deleted")
def delete(self, visualization):
"""Delete a visualization given its identifier"""
if visualization.deleted_at:
api.abort(410, "Visualization has been deleted")

visualization.permissions["delete"].test()

visualization.deleted_at = datetime.now(UTC)
visualization.save()
return "", 204
36 changes: 36 additions & 0 deletions udata/core/visualizations/factories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import factory

from udata.factories import ModelFactory

from .models import Chart, DataSeries, XAxis, YAxis


class XAxisFactory(ModelFactory):
class Meta:
model = XAxis

column_x = factory.Faker("word")
type = "discrete"


class YAxisFactory(ModelFactory):
class Meta:
model = YAxis

label = factory.Faker("word")


class DataSeriesFactory(ModelFactory):
class Meta:
model = DataSeries

type = "line"
column_y = factory.Faker("word")
Comment on lines +24 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing ressource_id?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not resolved? This factory is never used?



class ChartFactory(ModelFactory):
class Meta:
model = Chart

title = factory.Faker("sentence")
description = factory.Faker("text")
Loading