diff --git a/.vscode/settings.json b/.vscode/settings.json index eb5fd6e..ae37769 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "**/.venv/*/**": true }, "cSpell.words": [ + "backref", "bookname", "Btns", "flowbite", diff --git a/app/controllers/get_or_create_permission.py b/app/controllers/get_or_create_permission.py index 7c288b5..8fcdaa6 100644 --- a/app/controllers/get_or_create_permission.py +++ b/app/controllers/get_or_create_permission.py @@ -1,10 +1,14 @@ from app import models as m +from app.logger import log -def get_or_create_permission(access: int, entity: m.Permission.Entity): +def get_or_create_permission(access: int, entity_type: m.Permission.Entity): permission: m.Permission = m.Permission.query.filter_by( - access=access, entity=entity + access=access, entity_type=entity_type ).first() if not permission: - permission: m.Permission = m.Permission(access=access, entity=entity).save() + log(log.INFO, "Create permission [%d] for entity [%s]", access, entity_type) + permission: m.Permission = m.Permission( + access=access, entity_type=entity_type + ).save() return permission diff --git a/app/controllers/init_access_groups.py b/app/controllers/init_access_groups.py new file mode 100644 index 0000000..6a03920 --- /dev/null +++ b/app/controllers/init_access_groups.py @@ -0,0 +1,29 @@ +from app import models as m +from app.logger import log +from .get_or_create_permission import get_or_create_permission + + +def create_moderator_group(): + log(log.INFO, "Create moderator access group") + group: m.AccessGroup = m.AccessGroup(name="moderator").save() + + interpretation_DA = get_or_create_permission( + access=m.Permission.Access.D | m.Permission.Access.A, + entity_type=m.Permission.Entity.INTERPRETATION, + ) + comment_DA = get_or_create_permission( + access=m.Permission.Access.D | m.Permission.Access.A, + entity_type=m.Permission.Entity.COMMENT, + ) + + log(log.INFO, "Add permission [%d] to group[%d]", interpretation_DA.id, group.id) + m.PermissionAccessGroups( + permission_id=interpretation_DA.id, access_group_id=group.id + ).save() + + log(log.INFO, "Add permission [%d] to group[%d]", comment_DA.id, group.id) + m.PermissionAccessGroups( + permission_id=comment_DA.id, access_group_id=group.id + ).save() + + return group diff --git a/app/models/__init__.py b/app/models/__init__.py index 62e385f..079683f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -18,4 +18,8 @@ from .permission import ( AccessGroup, UserAccessGroups, PermissionAccessGroups, + BookAccessGroups, + CollectionAccessGroups, + SectionAccessGroups, + InterpretationAccessGroups, ) diff --git a/app/models/book.py b/app/models/book.py index 0cff49a..12d252e 100644 --- a/app/models/book.py +++ b/app/models/book.py @@ -19,6 +19,13 @@ class Book(BaseModel): stars = db.relationship("User", secondary="books_stars", back_populates="stars") contributors = db.relationship("BookContributor") versions = db.relationship("BookVersion", order_by="asc(BookVersion.id)") + list_access_groups = db.relationship( + "AccessGroup" + ) # all access_groups in current book(in nested entities) + access_groups = db.relationship( + "AccessGroup", + secondary="books_access_groups", + ) # access_groups related to current entity def __repr__(self): return f"<{self.id}: {self.label}>" diff --git a/app/models/collection.py b/app/models/collection.py index 39c6313..3d6d13f 100644 --- a/app/models/collection.py +++ b/app/models/collection.py @@ -25,6 +25,10 @@ class Collection(BaseModel): order_by="asc(Collection.id)", ) sections = db.relationship("Section") + access_groups = db.relationship( + "AccessGroup", + secondary="collections_access_groups", + ) # access_groups related to current entity def __repr__(self): return f"<{self.id}: {self.label}>" diff --git a/app/models/interpretation.py b/app/models/interpretation.py index e9a88ba..5fbd2fb 100644 --- a/app/models/interpretation.py +++ b/app/models/interpretation.py @@ -25,6 +25,10 @@ class Interpretation(BaseModel): secondary="interpretation_tags", back_populates="interpretations", ) + access_groups = db.relationship( + "AccessGroup", + secondary="interpretations_access_groups", + ) # access_groups related to current entity @property def vote_count(self): diff --git a/app/models/permission/__init__.py b/app/models/permission/__init__.py index 98c8b47..a8e8c46 100644 --- a/app/models/permission/__init__.py +++ b/app/models/permission/__init__.py @@ -3,6 +3,10 @@ from .access_group import AccessGroup from .permission import Permission from .user_access_groups import UserAccessGroups from .permission_access_groups import PermissionAccessGroups +from .book_access_groups import BookAccessGroups +from .collection_access_groups import CollectionAccessGroups +from .section_access_groups import SectionAccessGroups +from .interpretation_access_groups import InterpretationAccessGroups # access groups # moderators(by default empty) -> root collection -> CRUD Interpretation, Comment @@ -26,6 +30,7 @@ from .permission_access_groups import PermissionAccessGroups # Book +# Version: # Root Collection # Collection A # Section @@ -40,3 +45,13 @@ from .permission_access_groups import PermissionAccessGroups # If the user has CRUD access to Collection B it means that # it has access to all nested entities(SubCollection B.1/B.2, Sections) + + +# 1) Create moderator_AG and editor_AG on book create +# 2) Inherit parent's access groups + +# TODO many to many +# book -> access_group +# collections -> access_group +# section -> access_group +# interpretation -> access_group diff --git a/app/models/permission/access_group.py b/app/models/permission/access_group.py index 1620e7d..6f8c63e 100644 --- a/app/models/permission/access_group.py +++ b/app/models/permission/access_group.py @@ -5,9 +5,13 @@ from app.models.utils import BaseModel class AccessGroup(BaseModel): __tablename__ = "access_groups" - name = db.Column(db.String(32), unique=True, nullable=False) + name = db.Column(db.String(32), nullable=False) + + # Foreign Keys + book_id = db.Column(db.Integer, db.ForeignKey("books.id")) # Relationships + book = db.relationship("Book", viewonly=True) permissions = db.relationship( "Permission", secondary="permissions_access_groups", diff --git a/app/models/permission/book_access_groups.py b/app/models/permission/book_access_groups.py new file mode 100644 index 0000000..2a8f83f --- /dev/null +++ b/app/models/permission/book_access_groups.py @@ -0,0 +1,13 @@ +from app import db +from app.models.utils import BaseModel + + +class BookAccessGroups(BaseModel): + __tablename__ = "books_access_groups" + + # Foreign keys + book_id = db.Column(db.Integer, db.ForeignKey("books.id")) + access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id")) + + def __repr__(self): + return f" :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(1rem * var(--tw-space-x-reverse)); - margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); -} - .divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); @@ -2119,6 +2121,11 @@ input:checked + .toggle-bg { background-color: rgb(200 30 30 / var(--tw-bg-opacity)); } +.bg-sky-300 { + --tw-bg-opacity: 1; + background-color: rgb(125 211 252 / var(--tw-bg-opacity)); +} + .bg-slate-300 { --tw-bg-opacity: 1; background-color: rgb(203 213 225 / var(--tw-bg-opacity)); @@ -2185,6 +2192,10 @@ input:checked + .toggle-bg { stroke: #F05252; } +.\!p-0 { + padding: 0px !important; +} + .p-0 { padding: 0px; } @@ -2226,11 +2237,6 @@ input:checked + .toggle-bg { padding-right: 0px !important; } -.px-0 { - padding-left: 0px; - padding-right: 0px; -} - .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -2286,6 +2292,11 @@ input:checked + .toggle-bg { padding-bottom: 1rem; } +.px-0 { + padding-left: 0px; + padding-right: 0px; +} + .pb-3 { padding-bottom: 0.75rem; } @@ -2466,6 +2477,11 @@ input:checked + .toggle-bg { color: rgb(14 159 110 / var(--tw-text-opacity)); } +.text-orange-500 { + --tw-text-opacity: 1; + color: rgb(255 90 31 / var(--tw-text-opacity)); +} + .text-red-500 { --tw-text-opacity: 1; color: rgb(240 82 82 / var(--tw-text-opacity)); @@ -2489,6 +2505,10 @@ input:checked + .toggle-bg { text-decoration-line: line-through; } +.\!no-underline { + text-decoration-line: none !important; +} + .opacity-0 { opacity: 0; } @@ -2626,6 +2646,11 @@ input:checked + .toggle-bg { background-color: rgb(155 28 28 / var(--tw-bg-opacity)); } +.hover\:bg-sky-400:hover { + --tw-bg-opacity: 1; + background-color: rgb(56 189 248 / var(--tw-bg-opacity)); +} + .hover\:bg-white:hover { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -2680,12 +2705,6 @@ input:checked + .toggle-bg { outline-offset: 2px; } -.focus\:ring-0:focus { - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); -} - .focus\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); @@ -2698,6 +2717,12 @@ input:checked + .toggle-bg { box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-0:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + .focus\:ring-blue-100:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(225 239 254 / var(--tw-ring-opacity)); diff --git a/tests/test_get_or_create_permission.py b/tests/test_get_or_create_permission.py index 82981ff..e092ea1 100644 --- a/tests/test_get_or_create_permission.py +++ b/tests/test_get_or_create_permission.py @@ -4,27 +4,29 @@ from app.controllers.get_or_create_permission import get_or_create_permission def test_get_or_create_permission(client): access = m.Permission.Access - entity = m.Permission.Entity + entity_type = m.Permission.Entity book_u: m.Permission = m.Permission.query.filter_by( - access=access.U, entity=entity.BOOK + access=access.U, entity_type=entity_type.BOOK ).first() assert not book_u assert not m.Permission.query.count() - book_u: m.Permission = get_or_create_permission(access=access.U, entity=entity.BOOK) + book_u: m.Permission = get_or_create_permission( + access=access.U, entity_type=entity_type.BOOK + ) assert book_u assert book_u.access == access.U - assert book_u.entity == entity.BOOK + assert book_u.entity_type == entity_type.BOOK assert m.Permission.query.count() == 1 book_u: m.Permission = m.Permission.query.filter_by( - access=access.U, entity=entity.BOOK + access=access.U, entity_type=entity_type.BOOK ).first() assert book_u assert book_u.access == access.U - assert book_u.entity == entity.BOOK + assert book_u.entity_type == entity_type.BOOK - get_or_create_permission(access=access.U, entity=entity.BOOK) + get_or_create_permission(access=access.U, entity_type=entity_type.BOOK) assert m.Permission.query.count() == 1 diff --git a/tests/test_init_access_groups.py b/tests/test_init_access_groups.py new file mode 100644 index 0000000..bd166ed --- /dev/null +++ b/tests/test_init_access_groups.py @@ -0,0 +1,31 @@ +from app.controllers.init_access_groups import create_moderator_group +from app import models as m + + +def test_init_moderator_group(client): + create_moderator_group() + + group: m.AccessGroup = m.AccessGroup.query.filter_by(name="moderator").first() + assert group + assert not group.users + assert group.permissions + + permissions = group.permissions + + access = m.Permission.Access + + interpretation_DA: m.Permission = m.Permission.query.filter_by( + access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION + ).first() + assert interpretation_DA + assert interpretation_DA in permissions + + comment_DA: m.Permission = m.Permission.query.filter_by( + access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT + ).first() + assert comment_DA + assert comment_DA in permissions + + create_moderator_group() + groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="moderator").all() + assert len(groups) == 2