Skip to content

Commit

Permalink
Group management, Group documents, and User settings
Browse files Browse the repository at this point in the history
Now you can create groups, assign roles, and control who has access to group documents. Group documents function similarly to your documents. You can chat with them via chat or from the Group documents page. You may be a member of one or more groups. Your active group is how you interact with Group documents.

User settings are new and are used to save your active group.
  • Loading branch information
paullizer committed Jan 27, 2025
1 parent e344367 commit b541dbe
Show file tree
Hide file tree
Showing 31 changed files with 3,428 additions and 363 deletions.
24 changes: 23 additions & 1 deletion application/single_app/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# app.py

from config import *

from functions_authentication import *
Expand All @@ -12,10 +14,15 @@
from route_frontend_documents import *
from route_frontend_chats import *
from route_frontend_conversations import *
from route_frontend_groups import *
from route_frontend_group_documents import *

from route_backend_chats import *
from route_backend_conversations import *
from route_backend_documents import *
from route_backend_groups import *
from route_backend_users import *
from route_backend_group_documents import *

# =================== Helper Functions ===================
@app.context_processor
Expand All @@ -36,7 +43,7 @@ def format_datetime_filter(value):
def index():
return render_template('index.html')

@app.route('/robots.txt')
@app.route('/robots933456.txt')
def robots():
return send_from_directory('static', 'robots.txt')

Expand All @@ -63,6 +70,12 @@ def favicon():
# ------------------- Documents Routes -------------------
register_route_frontend_documents(app)

# ------------------- Groups Routes ----------------------
register_route_frontend_groups(app)

# ------------------- Group Documents Routes -------------
register_route_frontend_group_documents(app)

# =================== Back End Routes ====================
# ------------------- API Chat Routes --------------------
register_route_backend_chats(app)
Expand All @@ -73,5 +86,14 @@ def favicon():
# ------------------- API Documents Routes ---------------
register_route_backend_documents(app)

# ------------------- API Groups Routes ------------------
register_route_backend_groups(app)

# ------------------- API User Routes --------------------
register_route_backend_users(app)

# ------------------- API Group Documents Routes ---------
register_route_backend_group_documents(app)

if __name__ == '__main__':
app.run(debug=True)
36 changes: 33 additions & 3 deletions application/single_app/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# config.py
import os
import requests
import uuid
Expand All @@ -20,22 +21,23 @@
from threading import Thread
from openai import AzureOpenAI
from cryptography.fernet import Fernet, InvalidToken
from urllib.parse import quote

from azure.cosmos import CosmosClient, PartitionKey
from azure.cosmos import CosmosClient, PartitionKey, exceptions
from azure.cosmos.exceptions import CosmosResourceNotFoundError
from azure.core.credentials import AzureKeyCredential
from azure.ai.documentintelligence import DocumentIntelligenceClient
from azure.ai.formrecognizer import DocumentAnalysisClient
from azure.search.documents import SearchClient, IndexDocumentsBatch
from azure.search.documents.models import VectorizedQuery
from azure.core.exceptions import AzureError
from azure.core.exceptions import AzureError, ResourceNotFoundError
from azure.core.polling import LROPoller

app = Flask(__name__)

app.config['SECRET_KEY'] = os.getenv("SECRET_KEY")
app.config['SESSION_TYPE'] = 'filesystem'
app.config['VERSION'] = '0.176.doc_target.1'
app.config['VERSION'] = '0.179.group_documents.13'
Session(app)

