diff --git a/backend/main.py b/backend/main.py index bc2e666..95dbbed 100644 --- a/backend/main.py +++ b/backend/main.py @@ -37,20 +37,19 @@ async def validate_file(file: UploadFile): """ Validate the uploaded Excel file format """ - try: - if not file.filename or not file.filename.endswith(('.xls', '.xlsx')): - return FormValidation( - valid=False, - message="Invalid file format. Only .xls/.xlsx files are allowed.", - sheets=[], - form_metadata={}, - questions_count=0, - options_count=0 - ) + if not file.filename or not file.filename.endswith(('.xls', '.xlsx')): + return FormValidation( + valid=False, + message="Invalid file format. Only .xls/.xlsx files are allowed.", + sheets=[], + form_metadata={}, + questions_count=0, + options_count=0 + ) + try: parser = XLSFormParser() validation_result = await parser.validate_file(file) - return FormValidation(**validation_result) except Exception as e: logger.error(f"Error validating file: {str(e)}") @@ -90,10 +89,10 @@ async def get_form_by_id(form_id: str): form = await db_service.get_form_by_id(form_id) if not form: raise HTTPException(status_code=404, detail="Form not found") - + questions = await db_service.get_questions_by_form_id(form_id) options = await db_service.get_options_by_form_id(form_id) - + return { "form": form, "questions": questions, @@ -116,7 +115,7 @@ async def delete_form(form_id: str): success = await db_service.delete_form(form_id) if not success: raise HTTPException(status_code=404, detail="Form not found") - + return {"message": "Form deleted successfully"} except HTTPException: raise diff --git a/backend/services/database_service.py b/backend/services/database_service.py index c63b785..d9659ea 100644 --- a/backend/services/database_service.py +++ b/backend/services/database_service.py @@ -6,22 +6,22 @@ import logging logger = logging.getLogger(__name__) class DatabaseService: - + async def save_form(self, form_data: Dict[str, Any]) -> str: """Save form metadata to database""" try: if 'id' in form_data: del form_data['id'] - + form_data['_id'] = ObjectId() - + result = await forms_collection.insert_one(form_data) logger.info(f"Form saved with ID: {result.inserted_id}") return str(result.inserted_id) except Exception as e: logger.error(f"Error saving form: {e}") raise e - + async def save_questions(self, questions: List[Dict[str, Any]], form_id: str) -> List[str]: """Save questions to database""" try: @@ -29,16 +29,16 @@ class DatabaseService: for question in questions: question['form_id'] = form_id question['_id'] = ObjectId() - + result = await questions_collection.insert_one(question) question_ids.append(str(result.inserted_id)) - + logger.info(f"Saved {len(questions)} questions for form {form_id}") return question_ids except Exception as e: logger.error(f"Error saving questions: {e}") raise e - + async def save_options(self, options: List[Dict[str, Any]], form_id: str) -> List[str]: """Save answer options to database""" try: @@ -46,16 +46,16 @@ class DatabaseService: for option in options: option['form_id'] = form_id option['_id'] = ObjectId() - + result = await options_collection.insert_one(option) option_ids.append(str(result.inserted_id)) - + logger.info(f"Saved {len(options)} options for form {form_id}") return option_ids except Exception as e: logger.error(f"Error saving options: {e}") raise e - + async def get_form_by_id(self, form_id: str) -> Optional[Dict[str, Any]]: """Get form by ID""" try: @@ -67,7 +67,7 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting form: {e}") return None - + async def get_questions_by_form_id(self, form_id: str) -> List[Dict[str, Any]]: """Get all questions for a form""" try: @@ -79,7 +79,7 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting questions: {e}") return [] - + async def get_options_by_form_id(self, form_id: str) -> List[Dict[str, Any]]: """Get all options for a form""" try: @@ -91,7 +91,7 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting options: {e}") return [] - + async def get_all_forms(self) -> List[Dict[str, Any]]: """Get all forms""" try: @@ -103,16 +103,16 @@ class DatabaseService: except Exception as e: logger.error(f"Error getting all forms: {e}") return [] - + async def delete_form(self, form_id: str) -> bool: """Delete form and all related data""" try: form_result = await forms_collection.delete_one({"_id": ObjectId(form_id)}) - + questions_result = await questions_collection.delete_many({"form_id": form_id}) - + options_result = await options_collection.delete_many({"form_id": form_id}) - + logger.info(f"Deleted form {form_id} with {questions_result.deleted_count} questions and {options_result.deleted_count} options") return form_result.deleted_count > 0 except Exception as e: diff --git a/backend/services/xlsform_parser.py b/backend/services/xlsform_parser.py index 766d898..72fbd6d 100644 --- a/backend/services/xlsform_parser.py +++ b/backend/services/xlsform_parser.py @@ -20,17 +20,17 @@ class XLSFormParser: async def validate_file(self, file: UploadFile) -> Dict[str, Any]: try: df_dict = pd.read_excel(file.file, sheet_name=None) - + sheets_validation = [] form_metadata = {} questions_count = 0 options_count = 0 - + forms_validation = self._validate_sheet( df_dict, 'Forms', self.REQUIRED_FORMS_COLUMNS ) sheets_validation.append(forms_validation) - + if forms_validation['exists'] and not forms_validation['missing_columns']: forms_df = df_dict['Forms'] if not forms_df.empty: @@ -38,27 +38,27 @@ class XLSFormParser: 'language': forms_df.iloc[0].get('Language', 'Unknown'), 'title': forms_df.iloc[0].get('Title', 'Untitled') } - + questions_validation = self._validate_sheet( df_dict, 'Questions Info', self.REQUIRED_QUESTIONS_COLUMNS ) sheets_validation.append(questions_validation) - + if questions_validation['exists'] and not questions_validation['missing_columns']: questions_df = df_dict['Questions Info'] questions_count = len(questions_df) - + options_validation = self._validate_sheet( df_dict, 'Answer Options', self.REQUIRED_OPTIONS_COLUMNS ) sheets_validation.append(options_validation) - + if options_validation['exists'] and not options_validation['missing_columns']: options_df = df_dict['Answer Options'] options_count = len(options_df) - + is_valid = all(sheet['exists'] and not sheet['missing_columns'] for sheet in sheets_validation) - + return { 'valid': is_valid, 'message': "File format is valid." if is_valid else "Invalid XLSForm structure.", @@ -86,7 +86,7 @@ class XLSFormParser: columns = list(df_dict[sheet_name].columns) if exists else [] missing_columns = [col for col in required_columns if col not in columns] row_count = len(df_dict[sheet_name]) if exists else 0 - + return { 'name': sheet_name, 'exists': exists, @@ -105,12 +105,12 @@ class XLSFormParser: options_df = df_dict['Answer Options'] form_metadata = self._parse_form_metadata(forms_df) - + form_id = await self.db_service.save_form(form_metadata) - + questions_data = self._parse_questions_data(questions_df) question_ids = await self.db_service.save_questions(questions_data, form_id) - + options_data = self._parse_options_data(options_df) option_ids = await self.db_service.save_options(options_data, form_id) @@ -146,19 +146,19 @@ class XLSFormParser: 'version': '1.0.0', 'created_at': pd.Timestamp.now().isoformat() } - + if not forms_df.empty: if 'Language' in forms_df.columns: metadata['language'] = forms_df.iloc[0]['Language'] if 'Title' in forms_df.columns: metadata['title'] = forms_df.iloc[0]['Title'] - + return metadata def _parse_questions_data(self, questions_df: pd.DataFrame) -> List[Dict[str, Any]]: """Parse questions data for database storage""" questions_data = [] - + for _, row in questions_df.iterrows(): question_data = { 'order': int(row['Order']), @@ -168,13 +168,13 @@ class XLSFormParser: 'created_at': pd.Timestamp.now().isoformat() } questions_data.append(question_data) - + return questions_data def _parse_options_data(self, options_df: pd.DataFrame) -> List[Dict[str, Any]]: """Parse options data for database storage""" options_data = [] - + for _, row in options_df.iterrows(): option_data = { 'order': int(row['Order']), @@ -183,7 +183,7 @@ class XLSFormParser: 'created_at': pd.Timestamp.now().isoformat() } options_data.append(option_data) - + return options_data def _get_form_title(self, forms_df: pd.DataFrame) -> Dict[str, str]: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cdd8438..9ed162d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,7 +32,7 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/express": "^4.17.21", - "@types/node": "^18.19.100", + "@types/node": "^24.0.3", "typescript": "~5.7.2" } }, @@ -4979,13 +4979,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.100", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.100.tgz", - "integrity": "sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-forge": { @@ -12106,9 +12106,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, "license": "MIT" }, diff --git a/frontend/package.json b/frontend/package.json index b1f75a1..2b9769c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "serve:ssr:mform-upload": "node dist/mform-upload/server/server.mjs" + "serve:ssr:mform-upload": "node --no-deprecation dist/mform-upload/server/server.mjs" }, "private": true, "dependencies": { @@ -34,7 +34,7 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/express": "^4.17.21", - "@types/node": "^18.19.100", + "@types/node": "^24.0.3", "typescript": "~5.7.2" } } diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 62e552c..292fdb8 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -6,8 +6,6 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { NavbarComponent } from './components/navbar/navbar.component'; import { SearchComponent } from './components/search/search.component'; import { UploadComponent } from './components/upload/upload.component'; -import { DownloadComponent } from './components/download/download.component'; -import { FileUploadComponent } from './components/file-upload/file-upload.component'; @Component({ selector: 'app-root', @@ -19,9 +17,7 @@ import { FileUploadComponent } from './components/file-upload/file-upload.compon MatToolbarModule, NavbarComponent, SearchComponent, - UploadComponent, - DownloadComponent, - FileUploadComponent + UploadComponent ], template: `
{{form.language}} • {{form.version}} • {{form.created_at | date:'short'}}
-No forms have been parsed yet
- -Status: {{ validationResult.valid ? 'Valid' : 'Invalid' }}
- - - - -Exists: {{ sheet.exists ? 'Yes' : 'No' }}
-Rows: {{ sheet.row_count }}
-Columns: {{ sheet.columns.join(', ') }}
-Title: {{ parsedForm.title.default }}
-Version: {{ parsedForm.version }}
-Number of Groups: {{ parsedForm.groups.length }}
-- form-preview works! -
- `, - styles: `` -}) -export class FormPreviewComponent { - -} diff --git a/frontend/src/app/components/navbar/navbar.component.ts b/frontend/src/app/components/navbar/navbar.component.ts index e6b2638..6eaa637 100644 --- a/frontend/src/app/components/navbar/navbar.component.ts +++ b/frontend/src/app/components/navbar/navbar.component.ts @@ -1,29 +1,18 @@ import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @Component({ selector: 'app-navbar', standalone: true, - imports: [CommonModule, MatToolbarModule, MatButtonModule, MatIconModule], + imports: [CommonModule, MatToolbarModule, MatIconModule], template: `Status: {{ validationResult.valid ? 'Valid' : 'Invalid' }}
+ + + + +Exists: {{ sheet.exists ? 'Yes' : 'No' }}
+Rows: {{ sheet.row_count }}
+Columns: {{ sheet.columns.join(', ') }}
+{{form.language}} • {{form.version}} • {{form.created_at | date:'short'}}
+