Skip to content

Commit

Permalink
Merge pull request #324 from MerginMaps/develop-redesign-admin
Browse files Browse the repository at this point in the history
Admin redesign
  • Loading branch information
MarcelGeo authored Nov 12, 2024
2 parents 31ff49d + 190bec5 commit 777e0b6
Show file tree
Hide file tree
Showing 344 changed files with 5,100 additions and 19,055 deletions.
2 changes: 2 additions & 0 deletions server/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

Configuration.SERVER_TYPE = "ce"
Configuration.USER_SELF_REGISTRATION = False

application = create_app(
[
"DOCS_URL",
Expand All @@ -37,6 +38,7 @@
"GLOBAL_ADMIN",
"GLOBAL_READ",
"GLOBAL_WRITE",
"ENABLE_SUPERADMIN_ASSIGNMENT",
]
)
register_stats(application)
Expand Down
9 changes: 7 additions & 2 deletions server/mergin/auth/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ paths:
count:
type: integer
example: 10
users:
items:
type: array
items:
$ref: "#/components/schemas/User"
Expand Down Expand Up @@ -670,6 +670,11 @@ components:
type: string
format: date-time
example: 2023-07-30T08:47:58Z
registration_date:
nullable: true
type: string
format: date-time
example: 2023-07-30T08:47:58Z
profile:
$ref: "#/components/schemas/UserProfile"
PaginatedUsers:
Expand Down Expand Up @@ -839,4 +844,4 @@ components:
- editor
- reader
- guest
example: reader
example: reader
22 changes: 16 additions & 6 deletions server/mergin/auth/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytz
from datetime import datetime, timedelta
from connexion import NoContent
from sqlalchemy import func, desc, asc
from sqlalchemy import func, desc, asc, or_
from sqlalchemy.sql.operators import is_
from flask import request, current_app, jsonify, abort, render_template
from flask_login import login_user, logout_user, current_user
Expand All @@ -23,7 +23,7 @@
user_account_closed,
)
from .bearer import encode_token
from .models import User, LoginHistory
from .models import User, LoginHistory, UserProfile
from .schemas import UserSchema, UserSearchSchema, UserProfileSchema, UserInfoSchema
from .forms import (
LoginForm,
Expand Down Expand Up @@ -408,11 +408,17 @@ def update_user(username): # pylint: disable=W0613,W0612
form = UserForm.from_json(request.json)
if not form.validate_on_submit():
return jsonify(form.errors), 400
if request.json.get("is_admin") is not None and not current_app.config.get(
"ENABLE_SUPERADMIN_ASSIGNMENT"
):
abort(400, "Unable to assign super admin role")

user = User.query.filter_by(username=username).first_or_404("User not found")
form.update_obj(user)

# remove inactive since flag for ban or re-activation
user.inactive_since = None

db.session.add(user)
db.session.commit()
return jsonify(UserSchema().dump(user))
Expand Down Expand Up @@ -449,13 +455,17 @@ def get_paginated_users(
:rtype: Dict[str: List[User], str: Integer]
"""
users = User.query.filter(
users = User.query.join(UserProfile).filter(
is_(User.username.ilike("deleted_%"), False) | is_(User.active, True)
)

if like:
attr = User.email if "@" in like else User.username
users = users.filter(attr.ilike(f"%{like}%"))
users = users.filter(
User.username.ilike(f"%{like}%")
| User.email.ilike(f"%{like}%")
| UserProfile.first_name.ilike(f"%{like}%")
| UserProfile.last_name.ilike(f"%{like}%")
)

if descending and order_by:
users = users.order_by(desc(User.__table__.c[order_by]))
Expand All @@ -467,7 +477,7 @@ def get_paginated_users(

result_users = UserSchema(many=True).dump(result)

data = {"users": result_users, "total": total}
data = {"items": result_users, "count": total}
return data, 200


Expand Down
1 change: 1 addition & 0 deletions server/mergin/auth/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Meta:
"verified_email",
"profile",
"scheduled_removal",
"registration_date",
)
load_instance = True

Expand Down
4 changes: 4 additions & 0 deletions server/mergin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ class Configuration(object):

# build hash number
BUILD_HASH = config("BUILD_HASH", default="")
# Allow changing access to admin panel
ENABLE_SUPERADMIN_ASSIGNMENT = config(
"ENABLE_SUPERADMIN_ASSIGNMENT", default=True, cast=bool
)
# backend version
VERSION = config("VERSION", default=get_version())
SERVER_TYPE = config("SERVER_TYPE", default="ce")
29 changes: 11 additions & 18 deletions server/mergin/sync/private_api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,20 +183,13 @@ paths:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- $ref: "#/components/parameters/OrderParams"
- name: name
- name: like
in: query
description: Filter projects by name with ilike pattern
description: Filter projects by name or workspace name with ilike pattern
required: false
schema:
type: string
example: survey
- name: workspace
in: query
description: Filter projects by workspace with ilike pattern
required: false
schema:
type: string
example: my-workspace
responses:
"200":
description: List of projects
Expand All @@ -209,7 +202,7 @@ paths:
type: integer
description: Total number of all projects
example: 20
projects:
items:
type: array
items:
$ref: "#/components/schemas/ProjectListItem"
Expand Down Expand Up @@ -292,7 +285,7 @@ paths:
id:
type: string
format: uuid
example: 'd4ecda97-0595-40af-892c-e7522de70bd2'
example: "d4ecda97-0595-40af-892c-e7522de70bd2"
name:
type: string
example: survey
Expand Down Expand Up @@ -460,7 +453,7 @@ components:
- detail
UsersLimitHit:
allOf:
- $ref: '#/components/schemas/CustomError'
- $ref: "#/components/schemas/CustomError"
type: object
properties:
rejected_emails:
Expand All @@ -473,7 +466,7 @@ components:
example:
code: UsersLimitHit
detail: Maximum number of people in this workspace is reached. Please upgrade your subscription to add more people (UsersLimitHit)
rejected_emails: [ [email protected] ]
rejected_emails: [[email protected]]
users_quota: 6
ProjectAccessRequestList:
type: array
Expand All @@ -488,7 +481,7 @@ components:
project_id:
type: string
format: uuid
example: 'd4ecda97-0595-40af-892c-e7522de70bd2'
example: "d4ecda97-0595-40af-892c-e7522de70bd2"
project_name:
type: string
example: survey
Expand Down Expand Up @@ -619,25 +612,25 @@ components:
nullable: false
items:
type: string
example: [ john.doe ]
example: [john.doe]
writersnames:
type: array
nullable: false
items:
type: string
example: [ john.doe ]
example: [john.doe]
editorsnames:
type: array
nullable: false
items:
type: string
example: [ john.doe ]
example: [john.doe]
readersnames:
nullable: false
type: array
items:
type: string
example: [ john.doe ]
example: [john.doe]
public:
type: boolean
example: true
Expand Down
8 changes: 3 additions & 5 deletions server/mergin/sync/private_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,8 @@ def list_namespace_project_access_requests(


@auth_required(permissions=["admin"])
def list_projects(
page, per_page, order_params=None, name=None, workspace=None
): # noqa: E501
projects = current_app.ws_handler.projects_query(name, workspace)
def list_projects(page, per_page, order_params=None, like=None): # noqa: E501
projects = current_app.ws_handler.projects_query(like)
# do not fetch from db what is not needed
projects = projects.options(
defer(Project.storage_params),
Expand All @@ -211,7 +209,7 @@ def list_projects(
result = projects.paginate(page, per_page).items
total = projects.paginate(page, per_page).total
data = AdminProjectSchema(many=True).dump(result)
data = {"projects": data, "count": total}
data = {"items": data, "count": total}
return data, 200


Expand Down
3 changes: 2 additions & 1 deletion server/mergin/sync/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ class Meta:
class AdminProjectSchema(ma.SQLAlchemyAutoSchema):
id = fields.UUID(attribute="Project.id")
name = fields.Str(attribute="Project.name")
namespace = fields.Method("_workspace_name")
workspace = fields.Method("_workspace_name")
workspace_id = fields.Int(attribute="Project.workspace_id")
version = fields.Function(
lambda obj: ProjectVersion.to_v_name(obj.Project.latest_version)
)
Expand Down
13 changes: 7 additions & 6 deletions server/mergin/sync/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,16 +269,17 @@ def monthly_contributors_count():
.count()
)

def projects_query(self, name=None, workspace=None):
def projects_query(self, like: str = None):
ws = self.factory_method()
query = db.session.query(
Project, literal(ws.name).label("workspace_name")
Project,
literal(ws.name).label("workspace_name"),
).filter(Project.storage_params.isnot(None))

if name:
query = query.filter(Project.name.ilike(f"%{name}%"))
if workspace:
query = query.filter(literal(ws.name).ilike(f"%{workspace}%"))
if like:
query = query.filter(
Project.name.ilike(f"%{like}%") | literal(ws.name).ilike(f"%{like}%")
)
return query

@staticmethod
Expand Down
40 changes: 30 additions & 10 deletions server/mergin/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,25 @@ def test_api_user_profile(client):
def test_update_user(client):
login_as_admin(client)
user = User.query.filter_by(username="mergin").first()
data = {"active": True, "is_admin": True}
resp = client.patch(
url_for("/.mergin_auth_controller_update_user", username=user.username),
data=json.dumps(data),
headers=json_headers,
)
assert resp.status_code == 200
assert user.active
assert user.is_admin

client.application.config["ENABLE_SUPERADMIN_ASSIGNMENT"] = False
data = {"active": False, "is_admin": False}
resp = client.patch(
url_for("/.mergin_auth_controller_update_user", username=user.username),
data=json.dumps(data),
headers=json_headers,
)
assert resp.status_code == 400
assert user.active
data = {"active": False}
resp = client.patch(
url_for("/.mergin_auth_controller_update_user", username=user.username),
Expand All @@ -421,6 +440,7 @@ def test_update_user(client):
assert resp.status_code == 200
assert not user.active

client.application.config["ENABLE_SUPERADMIN_ASSIGNMENT"] = True
user.is_admin = False
db.session.add(user)
db.session.commit()
Expand Down Expand Up @@ -694,27 +714,27 @@ def test_paginate_users(client):
url = "/app/admin/users?page=1&per_page=10"
# get 5 users (default + 5 new added - 1 deleted & inactive)
resp = client.get(url)
list_of_usernames = [user["username"] for user in resp.json["users"]]
assert resp.json["total"] == 5
assert resp.json["users"][0]["username"] == "mergin"
list_of_usernames = [user["username"] for user in resp.json["items"]]
assert resp.json["count"] == 5
assert resp.json["items"][0]["username"] == "mergin"
assert user_inactive.username in list_of_usernames
assert deleted_active.username in list_of_usernames
assert deleted_inactive.username not in list_of_usernames
# order by username
resp = client.get(url + "&order_by=username")
assert resp.json["total"] == 5
assert resp.json["users"][0]["username"] == "alice"
assert resp.json["count"] == 5
assert resp.json["items"][0]["username"] == "alice"
# exact match with username
resp = client.get(url + "&like=bob")
assert resp.json["total"] == 1
assert resp.json["users"][0]["username"] == "bob"
assert resp.json["count"] == 1
assert resp.json["items"][0]["username"] == "bob"
# ilike search with email
resp = client.get(url + "&[email protected]")
assert resp.json["total"] == 5
assert resp.json["count"] == 5
# exact search by email
resp = client.get(url + "&[email protected]")
assert resp.json["total"] == 1
assert resp.json["users"][0]["username"] == "alice"
assert resp.json["count"] == 1
assert resp.json["items"][0]["username"] == "alice"
# invalid paging
assert client.get("/app/admin/users?page=2&per_page=10").status_code == 404

Expand Down
Loading

0 comments on commit 777e0b6

Please sign in to comment.