ALLOWED_EXTENSIONS = {
Expand All @@ -49,6 +51,7 @@
TENANT_ID = os.getenv("TENANT_ID")
AUTHORITY = f"https://login.microsoftonline.us/{TENANT_ID}"
SCOPE = ["User.Read"] # Adjust scope according to your needs
MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = os.getenv("MICROSOFT_PROVIDER_AUTHENTICATION_SECRET")

# Azure Document Intelligence Configuration
AZURE_DI_ENDPOINT = os.getenv("AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT")
Expand Down Expand Up @@ -82,6 +85,7 @@
AZURE_AI_SEARCH_ENDPOINT = os.getenv('AZURE_AI_SEARCH_ENDPOINT')
AZURE_AI_SEARCH_KEY = os.getenv('AZURE_AI_SEARCH_KEY')
AZURE_AI_SEARCH_USER_INDEX = os.getenv('AZURE_AI_SEARCH_USER_INDEX')
AZURE_AI_SEARCH_GROUP_INDEX = os.getenv('AZURE_AI_SEARCH_GROUP_INDEX')

BING_SEARCH_ENDPOINT = os.getenv("BING_SEARCH_ENDPOINT")
BING_SEARCH_KEY = os.getenv("BING_SEARCH_KEY")
Expand Down Expand Up @@ -111,10 +115,36 @@
credential=AzureKeyCredential(AZURE_AI_SEARCH_KEY)
)

search_client_group = SearchClient(
endpoint=AZURE_AI_SEARCH_ENDPOINT,
index_name=AZURE_AI_SEARCH_GROUP_INDEX,
credential=AzureKeyCredential(AZURE_AI_SEARCH_KEY)
)

settings_container_name = "settings"
settings_container = database.create_container_if_not_exists(
id=settings_container_name,
partition_key=PartitionKey(path="/id"),
offer_throughput=400
)

groups_container_name = "groups"
groups_container = database.create_container_if_not_exists(
id=groups_container_name,
partition_key=PartitionKey(path="/id"),
offer_throughput=400
)

group_documents_container_name = "group_documents"
group_documents_container = database.create_container_if_not_exists(
id=group_documents_container_name,
partition_key=PartitionKey(path="/id"),
offer_throughput=400
)

user_settings_container_name = "user_settings"
user_settings_container = database.create_container_if_not_exists(
id=user_settings_container_name,
partition_key=PartitionKey(path="/id"),
offer_throughput=400
)
14 changes: 13 additions & 1 deletion application/single_app/functions_authentication.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# functions_authentication.py

from config import *

def login_required(f):
Expand All @@ -13,4 +15,14 @@ def get_current_user_id():
user = session.get('user')
if user:
return user.get('oid')
return None
return None

def get_current_user_info():
user = session.get("user")
if not user:
return None
return {
"userId": user.get("oid"),
"email": user.get("preferred_username"),
"displayName": user.get("name")
}
2 changes: 2 additions & 0 deletions application/single_app/functions_bing_search.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# functions_bing_search.py

from config import *

def get_suggestions(query):
Expand Down
2 changes: 2 additions & 0 deletions application/single_app/functions_content.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# functions_content.py

from config import *
from functions_settings import *

Expand Down
36 changes: 35 additions & 1 deletion application/single_app/functions_documents.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# functions_documents.py

from config import *
from functions_content import *
from functions_settings import *
Expand Down Expand Up @@ -336,4 +338,36 @@ def get_document_versions(user_id, document_id):

except Exception as e:
#print(f'Error retrieving document versions: {str(e)}')
return []
return []

def detect_doc_type(document_id, user_id=None):
"""
Check Cosmos to see if this doc belongs to the user's docs (has user_id)
or the group's docs (has group_id).
Returns one of: "user", "group", or None if not found.
Optionally checks if user_id matches (for user docs).
"""

# 1) Try user docs container
try:
# For user docs, the container is "documents_container"
doc_item = documents_container.read_item(document_id, partition_key=document_id)
# If found, confirm it belongs to this user_id if given
if user_id and doc_item.get('user_id') != user_id:
# doesn't match the user -> not a user doc for this user
pass
else:
return "user"
except:
pass # Not found in user docs

# 2) Try group docs container
try:
group_doc_item = group_documents_container.read_item(document_id, partition_key=document_id)
# If found, it must be a group doc
return "group"
except:
pass

