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 For Group Permissions #15

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from .models import User, Organisation, School, Payment, TrainingTeam, \
CentralCoordinator, SchoolCoordinator, Teacher, Parent, Profile, \
Location, Condition, MessageType, Message
Location, Condition, MessageType, Message, Context, ContextAllowAssign, \
Permission, GroupPermission


class UserAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -92,3 +93,7 @@ class ParentAdmin(admin.ModelAdmin):
admin.site.register(Message)
admin.site.register(MessageType)
admin.site.register(Condition)
admin.site.register(Context)
admin.site.register(ContextAllowAssign)
admin.site.register(Permission)
admin.site.register(GroupPermission)
58 changes: 58 additions & 0 deletions accounts/helper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib.auth.hashers import make_password
from common.serializers import LocationSerializer
from .models import GroupPermission


def set_encrypted_password(password):
Expand Down Expand Up @@ -98,3 +99,60 @@ def save_location_data(location_data, obj=None):
ValidationError: If the location data is invalid.
"""
return save_data(LocationSerializer, location_data, obj)


def manage_group_permissions(group, permissions):
"""
Manages group permissions for a given group by processing a dictionary of permissions.

Args:
group (Group): The group for which permissions are managed.
permissions (dict): A dictionary containing permission data.

The `permissions` dictionary should have the following structure:
{
"permission_id_1": {
"status": True or False, # Indicates whether the permission
is granted or revoked.
"context": context_id or None # context associated with the permission.
},
# Additional permission entries...
}

For each permission in the dictionary, this function either grants or revokes
the permission for the group.
If "status" is True, the permission is granted. If "status" is False,
the permission is revoked.
The "context" field can specify additional context for the permission.

Returns:
None

This function performs the necessary operations on the `GroupPermission` model
based on the provided data.
It creates new permissions or deletes existing ones according to the specified status.
Any exceptions that occur during this process are printed to the console
for debugging purposes.
"""
for permission, permission_data in permissions.items():
permission_id = int(permission)
context_id = permission_data.get('context', None)
Copy link
Collaborator

Choose a reason for hiding this comment

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

don't give a default value for mandatory parameters.

status = permission_data.get('status', False)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will each field have a separate checkbox for changing status? If yes, this is okay.

don't give a default value for mandatory parameters.

try:
if status:
gp = GroupPermission.objects.filter(role=group, permission_id=permission_id
).first()
Comment on lines +142 to +144
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if status:
gp = GroupPermission.objects.filter(role=group, permission_id=permission_id
).first()
gps = GroupPermission.objects.filter(role=group, permission_id=permission_id)
if status:
gp = gps.first()

do filter and get separately

if gp:
gp.context_id = context_id
gp.save()
else:
GroupPermission.objects.create(role=group,
permission_id=permission_id,
context_id=context_id)
else:
gp = GroupPermission.objects.get(role=group,
permission_id=permission_id,
context_id=context_id)
Comment on lines +153 to +155
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
gp = GroupPermission.objects.get(role=group,
permission_id=permission_id,
context_id=context_id)
gp = gps.first()

use gps again.

gp.delete()
except Exception as e:
print(f"\033[93mException ** {e}\033[0m")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
print(f"\033[93mException ** {e}\033[0m")
print(f"Exception: {e}")

Don't use ANSI colours.

Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Generated by Django 4.2.5 on 2023-10-09 08:02

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("accounts", "0004_alter_location_pincode_alter_user_email_and_more"),
]

operations = [
migrations.CreateModel(
name="Context",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, unique=True)),
("description", models.TextField()),
("order", models.IntegerField()),
],
options={
"ordering": ["order"],
},
),
migrations.CreateModel(
name="Permission",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, unique=True)),
(
"resource",
models.CharField(
choices=[
("role", "role"),
("training", "training"),
("test", "test"),
("gallery", "gallery"),
("about", "about"),
("contact", "contact"),
("home", "home"),
],
max_length=100,
),
),
("description", models.TextField()),
],
),
migrations.CreateModel(
name="GroupPermission",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
(
"context",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="accounts.context",
),
),
(
"permission",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="accounts.permission",
),
),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="auth.group"
),
),
],
options={
"ordering": ["role__name", "context__order"],
"unique_together": {("role", "permission")},
},
),
migrations.CreateModel(
name="ContextAllowAssign",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"assignedLevel",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignedContext",
to="accounts.context",
),
),
(
"assigningLevel",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="assigningContext",
to="accounts.context",
),
),
],
options={
"db_table": "accounts_context_allow_assign",
"ordering": ["assigningLevel__order", "assignedLevel__order"],
"unique_together": {("assigningLevel", "assignedLevel")},
},
),
]
136 changes: 135 additions & 1 deletion accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import Group

from common.models import State, District, City, Language
from config import RESOURCES


MAX_CLASS = 12
CLASS_CHOICES = [(i, f"Class {i}") for i in range(1, MAX_CLASS+1)]
Expand Down Expand Up @@ -254,3 +255,136 @@ class Message(models.Model):
MessageType, on_delete=models.CASCADE,
related_name='message_type'
)


# Revised models
class Context(models.Model):
"""
A context defines the level at which a role operates or an action is performed
within the system. Example: Organization, city, class levels. It includes a name,
description, and an order to define its level of hierarchy.

