dhruv575 commited on
Commit
5854e13
·
1 Parent(s): 64725d7
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environment variables
2
+ .env
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ *.so
9
+ .Python
10
+ env/
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # Virtual Environment
27
+ venv/
28
+ ENV/
29
+
30
+ # IDE specific files
31
+ .idea/
32
+ .vscode/
33
+ *.swp
34
+ *.swo
35
+
36
+ # Logs
37
+ logs/
38
+ *.log
39
+
40
+ # OS specific
41
+ .DS_Store
42
+ Thumbs.db
43
+
44
+ # Uploaded files and temporary files
45
+ uploads/
46
+ tmp/
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies including Tesseract
6
+ RUN apt-get update && apt-get install -y \
7
+ tesseract-ocr \
8
+ libmagic1 \
9
+ && apt-get clean \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements file and install Python dependencies
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy application code
17
+ COPY . .
18
+
19
+ # Create a .env file if not mounted externally (using env vars from compose)
20
+ RUN if [ ! -f .env ]; then \
21
+ echo "Creating default .env file" \
22
+ && touch .env; \
23
+ fi
24
+
25
+ # Set environment variables
26
+ ENV FLASK_APP=app.py
27
+ ENV FLASK_ENV=production
28
+ ENV PYTHONUNBUFFERED=1
29
+
30
+ # Expose port for Flask
31
+ EXPOSE 5000
32
+
33
+ # Run the application
34
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
README.md CHANGED
@@ -8,3 +8,90 @@ pinned: false
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
11
+
12
+ # Enflow Backend API
13
+
14
+ Enflow (Enforcement Workflow) Backend API that allows law enforcement agencies to build, maintain, and use workflows that automate tasks based on officers' daily logs.
15
+
16
+ ## Setup Instructions
17
+
18
+ ### Prerequisites
19
+
20
+ - Python 3.10 or newer
21
+ - Docker and Docker Compose (optional, for containerized deployment)
22
+ - MongoDB (we're using MongoDB Atlas in the current setup)
23
+
24
+ ### Environment Setup
25
+
26
+ 1. Clone the repository
27
+ 2. Create a `.env` file in the backend directory with the following variables:
28
+ ```
29
+ MONGO_URI=your_mongodb_connection_string
30
+ JWT_SECRET=your_jwt_secret
31
+ CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
32
+ CLOUDINARY_API_KEY=your_cloudinary_api_key
33
+ CLOUDINARY_API_SECRET=your_cloudinary_api_secret
34
+ OPENAI_API_KEY=your_openai_api_key
35
+ FLASK_ENV=development
36
+ ```
37
+
38
+ ### Local Development
39
+
40
+ 1. Create and activate a virtual environment:
41
+ ```
42
+ python -m venv venv
43
+ source venv/bin/activate # On Windows: venv\Scripts\activate
44
+ ```
45
+
46
+ 2. Install dependencies:
47
+ ```
48
+ pip install -r requirements.txt
49
+ ```
50
+
51
+ 3. Run the application:
52
+ ```
53
+ python app.py
54
+ ```
55
+
56
+ 4. The API will be available at http://localhost:5000
57
+
58
+ ### Docker Deployment
59
+
60
+ 1. Build and start the containers:
61
+ ```
62
+ docker-compose up -d
63
+ ```
64
+
65
+ 2. The API will be available at http://localhost:5000
66
+
67
+ ## API Documentation
68
+
69
+ ### Department Endpoints
70
+
71
+ - `POST /api/departments` - Create a new department with its first admin user
72
+ - `GET /api/departments` - Get all departments (requires authentication)
73
+ - `GET /api/departments/{department_id}` - Get department by ID (requires authentication)
74
+ - `PUT /api/departments/{department_id}` - Update department details (requires admin)
75
+ - `DELETE /api/departments/{department_id}` - Delete a department (requires admin)
76
+ - `GET /api/departments/{department_id}/members` - Get all members of a department (requires authentication)
77
+ - `POST /api/departments/{department_id}/members` - Add a member to a department (requires admin)
78
+ - `POST /api/departments/{department_id}/members/csv` - Add multiple members via CSV upload (requires admin)
79
+ - `DELETE /api/departments/{department_id}/members/{user_id}` - Remove a member from a department (requires admin)
80
+ - `PUT /api/departments/{department_id}/members/{user_id}/permissions` - Update a member's permissions (requires admin)
81
+
82
+ ### Testing
83
+
84
+ To test the department creation functionality, run:
85
+
86
+ ```
87
+ python test_department.py
88
+ ```
89
+
90
+ ## Project Structure
91
+
92
+ - `app.py` - Main Flask application
93
+ - `db.py` - Database connection and utilities
94
+ - `models/` - Data models
95
+ - `controllers/` - Controller functions
96
+ - `routes/` - API route definitions
97
+ - `utils/` - Utility functions and middleware
app.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, jsonify, request
3
+ from flask_cors import CORS
4
+ from dotenv import load_dotenv
5
+ import logging
6
+ from logging.handlers import RotatingFileHandler
7
+ import pymongo
8
+ import cloudinary
9
+ import cloudinary.uploader
10
+
11
+ # Import routes
12
+ from routes.department_routes import department_bp
13
+
14
+ # Load environment variables
15
+ load_dotenv()
16
+
17
+ # Create Flask app
18
+ app = Flask(__name__)
19
+ app.config['SECRET_KEY'] = os.environ.get('JWT_SECRET')
20
+ app.config['MONGO_URI'] = os.environ.get('MONGO_URI')
21
+
22
+ # Configure Cloudinary
23
+ cloudinary.config(
24
+ cloud_name=os.environ.get('CLOUDINARY_CLOUD_NAME'),
25
+ api_key=os.environ.get('CLOUDINARY_API_KEY'),
26
+ api_secret=os.environ.get('CLOUDINARY_API_SECRET')
27
+ )
28
+
29
+ # Enable CORS
30
+ CORS(app)
31
+
32
+ # Register blueprints
33
+ app.register_blueprint(department_bp, url_prefix='/api/departments')
34
+ # Other blueprints will be registered later
35
+
36
+ # Create uploads directory if it doesn't exist
37
+ if not os.path.exists('uploads'):
38
+ os.makedirs('uploads')
39
+ app.config['UPLOAD_FOLDER'] = 'uploads'
40
+
41
+ # Error handler
42
+ @app.errorhandler(Exception)
43
+ def handle_exception(e):
44
+ app.logger.error(f"Unhandled exception: {str(e)}")
45
+ return jsonify({"error": "An unexpected error occurred"}), 500
46
+
47
+ # Root route
48
+ @app.route('/')
49
+ def index():
50
+ return jsonify({
51
+ "message": "Enflow API is running",
52
+ "version": "1.0.0"
53
+ })
54
+
55
+ # Health check route
56
+ @app.route('/health')
57
+ def health_check():
58
+ try:
59
+ # Check MongoDB connection
60
+ from db import Database
61
+ db = Database.get_instance().get_db()
62
+ db.command('ping')
63
+
64
+ # All checks passed
65
+ return jsonify({
66
+ "status": "healthy",
67
+ "mongo": "connected"
68
+ }), 200
69
+ except Exception as e:
70
+ app.logger.error(f"Health check failed: {str(e)}")
71
+ return jsonify({
72
+ "status": "unhealthy",
73
+ "error": str(e)
74
+ }), 500
75
+
76
+ # Setup logging
77
+ if not os.path.exists('logs'):
78
+ os.makedirs('logs')
79
+
80
+ handler = RotatingFileHandler('logs/app.log', maxBytes=10000, backupCount=3)
81
+ handler.setLevel(logging.INFO)
82
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
83
+ handler.setFormatter(formatter)
84
+ app.logger.addHandler(handler)
85
+ app.logger.setLevel(logging.INFO)
86
+
87
+ if __name__ == '__main__':
88
+ app.run(host='0.0.0.0', port=5000, debug=os.environ.get('FLASK_ENV') == 'development')
controllers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Controllers package initialization
controllers/department_controller.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import jsonify, request
2
+ from models.department import Department
3
+ from models.user import User
4
+ import csv
5
+ import io
6
+ import bcrypt
7
+ import random
8
+ import string
9
+ import logging
10
+
11
+ # Configure logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def generate_random_password(length=12):
15
+ """Generate a random password"""
16
+ characters = string.ascii_letters + string.digits
17
+ return ''.join(random.choice(characters) for _ in range(length))
18
+
19
+ def create_department():
20
+ """Create a new department with its first admin user"""
21
+ data = request.get_json()
22
+
23
+ # Check if required fields are present
24
+ required_fields = ['name', 'address', 'website', 'admin_email', 'admin_name', 'admin_password']
25
+ for field in required_fields:
26
+ if field not in data:
27
+ return jsonify({'message': f'Missing required field: {field}'}), 400
28
+
29
+ # Check if department with this name already exists
30
+ existing_department = Department.find_by_name(data['name'])
31
+ if existing_department:
32
+ return jsonify({'message': 'Department with this name already exists'}), 400
33
+
34
+ # Check if user with this email already exists
35
+ existing_user = User.find_by_email(data['admin_email'])
36
+ if existing_user:
37
+ return jsonify({'message': 'User with this email already exists'}), 400
38
+
39
+ try:
40
+ # Create department
41
+ department = Department(
42
+ name=data['name'],
43
+ address=data['address'],
44
+ website=data['website']
45
+ )
46
+ department.save()
47
+
48
+ # Create admin user
49
+ hashed_password = User.hash_password(data['admin_password'])
50
+ admin_user = User(
51
+ email=data['admin_email'],
52
+ name=data['admin_name'],
53
+ password=hashed_password,
54
+ permissions='Admin',
55
+ position=data.get('admin_position', 'Administrator'),
56
+ department_id=department._id
57
+ )
58
+ admin_user.save()
59
+
60
+ # Add admin to department members
61
+ department.add_member(admin_user._id)
62
+
63
+ # Return success response
64
+ return jsonify({
65
+ 'message': 'Department and admin user created successfully',
66
+ 'department': department.to_dict(),
67
+ 'admin_user': admin_user.to_dict()
68
+ }), 201
69
+
70
+ except Exception as e:
71
+ logger.error(f"Error creating department: {str(e)}")
72
+ return jsonify({'message': f'Error creating department: {str(e)}'}), 500
73
+
74
+ def get_department(department_id):
75
+ """Get department by ID"""
76
+ department = Department.find_by_id(department_id)
77
+ if not department:
78
+ return jsonify({'message': 'Department not found'}), 404
79
+
80
+ return jsonify({'department': department.to_dict()}), 200
81
+
82
+ def update_department(department_id):
83
+ """Update department details"""
84
+ department = Department.find_by_id(department_id)
85
+ if not department:
86
+ return jsonify({'message': 'Department not found'}), 404
87
+
88
+ data = request.get_json()
89
+
90
+ # Update fields if provided
91
+ if 'name' in data:
92
+ department.name = data['name']
93
+ if 'address' in data:
94
+ department.address = data['address']
95
+ if 'website' in data:
96
+ department.website = data['website']
97
+
98
+ # Save changes
99
+ if department.save():
100
+ return jsonify({'message': 'Department updated successfully', 'department': department.to_dict()}), 200
101
+ else:
102
+ return jsonify({'message': 'Failed to update department'}), 500
103
+
104
+ def delete_department(department_id):
105
+ """Delete a department"""
106
+ department = Department.find_by_id(department_id)
107
+ if not department:
108
+ return jsonify({'message': 'Department not found'}), 404
109
+
110
+ # Delete all users in the department
111
+ users = User.find_by_department(department_id)
112
+ for user in users:
113
+ user.delete()
114
+
115
+ # Delete the department
116
+ if department.delete():
117
+ return jsonify({'message': 'Department and all its users deleted successfully'}), 200
118
+ else:
119
+ return jsonify({'message': 'Failed to delete department'}), 500
120
+
121
+ def get_all_departments():
122
+ """Get all departments"""
123
+ departments = Department.get_all()
124
+ return jsonify({'departments': [department.to_dict() for department in departments]}), 200
125
+
126
+ def add_members_csv(department_id):
127
+ """Add multiple members to a department using CSV upload"""
128
+ department = Department.find_by_id(department_id)
129
+ if not department:
130
+ return jsonify({'message': 'Department not found'}), 404
131
+
132
+ if 'file' not in request.files:
133
+ return jsonify({'message': 'No file part'}), 400
134
+
135
+ file = request.files['file']
136
+ if file.filename == '':
137
+ return jsonify({'message': 'No selected file'}), 400
138
+
139
+ if file and file.filename.endswith('.csv'):
140
+ try:
141
+ # Read CSV data
142
+ csv_data = file.read().decode('utf-8')
143
+ csv_reader = csv.DictReader(io.StringIO(csv_data))
144
+
145
+ # Process each row
146
+ new_users = []
147
+ errors = []
148
+
149
+ required_fields = ['email', 'name', 'position', 'permissions']
150
+
151
+ for row_index, row in enumerate(csv_reader, start=1):
152
+ # Check if all required fields are present
153
+ missing_fields = [field for field in required_fields if field not in row or not row[field]]
154
+
155
+ if missing_fields:
156
+ errors.append(f"Row {row_index}: Missing required fields: {', '.join(missing_fields)}")
157
+ continue
158
+
159
+ # Check if user already exists
160
+ if User.find_by_email(row['email']):
161
+ errors.append(f"Row {row_index}: User with email {row['email']} already exists")
162
+ continue
163
+
164
+ # Check if permissions are valid
165
+ if row['permissions'] not in ['Admin', 'User']:
166
+ row['permissions'] = 'User' # Default to User if invalid
167
+
168
+ # Generate random password
169
+ password = generate_random_password()
170
+ hashed_password = User.hash_password(password)
171
+
172
+ # Create new user
173
+ user = User(
174
+ email=row['email'],
175
+ name=row['name'],
176
+ password=hashed_password,
177
+ permissions=row['permissions'],
178
+ position=row['position'],
179
+ department_id=department._id
180
+ )
181
+
182
+ if user.save():
183
+ # Add user to department
184
+ department.add_member(user._id)
185
+
186
+ # Store for response (including the raw password)
187
+ user_dict = user.to_dict()
188
+ user_dict['raw_password'] = password
189
+ new_users.append(user_dict)
190
+ else:
191
+ errors.append(f"Row {row_index}: Failed to save user {row['email']}")
192
+
193
+ # Return results
194
+ return jsonify({
195
+ 'message': f"Processed {len(new_users)} new users with {len(errors)} errors",
196
+ 'new_users': new_users,
197
+ 'errors': errors
198
+ }), 200
199
+
200
+ except Exception as e:
201
+ logger.error(f"Error processing CSV: {str(e)}")
202
+ return jsonify({'message': f'Error processing CSV: {str(e)}'}), 500
203
+ else:
204
+ return jsonify({'message': 'File must be a CSV'}), 400
205
+
206
+ def add_member(department_id):
207
+ """Add a single member to a department"""
208
+ department = Department.find_by_id(department_id)
209
+ if not department:
210
+ return jsonify({'message': 'Department not found'}), 404
211
+
212
+ data = request.get_json()
213
+
214
+ # Check if required fields are present
215
+ required_fields = ['email', 'name', 'position', 'permissions']
216
+ for field in required_fields:
217
+ if field not in data:
218
+ return jsonify({'message': f'Missing required field: {field}'}), 400
219
+
220
+ # Check if user already exists
221
+ if User.find_by_email(data['email']):
222
+ return jsonify({'message': 'User with this email already exists'}), 400
223
+
224
+ # Check if permissions are valid
225
+ if data['permissions'] not in ['Admin', 'User']:
226
+ data['permissions'] = 'User' # Default to User if invalid
227
+
228
+ try:
229
+ # Generate random password if not provided
230
+ password = data.get('password', generate_random_password())
231
+ hashed_password = User.hash_password(password)
232
+
233
+ # Create new user
234
+ user = User(
235
+ email=data['email'],
236
+ name=data['name'],
237
+ password=hashed_password,
238
+ permissions=data['permissions'],
239
+ position=data['position'],
240
+ department_id=department._id
241
+ )
242
+
243
+ if user.save():
244
+ # Add user to department
245
+ department.add_member(user._id)
246
+
247
+ # Return the raw password in the response
248
+ user_dict = user.to_dict()
249
+ user_dict['raw_password'] = password
250
+
251
+ return jsonify({
252
+ 'message': 'User added successfully',
253
+ 'user': user_dict
254
+ }), 201
255
+ else:
256
+ return jsonify({'message': 'Failed to save user'}), 500
257
+
258
+ except Exception as e:
259
+ logger.error(f"Error adding member: {str(e)}")
260
+ return jsonify({'message': f'Error adding member: {str(e)}'}), 500
261
+
262
+ def get_department_members(department_id):
263
+ """Get all members of a department"""
264
+ department = Department.find_by_id(department_id)
265
+ if not department:
266
+ return jsonify({'message': 'Department not found'}), 404
267
+
268
+ users = User.find_by_department(department_id)
269
+ return jsonify({'members': [user.to_dict() for user in users]}), 200
270
+
271
+ def remove_member(department_id, user_id):
272
+ """Remove a member from a department"""
273
+ department = Department.find_by_id(department_id)
274
+ if not department:
275
+ return jsonify({'message': 'Department not found'}), 404
276
+
277
+ user = User.find_by_id(user_id)
278
+ if not user:
279
+ return jsonify({'message': 'User not found'}), 404
280
+
281
+ # Check if user belongs to the department
282
+ if str(user.department_id) != str(department._id):
283
+ return jsonify({'message': 'User does not belong to this department'}), 400
284
+
285
+ # Remove user from department
286
+ if department.remove_member(user_id) and user.delete():
287
+ return jsonify({'message': 'User removed successfully'}), 200
288
+ else:
289
+ return jsonify({'message': 'Failed to remove user'}), 500
290
+
291
+ def update_member_permissions(department_id, user_id):
292
+ """Update a member's permissions"""
293
+ department = Department.find_by_id(department_id)
294
+ if not department:
295
+ return jsonify({'message': 'Department not found'}), 404
296
+
297
+ user = User.find_by_id(user_id)
298
+ if not user:
299
+ return jsonify({'message': 'User not found'}), 404
300
+
301
+ # Check if user belongs to the department
302
+ if str(user.department_id) != str(department._id):
303
+ return jsonify({'message': 'User does not belong to this department'}), 400
304
+
305
+ data = request.get_json()
306
+
307
+ # Check if permissions are provided
308
+ if 'permissions' not in data:
309
+ return jsonify({'message': 'Permissions not provided'}), 400
310
+
311
+ # Check if permissions are valid
312
+ if data['permissions'] not in ['Admin', 'User']:
313
+ return jsonify({'message': 'Invalid permissions. Must be "Admin" or "User"'}), 400
314
+
315
+ # Update permissions
316
+ user.permissions = data['permissions']
317
+
318
+ if user.save():
319
+ return jsonify({'message': 'User permissions updated successfully', 'user': user.to_dict()}), 200
320
+ else:
321
+ return jsonify({'message': 'Failed to update user permissions'}), 500
db.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from pymongo import MongoClient
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables
7
+ load_dotenv()
8
+
9
+ # Set up logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class Database:
13
+ _instance = None
14
+
15
+ @classmethod
16
+ def get_instance(cls):
17
+ if cls._instance is None:
18
+ cls._instance = cls()
19
+ return cls._instance
20
+
21
+ def __init__(self):
22
+ if Database._instance is not None:
23
+ raise Exception("This class is a singleton!")
24
+
25
+ self.mongo_uri = os.environ.get('MONGO_URI')
26
+ if not self.mongo_uri:
27
+ logger.error("MONGO_URI environment variable not set")
28
+ raise ValueError("MONGO_URI environment variable not set")
29
+
30
+ try:
31
+ self.client = MongoClient(self.mongo_uri)
32
+ self.db = self.client['enflow']
33
+ logger.info("Connected to MongoDB successfully")
34
+ except Exception as e:
35
+ logger.error(f"Error connecting to MongoDB: {str(e)}")
36
+ raise
37
+
38
+ def get_db(self):
39
+ return self.db
40
+
41
+ def close(self):
42
+ if hasattr(self, 'client'):
43
+ self.client.close()
44
+ logger.info("MongoDB connection closed")
45
+
46
+ # Helper functions to get collections
47
+ def get_collection(collection_name):
48
+ db = Database.get_instance().get_db()
49
+ return db[collection_name]
50
+
51
+ def get_users_collection():
52
+ return get_collection('users')
53
+
54
+ def get_departments_collection():
55
+ return get_collection('departments')
56
+
57
+ def get_workflows_collection():
58
+ return get_collection('workflows')
59
+
60
+ def get_logs_collection():
61
+ return get_collection('logs')
62
+
63
+ def get_incidents_collection():
64
+ return get_collection('incidents')
docker-compose.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ ports:
7
+ - "5000:5000"
8
+ volumes:
9
+ - .:/app
10
+ env_file:
11
+ - .env
12
+ depends_on:
13
+ - redis
14
+ restart: unless-stopped
15
+
16
+ redis:
17
+ image: redis:7-alpine
18
+ ports:
19
+ - "6379:6379"
20
+ volumes:
21
+ - redis_data:/data
22
+ restart: unless-stopped
23
+
24
+ volumes:
25
+ redis_data:
models/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from models.user import User
2
+ from models.department import Department
3
+ from models.workflow import Workflow
4
+ from models.log import Log
5
+ from models.incident import Incident
6
+
7
+ __all__ = ['User', 'Department', 'Workflow', 'Log', 'Incident']
models/department.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bson import ObjectId
2
+ from datetime import datetime
3
+ from db import get_departments_collection
4
+
5
+ class Department:
6
+ def __init__(self, name, address, website, members=None, workflows=None, _id=None, created_at=None, updated_at=None):
7
+ self.name = name
8
+ self.address = address
9
+ self.website = website
10
+ self.members = members or []
11
+ self.workflows = workflows or []
12
+ self._id = _id
13
+ self.created_at = created_at or datetime.now()
14
+ self.updated_at = updated_at or datetime.now()
15
+
16
+ def to_dict(self):
17
+ """Convert instance to dictionary"""
18
+ department_dict = {
19
+ "name": self.name,
20
+ "address": self.address,
21
+ "website": self.website,
22
+ "members": [str(member_id) for member_id in self.members],
23
+ "workflows": [str(workflow_id) for workflow_id in self.workflows],
24
+ "created_at": self.created_at,
25
+ "updated_at": self.updated_at
26
+ }
27
+ if self._id:
28
+ department_dict["_id"] = str(self._id)
29
+ return department_dict
30
+
31
+ @classmethod
32
+ def from_dict(cls, department_dict):
33
+ """Create instance from dictionary"""
34
+ if "_id" in department_dict and department_dict["_id"]:
35
+ department_dict["_id"] = ObjectId(department_dict["_id"]) if isinstance(department_dict["_id"], str) else department_dict["_id"]
36
+
37
+ # Convert string IDs to ObjectIds for members and workflows
38
+ if "members" in department_dict and department_dict["members"]:
39
+ department_dict["members"] = [ObjectId(member_id) if isinstance(member_id, str) else member_id for member_id in department_dict["members"]]
40
+
41
+ if "workflows" in department_dict and department_dict["workflows"]:
42
+ department_dict["workflows"] = [ObjectId(workflow_id) if isinstance(workflow_id, str) else workflow_id for workflow_id in department_dict["workflows"]]
43
+
44
+ return cls(**department_dict)
45
+
46
+ def save(self):
47
+ """Save department to database"""
48
+ departments_collection = get_departments_collection()
49
+ department_dict = self.to_dict()
50
+
51
+ if self._id:
52
+ # Update existing department
53
+ department_dict["updated_at"] = datetime.now()
54
+ result = departments_collection.update_one(
55
+ {"_id": ObjectId(self._id)},
56
+ {"$set": department_dict}
57
+ )
58
+ return result.modified_count > 0
59
+ else:
60
+ # Insert new department
61
+ department_dict["created_at"] = datetime.now()
62
+ department_dict["updated_at"] = datetime.now()
63
+ result = departments_collection.insert_one(department_dict)
64
+ self._id = result.inserted_id
65
+ return result.acknowledged
66
+
67
+ @classmethod
68
+ def find_by_id(cls, department_id):
69
+ """Find department by ID"""
70
+ departments_collection = get_departments_collection()
71
+ department_data = departments_collection.find_one({"_id": ObjectId(department_id)})
72
+ if department_data:
73
+ return cls.from_dict(department_data)
74
+ return None
75
+
76
+ @classmethod
77
+ def find_by_name(cls, name):
78
+ """Find department by name"""
79
+ departments_collection = get_departments_collection()
80
+ department_data = departments_collection.find_one({"name": name})
81
+ if department_data:
82
+ return cls.from_dict(department_data)
83
+ return None
84
+
85
+ @classmethod
86
+ def get_all(cls):
87
+ """Get all departments"""
88
+ departments_collection = get_departments_collection()
89
+ departments_data = departments_collection.find()
90
+ return [cls.from_dict(department_data) for department_data in departments_data]
91
+
92
+ def delete(self):
93
+ """Delete department from database"""
94
+ if not self._id:
95
+ return False
96
+
97
+ departments_collection = get_departments_collection()
98
+ result = departments_collection.delete_one({"_id": ObjectId(self._id)})
99
+ return result.deleted_count > 0
100
+
101
+ def add_member(self, user_id):
102
+ """Add a member to department"""
103
+ if user_id not in self.members:
104
+ self.members.append(ObjectId(user_id))
105
+ return self.save()
106
+ return True
107
+
108
+ def remove_member(self, user_id):
109
+ """Remove a member from department"""
110
+ if ObjectId(user_id) in self.members:
111
+ self.members.remove(ObjectId(user_id))
112
+ return self.save()
113
+ return True
114
+
115
+ def add_workflow(self, workflow_id):
116
+ """Add a workflow to department"""
117
+ if workflow_id not in self.workflows:
118
+ self.workflows.append(ObjectId(workflow_id))
119
+ return self.save()
120
+ return True
121
+
122
+ def remove_workflow(self, workflow_id):
123
+ """Remove a workflow from department"""
124
+ if ObjectId(workflow_id) in self.workflows:
125
+ self.workflows.remove(ObjectId(workflow_id))
126
+ return self.save()
127
+ return True
models/incident.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bson import ObjectId
2
+ from datetime import datetime
3
+ from db import get_incidents_collection
4
+
5
+ class Incident:
6
+ def __init__(self, department_id, user_id, workflow_id, description, incident_date,
7
+ filled_forms=None, log_id=None, activity_text=None, form_data=None, _id=None,
8
+ created_at=None, updated_at=None, processing_status="pending"):
9
+ self.department_id = department_id
10
+ self.user_id = user_id
11
+ self.workflow_id = workflow_id
12
+ self.description = description
13
+ self.incident_date = incident_date
14
+ self.filled_forms = filled_forms or [] # List of file paths to filled forms
15
+ self.log_id = log_id # Source log
16
+ self.activity_text = activity_text # Original text from the log that triggered this incident
17
+ self.form_data = form_data or {} # Extracted data to fill forms
18
+ self._id = _id
19
+ self.created_at = created_at or datetime.now()
20
+ self.updated_at = updated_at or datetime.now()
21
+ self.processing_status = processing_status # "pending", "processing", "completed", "failed"
22
+
23
+ def to_dict(self):
24
+ """Convert instance to dictionary"""
25
+ incident_dict = {
26
+ "department_id": str(self.department_id) if self.department_id else None,
27
+ "user_id": str(self.user_id) if self.user_id else None,
28
+ "workflow_id": str(self.workflow_id) if self.workflow_id else None,
29
+ "description": self.description,
30
+ "incident_date": self.incident_date,
31
+ "filled_forms": self.filled_forms,
32
+ "log_id": str(self.log_id) if self.log_id else None,
33
+ "activity_text": self.activity_text,
34
+ "form_data": self.form_data,
35
+ "created_at": self.created_at,
36
+ "updated_at": self.updated_at,
37
+ "processing_status": self.processing_status
38
+ }
39
+ if self._id:
40
+ incident_dict["_id"] = str(self._id)
41
+ return incident_dict
42
+
43
+ @classmethod
44
+ def from_dict(cls, incident_dict):
45
+ """Create instance from dictionary"""
46
+ if "_id" in incident_dict and incident_dict["_id"]:
47
+ incident_dict["_id"] = ObjectId(incident_dict["_id"]) if isinstance(incident_dict["_id"], str) else incident_dict["_id"]
48
+
49
+ if "department_id" in incident_dict and incident_dict["department_id"]:
50
+ incident_dict["department_id"] = ObjectId(incident_dict["department_id"]) if isinstance(incident_dict["department_id"], str) else incident_dict["department_id"]
51
+
52
+ if "user_id" in incident_dict and incident_dict["user_id"]:
53
+ incident_dict["user_id"] = ObjectId(incident_dict["user_id"]) if isinstance(incident_dict["user_id"], str) else incident_dict["user_id"]
54
+
55
+ if "workflow_id" in incident_dict and incident_dict["workflow_id"]:
56
+ incident_dict["workflow_id"] = ObjectId(incident_dict["workflow_id"]) if isinstance(incident_dict["workflow_id"], str) else incident_dict["workflow_id"]
57
+
58
+ if "log_id" in incident_dict and incident_dict["log_id"]:
59
+ incident_dict["log_id"] = ObjectId(incident_dict["log_id"]) if isinstance(incident_dict["log_id"], str) else incident_dict["log_id"]
60
+
61
+ return cls(**incident_dict)
62
+
63
+ def save(self):
64
+ """Save incident to database"""
65
+ incidents_collection = get_incidents_collection()
66
+ incident_dict = self.to_dict()
67
+
68
+ if self._id:
69
+ # Update existing incident
70
+ incident_dict["updated_at"] = datetime.now()
71
+ result = incidents_collection.update_one(
72
+ {"_id": ObjectId(self._id)},
73
+ {"$set": incident_dict}
74
+ )
75
+ return result.modified_count > 0
76
+ else:
77
+ # Insert new incident
78
+ incident_dict["created_at"] = datetime.now()
79
+ incident_dict["updated_at"] = datetime.now()
80
+ result = incidents_collection.insert_one(incident_dict)
81
+ self._id = result.inserted_id
82
+ return result.acknowledged
83
+
84
+ @classmethod
85
+ def find_by_id(cls, incident_id):
86
+ """Find incident by ID"""
87
+ incidents_collection = get_incidents_collection()
88
+ incident_data = incidents_collection.find_one({"_id": ObjectId(incident_id)})
89
+ if incident_data:
90
+ return cls.from_dict(incident_data)
91
+ return None
92
+
93
+ @classmethod
94
+ def find_by_user(cls, user_id):
95
+ """Find all incidents for a user"""
96
+ incidents_collection = get_incidents_collection()
97
+ incidents_data = incidents_collection.find({"user_id": ObjectId(user_id)})
98
+ return [cls.from_dict(incident_data) for incident_data in incidents_data]
99
+
100
+ @classmethod
101
+ def find_by_department(cls, department_id):
102
+ """Find all incidents for a department"""
103
+ incidents_collection = get_incidents_collection()
104
+ incidents_data = incidents_collection.find({"department_id": ObjectId(department_id)})
105
+ return [cls.from_dict(incident_data) for incident_data in incidents_data]
106
+
107
+ @classmethod
108
+ def find_by_workflow(cls, workflow_id):
109
+ """Find all incidents for a workflow"""
110
+ incidents_collection = get_incidents_collection()
111
+ incidents_data = incidents_collection.find({"workflow_id": ObjectId(workflow_id)})
112
+ return [cls.from_dict(incident_data) for incident_data in incidents_data]
113
+
114
+ @classmethod
115
+ def find_by_date_range(cls, start_date, end_date, department_id=None, user_id=None, workflow_id=None):
116
+ """Find incidents by date range, optionally filtered by department, user, and/or workflow"""
117
+ incidents_collection = get_incidents_collection()
118
+ query = {
119
+ "incident_date": {
120
+ "$gte": start_date,
121
+ "$lte": end_date
122
+ }
123
+ }
124
+
125
+ if department_id:
126
+ query["department_id"] = ObjectId(department_id)
127
+
128
+ if user_id:
129
+ query["user_id"] = ObjectId(user_id)
130
+
131
+ if workflow_id:
132
+ query["workflow_id"] = ObjectId(workflow_id)
133
+
134
+ incidents_data = incidents_collection.find(query)
135
+ return [cls.from_dict(incident_data) for incident_data in incidents_data]
136
+
137
+ @classmethod
138
+ def find_by_log(cls, log_id):
139
+ """Find all incidents for a log"""
140
+ incidents_collection = get_incidents_collection()
141
+ incidents_data = incidents_collection.find({"log_id": ObjectId(log_id)})
142
+ return [cls.from_dict(incident_data) for incident_data in incidents_data]
143
+
144
+ def delete(self):
145
+ """Delete incident from database"""
146
+ if not self._id:
147
+ return False
148
+
149
+ incidents_collection = get_incidents_collection()
150
+ result = incidents_collection.delete_one({"_id": ObjectId(self._id)})
151
+ return result.deleted_count > 0
152
+
153
+ def update_processing_status(self, status):
154
+ """Update incident processing status"""
155
+ self.processing_status = status
156
+ return self.save()
157
+
158
+ def add_filled_form(self, form_path):
159
+ """Add a filled form to incident"""
160
+ if form_path not in self.filled_forms:
161
+ self.filled_forms.append(form_path)
162
+ return self.save()
163
+ return True
164
+
165
+ def update_form_data(self, form_data):
166
+ """Update form data"""
167
+ self.form_data = form_data
168
+ return self.save()
models/log.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bson import ObjectId
2
+ from datetime import datetime
3
+ from db import get_logs_collection
4
+
5
+ class Log:
6
+ def __init__(self, user_id, department_id, log_date, log_file_path, incidents=None,
7
+ _id=None, created_at=None, updated_at=None, processing_status="pending",
8
+ extracted_text=None, extracted_activities=None):
9
+ self.user_id = user_id
10
+ self.department_id = department_id
11
+ self.log_date = log_date
12
+ self.log_file_path = log_file_path
13
+ self.incidents = incidents or []
14
+ self._id = _id
15
+ self.created_at = created_at or datetime.now()
16
+ self.updated_at = updated_at or datetime.now()
17
+ self.processing_status = processing_status # "pending", "processing", "completed", "failed"
18
+ self.extracted_text = extracted_text # OCR extracted text from PDF
19
+ self.extracted_activities = extracted_activities or [] # Activities extracted by LLM
20
+
21
+ def to_dict(self):
22
+ """Convert instance to dictionary"""
23
+ log_dict = {
24
+ "user_id": str(self.user_id) if self.user_id else None,
25
+ "department_id": str(self.department_id) if self.department_id else None,
26
+ "log_date": self.log_date,
27
+ "log_file_path": self.log_file_path,
28
+ "incidents": [str(incident_id) for incident_id in self.incidents],
29
+ "created_at": self.created_at,
30
+ "updated_at": self.updated_at,
31
+ "processing_status": self.processing_status,
32
+ "extracted_text": self.extracted_text,
33
+ "extracted_activities": self.extracted_activities
34
+ }
35
+ if self._id:
36
+ log_dict["_id"] = str(self._id)
37
+ return log_dict
38
+
39
+ @classmethod
40
+ def from_dict(cls, log_dict):
41
+ """Create instance from dictionary"""
42
+ if "_id" in log_dict and log_dict["_id"]:
43
+ log_dict["_id"] = ObjectId(log_dict["_id"]) if isinstance(log_dict["_id"], str) else log_dict["_id"]
44
+
45
+ if "user_id" in log_dict and log_dict["user_id"]:
46
+ log_dict["user_id"] = ObjectId(log_dict["user_id"]) if isinstance(log_dict["user_id"], str) else log_dict["user_id"]
47
+
48
+ if "department_id" in log_dict and log_dict["department_id"]:
49
+ log_dict["department_id"] = ObjectId(log_dict["department_id"]) if isinstance(log_dict["department_id"], str) else log_dict["department_id"]
50
+
51
+ # Convert string IDs to ObjectIds for incidents
52
+ if "incidents" in log_dict and log_dict["incidents"]:
53
+ log_dict["incidents"] = [ObjectId(incident_id) if isinstance(incident_id, str) else incident_id for incident_id in log_dict["incidents"]]
54
+
55
+ return cls(**log_dict)
56
+
57
+ def save(self):
58
+ """Save log to database"""
59
+ logs_collection = get_logs_collection()
60
+ log_dict = self.to_dict()
61
+
62
+ if self._id:
63
+ # Update existing log
64
+ log_dict["updated_at"] = datetime.now()
65
+ result = logs_collection.update_one(
66
+ {"_id": ObjectId(self._id)},
67
+ {"$set": log_dict}
68
+ )
69
+ return result.modified_count > 0
70
+ else:
71
+ # Insert new log
72
+ log_dict["created_at"] = datetime.now()
73
+ log_dict["updated_at"] = datetime.now()
74
+ result = logs_collection.insert_one(log_dict)
75
+ self._id = result.inserted_id
76
+ return result.acknowledged
77
+
78
+ @classmethod
79
+ def find_by_id(cls, log_id):
80
+ """Find log by ID"""
81
+ logs_collection = get_logs_collection()
82
+ log_data = logs_collection.find_one({"_id": ObjectId(log_id)})
83
+ if log_data:
84
+ return cls.from_dict(log_data)
85
+ return None
86
+
87
+ @classmethod
88
+ def find_by_user(cls, user_id):
89
+ """Find all logs for a user"""
90
+ logs_collection = get_logs_collection()
91
+ logs_data = logs_collection.find({"user_id": ObjectId(user_id)})
92
+ return [cls.from_dict(log_data) for log_data in logs_data]
93
+
94
+ @classmethod
95
+ def find_by_department(cls, department_id):
96
+ """Find all logs for a department"""
97
+ logs_collection = get_logs_collection()
98
+ logs_data = logs_collection.find({"department_id": ObjectId(department_id)})
99
+ return [cls.from_dict(log_data) for log_data in logs_data]
100
+
101
+ @classmethod
102
+ def find_by_date_range(cls, start_date, end_date, department_id=None, user_id=None):
103
+ """Find logs by date range, optionally filtered by department and/or user"""
104
+ logs_collection = get_logs_collection()
105
+ query = {
106
+ "log_date": {
107
+ "$gte": start_date,
108
+ "$lte": end_date
109
+ }
110
+ }
111
+
112
+ if department_id:
113
+ query["department_id"] = ObjectId(department_id)
114
+
115
+ if user_id:
116
+ query["user_id"] = ObjectId(user_id)
117
+
118
+ logs_data = logs_collection.find(query)
119
+ return [cls.from_dict(log_data) for log_data in logs_data]
120
+
121
+ def delete(self):
122
+ """Delete log from database"""
123
+ if not self._id:
124
+ return False
125
+
126
+ logs_collection = get_logs_collection()
127
+ result = logs_collection.delete_one({"_id": ObjectId(self._id)})
128
+ return result.deleted_count > 0
129
+
130
+ def update_processing_status(self, status):
131
+ """Update log processing status"""
132
+ self.processing_status = status
133
+ return self.save()
134
+
135
+ def set_extracted_text(self, text):
136
+ """Set extracted text from OCR"""
137
+ self.extracted_text = text
138
+ return self.save()
139
+
140
+ def set_extracted_activities(self, activities):
141
+ """Set extracted activities from LLM"""
142
+ self.extracted_activities = activities
143
+ return self.save()
144
+
145
+ def add_incident(self, incident_id):
146
+ """Add an incident to log"""
147
+ if incident_id not in self.incidents:
148
+ self.incidents.append(ObjectId(incident_id))
149
+ return self.save()
150
+ return True
models/user.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ from bson import ObjectId
3
+ from datetime import datetime
4
+ from db import get_users_collection
5
+
6
+ class User:
7
+ def __init__(self, email, name, password=None, permissions="User", position="Officer",
8
+ department_id=None, logs=None, incidents=None, _id=None, created_at=None, updated_at=None):
9
+ self.email = email
10
+ self.name = name
11
+ self.password = password
12
+ self.permissions = permissions # "Admin" or "User"
13
+ self.position = position
14
+ self.department_id = department_id
15
+ self.logs = logs or []
16
+ self.incidents = incidents or []
17
+ self._id = _id
18
+ self.created_at = created_at or datetime.now()
19
+ self.updated_at = updated_at or datetime.now()
20
+
21
+ @staticmethod
22
+ def hash_password(password):
23
+ """Hash a password for storing."""
24
+ return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
25
+
26
+ @staticmethod
27
+ def verify_password(stored_password, provided_password):
28
+ """Verify a stored password against one provided by user"""
29
+ return bcrypt.checkpw(provided_password.encode('utf-8'), stored_password.encode('utf-8'))
30
+
31
+ def to_dict(self):
32
+ """Convert instance to dictionary (excluding password)"""
33
+ user_dict = {
34
+ "email": self.email,
35
+ "name": self.name,
36
+ "permissions": self.permissions,
37
+ "position": self.position,
38
+ "department_id": str(self.department_id) if self.department_id else None,
39
+ "logs": [str(log_id) for log_id in self.logs],
40
+ "incidents": [str(incident_id) for incident_id in self.incidents],
41
+ "created_at": self.created_at,
42
+ "updated_at": self.updated_at
43
+ }
44
+ if self._id:
45
+ user_dict["_id"] = str(self._id)
46
+ return user_dict
47
+
48
+ @classmethod
49
+ def from_dict(cls, user_dict):
50
+ """Create instance from dictionary"""
51
+ if "_id" in user_dict and user_dict["_id"]:
52
+ user_dict["_id"] = ObjectId(user_dict["_id"]) if isinstance(user_dict["_id"], str) else user_dict["_id"]
53
+
54
+ if "department_id" in user_dict and user_dict["department_id"]:
55
+ user_dict["department_id"] = ObjectId(user_dict["department_id"]) if isinstance(user_dict["department_id"], str) else user_dict["department_id"]
56
+
57
+ # Convert string IDs to ObjectIds for logs and incidents
58
+ if "logs" in user_dict and user_dict["logs"]:
59
+ user_dict["logs"] = [ObjectId(log_id) if isinstance(log_id, str) else log_id for log_id in user_dict["logs"]]
60
+
61
+ if "incidents" in user_dict and user_dict["incidents"]:
62
+ user_dict["incidents"] = [ObjectId(incident_id) if isinstance(incident_id, str) else incident_id for incident_id in user_dict["incidents"]]
63
+
64
+ return cls(**user_dict)
65
+
66
+ def save(self):
67
+ """Save user to database"""
68
+ users_collection = get_users_collection()
69
+ user_dict = self.to_dict()
70
+
71
+ # Add password for database storage
72
+ if self.password:
73
+ user_dict["password"] = self.password
74
+
75
+ if self._id:
76
+ # Update existing user
77
+ user_dict["updated_at"] = datetime.now()
78
+ result = users_collection.update_one(
79
+ {"_id": ObjectId(self._id)},
80
+ {"$set": user_dict}
81
+ )
82
+ return result.modified_count > 0
83
+ else:
84
+ # Insert new user
85
+ user_dict["created_at"] = datetime.now()
86
+ user_dict["updated_at"] = datetime.now()
87
+ result = users_collection.insert_one(user_dict)
88
+ self._id = result.inserted_id
89
+ return result.acknowledged
90
+
91
+ @classmethod
92
+ def find_by_id(cls, user_id):
93
+ """Find user by ID"""
94
+ users_collection = get_users_collection()
95
+ user_data = users_collection.find_one({"_id": ObjectId(user_id)})
96
+ if user_data:
97
+ return cls.from_dict(user_data)
98
+ return None
99
+
100
+ @classmethod
101
+ def find_by_email(cls, email):
102
+ """Find user by email"""
103
+ users_collection = get_users_collection()
104
+ user_data = users_collection.find_one({"email": email})
105
+ if user_data:
106
+ return cls.from_dict(user_data)
107
+ return None
108
+
109
+ @classmethod
110
+ def find_by_department(cls, department_id):
111
+ """Find all users in a department"""
112
+ users_collection = get_users_collection()
113
+ users_data = users_collection.find({"department_id": ObjectId(department_id)})
114
+ return [cls.from_dict(user_data) for user_data in users_data]
115
+
116
+ def delete(self):
117
+ """Delete user from database"""
118
+ if not self._id:
119
+ return False
120
+
121
+ users_collection = get_users_collection()
122
+ result = users_collection.delete_one({"_id": ObjectId(self._id)})
123
+ return result.deleted_count > 0
124
+
125
+ def add_log(self, log_id):
126
+ """Add a log to user's logs"""
127
+ if log_id not in self.logs:
128
+ self.logs.append(ObjectId(log_id))
129
+ return self.save()
130
+ return True
131
+
132
+ def add_incident(self, incident_id):
133
+ """Add an incident to user's incidents"""
134
+ if incident_id not in self.incidents:
135
+ self.incidents.append(ObjectId(incident_id))
136
+ return self.save()
137
+ return True
models/workflow.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bson import ObjectId
2
+ from datetime import datetime
3
+ from db import get_workflows_collection
4
+
5
+ class Workflow:
6
+ def __init__(self, title, description, data_requirements=None, raw_forms=None, form_fields=None,
7
+ department_id=None, _id=None, created_at=None, updated_at=None):
8
+ self.title = title
9
+ self.description = description
10
+ self.data_requirements = data_requirements or [] # List of (String, String) for fields and descriptions
11
+ self.raw_forms = raw_forms or [] # List of file paths
12
+ self.form_fields = form_fields or [] # List of (Number, String) for positions and fields
13
+ self.department_id = department_id
14
+ self._id = _id
15
+ self.created_at = created_at or datetime.now()
16
+ self.updated_at = updated_at or datetime.now()
17
+
18
+ def to_dict(self):
19
+ """Convert instance to dictionary"""
20
+ workflow_dict = {
21
+ "title": self.title,
22
+ "description": self.description,
23
+ "data_requirements": self.data_requirements,
24
+ "raw_forms": self.raw_forms,
25
+ "form_fields": self.form_fields,
26
+ "department_id": str(self.department_id) if self.department_id else None,
27
+ "created_at": self.created_at,
28
+ "updated_at": self.updated_at
29
+ }
30
+ if self._id:
31
+ workflow_dict["_id"] = str(self._id)
32
+ return workflow_dict
33
+
34
+ @classmethod
35
+ def from_dict(cls, workflow_dict):
36
+ """Create instance from dictionary"""
37
+ if "_id" in workflow_dict and workflow_dict["_id"]:
38
+ workflow_dict["_id"] = ObjectId(workflow_dict["_id"]) if isinstance(workflow_dict["_id"], str) else workflow_dict["_id"]
39
+
40
+ if "department_id" in workflow_dict and workflow_dict["department_id"]:
41
+ workflow_dict["department_id"] = ObjectId(workflow_dict["department_id"]) if isinstance(workflow_dict["department_id"], str) else workflow_dict["department_id"]
42
+
43
+ return cls(**workflow_dict)
44
+
45
+ def save(self):
46
+ """Save workflow to database"""
47
+ workflows_collection = get_workflows_collection()
48
+ workflow_dict = self.to_dict()
49
+
50
+ if self._id:
51
+ # Update existing workflow
52
+ workflow_dict["updated_at"] = datetime.now()
53
+ result = workflows_collection.update_one(
54
+ {"_id": ObjectId(self._id)},
55
+ {"$set": workflow_dict}
56
+ )
57
+ return result.modified_count > 0
58
+ else:
59
+ # Insert new workflow
60
+ workflow_dict["created_at"] = datetime.now()
61
+ workflow_dict["updated_at"] = datetime.now()
62
+ result = workflows_collection.insert_one(workflow_dict)
63
+ self._id = result.inserted_id
64
+ return result.acknowledged
65
+
66
+ @classmethod
67
+ def find_by_id(cls, workflow_id):
68
+ """Find workflow by ID"""
69
+ workflows_collection = get_workflows_collection()
70
+ workflow_data = workflows_collection.find_one({"_id": ObjectId(workflow_id)})
71
+ if workflow_data:
72
+ return cls.from_dict(workflow_data)
73
+ return None
74
+
75
+ @classmethod
76
+ def find_by_title(cls, title, department_id=None):
77
+ """Find workflow by title, optionally filtered by department"""
78
+ workflows_collection = get_workflows_collection()
79
+ query = {"title": title}
80
+ if department_id:
81
+ query["department_id"] = ObjectId(department_id)
82
+
83
+ workflow_data = workflows_collection.find_one(query)
84
+ if workflow_data:
85
+ return cls.from_dict(workflow_data)
86
+ return None
87
+
88
+ @classmethod
89
+ def find_by_department(cls, department_id):
90
+ """Find all workflows for a department"""
91
+ workflows_collection = get_workflows_collection()
92
+ workflows_data = workflows_collection.find({"department_id": ObjectId(department_id)})
93
+ return [cls.from_dict(workflow_data) for workflow_data in workflows_data]
94
+
95
+ @classmethod
96
+ def get_all(cls):
97
+ """Get all workflows"""
98
+ workflows_collection = get_workflows_collection()
99
+ workflows_data = workflows_collection.find()
100
+ return [cls.from_dict(workflow_data) for workflow_data in workflows_data]
101
+
102
+ def delete(self):
103
+ """Delete workflow from database"""
104
+ if not self._id:
105
+ return False
106
+
107
+ workflows_collection = get_workflows_collection()
108
+ result = workflows_collection.delete_one({"_id": ObjectId(self._id)})
109
+ return result.deleted_count > 0
110
+
111
+ def add_form(self, form_path):
112
+ """Add a form to workflow"""
113
+ if form_path not in self.raw_forms:
114
+ self.raw_forms.append(form_path)
115
+ return self.save()
116
+ return True
117
+
118
+ def remove_form(self, form_path):
119
+ """Remove a form from workflow"""
120
+ if form_path in self.raw_forms:
121
+ self.raw_forms.remove(form_path)
122
+ return self.save()
123
+ return True
124
+
125
+ def add_data_requirement(self, field, description):
126
+ """Add a data requirement (field and description)"""
127
+ requirement = (field, description)
128
+ if requirement not in self.data_requirements:
129
+ self.data_requirements.append(requirement)
130
+ return self.save()
131
+ return True
132
+
133
+ def add_form_field(self, position, field_name):
134
+ """Add a form field (position and field name)"""
135
+ form_field = (position, field_name)
136
+ if form_field not in self.form_fields:
137
+ self.form_fields.append(form_field)
138
+ return self.save()
139
+ return True
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask==2.2.3
2
+ python-dotenv==1.0.0
3
+ pymongo==4.5.0
4
+ flask-cors==4.0.0
5
+ bcrypt==4.0.1
6
+ PyJWT==2.8.0
7
+ gunicorn==21.2.0
8
+ cloudinary==1.35.0
9
+ openai==1.6.1
10
+ pytesseract==0.3.10
11
+ Pillow==10.1.0
12
+ python-magic==0.4.27
13
+ Flask-RESTful==0.3.10
14
+ Werkzeug==2.2.3
15
+ celery==5.3.4
16
+ redis==5.0.1
routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Routes package initialization
routes/department_routes.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint
2
+ from controllers.department_controller import (
3
+ create_department, get_department, update_department,
4
+ delete_department, get_all_departments, add_members_csv,
5
+ add_member, get_department_members, remove_member, update_member_permissions
6
+ )
7
+ from utils.auth import token_required, admin_required
8
+
9
+ # Create blueprint
10
+ department_bp = Blueprint('departments', __name__)
11
+
12
+ # Public route for creating a new department with first admin
13
+ department_bp.route('/', methods=['POST'])(create_department)
14
+
15
+ # Routes that require authentication
16
+ department_bp.route('/', methods=['GET'])(token_required(get_all_departments))
17
+ department_bp.route('/<department_id>', methods=['GET'])(token_required(get_department))
18
+
19
+ # Routes that require admin permissions
20
+ department_bp.route('/<department_id>', methods=['PUT'])(admin_required(update_department))
21
+ department_bp.route('/<department_id>', methods=['DELETE'])(admin_required(delete_department))
22
+
23
+ # Member management routes (admin only)
24
+ department_bp.route('/<department_id>/members', methods=['GET'])(token_required(get_department_members))
25
+ department_bp.route('/<department_id>/members', methods=['POST'])(admin_required(add_member))
26
+ department_bp.route('/<department_id>/members/csv', methods=['POST'])(admin_required(add_members_csv))
27
+ department_bp.route('/<department_id>/members/<user_id>', methods=['DELETE'])(admin_required(remove_member))
28
+ department_bp.route('/<department_id>/members/<user_id>/permissions', methods=['PUT'])(admin_required(update_member_permissions))
setup_env.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Setup environment variables for local development.
3
+ This script creates a .env file with the required environment variables.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+
9
+ def create_env_file():
10
+ """Create a .env file with required environment variables."""
11
+ # Define environment variables
12
+ env_vars = {
13
+ "MONGO_URI": "mongodb+srv://dg:123@enflow.9w95qq5.mongodb.net/?retryWrites=true&w=majority&appName=enflow",
14
+ "JWT_SECRET": "fde5be3fca2f594bb39592a9d3781f5dabd1226bfea2d6fc1a51d65fdf031d23b05d4c8e46feb01531c9be214411bbec1523f735139db8f6d15320974e2d5ef3503db53ca1502a0f9489d310f55100baabcf6ebb3b10e975f3b445f70dee4c8c2bced6618ecf0f13ec4029914e05935bc02e7849a55348899fbba06bf6882ef1fcf67e33ce15b8afc08fc81d60868792f69f2a407301bb2f421655dfd8bbfe3481b86fe5ff01f02b30de35df5a35ec3c58a9b7a93b0baddded92c453a06fc5a2bd72ae739ee4ddc785fce6399dfe97f74945ef06023a64cb173800dbd85f10ba8847f9f8391422683d365bfdcabf7949d70a54f919c304463d1448820d6aa1dc",
15
+ "CLOUDINARY_CLOUD_NAME": "djt4gxy9s",
16
+ "CLOUDINARY_API_KEY": "551626585336911",
17
+ "CLOUDINARY_API_SECRET": "d6HCnsoaDBypM1dXCReFoJqkZDA",
18
+ "OPENAI_API_KEY": "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",
19
+ "FLASK_ENV": "development"
20
+ }
21
+
22
+ # Check if .env file already exists
23
+ if os.path.exists('.env'):
24
+ print("Warning: .env file already exists.")
25
+ response = input("Do you want to overwrite it? (y/n): ")
26
+ if response.lower() != 'y':
27
+ print("Operation canceled.")
28
+ return
29
+
30
+ # Write environment variables to .env file
31
+ with open('.env', 'w') as f:
32
+ for key, value in env_vars.items():
33
+ f.write(f"{key}={value}\n")
34
+
35
+ print(".env file created successfully.")
36
+
37
+ if __name__ == "__main__":
38
+ print("Setting up environment variables for Enflow Backend...")
39
+ create_env_file()
40
+ print("Setup complete. You can now run the application.")
41
+ print("To start the application, run: python app.py")
test_department.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import json
4
+ from dotenv import load_dotenv
5
+
6
+ # Load environment variables
7
+ load_dotenv()
8
+
9
+ # Base URL for API
10
+ BASE_URL = "http://localhost:5000/api"
11
+
12
+ def test_create_department():
13
+ """Test creating a new department with an admin user"""
14
+ # Department data
15
+ department_data = {
16
+ "name": "Test Police Department",
17
+ "address": "123 Test Street, Test City, TS 12345",
18
+ "website": "https://test-pd.example.com",
19
+ "admin_email": "admin@test-pd.example.com",
20
+ "admin_name": "Admin User",
21
+ "admin_password": "SecurePassword123"
22
+ }
23
+
24
+ # Make POST request to create department
25
+ response = requests.post(f"{BASE_URL}/departments", json=department_data)
26
+
27
+ # Print response details
28
+ print(f"Status Code: {response.status_code}")
29
+ print("Response:")
30
+ print(json.dumps(response.json(), indent=2))
31
+
32
+ return response.json()
33
+
34
+ def main():
35
+ """Run test functions"""
36
+ print("=== Testing Department Creation ===")
37
+ result = test_create_department()
38
+
39
+ # If department was created successfully, store admin details
40
+ if result.get('department') and result.get('admin_user'):
41
+ print("\n=== Department Created Successfully ===")
42
+ print(f"Department Name: {result['department']['name']}")
43
+ print(f"Admin Email: {result['admin_user']['email']}")
44
+ print("You can now use these details to test the auth endpoints")
45
+
46
+ if __name__ == "__main__":
47
+ main()
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Utils package initialization
utils/auth.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import jwt
3
+ from datetime import datetime, timedelta
4
+ from functools import wraps
5
+ from flask import request, jsonify
6
+ from models.user import User
7
+
8
+ # Secret key for JWT
9
+ SECRET_KEY = os.environ.get('JWT_SECRET')
10
+
11
+ def generate_token(user_id, permissions, expiration_hours=24*30): # 30-day token by default
12
+ """Generate JWT token for authentication"""
13
+ payload = {
14
+ 'user_id': str(user_id),
15
+ 'permissions': permissions,
16
+ 'exp': datetime.utcnow() + timedelta(hours=expiration_hours)
17
+ }
18
+ return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
19
+
20
+ def decode_token(token):
21
+ """Decode and validate JWT token"""
22
+ try:
23
+ return jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
24
+ except jwt.ExpiredSignatureError:
25
+ return None
26
+ except jwt.InvalidTokenError:
27
+ return None
28
+
29
+ def token_required(f):
30
+ """Decorator for routes that require a valid token"""
31
+ @wraps(f)
32
+ def decorated(*args, **kwargs):
33
+ token = None
34
+ auth_header = request.headers.get('Authorization')
35
+
36
+ if auth_header:
37
+ if auth_header.startswith('Bearer '):
38
+ token = auth_header.split(" ")[1]
39
+
40
+ if not token:
41
+ return jsonify({'message': 'Token is missing'}), 401
42
+
43
+ data = decode_token(token)
44
+ if not data:
45
+ return jsonify({'message': 'Token is invalid or expired'}), 401
46
+
47
+ current_user = User.find_by_id(data['user_id'])
48
+ if not current_user:
49
+ return jsonify({'message': 'User not found'}), 401
50
+
51
+ return f(current_user, *args, **kwargs)
52
+
53
+ return decorated
54
+
55
+ def admin_required(f):
56
+ """Decorator for routes that require admin permissions"""
57
+ @wraps(f)
58
+ def decorated(*args, **kwargs):
59
+ token = None
60
+ auth_header = request.headers.get('Authorization')
61
+
62
+ if auth_header:
63
+ if auth_header.startswith('Bearer '):
64
+ token = auth_header.split(" ")[1]
65
+
66
+ if not token:
67
+ return jsonify({'message': 'Token is missing'}), 401
68
+
69
+ data = decode_token(token)
70
+ if not data:
71
+ return jsonify({'message': 'Token is invalid or expired'}), 401
72
+
73
+ if data['permissions'] != 'Admin':
74
+ return jsonify({'message': 'Admin permissions required'}), 403
75
+
76
+ current_user = User.find_by_id(data['user_id'])
77
+ if not current_user:
78
+ return jsonify({'message': 'User not found'}), 401
79
+
80
+ return f(current_user, *args, **kwargs)
81
+
82
+ return decorated