# If not found in either container
return None
169 changes: 169 additions & 0 deletions application/single_app/functions_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# functions_group.py
from config import *
from functions_authentication import *
from functions_settings import *


def create_group(name, description):
"""Creates a new group. The creator is the Owner by default."""
user_info = get_current_user_info()
if not user_info:
raise Exception("No user in session")

new_group_id = str(uuid.uuid4())
now_str = datetime.utcnow().isoformat()

group_doc = {
"id": new_group_id,
"name": name,
"description": description,
"owner":
{
"id": user_info["userId"],
"email": user_info["email"],
"displayName": user_info["displayName"]
},
"admins": [],
"documentManagers": [],
"users": [
{
"userId": user_info["userId"],
"email": user_info["email"],
"displayName": user_info["displayName"]
}
],
"pendingUsers": [],
"createdDate": now_str,
"modifiedDate": now_str
}
groups_container.create_item(group_doc)
return group_doc

def search_groups(search_query, user_id):
"""
Return a list of groups the user is in or (optionally)
you can expand to also return public groups.
For simplicity, this only returns groups where the user is a member.
"""
query = query = """
SELECT *
FROM c
WHERE EXISTS (
SELECT VALUE u
FROM u IN c.users
WHERE u.userId = @user_id
)
"""

params = [
{ "name": "@user_id", "value": user_id }
]
if search_query:
# You can refine this to also match group name or description
query += " AND CONTAINS(c.name, @search) "
params.append({"name": "@search", "value": search_query})

results = list(groups_container.query_items(
query=query,
parameters=params,
enable_cross_partition_query=True
))
return results

def get_user_groups(user_id):
"""
Fetch all groups for which this user is a member.
"""
query = query = """
SELECT *
FROM c
WHERE EXISTS (
SELECT VALUE x
FROM x IN c.users
WHERE x.userId = @user_id
)
"""

params = [{ "name": "@user_id", "value": user_id }]
results = list(groups_container.query_items(
query=query,
parameters=params,
enable_cross_partition_query=True
))
return results

def find_group_by_id(group_id):
"""Retrieve a single group doc by its ID."""
try:
group_doc = groups_container.read_item(
item=group_id,
partition_key=group_id
)
return group_doc
except exceptions.CosmosResourceNotFoundError:
return None

def update_active_group_for_user(user_id, group_id):
new_settings = {
"settings": {
"activeGroupOid": group_id
},
"lastUpdated": datetime.utcnow().isoformat()
}
update_user_settings(user_id, new_settings)

def get_user_role_in_group(group_doc, user_id):
"""Determine the user's role in the given group doc."""
if not group_doc:
return None

if group_doc.get("owner", {}).get("id") == user_id:
return "Owner"
elif user_id in group_doc.get("admins", []):
return "Admin"
elif user_id in group_doc.get("documentManagers", []):
return "DocumentManager"
else:
for u in group_doc.get("users", []):
if u["userId"] == user_id:
return "User"

return None


def map_group_list_for_frontend(groups, current_user_id):
"""
Utility to produce a simplified list of group data
for the front-end, including userRole and isActive.
"""
active_group_id = session.get("active_group")
response = []
for g in groups:
role = get_user_role_in_group(g, current_user_id)
response.append({
"id": g["id"],
"name": g["name"],
"description": g.get("description", ""),
"userRole": role,
"isActive": (g["id"] == active_group_id)
})
return response

def delete_group(group_id):
"""
Deletes a group from Cosmos DB. Typically only owner can do this.
"""
groups_container.delete_item(item=group_id, partition_key=group_id)

def is_user_in_group(group_doc, user_id):
"""
Helper to check if a user is in the given group's users[] or is the owner.
"""
if group_doc.get("owner", {}).get("id") == user_id:
return True

# Or if userId appears in the 'users' array
for u in group_doc.get("users", []):
if u["userId"] == user_id:
return True
return False
Loading

0 comments on commit b541dbe

Please sign in to comment.