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

Improve Project-User Management and Accessibility #275

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open

Conversation

mihow
Copy link
Collaborator

@mihow mihow commented Oct 11, 2023

This pull request enhances how projects and project members (users) are managed and accessed. It introduces a many-to-many relationship between projects and members, ensuring that users are automatically assigned to a project when they create it. Django admin panel is updated to allow manual assignment of members to a project. A filtering by user_id is added to the /projects to filter projects that the current user is involved in.My Projects tab is added at the top of the home page to make it easier for users to find and access their projects.

@netlify
Copy link

netlify bot commented Oct 11, 2023

Deploy Preview for ami-dev canceled.

Name Link
🔨 Latest commit ab6fddf
🔍 Latest deploy log https://app.netlify.com/sites/ami-dev/deploys/67928b793df3ca0008645f8a

@netlify
Copy link

netlify bot commented Oct 11, 2023

Deploy Preview for ami-storybook canceled.

Name Link
🔨 Latest commit 972243a
🔍 Latest deploy log https://app.netlify.com/sites/ami-storybook/deploys/6667858663a7490008b286fa

@mihow
Copy link
Collaborator Author

mihow commented Nov 2, 2023

  • Every view query and nested serializer query should have a filter to enforce that the current user is owner of the associated project
  • Consider prefixing all api endpoints by /project//

@mihow
Copy link
Collaborator Author

mihow commented Nov 3, 2023

All queries should include objects from public projects as well.

@mihow mihow changed the title Initial support for project owners [Draft] Initial support for project owners Jun 19, 2024
@mohamedelabbas1996
Copy link
Contributor

mohamedelabbas1996 commented Jan 20, 2025

Changes in This PR

  • Add many-to-many relationship between projects & users - make a new migration & commit file
  • Auto-assign a user to a project when they create it
  • Expose the projects a user belongs to in the user profile API endpoint (/me)
  • Update Django admin for projects to manually add users to a project until public UI is ready
  • Add tests:
    • Ensure the current user is added to a project when it is created via the API
    • Ensure the current user gets only the projects they're involved in as (owner or user) through /users/me/ endpoint.
  • See possible UI for "My projects" section on top of the home page

@mohamedelabbas1996
Copy link
Contributor

Closes #402

@annavik
Copy link
Member

annavik commented Jan 20, 2025

Me and @mohamedelabbas1996 had a chat about this today, regarding some frontend perspectives:

  • We talked about extending the endpoint /projects with a filter, to make it possible to use the same endpoint to fetch "all projects" and "my projects". This might be a bit more practical than including project data with the /me response? For example, we can skip extra requests to get the full project details and we would also get the paginated response. That would mean we can streamline the frontend code a bit better.
  • We also talked about how to present this in UI. @mohamedelabbas1996 had a great idea about just adding tabs to the projects list. We can follow a similar UI pattern like we do for the project overview tabs.

We decided to discuss more in the team on the meeting tomorrow about next steps! :)

@mihow
Copy link
Collaborator Author

mihow commented Jan 20, 2025

Thanks for collaborating on this PR @mohamedelabbas1996 and @annavik! I look forward to reviewing your commits and suggestions.

@annavik
Copy link
Member

annavik commented Jan 22, 2025

Frontend updates pushed. Me and @mohamedelabbas1996 solved it by adding tabs to the header:

Screenshot 2025-01-22 at 17 46 55 Screenshot 2025-01-22 at 17 47 06

const { pagination, setPage } = usePagination()
const filters =
user.loggedIn && selectedTab === TABS.USER_PROJECTS
? [{ field: 'public', value: 'false' }]
Copy link
Member

@annavik annavik Jan 22, 2025

Choose a reason for hiding this comment

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

@mohamedelabbas1996 here is where the params we pass to the backend can be tweaked if needed

@annavik
Copy link
Member

annavik commented Jan 22, 2025

For the filter param name public, I think it's slightly misleading since user projects can also be public. I think this filtering is more related to membership rather than visibility. But I'm also fine keeping things as is, just a minor! :)

If you change the filter name, I added a comment (to the PR) so you can see where in the FE code to tweak.

@mihow @mohamedelabbas1996

@mohamedelabbas1996
Copy link
Contributor

mohamedelabbas1996 commented Jan 22, 2025

Thanks @annavik for adding the changes on the frontend. For the filter name, we can rename it to current_user. I think this makes more sense. @mihow what are your suggestions?

@mihow
Copy link
Collaborator Author

mihow commented Jan 22, 2025

@mohamedelabbas1996 there is a chance for some inconstancy when retrieving "my projects" since there is both a project owner and project members. Some ideas to address this:

  • When filtering projects by user_id, search both members and the project owners
  • Add project owner as a member in the Project save method, if they are not already a member

My recommendation is to add a new reusable method to to Project QuerySet class that can be used everywhere. Project.objects.can_view(user) and Project.objects.can_edit(user) or can_access() or filter_by_user()