Attributes:
name (str): The name of the context.
description (str): A detailed description of the context.
order (int): The level of hierarchy for the context.

Meta:
ordering (list): The instances are ordered by their 'order' attribute in
ascending order.

Methods:
__str__(): Returns a string representation of the context in the format
'order - name'.
"""
name = models.CharField(max_length=100, null=False, unique=True)
description = models.TextField(null=False)
order = models.IntegerField(null=False) # Level of hierarchy

Copy link
Collaborator

Choose a reason for hiding this comment

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

should have a parent context also. for example, we will have state context with parent of all India context and various districts with parent context of their state context,

We will discuss this further.

def __str__(self):
return f"{self.order} - {self.name}"

class Meta:
ordering = ['order']


class ContextAllowAssign(models.Model):
"""
This model maintains a relationship between two Context instances,
indicating that one context can assign roles in another context.

Attributes:
assigningLevel (ForeignKey): A ForeignKey relationship to the Context
instance that has the permission to assign roles.
assignedLevel (ForeignKey): A ForeignKey relationship to the Context
instance where roles can be assigned.

Meta:
unique_together (list of str): The combination of 'assigningLevel' and
'assignedLevel' must be unique.
ordering (list of str): The default ordering of ContextAllowAssign
instances is by the
'assigningLevel__order' and 'assignedLevel__order' attributes.
db_table (str): The name of the database table for this model.

Methods:
__str__(): Returns a string representation of the permission.
"""
assigningLevel = models.ForeignKey(Context, on_delete=models.CASCADE,
related_name='assigningContext')
assignedLevel = models.ForeignKey(Context, on_delete=models.CASCADE,
related_name='assignedContext')

class Meta:
unique_together = ['assigningLevel', 'assignedLevel']
ordering = ['assigningLevel__order', 'assignedLevel__order']
db_table = 'accounts_context_allow_assign'

def __str__(self):
return f"{self.assigningLevel} can assign roles at level: {self.assignedLevel} "


class Permission(models.Model):
"""
This model represents a permission within the system, which is used to
control access to various resources.

Attributes:
name (str): The name of the permission. Example: 'Can add testimonials'.
resource (str): The resource to which the permission is associated.
Example: 'testimonials', 'trainings'.
description (str): A detailed description of the permission.

Methods:
__str__(): Returns a string representation of the permission.

"""
RESOURCES = RESOURCES
Copy link
Collaborator

Choose a reason for hiding this comment

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

This line is not required.

name = models.CharField(max_length=100, null=False, unique=True)
resource = models.CharField(max_length=100, null=False, choices=RESOURCES)
description = models.TextField(null=False)

def __str__(self):
return f"{self.name}"


class GroupPermission(models.Model):
"""
This model defines that a specific role has a particular permission
within a specific context.

Attributes:
role (ForeignKey to Group): The role (group) to which the permission
is assigned.
permission (ForeignKey to Permission): The permission being assigned to
the role.
context (ForeignKey to Context): The context in which the permission is
granted.
created (DateTimeField): The timestamp when this role-permission
association was created.
updated (DateTimeField): The timestamp when this role-permission
association was last updated.

Meta:
unique_together (list of str): The combination of 'role' and
'permission' must be unique.
ordering (list of str): The default ordering of GroupPermission instances is
by the 'role__name' and 'context__order' attributes.

Methods:
__str__(): Returns a string representation of the role-permission relationship.
"""
role = models.ForeignKey(Group, on_delete=models.CASCADE)
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
context = models.ForeignKey(Context, on_delete=models.CASCADE)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will need to discuss this context more.

# ToDo For more granular control?
# attribute = models.CharField(max_length=100, null=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)

class Meta:
unique_together = ['role', 'permission']
ordering = ['role__name', 'context__order']

def __str__(self):
return f"{self.role} has permission {self.permission} at level: {self.context}"
9 changes: 8 additions & 1 deletion accounts/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from accounts.api.user_api import CentralCoordinatorViewset, SchoolCoordinatorViewset, \
TeacherViewset, ParentViewset
from rest_framework import routers
from .views import LogoutView, MessageWithinCommunity
from .views import LogoutView, MessageWithinCommunity, \
PermissionsView, PermissionsDetailView, get_group_data
app_name = "accounts"

router = routers.DefaultRouter(trailing_slash=False)
Expand All @@ -24,4 +25,10 @@
path('', include(router.urls)),
path('logout/', LogoutView.as_view(), name='logout'),
path('message-list', MessageWithinCommunity.as_view(), name='message-list'),

# APIs for group permission form
path('api/get_group_permissions/', get_group_data, name='get_group_permissions'),
path('api/permissions', PermissionsView.as_view(), name='permissions-view'),
path('api/permissions/<int:pk>', PermissionsDetailView.as_view(),
name='permissions-detail-view'),
]
Loading