diff --git a/client/helpers/document_status.py b/client/helpers/document_status.py index 0bc5404..144b7cf 100644 --- a/client/helpers/document_status.py +++ b/client/helpers/document_status.py @@ -15,7 +15,7 @@ class DocumentsStatus: mapping = { DocumentsStatus.NEW: 'Nou', DocumentsStatus.ANALISE: 'Analiza', - DocumentsStatus.IN_PROGRESS: 'In progres', + DocumentsStatus.IN_PROGRESS: 'In lucru', DocumentsStatus.WAITING_FOR_PAYMENT: 'Asteptam plata', DocumentsStatus.COMPLETED: 'Complet', DocumentsStatus.CANCELED: 'Anulat' diff --git a/client/navigation/ba.py b/client/navigation/ba.py index 8653573..49ac2cf 100644 --- a/client/navigation/ba.py +++ b/client/navigation/ba.py @@ -1,5 +1,6 @@ import flet as ft from pages.documents.ba import Documents +from pages.publications.articles import Articles as ExpertArticlesPage from pages.settings.settings import Settings class NavigationBA: @@ -13,8 +14,8 @@ class NavigationBA: label="Documente Custom", ) self.articole_si_publicatii = ft.NavigationRailDestination( - icon=ft.Icons.ARTICLE_OUTLINED, - selected_icon=ft.Icons.ARTICLE, + icon=ft.Icons.MENU_BOOK_OUTLINED, # Icon mai potrivit pentru articole/blog + selected_icon=ft.Icons.MENU_BOOK, label="Articole si Publicatii", ) self.comunicare = ft.NavigationRailDestination( @@ -61,12 +62,7 @@ class NavigationBA: def build(self): return [ self.documente_juridice, - # self.articole_si_publicatii, - # self.comunicare, - # self.consultanta, - # self.convocator, - # self.licitatii_si_lucrari, - # self.profil, + self.articole_si_publicatii, self.abonamente, self.setari, self.logout @@ -76,21 +72,31 @@ class NavigationBA: self.docs = Documents(self.page, self.home) return self.docs.build() + def build_articole_si_publicatii_expert(self): + self.articles_page = ExpertArticlesPage(self.page, self.home) + return self.articles_page.build() + + def build_subscriptions(self): + from pages.subscriptions.subscriptions import SubscriptionsPage + self.subs_page = SubscriptionsPage(self.page) + return self.subs_page.build() + async def on_nav_change(self, e): print( "Selected destination:", e.control.selected_index) if e.control.selected_index == 0: self.home.placeholder.content = self.build_documente_juridice() self.page.update() - - if e.control.selected_index == 1: - pass - - if e.control.selected_index == 2: + elif e.control.selected_index == 1: # Index for Articole si Publicatii + self.home.placeholder.content = self.build_articole_si_publicatii_expert() + self.page.update() + elif e.control.selected_index == 2: # Abonamente + self.home.placeholder.content = self.build_subscriptions() + self.page.update() + elif e.control.selected_index == 3: # Setari self.settings = Settings(self.page, self) self.home.placeholder.content = self.settings.build() self.page.update() # Actualizează întreaga pagină inclusiv overlay-ul - - if e.control.selected_index == 3: + elif e.control.selected_index == 4: # Logout await ft.SharedPreferences().clear() self.page.session.store.clear() self.page.go('/auth') \ No newline at end of file diff --git a/client/navigation/user.py b/client/navigation/user.py index f08f638..2714903 100644 --- a/client/navigation/user.py +++ b/client/navigation/user.py @@ -1,5 +1,6 @@ import flet as ft from pages.documents.home import DocumentsHome +from pages.publications.articles import Articles as ClientArticlesPage class NavigationUser: def __init__(self, page: ft.Page, home): @@ -12,8 +13,8 @@ class NavigationUser: label="Documente Juridice", ) self.articole_si_publicatii = ft.NavigationRailDestination( - icon=ft.Icons.ARTICLE_OUTLINED, - selected_icon=ft.Icons.ARTICLE, + icon=ft.Icons.MENU_BOOK_OUTLINED, # Icon mai potrivit pentru articole/blog + selected_icon=ft.Icons.MENU_BOOK, label="Articole si Publicatii", ) self.comunicare = ft.NavigationRailDestination( @@ -75,19 +76,23 @@ class NavigationUser: self.docs = DocumentsHome(self.page, self.home) return self.docs.build() + def build_articole_si_publicatii_client(self): + self.articles_page = ClientArticlesPage(self.page, self.home) + return self.articles_page.build() + async def on_nav_change(self, e): print( "Selected destination:", e.control.selected_index) if e.control.selected_index == 0: self.home.placeholder.content = self.build_documente_juridice() self.page.update() - - if e.control.selected_index == 1: - pass + elif e.control.selected_index == 1: # Index for Articole si Publicatii + self.home.placeholder.content = self.build_articole_si_publicatii_client() + self.page.update() if e.control.selected_index == 2: pass - if e.control.selected_index == 3: + if e.control.selected_index == 9: await ft.SharedPreferences().clear() self.page.session.store.clear() self.page.go('/auth') \ No newline at end of file diff --git a/client/pages/auth/forgot_password.py b/client/pages/auth/forgot_password.py index cfa9fb9..542fb9d 100644 --- a/client/pages/auth/forgot_password.py +++ b/client/pages/auth/forgot_password.py @@ -47,6 +47,7 @@ class ForgotPassword: self.go_to_login ] ) + self.verifica_btn = ft.Button("Verifica", width=150, on_click=self.verfy_code) def _generate_numeric_code(self) -> str: digits = string.digits @@ -58,7 +59,7 @@ class ForgotPassword: self.main_column.controls.append(self.title) self.main_column.controls.append(self.inserted_code) self.main_column.controls.append(self.error_message) - self.main_column.controls.append("Verifica", width=150, on_click=self.verfy_code) + self.main_column.controls.append(self.verifica_btn) self.main_column.update() #print(self.otp_code) send_gmail( diff --git a/client/pages/dashboard/home.py b/client/pages/dashboard/home.py index 03dfd72..286e577 100644 --- a/client/pages/dashboard/home.py +++ b/client/pages/dashboard/home.py @@ -1,7 +1,5 @@ import flet as ft import requests - - from navigation.ba import NavigationBA from navigation.user import NavigationUser diff --git a/client/pages/documents/ba.py b/client/pages/documents/ba.py index 5476b8e..3694342 100644 --- a/client/pages/documents/ba.py +++ b/client/pages/documents/ba.py @@ -4,6 +4,7 @@ from datetime import datetime from helpers.document_status import DocumentsStatus from helpers.emails import send_gmail from dataclasses import dataclass, field +import asyncio @dataclass class State: @@ -11,6 +12,7 @@ class State: file_picker: ft.FilePicker | None = None picked_files: list[ft.FilePickerFile] = field(default_factory=list) + state = State() class Documents: @@ -21,11 +23,11 @@ class Documents: self.token = self.page.session.store.get('token') self.user = self.page.session.store.get('user') self.user_id = self.user['id'] if self.user else None + self.documenet_title = None self.all_requests = [] self.current_selected_request = None - # Elemente interfață: Căutare și Listă self.search_bar = ft.TextField( label="Căutare solicitări (Text solicitare sau comentariu)", @@ -87,6 +89,11 @@ class Documents: self.doc_id_info = ft.Text("Document Final: Niciunul", color=ft.Colors.GREY_700) + self.download_button = ft.IconButton( + icon=ft.Icons.DOWNLOAD, + on_click=self._on_download_button_click, + ) + self.details_panel = ft.Column( [ self.req_id_text, @@ -108,15 +115,23 @@ class Documents: ]), ft.Divider(), ft.Text("Finalizare și Încărcare Document:", weight=ft.FontWeight.BOLD), - ft.Row([ - ft.FilledButton( - "Încarcă Document Final", - icon=ft.Icons.UPLOAD_FILE, - on_click=self._handle_file_upload, - bgcolor=ft.Colors.GREEN_700 - ), - self.doc_id_info - ]) + + ft.Row( + [ + ft.Row([ + ft.FilledButton( + "Încarcă Document Final", + icon=ft.Icons.UPLOAD_FILE, + on_click=self._handle_file_upload, + bgcolor=ft.Colors.GREEN_700 + ), + self.doc_id_info, + self.download_button + ]), + ft.FilledButton("Salveaza si trimite", on_click=self._on_file_saved) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ) ], visible=False, expand=True, @@ -301,8 +316,8 @@ class Documents: print(f"Update failed: {e}") return False - async def _handle_file_upload(self, e): - if not self.current_selected_request: return + async def _handle_file_upload(self, e: ft.Event[ft.Button]): + print('File uploaded') try: state.file_picker = ft.FilePicker() files = await state.file_picker.pick_files(allow_multiple=False) @@ -310,36 +325,69 @@ class Documents: state.picked_files = files uploaded_file_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{state.picked_files[0].name}" - await state.file_picker.upload( - files=[ - ft.FilePickerUploadFile( - name=file.name, - upload_url=self.page.get_upload_url(uploaded_file_name, 60), - - ) - - for file in state.picked_files - ] + await state.file_picker.upload( + files=[ + ft.FilePickerUploadFile( + name=file.name, + upload_url=self.page.get_upload_url(uploaded_file_name, 60), + + ) + + for file in state.picked_files + ] ) + self.documenet_title = uploaded_file_name + self.doc_id_info.value = f"ID Document asociat: {self.documenet_title}" + return f'{uploaded_file_name}' + except Exception as e: + print(e) - # 2. Înregistrare în tabela documents_custom + def _on_file_saved(self, e): + + reg_resp = requests.post( + f"{self.base_url}/documents/customs/add", + json={"name": f"Document Final Solicitare #{self.current_selected_request['id']}", "path": self.documenet_title}, + headers={'Authorization': f'Bearer {self.token}'} + ) - reg_resp = requests.post( - f"{self.base_url}/documents/customs/add", - json={"name": f"Document Final Solicitare #{self.current_selected_request['id']}", "path": uploaded_file_name}, + if reg_resp.status_code == 201: + doc_id = reg_resp.json().get('id') + # 3. Legare document de solicitare și marcare ca finalizat + self._update_request_api({ + "document_id": doc_id, + "status": DocumentsStatus.COMPLETED, + "expert_id": self.user_id, + }) + + async def _on_download_button_click(self, e): + """Handles the download of the final document.""" + if not self.current_selected_request or not self.current_selected_request.get('document_id'): + self.page.show_dialog(ft.SnackBar(ft.Text("Nu există un document final asociat acestei solicitări."), open=True)) + self.page.update() + return + + document_id = self.current_selected_request['document_id'] + try: + # Fetch document details to get the path + response = requests.get( + f"{self.base_url}/documents/customs/{document_id}", headers={'Authorization': f'Bearer {self.token}'} ) - - if reg_resp.status_code == 201: - doc_id = reg_resp.json().get('id') - # 3. Legare document de solicitare și marcare ca finalizat - self._update_request_api({ - "document_id": doc_id, - "status": DocumentsStatus.COMPLETED, - "expert_id": self.user_id, - }) + if response.status_code == 200: + document_data = response.json() + document_path = document_data.get('path') + if document_path: + download_url = f"{self.base_url}/documents/download?path={document_path}&token={self.token}" + await self.page.launch_url(download_url) + else: + self.page.show_dialog(ft.SnackBar(ft.Text("Calea documentului nu a putut fi găsită."), open=True)) + else: + self.page.show_dialog(ft.SnackBar(ft.Text(f"Eroare la preluarea detaliilor documentului: {response.status_code}"), open=True)) + except requests.exceptions.RequestException as err: + self.page.show_dialog(ft.SnackBar(ft.Text(f"Eroare de rețea la descărcarea documentului: {err}"), open=True)) except Exception as ex: - print(f"Error during file upload or registration: {ex}") + self.page.show_dialog(ft.SnackBar(ft.Text(f"A apărut o eroare neașteptată: {ex}"), open=True)) + self.page.update() def build(self): return ft.Container( diff --git a/client/pages/documents/custom.py b/client/pages/documents/custom.py index 4998d6c..8cc1b25 100644 --- a/client/pages/documents/custom.py +++ b/client/pages/documents/custom.py @@ -73,6 +73,13 @@ class Documents: disabled=True, # Will be enabled based on status visible=False # Initially hidden ) + self.download_button = ft.FilledButton( + "Descarcă Document Final", + icon=ft.Icons.DOWNLOAD, + on_click=self._on_download_button_click, + disabled=True, # Will be enabled based on status and document_id + visible=False # Initially hidden + ) self.comment_text_field = ft.TextField( label="Adauga un comentariu", multiline=True, @@ -96,11 +103,13 @@ class Documents: ft.Text("Pret:", weight=ft.FontWeight.BOLD), self.request_price_text, self.pay_button, + self.download_button, ft.Divider(), ft.Text("Adauga Comentariu:", weight=ft.FontWeight.BOLD), ft.Row([self.comment_text_field, self.add_comment_button]), ], expand=True, + scroll=ft.ScrollMode.ADAPTIVE, visible=False # Initially hidden ) @@ -186,6 +195,14 @@ class Documents: self.pay_button.visible = False self.pay_button.disabled = True + # Handle Download button visibility and state + if request_data.get('status') == DocumentsStatus.COMPLETED and request_data.get('document_id'): + self.download_button.visible = True + self.download_button.disabled = False + else: + self.download_button.visible = False + self.download_button.disabled = True + # Enable comment section self.add_comment_button.disabled = False self.comment_text_field.disabled = False @@ -308,6 +325,36 @@ class Documents: )) self.page.update() + async def _on_download_button_click(self, e): + """Handles the download of the final document.""" + if not self.current_selected_request or not self.current_selected_request.get('document_id'): + self.page.show_dialog(ft.SnackBar(ft.Text("Nu există un document final asociat acestei solicitări."), open=True)) + self.page.update() + return + + document_id = self.current_selected_request['document_id'] + try: + # Fetch document details to get the path + response = requests.get( + f"{self.base_url}/documents/customs/{document_id}", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + document_data = response.json() + document_path = document_data.get('path') + if document_path: + download_url = f"{self.base_url}/documents/download?path={document_path}&token={self.token}" + await self.page.launch_url(download_url) + else: + self.page.show_dialog(ft.SnackBar(ft.Text("Calea documentului nu a putut fi găsită."), open=True)) + else: + self.page.show_dialog(ft.SnackBar(ft.Text(f"Eroare la preluarea detaliilor documentului: {response.status_code}"), open=True)) + except requests.exceptions.RequestException as err: + self.page.show_dialog(ft.SnackBar(ft.Text(f"Eroare de rețea la descărcarea documentului: {err}"), open=True)) + except Exception as ex: + self.page.show_dialog(ft.SnackBar(ft.Text(f"A apărut o eroare neașteptată: {ex}"), open=True)) + self.page.update() + def _close_dialog(self, e): self.page.pop_dialog() self.page.update() diff --git a/client/pages/publications/__pycache__/articles.cpython-313.pyc b/client/pages/publications/__pycache__/articles.cpython-313.pyc new file mode 100644 index 0000000..5c52842 Binary files /dev/null and b/client/pages/publications/__pycache__/articles.cpython-313.pyc differ diff --git a/client/pages/publications/articles.py b/client/pages/publications/articles.py new file mode 100644 index 0000000..8ef65af --- /dev/null +++ b/client/pages/publications/articles.py @@ -0,0 +1,334 @@ +import flet as ft +import requests +from helpers.roles import Roles + +class Articles: + def __init__(self, page: ft.Page, home): + self.page = page + self.home = home + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + self.user = self.page.session.store.get('user') or {} + + self.all_articles = [] + self.selected_article_id = None + + # UI Elements for Search + self.search_bar = ft.TextField( + label="Caută articole după titlu...", + prefix_icon=ft.Icons.SEARCH, + on_change=self.on_search_change, + expand=True, + hint_text="Introdu titlul sau fragmente din titlu..." + ) + + # UI list container + self.articles_container = ft.Column( + scroll=ft.ScrollMode.AUTO, + spacing=15, + expand=True + ) + + # Delete confirmation dialog + self.article_id_to_delete = None + self.delete_confirm_dialog = ft.AlertDialog( + title=ft.Text("Confirmare Ștergere"), + content=ft.Text("Sigur doriți să ștergeți acest articol? Această acțiune este ireversibilă."), + actions=[ + ft.FilledButton("Șterge", on_click=self.confirm_delete_article, bgcolor=ft.Colors.RED), + ft.FilledButton("Anulează", on_click=self.close_dialog, bgcolor=ft.Colors.GREY) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + # Read Article Modal Dialog + self.read_title = ft.Text(size=22, weight=ft.FontWeight.BOLD) + self.read_meta = ft.Text(size=12, italic=True, color=ft.Colors.GREY_600) + self.read_content = ft.Markdown(selectable=True, expand=True) + + self.read_article_dialog = ft.AlertDialog( + content=ft.Container( + content=ft.Column( + [ + self.read_title, + self.read_meta, + ft.Divider(height=10), + ft.Column( + [self.read_content], + scroll=ft.ScrollMode.AUTO, + expand=True + ) + ], + spacing=10, + ), + width=650, + height=500, + ), + actions=[ + ft.TextButton("Închide", on_click=self.close_dialog) + ] + ) + + def load_articles(self): + try: + response = requests.get( + f"{self.base_url}/articles/", + headers={"Authorization": f"Bearer {self.token}"} + ) + if response.status_code == 200: + self.all_articles = response.json() + else: + self.all_articles = [] + except Exception as e: + print(f"Error fetching articles: {e}") + self.all_articles = [] + + def populate_articles_list(self, query=""): + self.articles_container.controls.clear() + query = query.strip().lower() + + filtered = [ + art for art in self.all_articles + if query in art.get('title', '').lower() + ] + + if not filtered: + self.articles_container.controls.append( + ft.Container( + content=ft.Text("Nu s-au găsit articole publicate.", size=16, color=ft.Colors.GREY_600), + alignment=ft.Alignment.CENTER, + padding=40 + ) + ) + else: + can_publish = self.user.get('can_create_articles') == 1 + user_id = self.user.get('id') + + for art in filtered: + # Curățăm formatarea markdown pentru previzualizare + preview = self.strip_markdown(art.get('content', '')) + if len(preview) > 180: + preview = preview[:180] + "..." + + # Check if logged-in user is the author of this post + is_author = art.get('author_id') == user_id + + # Formatează data + raw_date = art.get('created_at', '') + formatted_date = raw_date.replace("T", " ").split(".")[0] if raw_date else "Dată necunoscută" + + # Row of actions + actions_row = ft.Row( + [ + ft.TextButton( + "Citește Articolul", + icon=ft.Icons.ARROW_FORWARD, + on_click=lambda e, a=art: self.open_read_dialog(a) + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ) + + # Daca utilizatorul este autorul, adaugam butoane de Edit/Delete + if can_publish and is_author: + actions_row.controls.append( + ft.Row( + [ + ft.IconButton( + icon=ft.Icons.EDIT_OUTLINED, + tooltip="Editează articolul", + icon_color=ft.Colors.BLUE_700, + on_click=lambda e, a=art: self.page.run_task(self.open_edit_dialog, a) + ), + ft.IconButton( + icon=ft.Icons.DELETE_OUTLINE, + tooltip="Șterge articolul", + icon_color=ft.Colors.RED_600, + on_click=lambda e, a_id=art['id']: self.open_delete_confirm(a_id) + ) + ], + spacing=5 + ) + ) + + self.articles_container.controls.append( + ft.Container( + content=ft.Column( + [ + ft.Text(art.get('title', 'Fără Titlu'), weight=ft.FontWeight.BOLD, size=18, color=ft.Colors.BLUE_GREY_900), + ft.Row( + [ + ft.Icon(ft.Icons.PERSON_OUTLINE, size=14, color=ft.Colors.GREY_500), + ft.Text(f"Autor: {art.get('author_name', 'Necunoscut')}", size=12, color=ft.Colors.GREY_600), + ft.VerticalDivider(width=1, color=ft.Colors.GREY_300), + ft.Icon(ft.Icons.ACCESS_TIME, size=14, color=ft.Colors.GREY_500), + ft.Text(f"Publicat: {formatted_date}", size=12, color=ft.Colors.GREY_600), + ], + spacing=10 + ), + ft.Divider(height=5, color=ft.Colors.TRANSPARENT), + ft.Text(preview, size=14, color=ft.Colors.GREY_700), + ft.Divider(height=10), + actions_row + ], + spacing=10 + ), + bgcolor=ft.Colors.WHITE, + padding=20, + border_radius=12, + border=ft.Border.all(1, ft.Colors.GREY_200), + shadow=ft.BoxShadow( + blur_radius=8, + color=ft.Colors.with_opacity(0.04, ft.Colors.BLACK), + offset=ft.Offset(0, 3) + ) + ) + ) + #self.articles_container.update() + + def on_search_change(self, e): + self.populate_articles_list(self.search_bar.value) + + def strip_markdown(self, text): + if not text: + return "" + import re + # Elimină antetele (ex: # Titlu 1) + text = re.sub(r'(?m)^#{1,6}\s+', '', text) + # Elimină marcatorii de listă (ex: - element) + text = re.sub(r'(?m)^[\-\*\+]\s+', '', text) + # Elimină caracterele pentru bold, italic și inline-code + text = re.sub(r'\*\*|__|\*|_|`', '', text) + # Înlocuiește multiplele spații sau linii noi cu un singur spațiu + text = re.sub(r'\s+', ' ', text) + return text.strip() + + # Creation/Editing Dialog (deschise în tab separat) + async def open_add_dialog(self, e): + editor_url = f"{self.base_url}/articles/editor?token={self.token}" + await self.page.launch_url(editor_url) + + async def open_edit_dialog(self, article): + print('edit') + editor_url = f"{self.base_url}/articles/editor?token={self.token}&article_id={article['id']}" + await self.page.launch_url(editor_url) + + # Delete Dialog + def open_delete_confirm(self, article_id): + self.article_id_to_delete = article_id + self.page.show_dialog(self.delete_confirm_dialog) + self.page.update() + + def confirm_delete_article(self, e): + if not self.article_id_to_delete: + return + + try: + response = requests.delete( + f"{self.base_url}/articles/delete/{self.article_id_to_delete}", + headers={"Authorization": f"Bearer {self.token}"} + ) + if response.status_code == 200: + self.page.pop_dialog() + self.load_articles() + self.populate_articles_list(self.search_bar.value) + self.show_snack_bar("Articolul a fost șters cu succes.", ft.Colors.GREEN) + else: + self.page.pop_dialog() + err = response.json().get('error', 'Eroare la ștergerea articolului.') + self.show_snack_bar(err, ft.Colors.RED) + except Exception as ex: + print(f"Error deleting article: {ex}") + self.page.pop_dialog() + self.show_snack_bar("Eroare de conexiune la server.", ft.Colors.RED) + + # Read modal dialog + def open_read_dialog(self, article): + self.read_title.value = article.get('title', 'Fără Titlu') + raw_date = article.get('created_at', '') + formatted_date = raw_date.replace("T", " ").split(".")[0] if raw_date else "Dată necunoscută" + self.read_meta.value = f"Scris de {article.get('author_name', 'Necunoscut')} la data de {formatted_date}" + self.read_content.value = article.get('content', '') + + self.page.show_dialog(self.read_article_dialog) + self.page.update() + + def close_dialog(self, e): + self.page.pop_dialog() + + def show_snack_bar(self, message, color): + self.page.snack_bar = ft.SnackBar( + content=ft.Text(message, color=ft.Colors.WHITE), + bgcolor=color, + duration=3000 + ) + self.page.snack_bar.open = True + self.page.update() + + def on_refresh_click(self, e): + self.load_articles() + self.populate_articles_list(self.search_bar.value) + self.show_snack_bar("Lista de articole a fost actualizată.", ft.Colors.GREEN) + + def build(self): + self.load_articles() + self.populate_articles_list() + + # Check if the user is an expert with posting privileges + can_publish = self.user.get('can_create_articles') == 1 + + header_row = ft.Row( + [ + ft.Column( + [ + ft.Text("Articole și Publicații", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_GREY_900), + ft.Text("Răsfoiți noutățile, ghidurile și publicațiile juridice recente.", size=14, color=ft.Colors.GREY_600) + ], + spacing=2 + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ) + + # Dacă utilizatorul are bifa de publicare, adăugăm butonul de Adăugare + if can_publish: + header_row.controls.append( + ft.FilledButton( + "Adaugă Articol", + icon=ft.Icons.ADD, + #on_click=lambda e: self.page.run_task(self.open_add_dialog(e)), + on_click=self.open_add_dialog, + style=ft.ButtonStyle( + shape=ft.RoundedRectangleBorder(radius=8) + ) + ) + ) + + refresh_btn = ft.IconButton( + icon=ft.Icons.REFRESH, + tooltip="Reîncarcă articolele", + icon_color=ft.Colors.BLUE_700, + on_click=self.on_refresh_click + ) + + return ft.Container( + content=ft.Column( + [ + header_row, + ft.Divider(height=10, color=ft.Colors.TRANSPARENT), + ft.Row( + [ + self.search_bar, + refresh_btn + ], + spacing=10 + ), + ft.Divider(height=10, color=ft.Colors.GREY_100), + self.articles_container + ], + expand=True + ), + expand=True, + padding=20, + bgcolor=ft.Colors.GREY_50 + ) diff --git a/client/pages/settings/articles.py b/client/pages/settings/articles.py new file mode 100644 index 0000000..f8dea43 --- /dev/null +++ b/client/pages/settings/articles.py @@ -0,0 +1,188 @@ +import flet as ft +import requests +from helpers.roles import Roles + +class ArticlesSettings: + def __init__(self, page: ft.Page): + self.page = page + self.base_url = self.page.session.store.get('api_base_url') + self.token = self.page.session.store.get('token') + + self.search_bar = ft.TextField( + label="Caută expert...", + prefix_icon=ft.Icons.SEARCH, + on_change=self.on_search_change, + expand=True + ) + + self.experts_list = ft.ListView( + spacing=10, + expand=True, + padding=10 + ) + + self.all_experts = [] + + def get_experts(self): + try: + response = requests.get( + f"{self.base_url}/users/", + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + users = response.json() + # Filtrăm doar utilizatorii cu rol de expert sau ba + self.all_experts = [u for u in users if u.get('role') in [Roles.EXPERT, Roles.BA]] + return self.all_experts + except Exception as e: + print(f"Error fetching experts: {e}") + return [] + + def load_experts_list(self, query=""): + self.experts_list.controls.clear() + query = query.strip().lower() + + filtered = [ + exp for exp in self.all_experts + if query in f"{exp.get('first_name', '')} {exp.get('last_name', '')} {exp.get('email', '')}".lower() + ] + + if not filtered: + self.experts_list.controls.append( + ft.Container( + content=ft.Text("Nu s-au găsit experți.", size=16, color=ft.Colors.GREY_600), + alignment=ft.Alignment.CENTER, + padding=20 + ) + ) + else: + for exp in filtered: + expert_id = exp['id'] + name = f"{exp.get('first_name') or ''} {exp.get('last_name') or ''}".strip() or "Nume nespecificat" + email = exp.get('email', 'Email nespecificat') + role_label = exp.get('role', '').upper() + can_create = exp.get('can_create_articles') == 1 + + self.experts_list.controls.append( + ft.Container( + content=ft.Row( + [ + ft.Row( + [ + ft.CircleAvatar( + content=ft.Icon(ft.Icons.PERSON, color=ft.Colors.BLUE_800), + bgcolor=ft.Colors.BLUE_50, + radius=20 + ), + ft.Column( + [ + ft.Text(name, weight=ft.FontWeight.BOLD, size=15), + ft.Row( + [ + ft.Text(email, size=13, color=ft.Colors.GREY_600), + ft.Container( + content=ft.Text(role_label, size=10, color=ft.Colors.BLUE_800, weight=ft.FontWeight.BOLD), + bgcolor=ft.Colors.BLUE_50, + padding=ft.Padding(5, 2, 5, 2), + border_radius=4 + ) + ], + spacing=10 + ) + ], + alignment=ft.MainAxisAlignment.CENTER, + spacing=2 + ) + ], + spacing=15 + ), + ft.Switch( + value=can_create, + on_change=lambda e, uid=expert_id, uemail=email: self.toggle_permission(e, uid, uemail) + ) + ], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN + ), + bgcolor=ft.Colors.WHITE, + padding=15, + border_radius=10, + border=ft.Border.all(1, ft.Colors.GREY_200), + shadow=ft.BoxShadow( + blur_radius=4, + color=ft.Colors.with_opacity(0.05, ft.Colors.BLACK), + offset=ft.Offset(0, 2) + ) + ) + ) + + def toggle_permission(self, e, user_id, user_email): + new_val = 1 if e.control.value else 0 + try: + payload = {"can_create_articles": new_val} + response = requests.put( + f"{self.base_url}/users/update/{user_id}", + json=payload, + headers={'Authorization': f'Bearer {self.token}'} + ) + if response.status_code == 200: + # Update local data model + for exp in self.all_experts: + if exp['id'] == user_id: + exp['can_create_articles'] = new_val + break + + status_msg = "permisiune acordată" if new_val == 1 else "permisiune retrasă" + self.show_snack_bar(f"Drepturi actualizate pentru {user_email}: {status_msg}.", ft.Colors.GREEN) + else: + e.control.value = not e.control.value + e.control.update() + self.show_snack_bar("Eroare la salvarea permisiunilor.", ft.Colors.RED) + except Exception as ex: + print(f"Error toggling permission: {ex}") + e.control.value = not e.control.value + e.control.update() + self.show_snack_bar("Eroare de conexiune la server.", ft.Colors.RED) + + def on_search_change(self, e): + self.load_experts_list(self.search_bar.value) + + def show_snack_bar(self, message, color): + self.page.snack_bar = ft.SnackBar( + content=ft.Text(message, color=ft.Colors.WHITE), + bgcolor=color, + duration=3000 + ) + self.page.snack_bar.open = True + self.page.update() + + def build(self): + self.get_experts() + self.load_experts_list() + + return ft.Container( + content=ft.Column( + [ + ft.Container( + content=ft.Column( + [ + ft.Text("Configurare Drepturi Publicare", size=20, weight=ft.FontWeight.BOLD), + ft.Text( + "Permiteți sau blocați dreptul experților de a crea, edita și șterge publicații de pe blog.", + size=14, + color=ft.Colors.GREY_600 + ), + ], + spacing=5 + ), + margin=ft.Margin(0, 0, 0, 10) + ), + ft.Row([self.search_bar]), + ft.Divider(height=10, color=ft.Colors.GREY_100), + self.experts_list + ], + expand=True + ), + expand=True, + padding=20, + bgcolor=ft.Colors.GREY_50 + ) diff --git a/client/pages/settings/settings.py b/client/pages/settings/settings.py index f21300b..6994d32 100644 --- a/client/pages/settings/settings.py +++ b/client/pages/settings/settings.py @@ -2,6 +2,7 @@ import flet as ft from pages.settings.documente_juridice import DocumenteJuridice from pages.settings.users import UsersSettings from pages.settings.payment_and_subscription import PaymentAndSubscription +from pages.settings.articles import ArticlesSettings class Settings: def __init__(self, page: ft.Page, home): @@ -11,6 +12,7 @@ class Settings: self.doc_juridice = DocumenteJuridice(self.page) self.users_settings = UsersSettings(self.page) self.payment_and_subscription = PaymentAndSubscription(self.page) + self.articles_settings = ArticlesSettings(self.page) def build(self): return ft.Tabs( @@ -42,8 +44,9 @@ class Settings: expand=True ), ft.Container( - content=ft.Text("This is Tab 2"), + content=self.articles_settings.build(), alignment=ft.Alignment.CENTER, + expand=True ), ft.Container( content=ft.Text("This is Tab 3"), diff --git a/documents/Custom/20260616214931_3.png b/documents/Custom/20260616214931_3.png new file mode 100644 index 0000000..ce1fccf Binary files /dev/null and b/documents/Custom/20260616214931_3.png differ diff --git a/documents/Custom/20260616215606_9.PNG b/documents/Custom/20260616215606_9.PNG new file mode 100644 index 0000000..c33e21a Binary files /dev/null and b/documents/Custom/20260616215606_9.PNG differ diff --git a/documents/Custom/20260616215900_9.PNG b/documents/Custom/20260616215900_9.PNG new file mode 100644 index 0000000..c33e21a Binary files /dev/null and b/documents/Custom/20260616215900_9.PNG differ diff --git a/documents/Custom/20260616220001_8.png b/documents/Custom/20260616220001_8.png new file mode 100644 index 0000000..ccefeea Binary files /dev/null and b/documents/Custom/20260616220001_8.png differ diff --git a/server/app.py b/server/app.py index a9bcb74..84633de 100644 --- a/server/app.py +++ b/server/app.py @@ -8,6 +8,7 @@ from routes.documents import documents_bp from routes.users import users_bp from routes.payments import payments_bp from routes.subscriptions import subscriptions_bp +from routes.articles import articles_bp def create_app(test_config=None): # create and configure the app @@ -50,6 +51,7 @@ def create_app(test_config=None): app.register_blueprint(users_bp, url_prefix="/users") app.register_blueprint(payments_bp, url_prefix="/payments") app.register_blueprint(subscriptions_bp, url_prefix="/subscriptions") + app.register_blueprint(articles_bp, url_prefix="/articles") return app diff --git a/server/instance/app_database.db b/server/instance/app_database.db index d18800d..d4fd00d 100644 Binary files a/server/instance/app_database.db and b/server/instance/app_database.db differ diff --git a/server/models/publications/__pycache__/articles.cpython-313.pyc b/server/models/publications/__pycache__/articles.cpython-313.pyc new file mode 100644 index 0000000..8540355 Binary files /dev/null and b/server/models/publications/__pycache__/articles.cpython-313.pyc differ diff --git a/server/models/publications/articles.py b/server/models/publications/articles.py new file mode 100644 index 0000000..793dd29 --- /dev/null +++ b/server/models/publications/articles.py @@ -0,0 +1,146 @@ +import sqlite3 +from dataclasses import dataclass +from typing import Optional, List + +@dataclass +class ArticleModel: + id: Optional[int] = None + title: Optional[str] = None + content: Optional[str] = None + author_id: Optional[int] = None + author_name: Optional[str] = None # To hold joint first_name + last_name + created_at: Optional[str] = None + +class Articles: + def __init__(self, db_path="instance/app_database.db"): + self.db_path = db_path + self._create_articles_table() + + def _create_articles_table(self): + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS articles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + author_id INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (author_id) REFERENCES users (id) + ); + """ + ) + conn.commit() + + def add_article(self, title: str, content: str, author_id: int) -> Optional[int]: + """Insert a new article and return its ID.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO articles (title, content, author_id) + VALUES (?, ?, ?) + """, + (title, content, author_id), + ) + conn.commit() + return cursor.lastrowid + except sqlite3.Error as e: + print(f"Error adding article: {e}") + return None + + def get_article(self, article_id: int) -> Optional[ArticleModel]: + """Fetch a single article by ID, joining users to get the author's name.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT a.id, a.title, a.content, a.author_id, u.first_name, u.last_name, a.created_at + FROM articles a + JOIN users u ON a.author_id = u.id + WHERE a.id = ? + """, + (article_id,), + ) + row = cursor.fetchone() + if not row: + return None + + author_name = f"{row[4] or ''} {row[5] or ''}".strip() or "Autor necunoscut" + return ArticleModel( + id=row[0], + title=row[1], + content=row[2], + author_id=row[3], + author_name=author_name, + created_at=row[6], + ) + except sqlite3.Error as e: + print(f"Error getting article: {e}") + return None + + def get_all_articles(self) -> List[ArticleModel]: + """Fetch all articles ordered by created_at DESC (newest first).""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT a.id, a.title, a.content, a.author_id, u.first_name, u.last_name, a.created_at + FROM articles a + JOIN users u ON a.author_id = u.id + ORDER BY a.created_at DESC + """ + ) + rows = cursor.fetchall() + articles = [] + for row in rows: + author_name = f"{row[4] or ''} {row[5] or ''}".strip() or "Autor necunoscut" + articles.append( + ArticleModel( + id=row[0], + title=row[1], + content=row[2], + author_id=row[3], + author_name=author_name, + created_at=row[6], + ) + ) + return articles + except sqlite3.Error as e: + print(f"Error fetching all articles: {e}") + return [] + + def update_article(self, article_id: int, title: str, content: str) -> bool: + """Update the title and content of an article.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """ + UPDATE articles + SET title = ?, content = ? + WHERE id = ? + """, + (title, content, article_id), + ) + conn.commit() + return cursor.rowcount > 0 + except sqlite3.Error as e: + print(f"Error updating article: {e}") + return False + + def delete_article(self, article_id: int) -> bool: + """Delete an article by ID.""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM articles WHERE id = ?", (article_id,)) + conn.commit() + return cursor.rowcount > 0 + except sqlite3.Error as e: + print(f"Error deleting article: {e}") + return False diff --git a/server/models/users.py b/server/models/users.py index f42bf06..3b83e34 100644 --- a/server/models/users.py +++ b/server/models/users.py @@ -20,6 +20,7 @@ class UserModel: otp_code: Optional[str] = None otp_expiration: Optional[str] = None active: Optional[int] = None + can_create_articles: Optional[int] = 0 class Users: def __init__(self, db_path="instance/app_database.db"): @@ -46,10 +47,15 @@ class Users: created_at DATETIME DEFAULT CURRENT_TIMESTAMP, otp_code TEXT, otp_expiration TIMESTAMPTZ, - active INTEGER DEFAULT 1 + active INTEGER DEFAULT 1, + can_create_articles INTEGER DEFAULT 0 ); """ ) + try: + cursor.execute("ALTER TABLE users ADD COLUMN can_create_articles INTEGER DEFAULT 0;") + except sqlite3.OperationalError: + pass conn.commit() def update_user_otp(self, user_id, otp_code, expiration): @@ -117,7 +123,8 @@ class Users: created_at=row[11], otp_code=row[12], otp_expiration=row[13], - active=row[14] + active=row[14], + can_create_articles=row[15] if len(row) > 15 else 0 ) def register_user(self, email, password, workspace_id): @@ -141,10 +148,10 @@ class Users: cursor = conn.cursor() cursor.execute( """ - INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic, can_create_articles) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, - (user.workspace_id, user.first_name, user.last_name, user.email, user.password, user.address, user.profession, user.role, user.status, user.profile_pic), + (user.workspace_id, user.first_name, user.last_name, user.email, user.password, user.address, user.profession, user.role, user.status, user.profile_pic, user.can_create_articles or 0), ) conn.commit() return cursor.lastrowid @@ -174,7 +181,8 @@ class Users: created_at=row[11], otp_code=row[12], otp_expiration=row[13], - active=row[14] + active=row[14], + can_create_articles=row[15] if len(row) > 15 else 0 ) def get_user_by_email(self, email: str) -> UserModel | None: @@ -201,7 +209,8 @@ class Users: created_at=row[11], otp_code=row[12], otp_expiration=row[13], - active=row[14] + active=row[14], + can_create_articles=row[15] if len(row) > 15 else 0 ) def get_users_by_workspace_id(self, workspace_id): @@ -225,7 +234,8 @@ class Users: created_at=row[11], otp_code=row[12], otp_expiration=row[13], - active=row[14] + active=row[14], + can_create_articles=row[15] if len(row) > 15 else 0 ) for row in rows ] @@ -251,13 +261,14 @@ class Users: created_at=row[11], otp_code=row[12], otp_expiration=row[13], - active=row[14] + active=row[14], + can_create_articles=row[15] if len(row) > 15 else 0 ) for row in rows ] - def update_user(self, user_id, first_name=None, last_name=None, email=None, password = None, address = None, profession = None, role = None, status = None, profile_pic=None, active=None): - if first_name is None and last_name is None and email is None and password is None and address is None and profession is None and role is None and status is None and profile_pic is None and active is None: + def update_user(self, user_id, first_name=None, last_name=None, email=None, password = None, address = None, profession = None, role = None, status = None, profile_pic=None, active=None, can_create_articles=None): + if first_name is None and last_name is None and email is None and password is None and address is None and profession is None and role is None and status is None and profile_pic is None and active is None and can_create_articles is None: return False fields = [] @@ -293,6 +304,9 @@ class Users: if active is not None: fields.append("active = ?") params.append(active) + if can_create_articles is not None: + fields.append("can_create_articles = ?") + params.append(can_create_articles) params.append(user_id) query = f"UPDATE users SET {', '.join(fields)} WHERE id = ?" diff --git a/server/routes/articles.py b/server/routes/articles.py new file mode 100644 index 0000000..4706a54 --- /dev/null +++ b/server/routes/articles.py @@ -0,0 +1,111 @@ +from flask import Blueprint, request, jsonify, render_template +from flask_jwt_extended import jwt_required, get_jwt_identity +from models.publications.articles import Articles, ArticleModel +from models.users import Users +from models.audit import Audit, AuditModel + +articles_bp = Blueprint("articles", __name__) +audit = Audit() + +@articles_bp.route("/editor", methods=["GET"]) +@jwt_required() +def show_editor(): + token = request.args.get("token", "") + article_id = request.args.get("article_id", "None") + return render_template("editor.html", token=token, article_id=article_id) + +@articles_bp.route("/", methods=["GET"]) +@jwt_required() +def get_all_articles(): + articles_repo = Articles() + articles = articles_repo.get_all_articles() + return jsonify([vars(a) for a in articles]), 200 + +@articles_bp.route("/", methods=["GET"]) +@jwt_required() +def get_article(article_id): + articles_repo = Articles() + article = articles_repo.get_article(article_id) + if not article: + return jsonify({"error": "Articolul nu a fost gasit"}), 404 + return jsonify(vars(article)), 200 + +@articles_bp.route("/add", methods=["POST"]) +@jwt_required() +def add_article(): + current_user_id = int(get_jwt_identity()) + user_repo = Users() + user = user_repo.get_user(current_user_id) + + if not user: + return jsonify({"error": "Utilizatorul nu a fost gasit"}), 404 + + # Verifică dacă utilizatorul are permisiunea de a crea articole + if not getattr(user, 'can_create_articles', 0) == 1: + audit.new_entry(AuditModel(user_id=current_user_id, action="Attempt to add article without permission", status="403 - Forbidden")) + return jsonify({"error": "Nu aveti permisiunea de a publica articole"}), 403 + + data = request.get_json() + title = data.get("title") + content = data.get("content") + + if not title or not content: + return jsonify({"error": "Titlul si continutul sunt obligatorii"}), 400 + + articles_repo = Articles() + article_id = articles_repo.add_article(title, content, current_user_id) + + if article_id: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Added article: {title}", status="201 - Created")) + return jsonify({"message": "Articol adaugat cu succes", "id": article_id}), 201 + + return jsonify({"error": "Eroare la adaugarea articolului"}), 500 + +@articles_bp.route("/update/", methods=["PUT"]) +@jwt_required() +def update_article(article_id): + current_user_id = int(get_jwt_identity()) + articles_repo = Articles() + article = articles_repo.get_article(article_id) + + if not article: + return jsonify({"error": "Articolul nu a fost gasit"}), 404 + + # Permite modificarea doar dacă utilizatorul curent este autorul articolului + if article.author_id != current_user_id: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Attempt to update article ID {article_id} owned by other user", status="403 - Forbidden")) + return jsonify({"error": "Puteti modifica doar articolele scrise de dumneavoastra"}), 403 + + data = request.get_json() + title = data.get("title") + content = data.get("content") + + if not title or not content: + return jsonify({"error": "Titlul si continutul sunt obligatorii"}), 400 + + if articles_repo.update_article(article_id, title, content): + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Updated article ID: {article_id}", status="200 - OK")) + return jsonify({"message": "Articol modificat cu succes"}), 200 + + return jsonify({"error": "Nu s-a putut modifica articolul"}), 500 + +@articles_bp.route("/delete/", methods=["DELETE"]) +@jwt_required() +def delete_article(article_id): + current_user_id = int(get_jwt_identity()) + articles_repo = Articles() + article = articles_repo.get_article(article_id) + + if not article: + return jsonify({"error": "Articolul nu a fost gasit"}), 404 + + # Permite ștergerea doar dacă utilizatorul curent este autorul articolului + if article.author_id != current_user_id: + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Attempt to delete article ID {article_id} owned by other user", status="403 - Forbidden")) + return jsonify({"error": "Puteti sterge doar articolele scrise de dumneavoastra"}), 403 + + if articles_repo.delete_article(article_id): + audit.new_entry(AuditModel(user_id=current_user_id, action=f"Deleted article ID: {article_id}", status="200 - OK")) + return jsonify({"message": "Articol sters cu succes"}), 200 + + return jsonify({"error": "Nu s-a putut sterge articolul"}), 500 diff --git a/server/routes/auth.py b/server/routes/auth.py index 2858567..8ee9094 100644 --- a/server/routes/auth.py +++ b/server/routes/auth.py @@ -94,9 +94,7 @@ def verify_code(): return jsonify({"error": "Missing email or verification code"}), 400 user = users.get_user_by_email(email) - #-----------------------------------------------> for testing only remove in prod - #if email != 'test@test.com': - #-----------------------------------------------> for testing only remove in prod + if not user or user.otp_code != code: entry = AuditModel(user_id=user.id, action=f"Attempt to verify code: {email}", status='401 - Invalid code!') audit.new_entry(entry) @@ -235,7 +233,8 @@ def me(): 'profile_pic': user.profile_pic, 'created_at': user.created_at, 'otp_code': user.otp_code, - 'otp_expiration': user.otp_expiration + 'otp_expiration': user.otp_expiration, + 'can_create_articles': user.can_create_articles }), 200 diff --git a/server/routes/documents.py b/server/routes/documents.py index 8857a39..99f8e49 100644 --- a/server/routes/documents.py +++ b/server/routes/documents.py @@ -207,6 +207,8 @@ def add_standard(): # Sursa este folderul unde Flet salvează implicit upload-urile source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) dest_path = os.path.join(DOCUMENTS_ROOT, path) + print(source_path) + print(dest_path) if os.path.exists(source_path): os.makedirs(os.path.dirname(dest_path), exist_ok=True) @@ -265,8 +267,8 @@ def delete_standard(id): def add_custom(): user_id = get_jwt_identity() data = request.get_json() - name = data.get("name") - path = data.get("path") + name = data.get("name", "").strip() + path = data.get("path", "").strip() access = data.get("access") if not name or not path: @@ -274,17 +276,31 @@ def add_custom(): audit.new_entry(entry) return jsonify({"error": "Missing name or path"}), 400 - # Physical file move from upload folder to documents root - source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) - dest_path = os.path.join(DOCUMENTS_ROOT, path) + # Physical file move from upload folder to documents/Custom folder + filename = os.path.basename(path) + # The 'path' variable here is the filename from the client, not the full path. + # We construct the full path for the database entry later. + source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) + dest_dir = os.path.normpath(os.path.join(DOCUMENTS_ROOT, "Custom")) + dest_path = os.path.join(dest_dir, filename) # This is the actual destination path for shutil.move + print(source_path) + print(dest_path) if os.path.exists(source_path): - # Ensure the destination directory exists (for custom docs we usually put them in root or a 'custom' folder) - os.makedirs(os.path.dirname(dest_path), exist_ok=True) + os.makedirs(dest_dir, exist_ok=True) # Ensure the destination directory exists shutil.move(source_path, dest_path) + # Update the 'path' variable to be stored in the database to reflect its new location + db_path = f"Custom/{filename}" + else: + # Log an error and return a response if the source file doesn't exist + error_message = f"Source file not found for custom document: {source_path}" + print(f"ERROR: {error_message}") # For immediate debug visibility + entry = AuditModel(user_id=user_id, action=f"Failed to create custom document (file not found): {name}", status="404 - File not found") + audit.new_entry(entry) + return jsonify({"error": "Uploaded file not found on server."}), 404 docs_custom = DocumentsCustom() - custom = DocumentsCustomModel(user_id=user_id, name=name, path=path, access=access) + custom = DocumentsCustomModel(user_id=user_id, name=name, path=db_path, access=access) # Use db_path here result = docs_custom.new_entry(custom) if result: @@ -292,6 +308,10 @@ def add_custom(): audit.new_entry(entry) return jsonify({"message": "Custom document created successfully", "id": result}), 201 + # If we reach here, it means the DB entry failed after the file was moved. + # This is a potential issue, as the file is moved but not recorded. + entry = AuditModel(user_id=user_id, action=f"Failed to create custom document (DB entry failed): {name}", status="500 - DB error") + audit.new_entry(entry) return jsonify({"error": "Failed to create custom document"}), 500 @documents_bp.route("/customs", methods=["GET"]) diff --git a/server/routes/users.py b/server/routes/users.py index 83dc802..969fcaa 100644 --- a/server/routes/users.py +++ b/server/routes/users.py @@ -86,7 +86,8 @@ def update_user(user_id): role=data.get("role"), status=data.get("status"), profile_pic=data.get("profile_pic"), - active=data.get("active") + active=data.get("active"), + can_create_articles=data.get("can_create_articles") ) if success: diff --git a/server/templates/editor.html b/server/templates/editor.html new file mode 100644 index 0000000..fd54dc4 --- /dev/null +++ b/server/templates/editor.html @@ -0,0 +1,171 @@ + + + + + + Editor Publicație + + + + + + + + +
+
+ + + + +

Adaugă Articol Nou

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + + + + +