# Copyright (C) 2011-2024 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/


"""
repo group model for RhodeCode
"""

import os
import datetime
import itertools
import logging
import shutil
import time
import traceback
import string


from rhodecode import events
from rhodecode.model import BaseModel
from rhodecode.model.db import (
    _hash_key,
    func,
    or_,
    in_filter_generator,
    Session,
    RepoGroup,
    UserRepoGroupToPerm,
    User,
    Permission,
    UserGroupRepoGroupToPerm,
    UserGroup,
    Repository,
)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.settings import SettingsModel
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.utils2 import action_logger_generic

log = logging.getLogger(__name__)


class RepoGroupModel(BaseModel):
    cls = RepoGroup
    PERSONAL_GROUP_DESC = "personal repo group of user `%(username)s`"
    PERSONAL_GROUP_PATTERN = "${username}"  # default

    def _get_user_group(self, users_group):
        return self._get_instance(UserGroup, users_group, callback=UserGroup.get_by_group_name)

    def _get_repo_group(self, repo_group):
        return self._get_instance(RepoGroup, repo_group, callback=RepoGroup.get_by_group_name)

    def get_repo_group(self, repo_group):
        return self._get_repo_group(repo_group)

    def get_by_group_name(self, repo_group_name, cache=None):
        repo = self.sa.query(RepoGroup).filter(RepoGroup.group_name == repo_group_name)

        if cache:
            name_key = _hash_key(repo_group_name)
            repo = repo.options(FromCache("sql_cache_short", f"get_repo_group_{name_key}"))
        return repo.scalar()

    def get_default_create_personal_repo_group(self):
        value = SettingsModel().get_setting_by_name("create_personal_repo_group")
        return value.app_settings_value if value else None or False

    def get_personal_group_name_pattern(self):
        value = SettingsModel().get_setting_by_name("personal_repo_group_pattern")
        val = value.app_settings_value if value else None
        group_template = val or self.PERSONAL_GROUP_PATTERN

        group_template = group_template.lstrip("/")
        return group_template

    def get_personal_group_name(self, user):
        template = self.get_personal_group_name_pattern()
        return string.Template(template).safe_substitute(
            username=user.username,
            user_id=user.user_id,
            first_name=user.first_name,
            last_name=user.last_name,
        )

    def create_personal_repo_group(self, user, commit_early=True):
        desc = self.PERSONAL_GROUP_DESC % {"username": user.username}
        personal_repo_group_name = self.get_personal_group_name(user)

        # create a new one
        RepoGroupModel().create(
            group_name=personal_repo_group_name,
            group_description=desc,
            owner=user.username,
            personal=True,
            commit_early=commit_early,
        )

    def _create_default_perms(self, new_group):
        # create default permission
        default_perm = "group.read"
        def_user = User.get_default_user()
        for p in def_user.user_perms:
            if p.permission.permission_name.startswith("group."):
                default_perm = p.permission.permission_name
                break

        repo_group_to_perm = UserRepoGroupToPerm()
        repo_group_to_perm.permission = Permission.get_by_key(default_perm)

        repo_group_to_perm.group = new_group
        repo_group_to_perm.user = def_user
        return repo_group_to_perm

    def _get_group_name_and_parent(self, group_name_full, repo_in_path=False, get_object=False):
        """
        Get's the group name and a parent group name from given group name.
        If repo_in_path is set to truth, we asume the full path also includes
        repo name, in such case we clean the last element.

        :param group_name_full:
        """
        split_paths = 1
        if repo_in_path:
            split_paths = 2
        _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)

        if repo_in_path and len(_parts) > 1:
            # such case last element is the repo_name
            _parts.pop(-1)
        group_name_cleaned = _parts[-1]  # just the group name
        parent_repo_group_name = None

        if len(_parts) > 1:
            parent_repo_group_name = _parts[0]

        parent_group = None
        if parent_repo_group_name:
            parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)

        if get_object:
            return group_name_cleaned, parent_repo_group_name, parent_group

        return group_name_cleaned, parent_repo_group_name

    def check_exist_filesystem(self, group_name, exc_on_failure=True):
        create_path = os.path.join(self.repos_path, group_name)
        log.debug("checking FS presence for repo group in %s", create_path)

        if os.path.isdir(create_path):
            if exc_on_failure:
                abs_create_path = os.path.abspath(create_path)
                raise Exception(f"Directory `{abs_create_path}` already exists !")
            return False
        return True

    def _create_group(self, group_name):
        """
        makes repository group on filesystem

        :param repo_name:
        :param parent_id:
        """

        self.check_exist_filesystem(group_name)
        create_path = os.path.join(self.repos_path, group_name)
        log.debug("creating new group in %s", create_path)
        os.makedirs(create_path, mode=0o755)
        log.debug("created group in %s", create_path)

    def _rename_group(self, old, new):
        """
        Renames a group on filesystem

        :param group_name:
        """

        if old == new:
            log.debug("skipping group rename")
            return

        log.debug("renaming repository group from %s to %s", old, new)

        old_path = os.path.join(self.repos_path, old)
        new_path = os.path.join(self.repos_path, new)

        log.debug("renaming repos paths from %s to %s", old_path, new_path)

        if os.path.isdir(new_path):
            raise Exception("Was trying to rename to already existing dir %s" % new_path)
        shutil.move(old_path, new_path)

    def _delete_filesystem_group(self, group, force_delete=False):
        """
        Deletes a group from a filesystem

        :param group: instance of group from database
        :param force_delete: use shutil rmtree to remove all objects
        """
        paths = group.full_path.split(RepoGroup.url_sep())
        paths = os.sep.join(paths)

        rm_path = os.path.join(self.repos_path, paths)
        log.info("Removing group %s", rm_path)
        # delete only if that path really exists
        if os.path.isdir(rm_path):
            if force_delete:
                shutil.rmtree(rm_path)
            else:
                # archive that group`
                _now = datetime.datetime.now()
                _ms = str(_now.microsecond).rjust(6, "0")
                _d = "rm__{}_GROUP_{}".format(_now.strftime("%Y%m%d_%H%M%S_" + _ms), group.name)
                shutil.move(rm_path, os.path.join(self.repos_path, _d))

    def create(
        self,
        group_name,
        group_description,
        owner,
        just_db=False,
        copy_permissions=False,
        personal=None,
        commit_early=True,
    ):
        (group_name_cleaned, parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)

        parent_group = None
        if parent_group_name:
            parent_group = self._get_repo_group(parent_group_name)
            if not parent_group:
                # we tried to create a nested group, but the parent is not
                # existing
                raise ValueError(
                    "Parent group `%s` given in `%s` group name is not yet existing." % (parent_group_name, group_name)
                )

        # because we are doing a cleanup, we need to check if such directory
        # already exists. If we don't do that we can accidentally delete
        # existing directory via cleanup that can cause data issues, since
        # delete does a folder rename to special syntax later cleanup
        # functions can delete this
        cleanup_group = self.check_exist_filesystem(group_name, exc_on_failure=False)
        user = self._get_user(owner)
        if not user:
            raise ValueError("Owner %s not found as rhodecode user", owner)

        try:
            new_repo_group = RepoGroup()
            new_repo_group.user = user
            new_repo_group.group_description = group_description or group_name
            new_repo_group.parent_group = parent_group
            new_repo_group.group_name = group_name
            new_repo_group.personal = personal

            self.sa.add(new_repo_group)

            # create an ADMIN permission for owner except if we're super admin,
            # later owner should go into the owner field of groups
            if not user.is_admin:
                self.grant_user_permission(repo_group=new_repo_group, user=owner, perm="group.admin")

            if parent_group and copy_permissions:
                # copy permissions from parent
                user_perms = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == parent_group).all()

                group_perms = (
                    UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == parent_group).all()
                )

                for perm in user_perms:
                    # don't copy over the permission for user who is creating
                    # this group, if he is not super admin he get's admin
                    # permission set above
                    if perm.user != user or user.is_admin:
                        UserRepoGroupToPerm.create(perm.user, new_repo_group, perm.permission)

                for perm in group_perms:
                    UserGroupRepoGroupToPerm.create(perm.users_group, new_repo_group, perm.permission)
            else:
                perm_obj = self._create_default_perms(new_repo_group)
                self.sa.add(perm_obj)

            # now commit the changes, earlier so we are sure everything is in
            # the database.
            if commit_early:
                self.sa.commit()
            if not just_db:
                self._create_group(new_repo_group.group_name)

            # trigger the post hook
            from rhodecode.lib import hooks_base

            repo_group = RepoGroup.get_by_group_name(group_name)

            # update repo group commit caches initially
            repo_group.update_commit_cache()

            hooks_base.create_repository_group(created_by=user.username, **repo_group.get_dict())

            # Trigger create event.
            events.trigger(events.RepoGroupCreateEvent(repo_group))

            return new_repo_group
        except Exception:
            self.sa.rollback()
            log.exception("Exception occurred when creating repository group, doing cleanup...")
            # rollback things manually !
            repo_group = RepoGroup.get_by_group_name(group_name)
            if repo_group:
                RepoGroup.delete(repo_group.group_id)
                self.sa.commit()
                if cleanup_group:
                    RepoGroupModel()._delete_filesystem_group(repo_group)
            raise

    def update_permissions(
        self,
        repo_group,
        perm_additions=None,
        perm_updates=None,
        perm_deletions=None,
        recursive=None,
        check_perms=True,
        cur_user=None,
    ):
        from rhodecode.model.repo import RepoModel
        from rhodecode.lib.auth import HasUserGroupPermissionAny

        if not perm_additions:
            perm_additions = []
        if not perm_updates:
            perm_updates = []
        if not perm_deletions:
            perm_deletions = []

        req_perms = ("usergroup.read", "usergroup.write", "usergroup.admin")

        changes = {"added": [], "updated": [], "deleted": [], "default_user_changed": None}

        def _set_perm_user(_obj: RepoGroup | Repository, _user_obj: User, _perm):
            if isinstance(_obj, RepoGroup):
                self.grant_user_permission(repo_group=_obj, user=_user_obj, perm=_perm)
            elif isinstance(_obj, Repository):
                # private repos will not allow to change the default
                # permissions using recursive mode
                if _obj.private and _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]:
                    log.debug("Skipping private repo %s for user %s", _obj, _user_obj)
                    return

                # we set group permission, we have to switch to repo permission definition
                new_perm = _perm.replace("group.", "repository.")
                RepoModel().grant_user_permission(repo=_obj, user=_user_obj, perm=new_perm)

        def _set_perm_group(_obj: RepoGroup | Repository, users_group: UserGroup, _perm):
            if isinstance(_obj, RepoGroup):
                self.grant_user_group_permission(repo_group=_obj, group_name=users_group, perm=_perm)
            elif isinstance(_obj, Repository):
                # we set group permission, we have to switch to repo permission definition
                new_perm = _perm.replace("group.", "repository.")
                RepoModel().grant_user_group_permission(repo=_obj, group_name=users_group, perm=new_perm)

        def _revoke_perm_user(_obj: RepoGroup | Repository, _user_obj: User):
            if isinstance(_obj, RepoGroup):
                self.revoke_user_permission(repo_group=_obj, user=_user_obj)
            elif isinstance(_obj, Repository):
                # private repos will not allow to change the default
                # permissions using recursive mode, also there's no revocation for default user, just update
                if _user_obj.username in [User.DEFAULT_USER, User.GHOST_USER, User.AI_USER]:
                    log.debug("Skipping private repo %s for user %s", _obj, _user_obj)
                    return
                RepoModel().revoke_user_permission(repo=_obj, user=_user_obj)

        def _revoke_perm_group(_obj: RepoGroup | Repository, user_group: UserGroup):
            if isinstance(_obj, RepoGroup):
                self.revoke_user_group_permission(repo_group=_obj, group_name=user_group)
            elif isinstance(_obj, Repository):
                RepoModel().revoke_user_group_permission(repo=_obj, group_name=user_group)

        # start updates
        log.debug("Now updating permissions for %s in recursive mode:%s", repo_group, recursive)

        # initialize check function, we'll call that multiple times
        has_group_perm = HasUserGroupPermissionAny(*req_perms)

        for obj in repo_group.recursive_groups_and_repos():
            # iterated obj is an instance of a repos group or repository in
            # that group, recursive option can be: none, repos, groups, all
            if recursive == "all":
                obj = obj
            elif recursive == "repos":
                # skip groups, other than this one
                if isinstance(obj, RepoGroup) and not obj == repo_group:
                    continue
            elif recursive == "groups":
                # skip repos
                if isinstance(obj, Repository):
                    continue
            else:  # recursive == 'none':
                #  DEFAULT option - don't apply to iterated objects
                # also we do a break at the end of this loop. if we are not
                # in recursive mode
                obj = repo_group

            change_obj = obj.get_api_data()

            # update permissions
            for member_id, perm, member_type in perm_updates:
                member_id = int(member_id)
                if member_type == "user":
                    member_obj = User.get(member_id)
                    member_name = member_obj.username
                    is_repo_group = isinstance(obj, RepoGroup) and obj == repo_group
                    if is_repo_group and member_name in [User.GHOST_USER, User.AI_USER]:
                        raise ValueError("Modify user permissions for %s user is not allowed." % member_name)

                    if is_repo_group and member_name == User.DEFAULT_USER:
                        # NOTE(dan): detect if we changed permissions for default user
                        perm_obj = (
                            self.sa.query(UserRepoGroupToPerm)
                            .filter(UserRepoGroupToPerm.user_id == member_id)
                            .filter(UserRepoGroupToPerm.group == repo_group)
                            .scalar()
                        )
                        if perm_obj and perm_obj.permission.permission_name != perm:
                            changes["default_user_changed"] = True

                    # this updates also current one if found
                    _set_perm_user(obj, member_obj, perm)
                elif member_type == "user_group":
                    member_obj = UserGroup.get(member_id)
                    member_name = member_obj.users_group_name
                    if not check_perms or has_group_perm(member_name, user=cur_user):
                        _set_perm_group(obj, member_obj, perm)
                else:
                    raise ValueError(f"member_type must be 'user' or 'user_group' got {member_type} instead")

                changes["updated"].append(
                    {
                        "change_obj": change_obj,
                        "type": member_type,
                        "id": member_id,
                        "name": member_name,
                        "new_perm": perm,
                    }
                )

            # set new permissions
            for member_id, perm, member_type in perm_additions:
                member_id = int(member_id)
                if member_type == "user":
                    member_obj = User.get(member_id)
                    member_name = member_obj.username
                    _set_perm_user(obj, member_obj, perm)
                elif member_type == "user_group":
                    # check if we have permissions to alter this usergroup
                    member_obj = UserGroup.get(member_id)
                    member_name = member_obj.users_group_name
                    if not check_perms or has_group_perm(member_name, user=cur_user):
                        _set_perm_group(obj, member_obj, perm)
                else:
                    raise ValueError(f"member_type must be 'user' or 'user_group' got {member_type} instead")

                changes["added"].append(
                    {
                        "change_obj": change_obj,
                        "type": member_type,
                        "id": member_id,
                        "name": member_name,
                        "new_perm": perm,
                    }
                )

            # delete permissions
            for member_id, perm, member_type in perm_deletions:
                member_id = int(member_id)
                if member_type == "user":
                    member_obj = User.get(member_id)
                    member_name = member_obj.username
                    _revoke_perm_user(obj, member_obj)
                elif member_type == "user_group":
                    # check if we have permissions to alter this usergroup
                    member_obj = UserGroup.get(member_id)
                    member_name = member_obj.users_group_name
                    if not check_perms or has_group_perm(member_name, user=cur_user):
                        _revoke_perm_group(obj, member_obj)
                else:
                    raise ValueError(f"member_type must be 'user' or 'user_group' got {member_type} instead")
                changes["deleted"].append(
                    {
                        "change_obj": change_obj,
                        "type": member_type,
                        "id": member_id,
                        "name": member_name,
                        "new_perm": perm,
                    }
                )

            # if it's not recursive call for all,repos,groups
            # break the loop and don't proceed with other changes
            if recursive not in ["all", "repos", "groups"]:
                break

        return changes

    def update(self, repo_group, form_data):
        try:
            repo_group = self._get_repo_group(repo_group)
            old_path = repo_group.full_path

            # change properties
            if "group_description" in form_data:
                repo_group.group_description = form_data["group_description"]

            if "enable_locking" in form_data:
                repo_group.enable_locking = form_data["enable_locking"]

            if "group_parent_id" in form_data:
                parent_group = self._get_repo_group(form_data["group_parent_id"])
                repo_group.group_parent_id = parent_group.group_id if parent_group else None
                repo_group.parent_group = parent_group

            # mikhail: to update the full_path, we have to explicitly
            # update group_name
            group_name = form_data.get("group_name", repo_group.name)
            repo_group.group_name = repo_group.get_new_name(group_name)

            new_path = repo_group.full_path

            affected_user_ids = []
            if "user" in form_data:
                old_owner_id = repo_group.user.user_id
                new_owner = User.get_by_username(form_data["user"])
                repo_group.user = new_owner

                if old_owner_id != new_owner.user_id:
                    affected_user_ids = [new_owner.user_id, old_owner_id]

            self.sa.add(repo_group)

            # iterate over all members of this groups and do fixes
            # set locking if given
            # if obj is a repoGroup also fix the name of the group according
            # to the parent
            # if obj is a Repo fix it's name
            # this can be potentially heavy operation
            for obj in repo_group.recursive_groups_and_repos():
                # set the value from it's parent
                obj.enable_locking = repo_group.enable_locking
                if isinstance(obj, RepoGroup):
                    new_name = obj.get_new_name(obj.name)
                    log.debug("Fixing group %s to new name %s", obj.group_name, new_name)
                    obj.group_name = new_name

                elif isinstance(obj, Repository):
                    # we need to get all repositories from this new group and
                    # rename them accordingly to new group path
                    new_name = obj.get_new_name(obj.just_name)
                    log.debug("Fixing repo %s to new name %s", obj.repo_name, new_name)
                    obj.repo_name = new_name

                self.sa.add(obj)

            self._rename_group(old_path, new_path)

            # Trigger update event.
            events.trigger(events.RepoGroupUpdateEvent(repo_group))

            if affected_user_ids:
                PermissionModel().trigger_permission_flush(affected_user_ids)

            return repo_group
        except Exception:
            log.error(traceback.format_exc())
            raise

    def delete(self, repo_group, force_delete=False, fs_remove=True, call_events=True):
        repo_group = self._get_repo_group(repo_group)
        if not repo_group:
            return False
        repo_group_name = repo_group.group_name
        try:
            self.sa.delete(repo_group)
            if fs_remove:
                self._delete_filesystem_group(repo_group, force_delete)
            else:
                log.debug("skipping removal from filesystem")

            # Trigger delete event.
            if call_events:
                events.trigger(events.RepoGroupDeleteEvent(repo_group))

        except Exception:
            log.error("Error removing repo_group %s", repo_group_name)
            raise

        return True

    def grant_user_permission(self, repo_group, user, perm):
        """
        Grant permission for user on given repository group, or update
        existing one if found

        :param repo_group: Instance of RepoGroup, repositories_group_id,
            or repositories_group name
        :param user: Instance of User, user_id or username
        :param perm: Instance of Permission, or permission_name
        """

        repo_group = self._get_repo_group(repo_group)
        user = self._get_user(user)
        permission = self._get_perm(perm)

        # check if we have that permission already
        obj = (
            self.sa.query(UserRepoGroupToPerm)
            .filter(UserRepoGroupToPerm.user == user)
            .filter(UserRepoGroupToPerm.group == repo_group)
            .scalar()
        )
        if obj is None:
            # create new !
            obj = UserRepoGroupToPerm()
        obj.group = repo_group
        obj.user = user
        obj.permission = permission
        self.sa.add(obj)
        log.debug("Granted perm %s to %s on %s", perm, user, repo_group)
        action_logger_generic(
            "granted permission: {} to user: {} on repogroup: {}".format(perm, user, repo_group),
            namespace="security.repogroup",
        )
        return obj

    def revoke_user_permission(self, repo_group, user):
        """
        Revoke permission for user on given repository group

        :param repo_group: Instance of RepoGroup, repositories_group_id,
            or repositories_group name
        :param user: Instance of User, user_id or username
        """

        repo_group = self._get_repo_group(repo_group)
        user = self._get_user(user)

        obj = (
            self.sa.query(UserRepoGroupToPerm)
            .filter(UserRepoGroupToPerm.user == user)
            .filter(UserRepoGroupToPerm.group == repo_group)
            .scalar()
        )
        if obj:
            self.sa.delete(obj)
            log.debug("Revoked perm on %s on %s", repo_group, user)
            action_logger_generic(
                "revoked permission from user: {} on repogroup: {}".format(user, repo_group),
                namespace="security.repogroup",
            )

    def grant_user_group_permission(self, repo_group, group_name, perm):
        """
        Grant permission for user group on given repository group, or update
        existing one if found

        :param repo_group: Instance of RepoGroup, repositories_group_id,
            or repositories_group name
        :param group_name: Instance of UserGroup, users_group_id,
            or user group name
        :param perm: Instance of Permission, or permission_name
        """
        repo_group = self._get_repo_group(repo_group)
        group_name = self._get_user_group(group_name)
        permission = self._get_perm(perm)

        # check if we have that permission already
        obj = (
            self.sa.query(UserGroupRepoGroupToPerm)
            .filter(UserGroupRepoGroupToPerm.group == repo_group)
            .filter(UserGroupRepoGroupToPerm.users_group == group_name)
            .scalar()
        )

        if obj is None:
            # create new
            obj = UserGroupRepoGroupToPerm()

        obj.group = repo_group
        obj.users_group = group_name
        obj.permission = permission
        self.sa.add(obj)
        log.debug("Granted perm %s to %s on %s", perm, group_name, repo_group)
        action_logger_generic(
            "granted permission: {} to usergroup: {} on repogroup: {}".format(perm, group_name, repo_group),
            namespace="security.repogroup",
        )
        return obj

    def revoke_user_group_permission(self, repo_group, group_name):
        """
        Revoke permission for user group on given repository group

        :param repo_group: Instance of RepoGroup, repositories_group_id,
            or repositories_group name
        :param group_name: Instance of UserGroup, users_group_id,
            or user group name
        """
        repo_group = self._get_repo_group(repo_group)
        group_name = self._get_user_group(group_name)

        obj = (
            self.sa.query(UserGroupRepoGroupToPerm)
            .filter(UserGroupRepoGroupToPerm.group == repo_group)
            .filter(UserGroupRepoGroupToPerm.users_group == group_name)
            .scalar()
        )
        if obj:
            self.sa.delete(obj)
            log.debug("Revoked perm to %s on %s", repo_group, group_name)
            action_logger_generic(
                "revoked permission from usergroup: {} on repogroup: {}".format(group_name, repo_group),
                namespace="security.repogroup",
            )

    @classmethod
    def update_commit_cache(cls, repo_groups=None):
        if not repo_groups:
            repo_groups = RepoGroup.getAll()
        for repo_group in repo_groups:
            repo_group.update_commit_cache()

    def get_repo_groups_as_dict(self, repo_group_list=None, admin=False, super_user_actions=False):
        from rhodecode.lib.pyramid_utils import get_current_request

        _render = get_current_request().get_partial_renderer("rhodecode:templates/data_table/_dt_elements.mako")
        c = _render.get_call_context()
        h = _render.get_helpers()

        def quick_menu(repo_group_name):
            return _render("quick_repo_group_menu", repo_group_name)

        def repo_group_lnk(repo_group_name):
            return _render("repo_group_name", repo_group_name)

        def last_change(last_change):
            if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
                ts = time.time()
                utc_offset = (
                    datetime.datetime.fromtimestamp(ts) - datetime.datetime.utcfromtimestamp(ts)
                ).total_seconds()
                last_change = last_change + datetime.timedelta(seconds=utc_offset)
            return _render("last_change", last_change)

        def desc(desc, personal):
            return _render("repo_group_desc", desc, personal, c.visual.stylify_metatags)

        def repo_group_actions(repo_group_id, repo_group_name, gr_count):
            return _render("repo_group_actions", repo_group_id, repo_group_name, gr_count)

        def repo_group_name(repo_group_name, children_groups):
            return _render("repo_group_name", repo_group_name, children_groups)

        def user_profile(username):
            return _render("user_profile", username)

        repo_group_data = []
        for group in repo_group_list:
            # NOTE(marcink): because we use only raw column we need to load it like that
            changeset_cache = RepoGroup._load_changeset_cache("", group._changeset_cache)
            last_commit_change = RepoGroup._load_commit_change(changeset_cache)
            row = {
                "menu": quick_menu(group.group_name),
                "name": repo_group_lnk(group.group_name),
                "name_raw": group.group_name,
                "last_change": last_change(last_commit_change),
                "last_changeset": "",
                "last_changeset_raw": "",
                "desc": desc(h.escape(group.group_description), group.personal),
                "top_level_repos": 0,
                "owner": user_profile(group.User.username),
            }
            if admin:
                repo_count = group.repositories.count()
                children_groups = list(
                    map(h.safe_str, itertools.chain((g.name for g in group.parents), (x.name for x in [group])))
                )
                row.update(
                    {
                        "action": repo_group_actions(group.group_id, group.group_name, repo_count),
                        "top_level_repos": repo_count,
                        "name": repo_group_name(group.group_name, children_groups),
                    }
                )
            repo_group_data.append(row)

        return repo_group_data

    def get_repo_groups_data_table(self, draw, start, limit, search_q, order_by, order_dir, auth_user, repo_group_id):
        from rhodecode.model.scm import RepoGroupList

        _perms = ["group.read", "group.write", "group.admin"]
        repo_groups = RepoGroup.query().filter(RepoGroup.group_parent_id == repo_group_id).all()
        auth_repo_group_list = RepoGroupList(repo_groups, perm_set=_perms, extra_kwargs=dict(user=auth_user))

        allowed_ids = [-1]
        for repo_group in auth_repo_group_list:
            allowed_ids.append(repo_group.group_id)

        repo_groups_data_total_count = (
            RepoGroup.query()
            .filter(RepoGroup.group_parent_id == repo_group_id)
            .filter(
                or_(
                    # generate multiple IN to fix limitation problems
                    *in_filter_generator(RepoGroup.group_id, allowed_ids)
                )
            )
            .count()
        )

        base_q = (
            Session.query(
                RepoGroup.group_name,
                RepoGroup.group_name_hash,
                RepoGroup.group_description,
                RepoGroup.group_id,
                RepoGroup.personal,
                RepoGroup.updated_on,
                RepoGroup._changeset_cache,
                User,
            )
            .filter(RepoGroup.group_parent_id == repo_group_id)
            .filter(
                or_(
                    # generate multiple IN to fix limitation problems
                    *in_filter_generator(RepoGroup.group_id, allowed_ids)
                )
            )
            .join(User, User.user_id == RepoGroup.user_id)
            .group_by(RepoGroup, User)
        )

        repo_groups_data_total_filtered_count = base_q.count()

        sort_defined = False

        if order_by == "group_name":
            sort_col = func.lower(RepoGroup.group_name)
            sort_defined = True
        elif order_by == "user_username":
            sort_col = User.username
        else:
            sort_col = getattr(RepoGroup, order_by, None)

        if sort_defined or sort_col:
            if order_dir == "asc":
                sort_col = sort_col.asc()
            else:
                sort_col = sort_col.desc()

        base_q = base_q.order_by(sort_col)
        base_q = base_q.offset(start).limit(limit)

        repo_group_list = base_q.all()

        repo_groups_data = RepoGroupModel().get_repo_groups_as_dict(repo_group_list=repo_group_list, admin=False)

        data = {
            "draw": draw,
            "data": repo_groups_data,
            "recordsTotal": repo_groups_data_total_count,
            "recordsFiltered": repo_groups_data_total_filtered_count,
        }
        return data

    def _get_defaults(self, repo_group_name):
        repo_group = RepoGroup.get_by_group_name(repo_group_name)

        if repo_group is None:
            return None

        defaults = repo_group.get_dict()
        defaults["repo_group_name"] = repo_group.name
        defaults["repo_group_description"] = repo_group.group_description
        defaults["repo_group_enable_locking"] = repo_group.enable_locking

        # we use -1 as this is how in HTML, we mark an empty group
        defaults["repo_group"] = defaults["group_parent_id"] or -1

        # fill owner
        if repo_group.user:
            defaults.update({"user": repo_group.user.username})
        else:
            replacement_user = User.get_first_super_admin().username
            defaults.update({"user": replacement_user})

        return defaults
