Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend/feature/5123 update community #5373

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from

Conversation

jasonwvh
Copy link
Contributor

@jasonwvh jasonwvh commented Dec 20, 2024

Database migrations:

  • deleted timestamp column for clusters, node, pages, page_versions

New endpoints:

  • UpdateCommunity
    • takes community_id and updates name, description, parent_node_id, and geojson
    • question: do we want to include admin_ids here?
  • DeleteCommunity
    • sets deleted to now() in clusters, node, pages, page_versions

partially closes #5123

Backend checklist

  • Formatted my code by running ruff check --select I --fix . && ruff check . && ruff format . in app/backend
  • Added tests for any new code or added a regression test if fixing a bug
  • All tests pass
  • Run the backend locally and it works
  • Added migrations if there are any database changes, rebased onto develop if necessary for linear migration history

Web frontend checklist

  • Formatted my code with yarn format
  • There are no warnings from yarn lint --fix
  • There are no console warnings when running the app
  • Added any new components to storybook
  • Added tests where relevant
  • All tests pass
  • Clicked around my changes running locally and it works
  • Checked Desktop, Mobile and Tablet screen sizes

Copy link

vercel bot commented Dec 20, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
couchers ✅ Ready (Inspect) Visit Preview Jan 15, 2025 7:35am

@jasonwvh jasonwvh changed the title Backend/feature/5123 update delete community [Draft] Backend/feature/5123 update delete community Dec 23, 2024
@jasonwvh jasonwvh requested review from aapeliv and removed request for aapeliv December 23, 2024 09:06
Copy link
Member

@aapeliv aapeliv left a comment

Choose a reason for hiding this comment

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

Thank you for working on this! I think the update community RPC is looking great and is mostly good to merge with some minor comments.

Sorry for taking so long to get to this, I've been in Oz and busy with family stuff.

I've added some general comments on the code otherwise too.


On the deletion stuff: I'm not entirely sure how we should handle the cases of dependent objects when their creator/owner/parent is deleted.

For example the main page of a community (cluster) is a designated page: if one (the cluster or the page) is deleted, the other should be too. Maybe this is easy by saying main pages cannot be deleted with a CHECK constraint (so deleted cluster => deleted page).

But what about when a cluster is deleted, all its sub-pages should be deleted too, it seems?

Or if a community is deleted, all sub-communities should also be deleted...

We will need to think about it. I guess there are three ways to go:

  1. Make only some things soft-deleteable (e.g. non-main pages) and hard-delete other things (nodes, clusters): I feel like having soft-deleted nodes would be a pain in the ass code-wise.
  2. Figure out some way to enforce the deletion hierarchy via database constraints or similar. Not sure how to do this, but this would be the cleanest.
  3. Enforce deletion hierarchy via our code, or via TRIGGERs. This is my least preferred solution since it's prone to leaving the database in a broken state, but if we can't think of other things, we might go with this.

@@ -1513,6 +1513,7 @@ class Node(Base):
parent_node_id = Column(ForeignKey("nodes.id"), nullable=True, index=True)
geom = deferred(Column(Geometry(geometry_type="MULTIPOLYGON", srid=4326), nullable=False))
created = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
deleted = Column(DateTime(timezone=True), nullable=True, default=None)
Copy link
Member

Choose a reason for hiding this comment

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

No need for default since it's nullable

new_node = Node(geom=geom, parent_node_id=parent_node_id)
session.add(new_node)

now = datetime.utcnow()
Copy link
Member

Choose a reason for hiding this comment

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

Please use now() from utils.py (to be consistent, also I'm not sure if utcnow (which is naive, I believe) works correctly if the server TZ is not UTC)

session.add(new_node)

now = datetime.utcnow()
session.execute(update(Node).where(Node.id == node.id).values({"deleted": now}))
Copy link
Member

Choose a reason for hiding this comment

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

It's better to do this through the ORM, you can simply assign to node.deleted, so this would be

node.deleted = now()

session.execute will actually issue a SQL statement here. Using the ORM way will issue them all at flush or commit (which happens automatically as well at the end of the RPC due to the session_scope context manager).

This applies to a bunch of places here. We only really need a low-level UPDATE when updating a lot of things (where it's undesirable to have SQLAlchemy load everything into python objects), or when updating based on a computed SQL expression we want rendered directly (e.g. UPDATE table SET somevalue = othervalue + 2... or similar).

I am also not entirely sure how this interacts with the SQLAlchemy ORM: it might cause a bunch of refreshing objects (extra SELECTs), or it might cause the object to be stale in other ways.

@@ -283,6 +285,7 @@ def ListPlaces(self, request, context, session):
places = (
node.official_cluster.owned_pages.where(Page.type == PageType.place)
.where(Page.id >= next_page_id)
.where(Page.deleted is None)
Copy link
Member

Choose a reason for hiding this comment

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

Compare to None with deleted == None. Or I think the actual right way to do this is to do Page.deleted.is_(None), but == None works too. I believe is None might cause issues.

This is weird minutiae of SQLalchemy ORM mapping.

@jasonwvh
Copy link
Contributor Author

jasonwvh commented Jan 3, 2025

@aapeliv seems like the Delete is a bit more complicated than I thought with the hierarchies and constraints. I've revised the Update codes and removed the Delete ones in this branch. I suggest we do them separately in another PR. What do you think?

@jasonwvh jasonwvh marked this pull request as ready for review January 3, 2025 09:15
@jasonwvh jasonwvh changed the title [Draft] Backend/feature/5123 update delete community Backend/feature/5123 update community Jan 3, 2025
@jasonwvh jasonwvh requested a review from aapeliv January 7, 2025 09:29
Copy link
Member

@aapeliv aapeliv left a comment

Choose a reason for hiding this comment

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

Sorry for the long time to review this :(

Anyway, it's very close, just two small things!

Comment on lines 295 to 301
cluster = session.execute(select(Cluster).where(Cluster.id == request.community_id)).scalar_one_or_none()
if not cluster:
context.abort(grpc.StatusCode.NOT_FOUND, errors.COMMUNITY_NOT_FOUND)

node = session.execute(select(Node).where(Node.id == cluster.parent_node_id)).scalar_one_or_none()
if not node:
context.abort(grpc.StatusCode.NOT_FOUND, errors.COMMUNITY_NOT_FOUND)
Copy link
Member

Choose a reason for hiding this comment

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

The community_id corresponds to node_id; so you ought to do

node = session.execute(select(Node).where(Node.id == request.community_id)).scalar_one_or_none()
if not node:
  context.abort(grpc.StatusCode.NOT_FOUND, errors.COMMUNITY_NOT_FOUND)
cluster = node.official_cluster

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it, so community_id is actually node_id not cluster_id

Comment on lines 310 to 313
geom = shape(json.loads(request.geojson))
if geom.geom_type != "MultiPolygon":
context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.NO_MULTIPOLYGON)
node.geom = from_shape(geom)
Copy link
Member

Choose a reason for hiding this comment

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

Could you please refactor this into a function, something like load_community_geom(geojson, context), then use it as well in CreateCommunity?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

new community-related APIs needed: update community details, delete community
2 participants