BananaSauce commited on
Commit
69a44c9
·
1 Parent(s): d2ed71e

jira implemented

Browse files
Files changed (4) hide show
  1. README.md +53 -1
  2. jira_integration.py +1635 -0
  3. multiple.py +436 -41
  4. requirements.txt +5 -1
README.md CHANGED
@@ -102,4 +102,56 @@ Compare scenarios across multiple environments to identify inconsistencies in te
102
  If you encounter issues:
103
  1. Ensure the file format follows the expected structure
104
  2. Check the logs for specific error messages
105
- 3. Try processing smaller files first to verify functionality
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  If you encounter issues:
103
  1. Ensure the file format follows the expected structure
104
  2. Check the logs for specific error messages
105
+ 3. Try processing smaller files first to verify functionality
106
+
107
+ # Jira Integration for Test Analysis
108
+
109
+ This application provides a Streamlit interface for analyzing test results and creating Jira tasks for failed scenarios.
110
+
111
+ ## Setup
112
+
113
+ 1. Clone the repository
114
+ 2. Install dependencies:
115
+ ```bash
116
+ pip install -r requirements.txt
117
+ ```
118
+
119
+ 3. Create a `.env` file in the root directory with the following variables:
120
+ ```env
121
+ JIRA_SERVER=your_jira_server_url
122
+ GROQ_API_KEY=your_groq_api_key
123
+ ```
124
+
125
+ ## Environment Variables
126
+
127
+ - `JIRA_SERVER`: Your Jira server URL (e.g., https://jira.yourdomain.com)
128
+ - `GROQ_API_KEY`: Your Groq API key for AI functionality
129
+
130
+ ## Running the Application
131
+
132
+ ```bash
133
+ streamlit run jira_integration.py
134
+ ```
135
+
136
+ ## Features
137
+
138
+ - Jira authentication and session management
139
+ - Test scenario analysis
140
+ - Automated Jira task creation
141
+ - Sprint statistics tracking
142
+ - Functional area mapping
143
+ - Customer field mapping
144
+
145
+ ## Deployment
146
+
147
+ This application is designed to be deployed on Huggingface Spaces. When deploying:
148
+
149
+ 1. Add the environment variables in the Huggingface Spaces settings
150
+ 2. Ensure all dependencies are listed in requirements.txt
151
+ 3. The application will automatically use the environment variables from Huggingface Spaces
152
+
153
+ ## Security Notes
154
+
155
+ - Never commit the `.env` file to version control
156
+ - Keep your Jira credentials secure
157
+ - Use environment variables for all sensitive information
jira_integration.py ADDED
@@ -0,0 +1,1635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import sys
4
+ import traceback
5
+ from datetime import datetime
6
+ import streamlit as st
7
+ from jira import JIRA
8
+ from dotenv import load_dotenv
9
+ from datetime import datetime, timedelta
10
+ import pandas as pd
11
+ import requests
12
+ import json
13
+ from groq import Groq
14
+ from difflib import SequenceMatcher
15
+ import time
16
+
17
+ # Configure logging based on environment
18
+ try:
19
+ # Try to create logs directory and file
20
+ log_dir = "logs"
21
+ if not os.path.exists(log_dir):
22
+ os.makedirs(log_dir)
23
+ log_file = os.path.join(log_dir, f"jira_debug_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
24
+
25
+ # Configure root logger with file handler
26
+ logging.basicConfig(
27
+ level=logging.DEBUG,
28
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
29
+ handlers=[
30
+ logging.FileHandler(log_file)
31
+ ]
32
+ )
33
+ except (OSError, IOError):
34
+ # If file logging fails (e.g., in Hugging Face Spaces), configure logging without file handler
35
+ logging.basicConfig(
36
+ level=logging.DEBUG,
37
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
38
+ handlers=[
39
+ logging.NullHandler()
40
+ ]
41
+ )
42
+
43
+ logger = logging.getLogger("jira_integration")
44
+ logger.info("Jira integration module loaded")
45
+
46
+ # Load environment variables
47
+ load_dotenv()
48
+
49
+ # Get API keys and configuration with default values for development
50
+ JIRA_SERVER = os.getenv("JIRA_SERVER")
51
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
52
+
53
+ # Validate required environment variables
54
+ if not JIRA_SERVER:
55
+ st.error("JIRA_SERVER not found in environment variables. Please check your .env file.")
56
+ if not GROQ_API_KEY:
57
+ st.error("GROQ_API_KEY not found in environment variables. Please check your .env file.")
58
+
59
+ def init_jira_session():
60
+ """Initialize Jira session state variables"""
61
+ if 'jira_client' not in st.session_state:
62
+ st.session_state.jira_client = None
63
+ if 'projects' not in st.session_state:
64
+ st.session_state.projects = None
65
+
66
+ def get_projects():
67
+ """Fetch all accessible projects"""
68
+ if not st.session_state.jira_client:
69
+ return None
70
+
71
+ try:
72
+ projects = st.session_state.jira_client.projects()
73
+ # Sort projects by key
74
+ return sorted(projects, key=lambda x: x.key)
75
+ except Exception as e:
76
+ st.error(f"Error fetching projects: {str(e)}")
77
+ return None
78
+
79
+ def get_board_configuration(board_id):
80
+ """Fetch board configuration including estimation field"""
81
+ if not st.session_state.jira_client:
82
+ return None
83
+
84
+ try:
85
+ url = f"{JIRA_SERVER}/rest/agile/1.0/board/{board_id}/configuration"
86
+ response = st.session_state.jira_client._session.get(url)
87
+ if response.status_code == 200:
88
+ config = response.json()
89
+ return config
90
+ return None
91
+ except Exception as e:
92
+ st.error(f"Error fetching board configuration: {str(e)}")
93
+ return None
94
+
95
+ def get_boards(project_key):
96
+ """Fetch all boards for a project"""
97
+ if not st.session_state.jira_client:
98
+ return None
99
+
100
+ try:
101
+ boards = st.session_state.jira_client.boards(projectKeyOrID=project_key)
102
+ board_details = []
103
+ for board in boards:
104
+ config = get_board_configuration(board.id)
105
+ board_type = config.get('type', 'Unknown') if config else 'Unknown'
106
+ estimation_field = None
107
+ if config and 'estimation' in config:
108
+ estimation_field = config['estimation'].get('field', {}).get('fieldId')
109
+ board_details.append({
110
+ 'id': board.id,
111
+ 'name': board.name,
112
+ 'type': board_type,
113
+ 'estimation_field': estimation_field
114
+ })
115
+ return board_details
116
+ except Exception as e:
117
+ st.error(f"Error fetching boards: {str(e)}")
118
+ return None
119
+
120
+ def get_current_sprint(board_id):
121
+ """Fetch the current sprint for a board"""
122
+ if not st.session_state.jira_client:
123
+ return None
124
+
125
+ try:
126
+ # Get all active and future sprints
127
+ sprints = st.session_state.jira_client.sprints(board_id, state='active,future')
128
+ if sprints:
129
+ # Look for sprints starting with 'RS'
130
+ rs_sprints = [sprint for sprint in sprints if sprint.name.startswith('RS')]
131
+ if rs_sprints:
132
+ # Sort sprints by name to get the latest one
133
+ latest_sprint = sorted(rs_sprints, key=lambda x: x.name, reverse=True)[0]
134
+ return latest_sprint
135
+ else:
136
+ st.warning("No RS sprints found. Available sprints: " + ", ".join([s.name for s in sprints]))
137
+ return None
138
+ except Exception as e:
139
+ if "board does not support sprints" in str(e).lower():
140
+ return None
141
+ st.error(f"Error fetching current sprint: {str(e)}")
142
+ return None
143
+
144
+ def get_board_issues(board_id, estimation_field=None):
145
+ """Fetch all issues on the board using Agile REST API"""
146
+ if not st.session_state.jira_client:
147
+ return None
148
+
149
+ try:
150
+ url = f"{JIRA_SERVER}/rest/agile/1.0/board/{board_id}/issue"
151
+ fields = ['summary', 'status', 'created', 'description', 'issuetype', 'assignee']
152
+
153
+ if estimation_field:
154
+ fields.append(estimation_field)
155
+
156
+ params = {
157
+ 'maxResults': 200,
158
+ 'fields': fields,
159
+ 'jql': f'assignee = currentUser()'
160
+ }
161
+
162
+ response = st.session_state.jira_client._session.get(url, params=params)
163
+ if response.status_code != 200:
164
+ st.error(f"Error fetching board issues: {response.text}")
165
+ return None
166
+
167
+ data = response.json()
168
+ issues = []
169
+ for issue_data in data['issues']:
170
+ issue = st.session_state.jira_client.issue(issue_data['key'])
171
+ issues.append(issue)
172
+ return issues
173
+ except Exception as e:
174
+ st.error(f"Error fetching board issues: {str(e)}")
175
+ return None
176
+
177
+ def get_sprint_issues(board_id, sprint_id, estimation_field=None):
178
+ """Fetch all issues in the current sprint using Agile REST API"""
179
+ if not st.session_state.jira_client:
180
+ return None
181
+
182
+ try:
183
+ # Cache key for sprint issues
184
+ cache_key = f"sprint_issues_{sprint_id}"
185
+ if cache_key in st.session_state and (datetime.now() - st.session_state.get('last_refresh', datetime.min)).total_seconds() < 60:
186
+ return st.session_state[cache_key]
187
+
188
+ # Use the Agile REST API endpoint to get issues from the sprint
189
+ url = f"{JIRA_SERVER}/rest/agile/1.0/board/{board_id}/sprint/{sprint_id}/issue"
190
+ params = {
191
+ 'maxResults': 200,
192
+ 'fields': [
193
+ 'summary',
194
+ 'status',
195
+ 'created',
196
+ 'description',
197
+ 'issuetype',
198
+ 'assignee'
199
+ ],
200
+ 'jql': 'assignee = currentUser()' # Filter for current user's issues
201
+ }
202
+
203
+ # Add estimation field if provided
204
+ if estimation_field:
205
+ params['fields'].append(estimation_field)
206
+
207
+ # Make a single API call with all required fields
208
+ response = st.session_state.jira_client._session.get(url, params=params)
209
+ if response.status_code != 200:
210
+ st.error(f"Error fetching sprint issues: {response.text}")
211
+ return None
212
+
213
+ # Process all issues in one go
214
+ data = response.json()
215
+ issues = [st.session_state.jira_client.issue(issue['key']) for issue in data['issues']]
216
+
217
+ # Cache the results
218
+ st.session_state[cache_key] = issues
219
+ st.session_state['last_refresh'] = datetime.now()
220
+
221
+ return issues
222
+ except Exception as e:
223
+ st.error(f"Error fetching sprint issues: {str(e)}")
224
+ return None
225
+
226
+ def calculate_points(issues, estimation_field):
227
+ """Calculate story points from issues"""
228
+ if not estimation_field:
229
+ return [], 0, 0, 0
230
+
231
+ try:
232
+ # Process all issues at once
233
+ field_id = estimation_field.replace('customfield_', '')
234
+ issues_data = []
235
+ total_points = completed_points = in_progress_points = 0
236
+
237
+ # Create status mappings for faster lookup
238
+ done_statuses = {'done', 'completed', 'closed'}
239
+ progress_statuses = {'in progress', 'development', 'in development'}
240
+
241
+ for issue in issues:
242
+ try:
243
+ # Get points value efficiently
244
+ points = getattr(issue.fields, field_id, None) or getattr(issue.fields, estimation_field, 0)
245
+ points = float(points) if points is not None else 0
246
+
247
+ # Get status efficiently
248
+ status_name = issue.fields.status.name.lower()
249
+
250
+ # Update points
251
+ total_points += points
252
+ if status_name in done_statuses:
253
+ completed_points += points
254
+ elif status_name in progress_statuses:
255
+ in_progress_points += points
256
+
257
+ # Build issue data
258
+ issues_data.append({
259
+ "Key": issue.key,
260
+ "Type": issue.fields.issuetype.name,
261
+ "Summary": issue.fields.summary,
262
+ "Status": issue.fields.status.name,
263
+ "Story Points": points,
264
+ "Assignee": issue.fields.assignee.displayName if issue.fields.assignee else "Unassigned"
265
+ })
266
+ except Exception as e:
267
+ st.error(f"Error processing points for issue {issue.key}: {str(e)}")
268
+ continue
269
+
270
+ return issues_data, total_points, completed_points, in_progress_points
271
+ except Exception as e:
272
+ st.error(f"Error calculating points: {str(e)}")
273
+ return [], 0, 0, 0
274
+
275
+ def create_maintenance_task(project_key, summary, description, issue_type='Task'):
276
+ """Create a task in Jira"""
277
+ if not st.session_state.jira_client:
278
+ st.error("Not authenticated with Jira. Please log in first.")
279
+ return None
280
+
281
+ try:
282
+ issue_dict = {
283
+ 'project': {'key': project_key},
284
+ 'summary': summary,
285
+ 'description': description,
286
+ 'issuetype': {'name': issue_type}
287
+ }
288
+
289
+ new_issue = st.session_state.jira_client.create_issue(fields=issue_dict)
290
+ return new_issue
291
+ except Exception as e:
292
+ st.error(f"Error creating task: {str(e)}")
293
+ return None
294
+
295
+ def render_jira_login():
296
+ """Render the Jira login form and handle authentication"""
297
+ # If already authenticated, just return True
298
+ if 'jira_client' in st.session_state and st.session_state.jira_client:
299
+ st.success("Connected to Jira")
300
+ return True
301
+
302
+ # Initialize session state for login form and attempts tracking
303
+ if 'jira_username' not in st.session_state:
304
+ st.session_state.jira_username = ""
305
+ if 'jira_password' not in st.session_state:
306
+ st.session_state.jira_password = ""
307
+ if 'login_blocked_until' not in st.session_state:
308
+ st.session_state.login_blocked_until = None
309
+
310
+ # Check if login is temporarily blocked
311
+ if st.session_state.login_blocked_until:
312
+ if datetime.now() < st.session_state.login_blocked_until:
313
+ wait_time = (st.session_state.login_blocked_until - datetime.now()).seconds
314
+ st.error(f"Login temporarily blocked due to too many failed attempts. Please wait {wait_time} seconds before trying again.")
315
+ st.info("If you need immediate access, please try logging in directly to Jira in your browser first, complete the CAPTCHA there, then return here.")
316
+ return False
317
+ else:
318
+ st.session_state.login_blocked_until = None
319
+
320
+ # Create login form
321
+ with st.form(key="jira_login_form"):
322
+ username = st.text_input("Jira Username", value=st.session_state.jira_username, key="username_input")
323
+ password = st.text_input("Password", value=st.session_state.jira_password, type="password", key="password_input")
324
+ submit_button = st.form_submit_button(label="Login")
325
+
326
+ if submit_button:
327
+ # Store credentials in session state
328
+ st.session_state.jira_username = username
329
+ st.session_state.jira_password = password
330
+
331
+ # Try to authenticate
332
+ try:
333
+ jira_client = JIRA(server=JIRA_SERVER, basic_auth=(username, password))
334
+ st.session_state.jira_client = jira_client
335
+
336
+ # Get projects
337
+ projects = get_projects()
338
+ if projects:
339
+ st.session_state.projects = projects
340
+ st.success("Connected to Jira")
341
+ return True
342
+ else:
343
+ st.error("Failed to fetch projects")
344
+ return False
345
+ except Exception as e:
346
+ error_message = str(e).lower()
347
+ if "captcha_challenge" in error_message:
348
+ # Set a 5-minute block on login attempts
349
+ st.session_state.login_blocked_until = datetime.now() + timedelta(minutes=5)
350
+ st.error("Too many failed login attempts. Please try one of the following:")
351
+ st.info("""
352
+ 1. Wait 5 minutes before trying again
353
+ 2. Log in to Jira in your browser first, complete the CAPTCHA there, then return here
354
+ 3. Clear your browser cookies and try again
355
+ """)
356
+ else:
357
+ st.error(f"Authentication failed: {str(e)}")
358
+ return False
359
+
360
+ return False
361
+
362
+ def get_cached_metadata(project_key):
363
+ """Get cached metadata or fetch new if cache is expired or doesn't exist"""
364
+ # Check if metadata cache exists in session state
365
+ if 'metadata_cache' not in st.session_state:
366
+ st.session_state.metadata_cache = {}
367
+ if 'metadata_cache_timestamp' not in st.session_state:
368
+ st.session_state.metadata_cache_timestamp = {}
369
+
370
+ current_time = datetime.now()
371
+ cache_expiry = timedelta(minutes=30) # Cache expires after 30 minutes
372
+
373
+ # Check if we have valid cached metadata
374
+ if (project_key in st.session_state.metadata_cache and
375
+ project_key in st.session_state.metadata_cache_timestamp and
376
+ current_time - st.session_state.metadata_cache_timestamp[project_key] < cache_expiry):
377
+ logger.info(f"Using cached metadata for project {project_key}")
378
+ return st.session_state.metadata_cache[project_key]
379
+
380
+ # If no valid cache, fetch new metadata
381
+ logger.info(f"Fetching fresh metadata for project {project_key}")
382
+ metadata = get_project_metadata_fresh(project_key)
383
+
384
+ if metadata:
385
+ # Update cache
386
+ st.session_state.metadata_cache[project_key] = metadata
387
+ st.session_state.metadata_cache_timestamp[project_key] = current_time
388
+ logger.info(f"Updated metadata cache for project {project_key}")
389
+
390
+ return metadata
391
+
392
+ def get_project_metadata_fresh(project_key):
393
+ """Fetch fresh metadata from Jira without using cache"""
394
+ logger.info(f"=== Getting fresh metadata for project {project_key} ===")
395
+
396
+ if not st.session_state.jira_client:
397
+ logger.error("Not authenticated with Jira. Please log in first.")
398
+ st.error("Not authenticated with Jira. Please log in first.")
399
+ return None
400
+
401
+ try:
402
+ # Get project
403
+ logger.info("Getting project...")
404
+ project = st.session_state.jira_client.project(project_key)
405
+ logger.info(f"Got project: {project.name}")
406
+
407
+ logger.info("Getting createmeta with expanded fields...")
408
+ # Get create metadata for the project with expanded field info
409
+ metadata = st.session_state.jira_client.createmeta(
410
+ projectKeys=project_key,
411
+ expand='projects.issuetypes.fields',
412
+ issuetypeNames='Story' # Specifically get Story type fields
413
+ )
414
+ logger.info("Got createmeta response")
415
+
416
+ if not metadata.get('projects'):
417
+ logger.error(f"No metadata found for project {project_key}")
418
+ st.error(f"No metadata found for project {project_key}")
419
+ return None
420
+
421
+ project_meta = metadata['projects'][0]
422
+ issue_types = project_meta.get('issuetypes', [])
423
+
424
+ # Log available issue types
425
+ logger.info(f"Available issue types: {[t.get('name') for t in issue_types]}")
426
+
427
+ # Try to get Story issue type first
428
+ story_type = next((t for t in issue_types if t['name'] == 'Story'), None)
429
+ if not story_type:
430
+ logger.error("Story issue type not found in project")
431
+ st.error("Story issue type not found in project")
432
+ return None
433
+
434
+ logger.info("Processing fields...")
435
+ # Get required fields and all fields
436
+ required_fields = {}
437
+ all_fields = {}
438
+
439
+ # Log all available fields before processing
440
+ logger.info("Available fields in Story type:")
441
+ for field_id, field in story_type['fields'].items():
442
+ field_name = field.get('name', 'Unknown')
443
+ field_type = field.get('schema', {}).get('type', 'Unknown')
444
+ logger.info(f"Field: {field_name} (ID: {field_id}, Type: {field_type})")
445
+
446
+ # Store complete field information including schema and allowed values
447
+ all_fields[field_id] = {
448
+ 'name': field['name'],
449
+ 'required': field.get('required', False),
450
+ 'schema': field.get('schema', {}),
451
+ 'allowedValues': field.get('allowedValues', []),
452
+ 'hasDefaultValue': field.get('hasDefaultValue', False),
453
+ 'defaultValue': field.get('defaultValue'),
454
+ 'operations': field.get('operations', []),
455
+ 'configuration': field.get('configuration', {})
456
+ }
457
+
458
+ # If this is a cascading select field, log its structure
459
+ if field.get('schema', {}).get('type') == 'option-with-child':
460
+ logger.info(f"Found cascading select field: {field_name}")
461
+ if 'allowedValues' in field:
462
+ for parent in field['allowedValues']:
463
+ parent_value = parent.get('value', 'Unknown')
464
+ logger.info(f" Parent value: {parent_value}")
465
+ if 'cascadingOptions' in parent:
466
+ child_values = [child.get('value') for child in parent['cascadingOptions']]
467
+ logger.info(f" Child values: {child_values}")
468
+
469
+ # Store required fields separately
470
+ if field.get('required', False):
471
+ required_fields[field_id] = all_fields[field_id]
472
+ logger.info(f"Required field: {field_name}")
473
+
474
+ logger.info(f"Found {len(all_fields)} total fields, {len(required_fields)} required fields")
475
+
476
+ metadata_result = {
477
+ 'project_name': project.name,
478
+ 'issue_type': 'Story',
479
+ 'required_fields': required_fields,
480
+ 'all_fields': all_fields
481
+ }
482
+
483
+ logger.info("Successfully processed project metadata")
484
+ return metadata_result
485
+
486
+ except Exception as e:
487
+ logger.exception(f"Error getting project metadata: {str(e)}")
488
+ st.error(f"Error getting project metadata: {str(e)}")
489
+ st.error("Full error details:")
490
+ st.error(str(e))
491
+ st.code(traceback.format_exc(), language="python")
492
+ return None
493
+
494
+ def get_project_metadata(project_key):
495
+ """Get project metadata, using cache if available"""
496
+ return get_cached_metadata(project_key)
497
+
498
+ def generate_task_content(filtered_scenarios_df):
499
+ """Generate task summary and description using template-based approach"""
500
+ try:
501
+ # Extract key information
502
+ environment = filtered_scenarios_df['Environment'].iloc[0]
503
+ functional_area = filtered_scenarios_df['Functional area'].iloc[0]
504
+ scenario_count = len(filtered_scenarios_df)
505
+
506
+ # Generate summary
507
+ summary = f"Maintenance: {environment} - {functional_area}"
508
+
509
+ # Generate description
510
+ description = "Performing maintenance on the following scenarios failing :\n\n"
511
+
512
+ # Add each scenario and its error, using enumerate to start from 1
513
+ for i, (_, row) in enumerate(filtered_scenarios_df.iterrows(), 1):
514
+ description += f"{i}. {row['Scenario Name']}\n"
515
+ description += f" Error: {row['Error Message']}\n\n"
516
+
517
+ return summary, description
518
+ except Exception as e:
519
+ st.error(f"Error generating task content: {str(e)}")
520
+ return None, None
521
+
522
+ def get_regression_board(project_key):
523
+ """Find the regression sprint board for the project"""
524
+ boards = get_boards(project_key)
525
+ if not boards:
526
+ return None
527
+
528
+ # Look specifically for the "Regression Sprints" board
529
+ regression_board = next((b for b in boards if b['name'].lower() == 'regression sprints' and b['type'].lower() == 'scrum'), None)
530
+
531
+ if not regression_board:
532
+ st.error("Could not find the 'Regression Sprints' board. Available boards: " +
533
+ ", ".join([f"{b['name']} ({b['type']})" for b in boards]))
534
+
535
+ return regression_board
536
+
537
+ def get_field_dependencies():
538
+ """Cache and return field dependencies and their allowed values"""
539
+ if 'field_dependencies' not in st.session_state:
540
+ try:
541
+ # Get project metadata for RS project
542
+ metadata = get_project_metadata("RS")
543
+ if not metadata:
544
+ return None
545
+
546
+ # Initialize dependencies dictionary with correct field IDs
547
+ dependencies = {
548
+ 'Customer': {
549
+ 'field_id': 'customfield_10427',
550
+ 'values': [],
551
+ 'dependencies': {}
552
+ },
553
+ 'Environment': {
554
+ 'field_id': 'customfield_14157', # Updated field ID
555
+ 'values': [],
556
+ 'dependencies': {}
557
+ },
558
+ 'Functional Areas': {
559
+ 'field_id': 'customfield_15303', # Updated field ID
560
+ 'values': [],
561
+ 'dependencies': {}
562
+ }
563
+ }
564
+
565
+ # Get field values and their dependencies
566
+ for field_name, field_info in dependencies.items():
567
+ field_id = field_info['field_id']
568
+ if field_id in metadata['all_fields']:
569
+ field_data = metadata['all_fields'][field_id]
570
+ if 'allowedValues' in field_data:
571
+ # Store allowed values
572
+ dependencies[field_name]['values'] = [
573
+ value.get('value', value.get('name', ''))
574
+ for value in field_data['allowedValues']
575
+ if isinstance(value, dict)
576
+ ]
577
+
578
+ # Store dependencies (if any)
579
+ if 'dependency' in field_data:
580
+ dep_field = field_data['dependency']
581
+ dependencies[field_name]['dependencies'] = {
582
+ 'field': dep_field['field']['name'],
583
+ 'field_id': dep_field['field']['id'],
584
+ 'values': dep_field.get('values', [])
585
+ }
586
+
587
+ # Cache the dependencies
588
+ st.session_state.field_dependencies = dependencies
589
+ return dependencies
590
+ except Exception as e:
591
+ st.error(f"Error fetching field dependencies: {str(e)}")
592
+ return None
593
+
594
+ return st.session_state.field_dependencies
595
+
596
+ def get_dependent_field_value(field_name, parent_value=None):
597
+ """Get the appropriate field value based on dependencies"""
598
+ dependencies = get_field_dependencies()
599
+ if not dependencies or field_name not in dependencies:
600
+ return None
601
+
602
+ field_info = dependencies[field_name]
603
+
604
+ # If this field depends on another field
605
+ if parent_value and field_info['dependencies']:
606
+ dep_info = field_info['dependencies']
607
+ # Find values that match the parent value
608
+ for value_mapping in dep_info.get('values', []):
609
+ if value_mapping.get('parent') == parent_value:
610
+ return value_mapping.get('value')
611
+
612
+ # If no dependency or no match, return first available value
613
+ return field_info['values'][0] if field_info['values'] else None
614
+
615
+ def display_project_fields():
616
+ """Display available fields for issue creation"""
617
+ project_key = "RS" # Using the fixed project key
618
+ metadata = get_project_metadata(project_key)
619
+
620
+ if metadata:
621
+ st.subheader("Project Fields")
622
+
623
+ # Display required fields
624
+ st.write("### Required Fields")
625
+ for field_id, field in metadata['required_fields'].items():
626
+ st.write(f"- {field['name']} ({field_id})")
627
+ if field.get('allowedValues'):
628
+ st.write(" Allowed values:")
629
+ for value in field['allowedValues']:
630
+ if isinstance(value, dict):
631
+ # Handle cascading select fields
632
+ if 'cascadingOptions' in value:
633
+ parent_value = value.get('value', 'Unknown')
634
+ st.write(f" - {parent_value}")
635
+ st.write(" Child options:")
636
+ for child in value['cascadingOptions']:
637
+ st.write(f" - {child.get('value', 'Unknown')}")
638
+ else:
639
+ st.write(f" - {value.get('value', value.get('name', 'Unknown'))}")
640
+ else:
641
+ st.write(f" - {value}")
642
+
643
+ # Display custom fields with dependencies
644
+ st.write("### Custom Fields and Dependencies")
645
+
646
+ # Customer field (customfield_10427) - Cascading Select
647
+ st.write("\n#### Customer (customfield_10427)")
648
+ cust_field = metadata['all_fields'].get('customfield_10427', {})
649
+ if cust_field.get('allowedValues'):
650
+ st.write("Cascading options:")
651
+ for value in cust_field['allowedValues']:
652
+ if isinstance(value, dict):
653
+ parent_value = value.get('value', 'Unknown')
654
+ st.write(f"- {parent_value}")
655
+ if 'cascadingOptions' in value:
656
+ st.write(" Child options:")
657
+ for child in value['cascadingOptions']:
658
+ st.write(f" - {child.get('value', 'Unknown')}")
659
+
660
+ # Functional Areas field (customfield_13100) - Cascading Select
661
+ st.write("\n#### Functional Areas (customfield_13100)")
662
+ func_field = metadata['all_fields'].get('customfield_13100', {})
663
+ if func_field.get('allowedValues'):
664
+ st.write("Cascading options:")
665
+ for value in func_field['allowedValues']:
666
+ if isinstance(value, dict):
667
+ parent_value = value.get('value', 'Unknown')
668
+ st.write(f"- {parent_value}")
669
+ if 'cascadingOptions' in value:
670
+ st.write(" Child options:")
671
+ for child in value['cascadingOptions']:
672
+ st.write(f" - {child.get('value', 'Unknown')}")
673
+
674
+ # Environment field (customfield_14157)
675
+ st.write("\n#### Environment (customfield_14157)")
676
+ env_field = metadata['all_fields'].get('customfield_14157', {})
677
+ if env_field.get('allowedValues'):
678
+ st.write("Allowed values:")
679
+ for value in env_field['allowedValues']:
680
+ if isinstance(value, dict):
681
+ st.write(f" - {value.get('value', value.get('name', 'Unknown'))}")
682
+
683
+ # Display dependencies
684
+ if any(field.get('dependency') for field in [env_field, func_field, cust_field]):
685
+ st.write("\n### Field Dependencies")
686
+ for field_name, field in [
687
+ ('Environment', env_field),
688
+ ('Functional Areas', func_field),
689
+ ('Customer', cust_field)
690
+ ]:
691
+ if field.get('dependency'):
692
+ st.write(f"\n{field_name} depends on:")
693
+ dep = field['dependency']
694
+ st.write(f" Field: {dep['field']['name']} ({dep['field']['id']})")
695
+ if dep.get('values'):
696
+ st.write(" Value mappings:")
697
+ for mapping in dep['values']:
698
+ parent_value = mapping.get('parent', 'Unknown')
699
+ child_value = mapping.get('value', 'Unknown')
700
+ st.write(f" - When parent is '{parent_value}' → '{child_value}'")
701
+
702
+ # Display other custom fields
703
+ st.write("\n### Other Custom Fields")
704
+ excluded_fields = ['customfield_14157', 'customfield_13100', 'customfield_10427']
705
+ custom_fields = {k: v for k, v in metadata['all_fields'].items()
706
+ if k.startswith('customfield_') and k not in excluded_fields}
707
+
708
+ for field_id, field in custom_fields.items():
709
+ st.write(f"- {field['name']} ({field_id})")
710
+ if field.get('allowedValues'):
711
+ st.write(" Allowed values:")
712
+ for value in field.get('allowedValues', []):
713
+ if isinstance(value, dict):
714
+ if 'cascadingOptions' in value:
715
+ parent_value = value.get('value', 'Unknown')
716
+ st.write(f" - {parent_value}")
717
+ st.write(" Child options:")
718
+ for child in value['cascadingOptions']:
719
+ st.write(f" - {child.get('value', 'Unknown')}")
720
+ else:
721
+ st.write(f" - {value.get('value', value.get('name', 'Unknown'))}")
722
+ else:
723
+ st.write(f" - {value}")
724
+
725
+ def get_closest_match(target, choices, threshold=60):
726
+ """
727
+ Find the closest matching string from choices using fuzzy matching.
728
+ Returns the best match if similarity is above threshold, otherwise None.
729
+ """
730
+ if not choices:
731
+ return None
732
+
733
+ try:
734
+ def similarity(a, b):
735
+ # Normalize strings for comparison
736
+ a = a.lower().replace('-', ' ').replace(' ', ' ').strip()
737
+ b = b.lower().replace('-', ' ').replace(' ', ' ').strip()
738
+ return SequenceMatcher(None, a, b).ratio() * 100
739
+
740
+ # Calculate similarities
741
+ similarities = [(choice, similarity(target, choice)) for choice in choices]
742
+ # Sort by similarity score
743
+ best_match = max(similarities, key=lambda x: x[1])
744
+
745
+ if best_match[1] >= threshold:
746
+ return best_match[0]
747
+ return None
748
+ except Exception as e:
749
+ st.error(f"Error in fuzzy matching: {str(e)}")
750
+ return None
751
+
752
+ def get_functional_area_values(metadata):
753
+ """Extract all available functional area values from metadata"""
754
+ logger.info("=== Starting get_functional_area_values ===")
755
+
756
+ if not metadata:
757
+ logger.error("No metadata provided")
758
+ return []
759
+
760
+ if 'all_fields' not in metadata:
761
+ logger.error("No 'all_fields' in metadata")
762
+ logger.debug(f"Available metadata keys: {list(metadata.keys())}")
763
+ return []
764
+
765
+ # Log all available field IDs for debugging
766
+ logger.info("Available fields:")
767
+ for field_id, field in metadata['all_fields'].items():
768
+ field_name = field.get('name', 'Unknown')
769
+ field_type = field.get('schema', {}).get('type', 'Unknown')
770
+ logger.info(f" {field_name} (ID: {field_id}, Type: {field_type})")
771
+
772
+ # List of possible field IDs for functional areas
773
+ functional_area_field_ids = [
774
+ 'customfield_15303', # New field ID
775
+ 'customfield_13100', # Old field ID
776
+ 'customfield_13101' # Another possible variation
777
+ ]
778
+
779
+ # Try to find the functional area field by name or ID
780
+ func_field = None
781
+ for field_id, field in metadata['all_fields'].items():
782
+ field_name = field.get('name', '').lower()
783
+ if field_id in functional_area_field_ids or 'functional area' in field_name:
784
+ func_field = field
785
+ logger.info(f"Found functional area field: {field.get('name')} (ID: {field_id})")
786
+ break
787
+
788
+ if not func_field:
789
+ logger.error("Could not find functional area field in metadata")
790
+ logger.info("Available field names:")
791
+ for field_id, field in metadata['all_fields'].items():
792
+ logger.info(f" {field.get('name', 'Unknown')} ({field_id})")
793
+ return []
794
+
795
+ # Check field type
796
+ field_type = func_field.get('schema', {}).get('type')
797
+ logger.info(f"Functional area field type: {field_type}")
798
+
799
+ allowed_values = []
800
+
801
+ if 'allowedValues' in func_field:
802
+ logger.info("Processing allowed values...")
803
+ for parent in func_field['allowedValues']:
804
+ if isinstance(parent, dict):
805
+ parent_value = parent.get('value', 'Unknown')
806
+ logger.info(f"Processing parent value: {parent_value}")
807
+
808
+ if 'cascadingOptions' in parent:
809
+ for child in parent['cascadingOptions']:
810
+ if isinstance(child, dict) and 'value' in child:
811
+ allowed_values.append(child['value'])
812
+ logger.debug(f"Added child value: {child['value']}")
813
+ elif 'value' in parent:
814
+ allowed_values.append(parent['value'])
815
+ logger.debug(f"Added value: {parent['value']}")
816
+
817
+ logger.info(f"Found {len(allowed_values)} allowed values")
818
+ if allowed_values:
819
+ logger.info(f"Sample of allowed values: {allowed_values[:5]}")
820
+ else:
821
+ logger.warning("No allowed values found in the field")
822
+
823
+ return allowed_values
824
+
825
+ def calculate_story_points(scenario_count):
826
+ """Calculate story points based on number of scenarios"""
827
+ if scenario_count <= 3:
828
+ return 1
829
+ elif scenario_count <= 5:
830
+ return 2
831
+ elif scenario_count <= 9:
832
+ return 3
833
+ elif scenario_count <= 15:
834
+ return 5
835
+ else:
836
+ return 8
837
+
838
+ def map_functional_area(functional_area, metadata):
839
+ """Map a functional area to its closest Jira allowed parent and child values using structured mapping."""
840
+ if not metadata or not functional_area:
841
+ logger.error("No metadata or functional area provided")
842
+ raise ValueError("Metadata and functional area are required")
843
+
844
+ # Get the functional area field from metadata
845
+ func_field = metadata['all_fields'].get('customfield_13100', {})
846
+ if not func_field or 'allowedValues' not in func_field:
847
+ logger.error("Could not find functional area field in metadata")
848
+ raise ValueError("Functional area field not found in metadata")
849
+
850
+ # Build a set of allowed child values for faster lookup
851
+ allowed_values = {}
852
+ for parent in func_field['allowedValues']:
853
+ if isinstance(parent, dict):
854
+ parent_value = parent.get('value')
855
+ if parent_value and 'children' in parent:
856
+ for child in parent['children']:
857
+ if isinstance(child, dict) and 'value' in child:
858
+ allowed_values[child['value']] = parent_value
859
+
860
+ logger.info(f"Input functional area: {functional_area}")
861
+
862
+ # Split the functional area into parts
863
+ parts = [p.strip() for p in functional_area.split(' - ')]
864
+ logger.info(f"Split into parts: {parts}")
865
+
866
+ # Try different combinations of parts joined with '-'
867
+ for i in range(len(parts)):
868
+ for j in range(i + 1, len(parts) + 1):
869
+ # Try joining parts with '-'
870
+ test_value = '-'.join(parts[i:j])
871
+ # Also try without spaces
872
+ test_value_no_spaces = test_value.replace(' ', '')
873
+
874
+ logger.info(f"Trying combination: {test_value}")
875
+
876
+ # Check both versions (with and without spaces)
877
+ if test_value in allowed_values:
878
+ logger.info(f"Found exact match: {test_value}")
879
+ return allowed_values[test_value], test_value
880
+ elif test_value_no_spaces in allowed_values:
881
+ logger.info(f"Found match without spaces: {test_value_no_spaces}")
882
+ return allowed_values[test_value_no_spaces], test_value_no_spaces
883
+
884
+ # Try category-specific matches
885
+ categories = ['Services', 'FIN', 'WARPSPEED']
886
+ for category in categories:
887
+ category_value = f"{category}-{test_value}"
888
+ category_value_no_spaces = category_value.replace(' ', '')
889
+
890
+ if category_value in allowed_values:
891
+ logger.info(f"Found category match: {category_value}")
892
+ return allowed_values[category_value], category_value
893
+ elif category_value_no_spaces in allowed_values:
894
+ logger.info(f"Found category match without spaces: {category_value_no_spaces}")
895
+ return allowed_values[category_value_no_spaces], category_value_no_spaces
896
+
897
+ # If no match found, try to find a suitable default based on the first part
898
+ first_part = parts[0].upper()
899
+ if 'SERVICE' in first_part or 'SERVICES' in first_part:
900
+ logger.info("No exact match found, defaulting to Services-Platform")
901
+ return "R&I", "Services-Platform"
902
+ elif 'FIN' in first_part:
903
+ logger.info("No exact match found, defaulting to FIN-Parameters")
904
+ return "R&I", "FIN-Parameters"
905
+ elif 'WARPSPEED' in first_part:
906
+ logger.info("No exact match found, defaulting to WARPSPEED-Parameters")
907
+ return "R&I", "WARPSPEED-Parameters"
908
+
909
+ # Final fallback to Data Exchange
910
+ logger.warning(f"No suitable match found for '{functional_area}', defaulting to Data Exchange")
911
+ return "R&I", "Data Exchange"
912
+
913
+ def get_customer_field_values(metadata):
914
+ """Extract all available customer field values and their child options from metadata"""
915
+ if not metadata or 'all_fields' not in metadata:
916
+ return {}
917
+
918
+ customer_field = metadata['all_fields'].get('customfield_10427', {})
919
+ customer_values = {}
920
+
921
+ if 'allowedValues' in customer_field:
922
+ for parent in customer_field['allowedValues']:
923
+ if isinstance(parent, dict):
924
+ parent_value = parent.get('value')
925
+ if parent_value:
926
+ child_values = []
927
+ if 'cascadingOptions' in parent:
928
+ child_values = [child.get('value') for child in parent['cascadingOptions'] if child.get('value')]
929
+ customer_values[parent_value] = child_values
930
+
931
+ return customer_values
932
+
933
+ def map_customer_value(environment_value, customer_values):
934
+ """Map environment value to appropriate customer field values"""
935
+ if not environment_value or not customer_values:
936
+ return "MIP Research and Innovation", "R&I General"
937
+
938
+ # Clean up environment value
939
+ env_value = environment_value.strip()
940
+
941
+ # Special case handling for specific environments
942
+ if any(env in env_value.lower() for env in ['legalwise', 'scorpion', 'lifewise', 'talksure']):
943
+ parent_value = "ILR"
944
+ child_value = env_value # Use the original environment value as child
945
+ logger.info(f"Mapped {env_value} to ILR parent with child {child_value}")
946
+ return parent_value, child_value
947
+
948
+ # Handle RI environments
949
+ if env_value.startswith('RI'):
950
+ parent_value = "MIP Research and Innovation"
951
+ # Remove 'RI' prefix and clean up
952
+ child_value = env_value[2:].strip()
953
+ if child_value:
954
+ child_value = f"R&I {child_value}"
955
+ else:
956
+ child_value = "R&I General"
957
+ logger.info(f"Mapped RI environment {env_value} to {parent_value} parent with child {child_value}")
958
+ return parent_value, child_value
959
+
960
+ # Default case - try to find matching values
961
+ for parent, children in customer_values.items():
962
+ if parent == "MIP Research and Innovation": # Default parent
963
+ # Look for exact match in child values
964
+ if env_value in children:
965
+ return parent, env_value
966
+ # Look for partial matches
967
+ for child in children:
968
+ if env_value in child or child in env_value:
969
+ return parent, child
970
+
971
+ # If no match found, return defaults
972
+ logger.warning(f"No specific mapping found for {env_value}, using defaults")
973
+ return "MIP Research and Innovation", "R&I General"
974
+
975
+ def create_regression_task(project_key, summary, description, environment, filtered_scenarios_df):
976
+ logger.debug(f"Entering create_regression_task with project_key={project_key}, summary={summary}, environment={environment}, DF_shape={filtered_scenarios_df.shape}")
977
+ logger.info("=== Starting create_regression_task function ===")
978
+ logger.info(f"Project: {project_key}, Summary: {summary}, Environment: {environment}")
979
+ logger.info(f"Filtered DF shape: {filtered_scenarios_df.shape if filtered_scenarios_df is not None else 'None'}")
980
+
981
+ try:
982
+ # Get metadata first to access field values
983
+ metadata = get_project_metadata(project_key)
984
+ if not metadata:
985
+ error_msg = "Could not get project metadata"
986
+ logger.error(error_msg)
987
+ st.error(error_msg)
988
+ return None
989
+
990
+ # Get customer field values and map environment
991
+ customer_values = get_customer_field_values(metadata)
992
+ parent_value, child_value = map_customer_value(environment, customer_values)
993
+ logger.info(f"Mapped customer values - Parent: {parent_value}, Child: {child_value}")
994
+
995
+ # Get Jira client
996
+ if "jira_client" not in st.session_state:
997
+ error_msg = "No Jira client available. Please connect to Jira first."
998
+ logger.error(error_msg)
999
+ return None
1000
+
1001
+ jira_client = st.session_state.jira_client
1002
+ logger.info("Got Jira client from session state")
1003
+
1004
+ # Get active sprint
1005
+ active_sprint = get_current_sprint(get_regression_board(project_key)['id'])
1006
+ if not active_sprint:
1007
+ error_msg = "No active sprint found"
1008
+ logger.error(error_msg)
1009
+ return None
1010
+
1011
+ logger.info(f"Found active sprint: {active_sprint.name} (ID: {active_sprint.id})")
1012
+
1013
+ # Extract functional area from filtered scenarios
1014
+ functional_areas = []
1015
+ try:
1016
+ if "Functional area" in filtered_scenarios_df.columns:
1017
+ functional_areas = filtered_scenarios_df["Functional area"].unique().tolist()
1018
+ logger.info(f"Extracted functional areas: {functional_areas}")
1019
+ except Exception as e:
1020
+ logger.exception(f"Error extracting functional areas: {str(e)}")
1021
+ st.error(f"Error extracting functional areas: {str(e)}")
1022
+ return None
1023
+
1024
+ # Calculate story points based on number of scenarios
1025
+ story_points = calculate_story_points(len(filtered_scenarios_df))
1026
+ logger.info(f"Calculated story points: {story_points}")
1027
+
1028
+ # Map functional area using metadata
1029
+ functional_area_parent, functional_area_child = map_functional_area(
1030
+ functional_areas[0] if functional_areas else "Data Exchange",
1031
+ metadata
1032
+ )
1033
+ logger.info(f"Mapped functional area to parent: {functional_area_parent}, child: {functional_area_child}")
1034
+
1035
+ # Prepare issue dictionary with all required fields
1036
+ issue_dict = {
1037
+ "project": {"key": project_key},
1038
+ "summary": summary,
1039
+ "description": description,
1040
+ "issuetype": {"name": "Story"},
1041
+ "components": [{"name": "Maintenance (Regression)"}],
1042
+ "customfield_10427": {
1043
+ "value": parent_value,
1044
+ "child": {
1045
+ "value": child_value
1046
+ }
1047
+ },
1048
+ "customfield_12730": {"value": "Non-Business Critical"}, # Regression Type field
1049
+ "customfield_13430": {"value": str(len(filtered_scenarios_df))}, # Number of Scenarios
1050
+ "customfield_13100": {
1051
+ "value": functional_area_parent,
1052
+ "child": {
1053
+ "value": functional_area_child
1054
+ }
1055
+ },
1056
+ "assignee": {"name": st.session_state.jira_username},
1057
+ "customfield_10002": story_points # Story Points field
1058
+ }
1059
+
1060
+ # Log the complete issue dictionary
1061
+ logger.info(f"Issue dictionary prepared: {issue_dict}")
1062
+
1063
+ # Create the issue
1064
+ logger.info("Attempting to create issue in Jira...")
1065
+
1066
+ try:
1067
+ # Create the issue with all fields
1068
+ new_issue = jira_client.create_issue(fields=issue_dict)
1069
+ logger.info(f"Issue created successfully: {new_issue.key}")
1070
+
1071
+ # Add issue to sprint
1072
+ try:
1073
+ logger.info(f"Attempting to add issue {new_issue.key} to sprint {active_sprint.id}...")
1074
+ jira_client.add_issues_to_sprint(active_sprint.id, [new_issue.key])
1075
+ logger.info(f"Added issue {new_issue.key} to sprint {active_sprint.name}")
1076
+ except Exception as sprint_error:
1077
+ logger.exception(f"Failed to add issue to sprint: {str(sprint_error)}")
1078
+ st.warning(f"⚠️ Could not add task to sprint. Error: {str(sprint_error)}")
1079
+
1080
+ # Display success message
1081
+ st.success(f"✅ Task created successfully: {new_issue.key}")
1082
+ return new_issue
1083
+
1084
+ except Exception as create_error:
1085
+ error_message = str(create_error)
1086
+ logger.exception(f"Failed to create issue: {error_message}")
1087
+
1088
+ # Try to extract the response content if it's a JIRA Error
1089
+ try:
1090
+ if hasattr(create_error, 'response'):
1091
+ status_code = getattr(create_error.response, 'status_code', 'N/A')
1092
+ logger.error(f"Response status code: {status_code}")
1093
+
1094
+ if hasattr(create_error.response, 'text'):
1095
+ response_text = create_error.response.text
1096
+ logger.error(f"Response text: {response_text}")
1097
+
1098
+ # Display the error to the user
1099
+ st.error(f"❌ Error creating task in Jira (Status: {status_code}):")
1100
+ st.error(response_text)
1101
+ except Exception as extract_error:
1102
+ logger.exception(f"Error extracting response details: {str(extract_error)}")
1103
+ return None
1104
+ except Exception as e:
1105
+ error_message = f"❌ Unexpected error in create_regression_task: {str(e)}"
1106
+ logger.exception(error_message)
1107
+ st.error(error_message)
1108
+ logger.error(f"Traceback: {''.join(traceback.format_exception(type(e), e, e.__traceback__))}")
1109
+ return None
1110
+
1111
+ def create_test_data():
1112
+ """Create test data for development/testing that matches the filtered scenarios from multiple.py"""
1113
+ test_data = {
1114
+ 'Environment': ['RI2008'] * 7, # Same environment for all scenarios
1115
+ 'Functional area': ['Data Exchange - Enquiries - Reports'] * 7,
1116
+ 'Scenario Name': [
1117
+ 'Add Missions Error Handling - Existing Code',
1118
+ 'Add Missions Error Handling - Incorrect Max Iterations',
1119
+ 'Add Missions Error Handling - No Badges',
1120
+ 'Add Missions Success - Individual',
1121
+ 'Add Missions Success - Team',
1122
+ 'Add Missions Success - Individual Iteration',
1123
+ 'Add Missions Success - Team Max Iterations'
1124
+ ],
1125
+ 'Error Message': [
1126
+ 'AssertionError [ERR_ASSERTION]: Error handling for existing code failed',
1127
+ 'AssertionError [ERR_ASSERTION]: Error handling for max iterations failed',
1128
+ 'AssertionError [ERR_ASSERTION]: Error handling for missing badges failed',
1129
+ 'AssertionError [ERR_ASSERTION]: Link validation failed',
1130
+ 'AssertionError [ERR_ASSERTION]: Link validation failed',
1131
+ 'AssertionError [ERR_ASSERTION]: Link validation failed',
1132
+ 'AssertionError [ERR_ASSERTION]: Link validation failed'
1133
+ ],
1134
+ 'Status': ['FAILED'] * 7,
1135
+ 'Time spent(m:s)': ['02:30'] * 7, # Example time spent
1136
+ 'Start datetime': [datetime.now()] * 7 # Current time as example
1137
+ }
1138
+
1139
+ # Create DataFrame
1140
+ df = pd.DataFrame(test_data)
1141
+
1142
+ # Add metadata that will be used for Jira task creation
1143
+ df.attrs['metadata'] = {
1144
+ 'Customer': 'MIP Research and Innovation - R&I 2008',
1145
+ 'Sprint': 'RS Sprint 195',
1146
+ 'Story Points': 5,
1147
+ 'Regression Type': 'Non-Business Critical',
1148
+ 'Component': 'Maintenance (Regression)',
1149
+ 'Priority': 'Lowest',
1150
+ 'Type': 'Story',
1151
+ 'Labels': 'None',
1152
+ 'Assignee': 'Daniel Akinsola',
1153
+ 'Reporter': 'Daniel Akinsola'
1154
+ }
1155
+
1156
+ return df
1157
+
1158
+ def process_failures_button(filtered_scenarios_df, environment=None):
1159
+ """Process failures and create Jira task"""
1160
+ # Use RS project key since we can see it's a Regression Sprint board
1161
+ project_key = "RS"
1162
+ project_name = "RS - Regression"
1163
+
1164
+ # Get environment from DataFrame if not provided
1165
+ if environment is None and 'Environment' in filtered_scenarios_df.columns:
1166
+ environment = filtered_scenarios_df['Environment'].iloc[0]
1167
+
1168
+ # Get unique functional areas from the DataFrame
1169
+ functional_areas = filtered_scenarios_df['Functional area'].unique()
1170
+ functional_area = functional_areas[0] if len(functional_areas) > 0 else 'R&I'
1171
+
1172
+ # Extract the main service category
1173
+ service_category = functional_area.split(' - ')[0] + '-' + functional_area.split(' - ')[1]
1174
+ service_category = service_category.replace(' ', '')
1175
+
1176
+ # Format environment value
1177
+ env_number = environment[2:] if environment.startswith('RI') else environment
1178
+ env_value = env_number if env_number.startswith('R&I') else f"R&I {env_number}"
1179
+
1180
+ # Get the current sprint from the regression board
1181
+ board = get_regression_board(project_key)
1182
+ sprint = None
1183
+ sprint_name = "No Active Sprint"
1184
+ if board:
1185
+ sprint = get_current_sprint(board['id'])
1186
+ if sprint:
1187
+ sprint_name = sprint.name
1188
+ st.write(f"Found active sprint: {sprint_name}")
1189
+ else:
1190
+ st.warning("No active sprint found")
1191
+
1192
+ # Create metadata dictionary with all required fields
1193
+ metadata = {
1194
+ 'Project Key': project_key,
1195
+ 'Project': project_name,
1196
+ 'Issue Type': 'Story',
1197
+ 'Customer': 'MIP Research and Innovation',
1198
+ 'Environment': env_value,
1199
+ 'Functional Areas': service_category,
1200
+ 'Sprint': sprint_name,
1201
+ 'Story Points': calculate_story_points(len(filtered_scenarios_df)),
1202
+ 'Regression Type': 'Non-Business Critical',
1203
+ 'Number of Scenarios': len(filtered_scenarios_df) if len(filtered_scenarios_df) <= 50 else 50
1204
+ }
1205
+
1206
+ # Initialize session states if not exists
1207
+ if 'task_content' not in st.session_state:
1208
+ st.session_state.task_content = None
1209
+ if 'task_created' not in st.session_state:
1210
+ st.session_state.task_created = False
1211
+ if 'created_task' not in st.session_state:
1212
+ st.session_state.created_task = None
1213
+ if 'show_success' not in st.session_state:
1214
+ st.session_state.show_success = False
1215
+ if 'last_task_key' not in st.session_state:
1216
+ st.session_state.last_task_key = None
1217
+ if 'last_task_url' not in st.session_state:
1218
+ st.session_state.last_task_url = None
1219
+
1220
+ # Store sprint information in session state for task creation
1221
+ if sprint:
1222
+ st.session_state.current_sprint = sprint
1223
+
1224
+ # If we have a recently created task, show the success message first
1225
+ if st.session_state.show_success and st.session_state.last_task_key:
1226
+ st.success(f"✅ Task created successfully!")
1227
+
1228
+ # Display task link in a more prominent way
1229
+ st.markdown(
1230
+ f"""
1231
+ <div style='padding: 10px; border-radius: 5px; border: 1px solid #90EE90; margin: 10px 0;'>
1232
+ <h3 style='margin: 0; color: #90EE90;'>Task Details</h3>
1233
+ <p style='margin: 10px 0;'>Task Key: {st.session_state.last_task_key}</p>
1234
+ <a href='{st.session_state.last_task_url}' target='_blank'
1235
+ style='background-color: #90EE90; color: black; padding: 5px 10px;
1236
+ border-radius: 3px; text-decoration: none; display: inline-block;'>
1237
+ View Task in Jira
1238
+ </a>
1239
+ </div>
1240
+ """,
1241
+ unsafe_allow_html=True
1242
+ )
1243
+
1244
+ # Add a button to create another task
1245
+ if st.button("Create Another Task", key="create_another"):
1246
+ # Clear all task-related state
1247
+ st.session_state.task_content = None
1248
+ st.session_state.task_created = False
1249
+ st.session_state.created_task = None
1250
+ st.session_state.show_success = False
1251
+ st.session_state.last_task_key = None
1252
+ st.session_state.last_task_url = None
1253
+ st.rerun()
1254
+ return
1255
+
1256
+ # Button to generate content
1257
+ if st.button("Generate Task Content"):
1258
+ with st.spinner("Generating task content..."):
1259
+ summary, description = generate_task_content(filtered_scenarios_df)
1260
+ if summary and description:
1261
+ st.session_state.task_content = {
1262
+ 'summary': summary,
1263
+ 'description': description,
1264
+ 'environment': environment,
1265
+ 'metadata': metadata
1266
+ }
1267
+ else:
1268
+ st.error("Failed to generate task content. Please try again.")
1269
+ return
1270
+
1271
+ # Display content and create task button if content exists
1272
+ if st.session_state.task_content:
1273
+ with st.expander("Generated Task Content", expanded=True):
1274
+ # Summary section with styling
1275
+ st.markdown("### Summary")
1276
+ st.markdown(f"""
1277
+ <div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0; color: #0f1629;'>
1278
+ {st.session_state.task_content['summary']}
1279
+ </div>
1280
+ """, unsafe_allow_html=True)
1281
+
1282
+ # Description section with styling
1283
+ st.markdown("### Description")
1284
+ st.markdown(f"""
1285
+ <div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0; color: #0f1629; white-space: pre-wrap;'>
1286
+ {st.session_state.task_content['description']}
1287
+ </div>
1288
+ """, unsafe_allow_html=True)
1289
+
1290
+ # Get and display available functional area values
1291
+ display_functional_areas(st.session_state.task_content['metadata'])
1292
+
1293
+ # Display metadata with actual field values
1294
+ st.markdown("### Fields to be Set")
1295
+ metadata = st.session_state.task_content['metadata']
1296
+ metadata_html = f"""
1297
+ <div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0; color: #0f1629;'>
1298
+ <p><strong>Project:</strong> {metadata['Project']}</p>
1299
+ <p><strong>Issue Type:</strong> {metadata['Issue Type']}</p>
1300
+ <p><strong>Customer:</strong> {metadata['Customer']}</p>
1301
+ <p><strong>Environment:</strong> {metadata['Environment']}</p>
1302
+ <p><strong>Functional Areas:</strong> {metadata['Functional Areas']}</p>
1303
+ <p><strong>Sprint:</strong> {metadata['Sprint']}</p>
1304
+ <p><strong>Story Points:</strong> {metadata['Story Points']}</p>
1305
+ <p><strong>Regression Type:</strong> {metadata['Regression Type']}</p>
1306
+ <p><strong>Number of Scenarios:</strong> {metadata['Number of Scenarios']}</p>
1307
+ </div>
1308
+ """
1309
+ st.markdown(metadata_html, unsafe_allow_html=True)
1310
+
1311
+ # Add buttons in columns for better layout
1312
+ col1, col2 = st.columns(2)
1313
+ with col1:
1314
+ if st.button("🔄 Regenerate Content", key="regenerate"):
1315
+ st.session_state.task_content = None
1316
+ st.rerun()
1317
+
1318
+ with col2:
1319
+ if st.button("📝 Create Jira Task", key="create"):
1320
+ task = create_regression_task(
1321
+ metadata['Project Key'],
1322
+ st.session_state.task_content['summary'],
1323
+ st.session_state.task_content['description'],
1324
+ st.session_state.task_content['environment'],
1325
+ filtered_scenarios_df
1326
+ )
1327
+
1328
+ if task:
1329
+ # Store task information in session state
1330
+ st.session_state.last_task_key = task.key
1331
+ st.session_state.last_task_url = f"{JIRA_SERVER}/browse/{task.key}"
1332
+ st.session_state.show_success = True
1333
+ # Clear the content
1334
+ st.session_state.task_content = None
1335
+ # Force refresh of sprint stats on next load
1336
+ st.session_state.force_sprint_refresh = True
1337
+ st.rerun()
1338
+ else:
1339
+ st.error("Failed to create task. Please try again.")
1340
+
1341
+ def display_functional_areas(metadata):
1342
+ """Display functional areas and customer fields in a tree-like structure with styling"""
1343
+ if not metadata:
1344
+ st.error("No metadata available")
1345
+ return
1346
+
1347
+ # If this is task metadata (not project metadata), get project metadata first
1348
+ if 'all_fields' not in metadata:
1349
+ project_metadata = get_project_metadata("RS") # RS is the fixed project key
1350
+ if not project_metadata:
1351
+ st.error("Could not fetch project metadata")
1352
+ return
1353
+ metadata = project_metadata
1354
+
1355
+ # Display Functional Areas
1356
+ func_field = metadata['all_fields'].get('customfield_13100', {})
1357
+ if func_field and 'allowedValues' in func_field:
1358
+ st.markdown("### Available Functional Areas")
1359
+
1360
+ # Log the raw allowedValues for debugging
1361
+ logger.info("=== Raw Functional Area Values ===")
1362
+ for value in func_field['allowedValues']:
1363
+ logger.info(f"Raw value: {value}")
1364
+
1365
+ # Create a dictionary to store parent-child relationships
1366
+ parent_child_map = {}
1367
+
1368
+ # First pass: collect all parent-child relationships
1369
+ for value in func_field['allowedValues']:
1370
+ if isinstance(value, dict):
1371
+ parent_value = value.get('value', 'Unknown')
1372
+ if parent_value:
1373
+ child_values = []
1374
+ logger.info(f"\nProcessing parent: {parent_value}")
1375
+
1376
+ if 'cascadingOptions' in value:
1377
+ logger.info(f"Found cascading options for {parent_value}:")
1378
+ for child in value['cascadingOptions']:
1379
+ logger.info(f"Raw child value: {child}")
1380
+ if isinstance(child, dict) and child.get('value'):
1381
+ child_value = child.get('value')
1382
+ child_values.append(child_value)
1383
+ logger.info(f" - Added child: {child_value}")
1384
+
1385
+ parent_child_map[parent_value] = sorted(child_values) if child_values else []
1386
+ logger.info(f"Final children for {parent_value}: {parent_child_map[parent_value]}")
1387
+
1388
+ # Second pass: display the relationships
1389
+ for parent_value in sorted(parent_child_map.keys()):
1390
+ child_values = parent_child_map[parent_value]
1391
+
1392
+ # Create a styled box for each parent and its children
1393
+ st.markdown(f"""
1394
+ <div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0; color: #0f1629; margin-bottom: 10px;'>
1395
+ <strong>{parent_value}</strong>
1396
+ {"<ul style='margin-bottom: 0; margin-top: 5px;'>" if child_values else ""}
1397
+ """, unsafe_allow_html=True)
1398
+
1399
+ # Display child values if they exist
1400
+ for child in child_values:
1401
+ st.markdown(f"<li>{child}</li>", unsafe_allow_html=True)
1402
+
1403
+ if child_values:
1404
+ st.markdown("</ul>", unsafe_allow_html=True)
1405
+ st.markdown("</div>", unsafe_allow_html=True)
1406
+
1407
+ # Log the parent-child relationship for debugging
1408
+ logger.info(f"Displaying Parent: {parent_value}")
1409
+ if child_values:
1410
+ logger.info(f" With Children: {', '.join(child_values)}")
1411
+ else:
1412
+ logger.info(" No children found")
1413
+ else:
1414
+ st.warning("No functional area values found in metadata")
1415
+ logger.warning("No functional area values found in metadata")
1416
+ if func_field:
1417
+ logger.info("Available func_field keys: " + str(list(func_field.keys())))
1418
+
1419
+ # Display Customer Field
1420
+ cust_field = metadata['all_fields'].get('customfield_10427', {})
1421
+ if cust_field and 'allowedValues' in cust_field:
1422
+ st.markdown("### Available Customer Values")
1423
+
1424
+ # Log the raw allowedValues for debugging
1425
+ logger.info("=== Raw Customer Field Values ===")
1426
+ for value in cust_field['allowedValues']:
1427
+ logger.info(f"Raw value: {value}")
1428
+
1429
+ # Create a dictionary to store parent-child relationships for customer field
1430
+ customer_parent_child_map = {}
1431
+
1432
+ # First pass: collect all parent-child relationships
1433
+ for value in cust_field['allowedValues']:
1434
+ if isinstance(value, dict):
1435
+ parent_value = value.get('value', 'Unknown')
1436
+ if parent_value:
1437
+ child_values = []
1438
+ logger.info(f"\nProcessing customer parent: {parent_value}")
1439
+
1440
+ if 'cascadingOptions' in value:
1441
+ logger.info(f"Found customer cascading options for {parent_value}:")
1442
+ for child in value['cascadingOptions']:
1443
+ logger.info(f"Raw child value: {child}")
1444
+ if isinstance(child, dict) and child.get('value'):
1445
+ child_value = child.get('value')
1446
+ child_values.append(child_value)
1447
+ logger.info(f" - Added child: {child_value}")
1448
+
1449
+ customer_parent_child_map[parent_value] = sorted(child_values) if child_values else []
1450
+ logger.info(f"Final customer children for {parent_value}: {customer_parent_child_map[parent_value]}")
1451
+
1452
+ # Second pass: display the relationships
1453
+ for parent_value in sorted(customer_parent_child_map.keys()):
1454
+ child_values = customer_parent_child_map[parent_value]
1455
+
1456
+ # Create a styled box for each parent and its children
1457
+ st.markdown(f"""
1458
+ <div style='background-color: #f0f2f6; padding: 10px; border-radius: 5px; border: 1px solid #e0e0e0; color: #0f1629; margin-bottom: 10px;'>
1459
+ <strong>{parent_value}</strong>
1460
+ {"<ul style='margin-bottom: 0; margin-top: 5px;'>" if child_values else ""}
1461
+ """, unsafe_allow_html=True)
1462
+
1463
+ # Display child values if they exist
1464
+ for child in child_values:
1465
+ st.markdown(f"<li>{child}</li>", unsafe_allow_html=True)
1466
+
1467
+ if child_values:
1468
+ st.markdown("</ul>", unsafe_allow_html=True)
1469
+ st.markdown("</div>", unsafe_allow_html=True)
1470
+
1471
+ # Log the parent-child relationship for debugging
1472
+ logger.info(f"Displaying Customer Parent: {parent_value}")
1473
+ if child_values:
1474
+ logger.info(f" With Children: {', '.join(child_values)}")
1475
+ else:
1476
+ logger.info(" No children found")
1477
+ else:
1478
+ st.warning("No customer field values found in metadata")
1479
+ logger.warning("No customer field values found in metadata")
1480
+ if cust_field:
1481
+ logger.info("Available cust_field keys: " + str(list(cust_field.keys())))
1482
+
1483
+ def display_story_points_stats(force_refresh=False):
1484
+ """Display story points statistics from current sprint"""
1485
+ if not st.session_state.jira_client:
1486
+ return
1487
+
1488
+ # Initialize session state for sprint data if not exists
1489
+ if 'sprint_data' not in st.session_state:
1490
+ st.session_state.sprint_data = None
1491
+
1492
+ # Initialize refresh timestamp if not exists
1493
+ if 'last_sprint_refresh' not in st.session_state:
1494
+ st.session_state.last_sprint_refresh = None
1495
+
1496
+ try:
1497
+ # Only fetch data if forced refresh, no data exists, or refresh timestamp is old
1498
+ current_time = datetime.now()
1499
+ refresh_needed = (
1500
+ force_refresh or
1501
+ st.session_state.sprint_data is None or
1502
+ (st.session_state.last_sprint_refresh and
1503
+ (current_time - st.session_state.last_sprint_refresh).total_seconds() > 300) # 5 minutes cache
1504
+ )
1505
+
1506
+ if refresh_needed:
1507
+ with st.spinner("Fetching sprint data..."):
1508
+ # Get regression board
1509
+ board = get_regression_board("RS")
1510
+ if not board:
1511
+ return
1512
+
1513
+ # Get current sprint
1514
+ sprint = get_current_sprint(board['id'])
1515
+ if not sprint:
1516
+ return
1517
+
1518
+ # Get sprint issues
1519
+ issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
1520
+ if not issues:
1521
+ return
1522
+
1523
+ # Calculate points
1524
+ issues_data, total_points, completed_points, in_progress_points = calculate_points(issues, board['estimation_field'])
1525
+
1526
+ # Store in session state
1527
+ st.session_state.sprint_data = {
1528
+ 'sprint_name': sprint.name,
1529
+ 'total_points': total_points,
1530
+ 'completed_points': completed_points,
1531
+ 'in_progress_points': in_progress_points,
1532
+ 'timestamp': current_time
1533
+ }
1534
+ st.session_state.last_sprint_refresh = current_time
1535
+
1536
+ # Display data from session state
1537
+ if st.session_state.sprint_data:
1538
+ sprint_data = st.session_state.sprint_data
1539
+
1540
+ # Create compact metrics display using custom HTML/CSS
1541
+ st.markdown(f"""
1542
+ <div style='background-color: #1E1E1E; padding: 10px; border-radius: 5px; margin-bottom: 10px;'>
1543
+ <div style='font-size: 0.8em; color: #E0E0E0; margin-bottom: 8px;'>Current Sprint: {sprint_data['sprint_name']}</div>
1544
+ <div style='display: grid; grid-template-columns: repeat(4, 1fr); gap: 5px; font-size: 0.9em;'>
1545
+ <div style='text-align: center;'>
1546
+ <div style='color: #E0E0E0;'>Total</div>
1547
+ <div style='font-size: 1.2em; font-weight: bold;'>{sprint_data['total_points']:.1f}</div>
1548
+ </div>
1549
+ <div style='text-align: center;'>
1550
+ <div style='color: #E0E0E0;'>Done</div>
1551
+ <div style='font-size: 1.2em; font-weight: bold;'>{sprint_data['completed_points']:.1f}</div>
1552
+ </div>
1553
+ <div style='text-align: center;'>
1554
+ <div style='color: #E0E0E0;'>In Progress</div>
1555
+ <div style='font-size: 1.2em; font-weight: bold;'>{sprint_data['in_progress_points']:.1f}</div>
1556
+ </div>
1557
+ <div style='text-align: center;'>
1558
+ <div style='color: #E0E0E0;'>Complete</div>
1559
+ <div style='font-size: 1.2em; font-weight: bold;'>{(sprint_data['completed_points'] / sprint_data['total_points'] * 100) if sprint_data['total_points'] > 0 else 0:.1f}%</div>
1560
+ </div>
1561
+ </div>
1562
+ </div>
1563
+ """, unsafe_allow_html=True)
1564
+
1565
+ # Show progress bar
1566
+ progress = sprint_data['completed_points'] / sprint_data['total_points'] if sprint_data['total_points'] > 0 else 0
1567
+ st.progress(progress)
1568
+
1569
+ # Add refresh button with key based on timestamp to prevent rerendering
1570
+ refresh_key = f"refresh_stats_{datetime.now().strftime('%Y%m%d%H%M%S')}"
1571
+ if st.button("🔄 Refresh", key=refresh_key, use_container_width=True):
1572
+ # Use a session state flag to trigger refresh on next rerun
1573
+ st.session_state.force_sprint_refresh = True
1574
+ st.rerun()
1575
+
1576
+ except Exception as e:
1577
+ st.error(f"Error updating story points: {str(e)}")
1578
+
1579
+ # Check if we need to force refresh (from button click)
1580
+ if 'force_sprint_refresh' in st.session_state and st.session_state.force_sprint_refresh:
1581
+ st.session_state.force_sprint_refresh = False
1582
+ return display_story_points_stats(force_refresh=True)
1583
+
1584
+ def main():
1585
+ st.title("Jira Integration Test")
1586
+
1587
+ # Add test data button
1588
+ if st.button("Load Test Data"):
1589
+ st.session_state.filtered_scenarios_df = create_test_data()
1590
+ st.success("Test data loaded!")
1591
+
1592
+ is_authenticated = render_jira_login()
1593
+
1594
+ if is_authenticated and st.session_state.projects:
1595
+ # Fixed project and board selection
1596
+ project_key = "RS"
1597
+ board_type = "scrum"
1598
+ board_name = "Regression Sprints"
1599
+
1600
+ # Display fixed selections in a more compact way
1601
+ st.markdown("""
1602
+ <div style='display: flex; gap: 10px; margin-bottom: 15px; font-size: 0.9em;'>
1603
+ <div style='flex: 1;'>
1604
+ <div style='color: #E0E0E0; margin-bottom: 4px;'>Project</div>
1605
+ <div style='background-color: #262730; padding: 5px 8px; border-radius: 4px; font-size: 0.9em;'>RS - Regression</div>
1606
+ </div>
1607
+ <div style='flex: 1;'>
1608
+ <div style='color: #E0E0E0; margin-bottom: 4px;'>Board</div>
1609
+ <div style='background-color: #262730; padding: 5px 8px; border-radius: 4px; font-size: 0.9em;'>Regression Sprints (scrum)</div>
1610
+ </div>
1611
+ </div>
1612
+ """, unsafe_allow_html=True)
1613
+
1614
+ # Display sprint stats (only fetch if no data exists)
1615
+ display_story_points_stats(force_refresh=False)
1616
+
1617
+ # Show test data if loaded
1618
+ if 'filtered_scenarios_df' in st.session_state:
1619
+ st.subheader("Failed Scenarios")
1620
+ st.dataframe(st.session_state.filtered_scenarios_df)
1621
+
1622
+ # Get environment directly from the DataFrame
1623
+ if 'Environment' in st.session_state.filtered_scenarios_df.columns:
1624
+ environment = st.session_state.filtered_scenarios_df['Environment'].iloc[0]
1625
+ st.info(f"Using environment from data: {environment}")
1626
+ process_failures_button(st.session_state.filtered_scenarios_df, environment)
1627
+ else:
1628
+ st.error("No environment information found in the data")
1629
+
1630
+ # Add project fields button at the bottom
1631
+ if st.button("Show Project Fields"):
1632
+ display_project_fields()
1633
+
1634
+ if __name__ == "__main__":
1635
+ main()
multiple.py CHANGED
@@ -3,6 +3,135 @@ import streamlit as st
3
  import matplotlib.pyplot as plt
4
  import numpy as np
5
  from pre import preprocess_uploaded_file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  # Define the function to perform analysis
8
  def perform_analysis(uploaded_dataframes):
@@ -36,12 +165,8 @@ def perform_analysis(uploaded_dataframes):
36
  selected_scenarios = None
37
 
38
  if selected_scenarios is not None:
39
- # st.write(f"Scenarios with status '{selected_status}' grouped by functional area:")
40
  st.markdown(f"### Scenarios with status '{selected_status}' grouped by functional area:")
41
 
42
- # # Display count of unique functional areas
43
- # st.write(f"Number of unique functional areas: {len(unique_areas) - 1}") # Subtract 1 for "All"
44
-
45
  # Select a range of functional areas to filter scenarios
46
  selected_functional_areas = st.multiselect("Select functional areas", unique_areas, ["All"])
47
 
@@ -80,37 +205,109 @@ def perform_analysis(uploaded_dataframes):
80
 
81
  # Filter scenarios based on selected functional area
82
  if selected_status == 'Failed':
 
 
 
 
 
 
 
 
 
 
83
  # Check if Failed Step column exists
84
  if 'Failed Step' in filtered_scenarios.columns:
85
- grouped_filtered_scenarios = filtered_scenarios.groupby('Environment')[['Functional area', 'Scenario Name', 'Error Message', 'Failed Step', 'Time spent(m:s)', 'Start datetime']].apply(lambda x: x.reset_index(drop=True))
86
  else:
87
- grouped_filtered_scenarios = filtered_scenarios.groupby('Environment')[['Functional area', 'Scenario Name', 'Error Message', 'Time spent(m:s)', 'Start datetime']].apply(lambda x: x.reset_index(drop=True))
 
88
  elif selected_status == 'Passed':
89
- grouped_filtered_scenarios = filtered_scenarios.groupby('Functional area')[['Scenario Name', 'Time spent(m:s)']].apply(lambda x: x.reset_index(drop=True))
 
 
 
 
 
90
  else:
91
  grouped_filtered_scenarios = None
92
- grouped_filtered_scenarios.reset_index(inplace=True)
93
 
94
- # Only drop 'level_1' if it exists in the DataFrame
95
- if 'level_1' in grouped_filtered_scenarios.columns:
96
- grouped_filtered_scenarios.drop(columns=['level_1'], inplace=True)
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- grouped_filtered_scenarios.index = grouped_filtered_scenarios.index + 1
99
- st.dataframe(grouped_filtered_scenarios)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- # Sort the average time spent table by start datetime
102
- average_time_spent_seconds = average_time_spent_seconds.sort_values(by='Start datetime')
103
-
104
- # # Display average time spent on each functional area in a table
105
- # st.markdown("### Total and Average Time Spent on Each Functional Area")
106
- # average_time_spent_seconds.index = average_time_spent_seconds.index + 1
107
- # # Rename the columns for clarity
108
- # average_time_spent_seconds.rename(columns={'Start datetime': 'Start Datetime', 'End datetime': 'End Datetime', 'Time spent':'Average Time Spent'}, inplace=True)
109
- # # Rearrange the columns
110
- # average_time_spent_seconds = average_time_spent_seconds[['Functional area', 'Total Time Spent', 'Start Datetime', 'End Datetime', 'Average Time Spent']]
111
- # st.dataframe(average_time_spent_seconds)
112
-
113
- # Check if selected_status is 'Failed' and grouped_filtered_scenarifos length is less than or equal to 400
114
  if selected_status != 'Passed':
115
  # Create and display bar graph of errors by functional area
116
  st.write(f"### Bar graph showing number of '{selected_status}' scenarios in each functional area:")
@@ -141,29 +338,227 @@ def perform_analysis(uploaded_dataframes):
141
  st.info(f"No '{selected_status}' scenarios found to display in the graph.")
142
  pass
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  def multiple_main():
 
 
 
 
 
 
 
 
 
 
 
145
 
146
- # num_environments = st.number_input("Enter the number of environments", min_value=1, value=1, step=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- # Initialize list to store uploaded dataframes
149
- uploaded_dataframes = []
 
 
 
 
 
 
 
 
 
150
 
151
- # Loop through the number of environments and create file uploaders
152
- # for i in range(num_environments):
153
- uploaded_files = st.file_uploader("Upload multiple XLSX files from different environments", type=["xlsx"], accept_multiple_files=True)
 
154
 
155
- for uploaded_file in uploaded_files:
156
- # Preprocess the uploaded file
157
- data = preprocess_uploaded_file(uploaded_file)
 
 
 
 
 
 
158
 
159
- # Append the dataframe to the list
160
- uploaded_dataframes.append(data)
 
161
 
162
- # Check if any files were uploaded
163
- if uploaded_dataframes:
164
  # Perform analysis for uploaded data
165
- perform_analysis(uploaded_dataframes)
 
 
 
 
 
166
  else:
167
  st.write("Please upload at least one file.")
168
 
169
- pass
 
 
 
3
  import matplotlib.pyplot as plt
4
  import numpy as np
5
  from pre import preprocess_uploaded_file
6
+ from jira_integration import (
7
+ render_jira_login,
8
+ get_current_sprint,
9
+ get_regression_board,
10
+ get_sprint_issues,
11
+ calculate_points,
12
+ create_regression_task,
13
+ generate_task_content,
14
+ calculate_story_points,
15
+ get_project_metadata,
16
+ get_field_dependencies,
17
+ get_dependent_field_value,
18
+ get_boards,
19
+ get_functional_area_values
20
+ )
21
+ from datetime import datetime, timedelta
22
+ import plotly.express as px
23
+ import plotly.graph_objects as go
24
+ import os
25
+ from dotenv import load_dotenv
26
+ import json
27
+ import logging
28
+ load_dotenv()
29
+ JIRA_SERVER = os.getenv("JIRA_SERVER")
30
+ # Initialize session state variables
31
+ if 'filtered_scenarios_df' not in st.session_state:
32
+ st.session_state.filtered_scenarios_df = None
33
+ if 'task_content' not in st.session_state:
34
+ st.session_state.task_content = None
35
+ if 'total_story_points' not in st.session_state:
36
+ st.session_state.total_story_points = 0
37
+ if 'completed_points' not in st.session_state:
38
+ st.session_state.completed_points = 0
39
+ if 'current_page' not in st.session_state:
40
+ st.session_state.current_page = "analysis"
41
+ if 'task_df' not in st.session_state:
42
+ st.session_state.task_df = None
43
+ if 'task_environment' not in st.session_state:
44
+ st.session_state.task_environment = None
45
+ if 'last_task_key' not in st.session_state:
46
+ st.session_state.last_task_key = None
47
+ if 'last_task_url' not in st.session_state:
48
+ st.session_state.last_task_url = None
49
+ if 'show_success' not in st.session_state:
50
+ st.session_state.show_success = False
51
+
52
+ # Get logger from jira_integration
53
+ logger = logging.getLogger("multiple")
54
+
55
+ # Function to capture button clicks with manual callback
56
+ def handle_task_button_click(summary, description, formatted_env, filtered_df):
57
+ logger.info("=== Task button clicked - Starting callback function ===")
58
+ try:
59
+ logger.info(f"Summary: {summary}")
60
+ logger.info(f"Description length: {len(description)}")
61
+ logger.info(f"Environment: {formatted_env}")
62
+ logger.info(f"DataFrame shape: {filtered_df.shape}")
63
+
64
+ # Import here to avoid circular imports
65
+ from jira_integration import create_regression_task
66
+
67
+ logger.info("Imported create_regression_task function")
68
+
69
+ # Call the actual function
70
+ with st.spinner("Creating task in Jira..."):
71
+ logger.info("About to call create_regression_task function")
72
+ task = create_regression_task(
73
+ project_key="RS",
74
+ summary=summary,
75
+ description=description,
76
+ environment=formatted_env,
77
+ filtered_scenarios_df=filtered_df
78
+ )
79
+
80
+ logger.info(f"create_regression_task returned: {task}")
81
+
82
+ if task:
83
+ logger.info(f"Task created successfully: {task.key}")
84
+ # Store task information in session state
85
+ st.session_state.last_task_key = task.key
86
+ st.session_state.last_task_url = f"{JIRA_SERVER}/browse/{task.key}"
87
+ st.session_state.show_success = True
88
+
89
+ # Display success message and task details
90
+ st.success("✅ Task created successfully!")
91
+ st.markdown(
92
+ f"""
93
+ <div style='padding: 10px; border-radius: 5px; border: 1px solid #90EE90; margin: 10px 0;'>
94
+ <h3 style='margin: 0; color: #90EE90;'>Task Details</h3>
95
+ <p style='margin: 10px 0;'>Task Key: {task.key}</p>
96
+ <a href='{JIRA_SERVER}/browse/{task.key}' target='_blank'
97
+ style='background-color: #90EE90; color: black; padding: 5px 10px;
98
+ border-radius: 3px; text-decoration: none; display: inline-block;'>
99
+ View Task in Jira
100
+ </a>
101
+ </div>
102
+ """,
103
+ unsafe_allow_html=True
104
+ )
105
+
106
+ # Clear task content
107
+ st.session_state.task_content = None
108
+
109
+ # Add button to create another task
110
+ if st.button("Create Another Task", key="create_another"):
111
+ # Clear all task-related state
112
+ st.session_state.task_content = None
113
+ st.session_state.last_task_key = None
114
+ st.session_state.last_task_url = None
115
+ st.session_state.show_success = False
116
+ st.rerun()
117
+
118
+ logger.info("Task creation process completed successfully")
119
+ return True
120
+ else:
121
+ logger.error("Task creation failed (returned None)")
122
+ st.error("❌ Task creation failed. Please check the error messages and try again.")
123
+ return False
124
+
125
+ except Exception as e:
126
+ logger.exception(f"Error in handle_task_button_click: {str(e)}")
127
+ st.error(f"❌ Error creating task: {str(e)}")
128
+ import traceback
129
+ error_trace = traceback.format_exc()
130
+ logger.error(f"Full traceback: {error_trace}")
131
+ st.error(error_trace)
132
+ return False
133
+ finally:
134
+ logger.info("=== Ending handle_task_button_click function ===")
135
 
136
  # Define the function to perform analysis
137
  def perform_analysis(uploaded_dataframes):
 
165
  selected_scenarios = None
166
 
167
  if selected_scenarios is not None:
 
168
  st.markdown(f"### Scenarios with status '{selected_status}' grouped by functional area:")
169
 
 
 
 
170
  # Select a range of functional areas to filter scenarios
171
  selected_functional_areas = st.multiselect("Select functional areas", unique_areas, ["All"])
172
 
 
205
 
206
  # Filter scenarios based on selected functional area
207
  if selected_status == 'Failed':
208
+ # Define columns in the exact order they appear in the table
209
+ columns_to_keep = [
210
+ 'Environment',
211
+ 'Functional area',
212
+ 'Scenario Name',
213
+ 'Error Message',
214
+ 'Failed Step',
215
+ 'Time spent(m:s)',
216
+ 'Start datetime'
217
+ ]
218
  # Check if Failed Step column exists
219
  if 'Failed Step' in filtered_scenarios.columns:
220
+ grouped_filtered_scenarios = filtered_scenarios[columns_to_keep].copy()
221
  else:
222
+ columns_to_keep.remove('Failed Step')
223
+ grouped_filtered_scenarios = filtered_scenarios[columns_to_keep].copy()
224
  elif selected_status == 'Passed':
225
+ grouped_filtered_scenarios = filtered_scenarios[[
226
+ 'Environment',
227
+ 'Functional area',
228
+ 'Scenario Name',
229
+ 'Time spent(m:s)'
230
+ ]].copy()
231
  else:
232
  grouped_filtered_scenarios = None
 
233
 
234
+ # Only proceed if we have data
235
+ if grouped_filtered_scenarios is not None:
236
+ # Reset the index to start from 1
237
+ grouped_filtered_scenarios.index = range(1, len(grouped_filtered_scenarios) + 1)
238
+ st.dataframe(grouped_filtered_scenarios)
239
+
240
+ # Show task creation button if:
241
+ # 1. User is authenticated
242
+ # 2. Status is Failed
243
+ # 3. Exactly one functional area is selected (not "All")
244
+ if ('jira_client' in st.session_state and
245
+ st.session_state.jira_client and
246
+ selected_status == 'Failed' and
247
+ len(selected_functional_areas) == 1 and
248
+ "All" not in selected_functional_areas):
249
 
250
+ # If we have a recently created task, show the success message first
251
+ if st.session_state.show_success and st.session_state.last_task_key:
252
+ st.success("✅ Task created successfully!")
253
+
254
+ # Display task link in a more prominent way
255
+ st.markdown(
256
+ f"""
257
+ <div style='padding: 10px; border-radius: 5px; border: 1px solid #90EE90; margin: 10px 0;'>
258
+ <h3 style='margin: 0; color: #90EE90;'>Task Details</h3>
259
+ <p style='margin: 10px 0;'>Task Key: {st.session_state.last_task_key}</p>
260
+ <a href='{st.session_state.last_task_url}' target='_blank'
261
+ style='background-color: #90EE90; color: black; padding: 5px 10px;
262
+ border-radius: 3px; text-decoration: none; display: inline-block;'>
263
+ View Task in Jira
264
+ </a>
265
+ </div>
266
+ """,
267
+ unsafe_allow_html=True
268
+ )
269
+
270
+ # Add a button to create another task
271
+ col1, col2, col3 = st.columns([1, 2, 1])
272
+ with col2:
273
+ if st.button("Create Another Task", key="create_another", use_container_width=True):
274
+ # Clear all task-related state
275
+ st.session_state.task_content = None
276
+ st.session_state.last_task_key = None
277
+ st.session_state.last_task_url = None
278
+ st.session_state.show_success = False
279
+ st.rerun()
280
+ else:
281
+ environment = filtered_scenarios['Environment'].iloc[0]
282
+ # Create columns for compact layout
283
+ col1, col2, col3 = st.columns([1, 2, 1])
284
+ with col2:
285
+ if st.button("📝 Log Jira Task", use_container_width=True):
286
+ st.write("Debug: Button clicked") # Debug line
287
+ # Use the properly structured DataFrame for task creation
288
+ task_df = grouped_filtered_scenarios.copy()
289
+ expected_columns = [
290
+ 'Environment',
291
+ 'Functional area',
292
+ 'Scenario Name',
293
+ 'Error Message',
294
+ 'Failed Step',
295
+ 'Time spent(m:s)',
296
+ 'Start datetime'
297
+ ]
298
+ missing_columns = [col for col in expected_columns if col not in task_df.columns]
299
+ if missing_columns:
300
+ st.error(f"Missing required columns: {', '.join(missing_columns)}")
301
+ st.error("Please ensure your data includes all required columns")
302
+ return
303
+
304
+ # Generate task content
305
+ summary, description = generate_task_content(task_df)
306
+ if summary and description:
307
+ # Call the task creation function
308
+ handle_task_button_click(summary, description, environment, task_df)
309
 
310
+ # Check if selected_status is 'Failed' and show bar graph
 
 
 
 
 
 
 
 
 
 
 
 
311
  if selected_status != 'Passed':
312
  # Create and display bar graph of errors by functional area
313
  st.write(f"### Bar graph showing number of '{selected_status}' scenarios in each functional area:")
 
338
  st.info(f"No '{selected_status}' scenarios found to display in the graph.")
339
  pass
340
 
341
+ def display_story_points_stats(force_refresh=False):
342
+ """Display story points statistics from current sprint"""
343
+ if not st.session_state.jira_client:
344
+ return
345
+
346
+ try:
347
+ with st.spinner("Fetching sprint data..."):
348
+ # Get regression board
349
+ board = get_regression_board("RS")
350
+ if not board:
351
+ return
352
+
353
+ # Get current sprint
354
+ sprint = get_current_sprint(board['id'])
355
+ if not sprint:
356
+ return
357
+
358
+ # Get sprint issues
359
+ issues = get_sprint_issues(board['id'], sprint.id, board['estimation_field'])
360
+ if not issues:
361
+ return
362
+
363
+ # Calculate points
364
+ issues_data, total_points, completed_points, in_progress_points = calculate_points(issues, board['estimation_field'])
365
+
366
+ # Update session state
367
+ st.session_state.total_story_points = total_points
368
+ st.session_state.completed_points = completed_points
369
+
370
+ # Create compact metrics display
371
+ metrics_container = st.container()
372
+ with metrics_container:
373
+ # Show sprint info
374
+ st.info(f"Current Sprint: {sprint.name}")
375
+
376
+ # Show metrics in a compact format
377
+ cols = st.columns(4)
378
+ with cols[0]:
379
+ st.metric("Total", f"{total_points:.1f}")
380
+ with cols[1]:
381
+ st.metric("Done", f"{completed_points:.1f}")
382
+ with cols[2]:
383
+ st.metric("In Progress", f"{in_progress_points:.1f}")
384
+ with cols[3]:
385
+ completion_rate = (completed_points / total_points * 100) if total_points > 0 else 0
386
+ st.metric("Complete", f"{completion_rate:.1f}%")
387
+
388
+ # Show progress bar
389
+ progress = completed_points / total_points if total_points > 0 else 0
390
+ st.progress(progress)
391
+
392
+ # Add refresh button
393
+ if st.button("🔄 Refresh", key="refresh_stats", use_container_width=True):
394
+ st.session_state.last_refresh = datetime.now()
395
+ return
396
+ except Exception as e:
397
+ st.error(f"Error updating story points: {str(e)}")
398
+
399
+ def show_task_creation_section(filtered_df, environment):
400
+ """Display the task creation section with detailed functional area mapping information."""
401
+
402
+ if "Functional area" in filtered_df.columns and len(filtered_df) > 0:
403
+ functional_areas = filtered_df["Functional area"].unique().tolist()
404
+ functional_area = functional_areas[0] if functional_areas else None
405
+ logger.debug(f"Found functional areas: {functional_areas}")
406
+
407
+ # Get project metadata to access allowed values
408
+ metadata = get_project_metadata("RS")
409
+ if metadata:
410
+ # Create expandable section for field structure
411
+ with st.expander("Functional Area Field Structure", expanded=False):
412
+ func_field = metadata['all_fields'].get('customfield_13100', {})
413
+ if func_field and 'allowedValues' in func_field:
414
+ st.write("Available parent-child mappings:")
415
+ for parent in func_field['allowedValues']:
416
+ if isinstance(parent, dict):
417
+ parent_value = parent.get('value', 'Unknown')
418
+ st.markdown(f"**Parent: {parent_value}**")
419
+ if 'cascadingOptions' in parent:
420
+ child_values = [child.get('value') for child in parent['cascadingOptions'] if child.get('value')]
421
+ st.write("Child options:")
422
+ for child in sorted(child_values):
423
+ st.write(f" • {child}")
424
+ st.write("")
425
+
426
+ # Display current functional area and mapping attempt
427
+ st.subheader("Functional Area Mapping")
428
+ col1, col2 = st.columns(2)
429
+
430
+ with col1:
431
+ st.markdown("**Input Functional Area:**")
432
+ st.info(functional_area)
433
+
434
+ st.markdown("**Split Parts:**")
435
+ parts = functional_area.split(' - ')
436
+ for i, part in enumerate(parts, 1):
437
+ st.write(f"{i}. {part}")
438
+
439
+ with col2:
440
+ # Try to map the functional area
441
+ parent, child = map_functional_area(functional_area, metadata)
442
+ st.markdown("**Mapped Values:**")
443
+ st.success(f"Parent: {parent}")
444
+ st.success(f"Child: {child}")
445
+
446
+ # Show normalized form
447
+ st.markdown("**Normalized Form:**")
448
+ norm_area = functional_area.lower().replace(' ', '-')
449
+ st.info(norm_area)
450
+
451
+ # Add warning if using default mapping
452
+ if parent == "R&I" and child == "Data Exchange" and functional_area.lower() != "data exchange":
453
+ st.warning("""
454
+ ⚠️ Using default mapping (R&I/Data Exchange). This might not be the best match.
455
+ Please check the 'Functional Area Field Structure' above for available values.
456
+ """)
457
+ else:
458
+ logger.warning("No functional area found in data")
459
+ st.warning("No functional area information found in the data")
460
+
461
+ # Create task button
462
+ if st.button("Create Task", key="create_task_button"):
463
+ handle_task_button_click(filtered_df, environment)
464
+
465
  def multiple_main():
466
+ # Initialize session state variables
467
+ if 'current_page' not in st.session_state:
468
+ st.session_state.current_page = "upload"
469
+ if 'task_df' not in st.session_state:
470
+ st.session_state.task_df = None
471
+ if 'selected_files' not in st.session_state:
472
+ st.session_state.selected_files = []
473
+ if 'uploaded_files' not in st.session_state:
474
+ st.session_state.uploaded_files = []
475
+ if 'filtered_scenarios_df' not in st.session_state:
476
+ st.session_state.filtered_scenarios_df = None
477
 
478
+ if 'jira_server' not in st.session_state:
479
+ st.session_state.jira_server = JIRA_SERVER
480
+ # Initialize session state for sprint data if not exists
481
+ if 'sprint_data_initialized' not in st.session_state:
482
+ st.session_state.sprint_data_initialized = False
483
+
484
+ # Add Jira login to sidebar (only once)
485
+ with st.sidebar:
486
+ st.subheader("Jira Integration (Optional)")
487
+
488
+ # Only render login if not already authenticated
489
+ if 'is_authenticated' not in st.session_state:
490
+ st.session_state.is_authenticated = render_jira_login()
491
+ else:
492
+ # Just display the status without re-rendering the login
493
+ if st.session_state.is_authenticated:
494
+ st.success("Connected to Jira")
495
+ else:
496
+ # Allow re-login if not authenticated
497
+ st.session_state.is_authenticated = render_jira_login()
498
+
499
+ # Only show story points in sidebar if authenticated
500
+ if st.session_state.is_authenticated and st.session_state.jira_client:
501
+ st.markdown("---")
502
+ st.subheader("Sprint Progress")
503
+
504
+ # Only fetch sprint data once or when refresh is clicked
505
+ if not st.session_state.sprint_data_initialized:
506
+ display_story_points_stats(force_refresh=True)
507
+ st.session_state.sprint_data_initialized = True
508
+ else:
509
+ display_story_points_stats(force_refresh=False)
510
+
511
+ st.title("Multiple File Analysis")
512
+
513
+ # Initialize session state for uploaded data
514
+ if 'uploaded_data' not in st.session_state:
515
+ st.session_state.uploaded_data = None
516
+ if 'last_refresh' not in st.session_state:
517
+ st.session_state.last_refresh = None
518
 
519
+ # Check if we're in task creation mode
520
+ if st.session_state.current_page == "create_task" and st.session_state.task_df is not None:
521
+ # Add a back button
522
+ if st.button("⬅️ Back to Analysis"):
523
+ st.session_state.current_page = "analysis"
524
+ st.rerun()
525
+ return
526
+
527
+ # Show task creation section
528
+ show_task_creation_section(st.session_state.task_df, st.session_state.task_environment)
529
+ return
530
 
531
+ # Main analysis page
532
+ uploaded_files = st.file_uploader("Upload CSV or Excel files",
533
+ type=['csv', 'xlsx'],
534
+ accept_multiple_files=True)
535
 
536
+ # Process uploaded files and store in session state
537
+ if uploaded_files:
538
+ all_data = []
539
+ for file in uploaded_files:
540
+ try:
541
+ df = preprocess_uploaded_file(file)
542
+ all_data.append(df)
543
+ except Exception as e:
544
+ st.error(f"Error processing {file.name}: {str(e)}")
545
 
546
+ if all_data:
547
+ # Store the processed data in session state
548
+ st.session_state.uploaded_data = all_data
549
 
550
+ # Use data from session state for analysis
551
+ if st.session_state.uploaded_data:
552
  # Perform analysis for uploaded data
553
+ perform_analysis(st.session_state.uploaded_data)
554
+
555
+ # Get combined data for Jira integration
556
+ combined_df = pd.concat(st.session_state.uploaded_data, ignore_index=True)
557
+
558
+
559
  else:
560
  st.write("Please upload at least one file.")
561
 
562
+ if __name__ == "__main__":
563
+ st.set_page_config(layout="wide")
564
+ multiple_main()
requirements.txt CHANGED
@@ -4,4 +4,8 @@ matplotlib>=3.0.0
4
  numpy>=1.20.0
5
  XlsxWriter==3.0.8
6
  plotly>=5.0.0
7
- openpyxl>=3.0.0
 
 
 
 
 
4
  numpy>=1.20.0
5
  XlsxWriter==3.0.8
6
  plotly>=5.0.0
7
+ openpyxl>=3.0.0
8
+ jira>=3.5.1
9
+ requests>=2.31.0
10
+ python-dotenv>=1.0.0
11
+ python-dateutil>=2.8.2