It takes a little more code to setup a QuerySet for a model the first time, but it's a pattern that I would like to continue using because it keeps the logic in one place. Here is one example for the SourceImage model. You define a QuerySet, a Manager and then assign the manager to the Model.

antenna/ami/main/models.py

Lines 1170 to 1171 in 14e9ec0

class SourceImageQuerySet(models.QuerySet):
def with_occurrences_count(self, classification_threshold: float = 0):

antenna/ami/main/models.py

Lines 1194 to 1196 in 14e9ec0

class SourceImageManager(models.Manager):
def get_queryset(self) -> SourceImageQuerySet:
return SourceImageQuerySet(self.model, using=self._db)

objects = SourceImageManager()

@mohamedelabbas1996 mohamedelabbas1996 changed the title [Draft] Initial support for project owners Improve Project-User Management and Accessibility Jan 22, 2025
@mohamedelabbas1996 mohamedelabbas1996 marked this pull request as ready for review January 22, 2025 21:35
@mohamedelabbas1996
Copy link
Contributor

Screenshot 2025-01-22 at 18 32 28 Screenshot 2025-01-22 at 18 30 12

@mohamedelabbas1996
Copy link
Contributor

Now project members can be managed from the Admin page

@mohamedelabbas1996
Copy link
Contributor

mohamedelabbas1996 commented Jan 22, 2025

@mohamedelabbas1996 there is a chance for some inconstancy when retrieving "my projects" since there is both a project owner and project members. Some ideas to address this:

* When filtering projects by `user_id`, search both members and the project owners 

* Add project owner as a member in the Project save method, if they are not already a member

I opted to add the owner to members if not already exists and then we can just filter by members

@mohamedelabbas1996
Copy link
Contributor

My recommendation is to add a new reusable method to to Project QuerySet class that can be used everywhere. Project.objects.can_view(user) and Project.objects.can_edit(user) or can_access() or filter_by_user()

It takes a little more code to setup a QuerySet for a model the first time, but it's a pattern that I would like to continue using because it keeps the logic in one place. Here is one example for the SourceImage model. You define a QuerySet, a Manager and then assign the manager to the Model.

Added the model manager. Thanks for the recommendation!

user_id = self.request.query_params.get("user_id")
if user_id:
user = User.objects.filter(pk=user_id).first()
if not (user == self.request.user or is_active_staff(self.request.user)):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Active "staff" is actually everyone who has edit permissions. Superusers are a different boolean field. But how about we simplify and just check for the matching user for now? Since superusers can see anything in the Django admin.

project = serializer.save(owner=self.request.user)

# Add owner to project members if not already a member
if not project.members.filter(id=self.request.user.id).exists():
Copy link
Collaborator Author

@mihow mihow Jan 23, 2025

Choose a reason for hiding this comment

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

I would move this check to the model Project.save() method instead, because there are other places where the owner can be set or changed. Like if you add a project in the Django admin, the owner is not made a member.

"""
Filters projects to include only those where the given user is a member.
"""
return self.filter(members=user)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Since you made this queryset method (thank you!), it's pretty easy to filter by member or owner.

self.filter(models.Q(members=user) | models.Q(owner=user)).distinct()

@final
class Project(BaseModel):
""" """

name = models.CharField(max_length=_POST_TITLE_MAX_LENGTH)
description = models.TextField()
image = models.ImageField(upload_to="projects", blank=True, null=True)

owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="projects")
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm glad we kept owner in this PR

@@ -882,3 +882,25 @@ def test_project_devices(self):
exepcted_device_ids = {device.id for device in Device.objects.filter(project=project)}
response_device_ids = {device.get("id") for device in response_data["results"]}
self.assertEqual(response_device_ids, exepcted_device_ids)


class TestProjectOwnerAutoAssignment(APITestCase):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the tests! remember that is_staff is different than is_superuser. We are using is_staff to give a user edit permissions to everything in the app. is_superuser are the only users who can delete some objects (like jobs). We should review this in the next permission PRs.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mihow Thank you for the clarification!

const { user } = useUser()
const { userInfo} = useUserInfo()

const [selectedTab, setSelectedTab] = useState(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@annavik can we add a URL path or param so we can link to all projects when necessary? With the new feature, if someone sends someone else the projects link, they likely won't see the same projects.

Copy link
Member

Choose a reason for hiding this comment

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

Yes good point, fixed! ✅

Screenshot 2025-01-23 at 09 41 00

Copy link
Contributor

@mohamedelabbas1996 mohamedelabbas1996 left a comment

Choose a reason for hiding this comment

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

@mihow I 've tried to move adding project owner to members to Project.save method. It works well from the api but it doesn't auto-add the owner if we create a project from the admin page

@@ -0,0 +1,25 @@
# Generated by Django 4.2.2 on 2023-10-11 02:15
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One more thing, will you squash these migrations into one or two? python manage.py squashmigrations first_one last_one. That will remove some clutter and extra steps when we merge into main (instead of adding the field, then renaming the field). I only do this for migrations that haven't modified live data yet (migrations in an branch like these).

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.

3 participants