add article and pubications

This commit is contained in:
2026-06-25 10:30:24 +03:00
parent 7fa8a9b7fc
commit 7206a0a0c5
25 changed files with 1180 additions and 86 deletions

View File

@@ -15,7 +15,7 @@ class DocumentsStatus:
mapping = { mapping = {
DocumentsStatus.NEW: 'Nou', DocumentsStatus.NEW: 'Nou',
DocumentsStatus.ANALISE: 'Analiza', DocumentsStatus.ANALISE: 'Analiza',
DocumentsStatus.IN_PROGRESS: 'In progres', DocumentsStatus.IN_PROGRESS: 'In lucru',
DocumentsStatus.WAITING_FOR_PAYMENT: 'Asteptam plata', DocumentsStatus.WAITING_FOR_PAYMENT: 'Asteptam plata',
DocumentsStatus.COMPLETED: 'Complet', DocumentsStatus.COMPLETED: 'Complet',
DocumentsStatus.CANCELED: 'Anulat' DocumentsStatus.CANCELED: 'Anulat'

View File

@@ -1,5 +1,6 @@
import flet as ft import flet as ft
from pages.documents.ba import Documents from pages.documents.ba import Documents
from pages.publications.articles import Articles as ExpertArticlesPage
from pages.settings.settings import Settings from pages.settings.settings import Settings
class NavigationBA: class NavigationBA:
@@ -13,8 +14,8 @@ class NavigationBA:
label="Documente Custom", label="Documente Custom",
) )
self.articole_si_publicatii = ft.NavigationRailDestination( self.articole_si_publicatii = ft.NavigationRailDestination(
icon=ft.Icons.ARTICLE_OUTLINED, icon=ft.Icons.MENU_BOOK_OUTLINED, # Icon mai potrivit pentru articole/blog
selected_icon=ft.Icons.ARTICLE, selected_icon=ft.Icons.MENU_BOOK,
label="Articole si Publicatii", label="Articole si Publicatii",
) )
self.comunicare = ft.NavigationRailDestination( self.comunicare = ft.NavigationRailDestination(
@@ -61,12 +62,7 @@ class NavigationBA:
def build(self): def build(self):
return [ return [
self.documente_juridice, self.documente_juridice,
# self.articole_si_publicatii, self.articole_si_publicatii,
# self.comunicare,
# self.consultanta,
# self.convocator,
# self.licitatii_si_lucrari,
# self.profil,
self.abonamente, self.abonamente,
self.setari, self.setari,
self.logout self.logout
@@ -76,21 +72,31 @@ class NavigationBA:
self.docs = Documents(self.page, self.home) self.docs = Documents(self.page, self.home)
return self.docs.build() 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): async def on_nav_change(self, e):
print( "Selected destination:", e.control.selected_index) print( "Selected destination:", e.control.selected_index)
if e.control.selected_index == 0: if e.control.selected_index == 0:
self.home.placeholder.content = self.build_documente_juridice() self.home.placeholder.content = self.build_documente_juridice()
self.page.update() self.page.update()
elif e.control.selected_index == 1: # Index for Articole si Publicatii
if e.control.selected_index == 1: self.home.placeholder.content = self.build_articole_si_publicatii_expert()
pass self.page.update()
elif e.control.selected_index == 2: # Abonamente
if e.control.selected_index == 2: self.home.placeholder.content = self.build_subscriptions()
self.page.update()
elif e.control.selected_index == 3: # Setari
self.settings = Settings(self.page, self) self.settings = Settings(self.page, self)
self.home.placeholder.content = self.settings.build() self.home.placeholder.content = self.settings.build()
self.page.update() # Actualizează întreaga pagină inclusiv overlay-ul self.page.update() # Actualizează întreaga pagină inclusiv overlay-ul
elif e.control.selected_index == 4: # Logout
if e.control.selected_index == 3:
await ft.SharedPreferences().clear() await ft.SharedPreferences().clear()
self.page.session.store.clear() self.page.session.store.clear()
self.page.go('/auth') self.page.go('/auth')

View File

@@ -1,5 +1,6 @@
import flet as ft import flet as ft
from pages.documents.home import DocumentsHome from pages.documents.home import DocumentsHome
from pages.publications.articles import Articles as ClientArticlesPage
class NavigationUser: class NavigationUser:
def __init__(self, page: ft.Page, home): def __init__(self, page: ft.Page, home):
@@ -12,8 +13,8 @@ class NavigationUser:
label="Documente Juridice", label="Documente Juridice",
) )
self.articole_si_publicatii = ft.NavigationRailDestination( self.articole_si_publicatii = ft.NavigationRailDestination(
icon=ft.Icons.ARTICLE_OUTLINED, icon=ft.Icons.MENU_BOOK_OUTLINED, # Icon mai potrivit pentru articole/blog
selected_icon=ft.Icons.ARTICLE, selected_icon=ft.Icons.MENU_BOOK,
label="Articole si Publicatii", label="Articole si Publicatii",
) )
self.comunicare = ft.NavigationRailDestination( self.comunicare = ft.NavigationRailDestination(
@@ -75,19 +76,23 @@ class NavigationUser:
self.docs = DocumentsHome(self.page, self.home) self.docs = DocumentsHome(self.page, self.home)
return self.docs.build() 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): async def on_nav_change(self, e):
print( "Selected destination:", e.control.selected_index) print( "Selected destination:", e.control.selected_index)
if e.control.selected_index == 0: if e.control.selected_index == 0:
self.home.placeholder.content = self.build_documente_juridice() self.home.placeholder.content = self.build_documente_juridice()
self.page.update() self.page.update()
elif e.control.selected_index == 1: # Index for Articole si Publicatii
if e.control.selected_index == 1: self.home.placeholder.content = self.build_articole_si_publicatii_client()
pass self.page.update()
if e.control.selected_index == 2: if e.control.selected_index == 2:
pass pass
if e.control.selected_index == 3: if e.control.selected_index == 9:
await ft.SharedPreferences().clear() await ft.SharedPreferences().clear()
self.page.session.store.clear() self.page.session.store.clear()
self.page.go('/auth') self.page.go('/auth')

View File

@@ -47,6 +47,7 @@ class ForgotPassword:
self.go_to_login self.go_to_login
] ]
) )
self.verifica_btn = ft.Button("Verifica", width=150, on_click=self.verfy_code)
def _generate_numeric_code(self) -> str: def _generate_numeric_code(self) -> str:
digits = string.digits digits = string.digits
@@ -58,7 +59,7 @@ class ForgotPassword:
self.main_column.controls.append(self.title) self.main_column.controls.append(self.title)
self.main_column.controls.append(self.inserted_code) self.main_column.controls.append(self.inserted_code)
self.main_column.controls.append(self.error_message) 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() self.main_column.update()
#print(self.otp_code) #print(self.otp_code)
send_gmail( send_gmail(

View File

@@ -1,7 +1,5 @@
import flet as ft import flet as ft
import requests import requests
from navigation.ba import NavigationBA from navigation.ba import NavigationBA
from navigation.user import NavigationUser from navigation.user import NavigationUser

View File

@@ -4,6 +4,7 @@ from datetime import datetime
from helpers.document_status import DocumentsStatus from helpers.document_status import DocumentsStatus
from helpers.emails import send_gmail from helpers.emails import send_gmail
from dataclasses import dataclass, field from dataclasses import dataclass, field
import asyncio
@dataclass @dataclass
class State: class State:
@@ -11,6 +12,7 @@ class State:
file_picker: ft.FilePicker | None = None file_picker: ft.FilePicker | None = None
picked_files: list[ft.FilePickerFile] = field(default_factory=list) picked_files: list[ft.FilePickerFile] = field(default_factory=list)
state = State() state = State()
class Documents: class Documents:
@@ -21,11 +23,11 @@ class Documents:
self.token = self.page.session.store.get('token') self.token = self.page.session.store.get('token')
self.user = self.page.session.store.get('user') self.user = self.page.session.store.get('user')
self.user_id = self.user['id'] if self.user else None self.user_id = self.user['id'] if self.user else None
self.documenet_title = None
self.all_requests = [] self.all_requests = []
self.current_selected_request = None self.current_selected_request = None
# Elemente interfață: Căutare și Listă # Elemente interfață: Căutare și Listă
self.search_bar = ft.TextField( self.search_bar = ft.TextField(
label="Căutare solicitări (Text solicitare sau comentariu)", 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.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.details_panel = ft.Column(
[ [
self.req_id_text, self.req_id_text,
@@ -108,15 +115,23 @@ class Documents:
]), ]),
ft.Divider(), ft.Divider(),
ft.Text("Finalizare și Încărcare Document:", weight=ft.FontWeight.BOLD), ft.Text("Finalizare și Încărcare Document:", weight=ft.FontWeight.BOLD),
ft.Row([
ft.FilledButton( ft.Row(
"Încarcă Document Final", [
icon=ft.Icons.UPLOAD_FILE, ft.Row([
on_click=self._handle_file_upload, ft.FilledButton(
bgcolor=ft.Colors.GREEN_700 "Încarcă Document Final",
), icon=ft.Icons.UPLOAD_FILE,
self.doc_id_info 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, visible=False,
expand=True, expand=True,
@@ -301,8 +316,8 @@ class Documents:
print(f"Update failed: {e}") print(f"Update failed: {e}")
return False return False
async def _handle_file_upload(self, e): async def _handle_file_upload(self, e: ft.Event[ft.Button]):
if not self.current_selected_request: return print('File uploaded')
try: try:
state.file_picker = ft.FilePicker() state.file_picker = ft.FilePicker()
files = await state.file_picker.pick_files(allow_multiple=False) files = await state.file_picker.pick_files(allow_multiple=False)
@@ -310,36 +325,69 @@ class Documents:
state.picked_files = files state.picked_files = files
uploaded_file_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{state.picked_files[0].name}" uploaded_file_name = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{state.picked_files[0].name}"
await state.file_picker.upload( await state.file_picker.upload(
files=[ files=[
ft.FilePickerUploadFile( ft.FilePickerUploadFile(
name=file.name, name=file.name,
upload_url=self.page.get_upload_url(uploaded_file_name, 60), upload_url=self.page.get_upload_url(uploaded_file_name, 60),
) )
for file in state.picked_files 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( if reg_resp.status_code == 201:
f"{self.base_url}/documents/customs/add", doc_id = reg_resp.json().get('id')
json={"name": f"Document Final Solicitare #{self.current_selected_request['id']}", "path": uploaded_file_name}, # 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}'} headers={'Authorization': f'Bearer {self.token}'}
) )
if response.status_code == 200:
if reg_resp.status_code == 201: document_data = response.json()
doc_id = reg_resp.json().get('id') document_path = document_data.get('path')
# 3. Legare document de solicitare și marcare ca finalizat if document_path:
self._update_request_api({ download_url = f"{self.base_url}/documents/download?path={document_path}&token={self.token}"
"document_id": doc_id, await self.page.launch_url(download_url)
"status": DocumentsStatus.COMPLETED, else:
"expert_id": self.user_id, 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: 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): def build(self):
return ft.Container( return ft.Container(

View File

@@ -73,6 +73,13 @@ class Documents:
disabled=True, # Will be enabled based on status disabled=True, # Will be enabled based on status
visible=False # Initially hidden 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( self.comment_text_field = ft.TextField(
label="Adauga un comentariu", label="Adauga un comentariu",
multiline=True, multiline=True,
@@ -96,11 +103,13 @@ class Documents:
ft.Text("Pret:", weight=ft.FontWeight.BOLD), ft.Text("Pret:", weight=ft.FontWeight.BOLD),
self.request_price_text, self.request_price_text,
self.pay_button, self.pay_button,
self.download_button,
ft.Divider(), ft.Divider(),
ft.Text("Adauga Comentariu:", weight=ft.FontWeight.BOLD), ft.Text("Adauga Comentariu:", weight=ft.FontWeight.BOLD),
ft.Row([self.comment_text_field, self.add_comment_button]), ft.Row([self.comment_text_field, self.add_comment_button]),
], ],
expand=True, expand=True,
scroll=ft.ScrollMode.ADAPTIVE,
visible=False # Initially hidden visible=False # Initially hidden
) )
@@ -186,6 +195,14 @@ class Documents:
self.pay_button.visible = False self.pay_button.visible = False
self.pay_button.disabled = True 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 # Enable comment section
self.add_comment_button.disabled = False self.add_comment_button.disabled = False
self.comment_text_field.disabled = False self.comment_text_field.disabled = False
@@ -308,6 +325,36 @@ class Documents:
)) ))
self.page.update() 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): def _close_dialog(self, e):
self.page.pop_dialog() self.page.pop_dialog()
self.page.update() self.page.update()

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -2,6 +2,7 @@ import flet as ft
from pages.settings.documente_juridice import DocumenteJuridice from pages.settings.documente_juridice import DocumenteJuridice
from pages.settings.users import UsersSettings from pages.settings.users import UsersSettings
from pages.settings.payment_and_subscription import PaymentAndSubscription from pages.settings.payment_and_subscription import PaymentAndSubscription
from pages.settings.articles import ArticlesSettings
class Settings: class Settings:
def __init__(self, page: ft.Page, home): def __init__(self, page: ft.Page, home):
@@ -11,6 +12,7 @@ class Settings:
self.doc_juridice = DocumenteJuridice(self.page) self.doc_juridice = DocumenteJuridice(self.page)
self.users_settings = UsersSettings(self.page) self.users_settings = UsersSettings(self.page)
self.payment_and_subscription = PaymentAndSubscription(self.page) self.payment_and_subscription = PaymentAndSubscription(self.page)
self.articles_settings = ArticlesSettings(self.page)
def build(self): def build(self):
return ft.Tabs( return ft.Tabs(
@@ -42,8 +44,9 @@ class Settings:
expand=True expand=True
), ),
ft.Container( ft.Container(
content=ft.Text("This is Tab 2"), content=self.articles_settings.build(),
alignment=ft.Alignment.CENTER, alignment=ft.Alignment.CENTER,
expand=True
), ),
ft.Container( ft.Container(
content=ft.Text("This is Tab 3"), content=ft.Text("This is Tab 3"),

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

View File

@@ -8,6 +8,7 @@ from routes.documents import documents_bp
from routes.users import users_bp from routes.users import users_bp
from routes.payments import payments_bp from routes.payments import payments_bp
from routes.subscriptions import subscriptions_bp from routes.subscriptions import subscriptions_bp
from routes.articles import articles_bp
def create_app(test_config=None): def create_app(test_config=None):
# create and configure the app # 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(users_bp, url_prefix="/users")
app.register_blueprint(payments_bp, url_prefix="/payments") app.register_blueprint(payments_bp, url_prefix="/payments")
app.register_blueprint(subscriptions_bp, url_prefix="/subscriptions") app.register_blueprint(subscriptions_bp, url_prefix="/subscriptions")
app.register_blueprint(articles_bp, url_prefix="/articles")
return app return app

Binary file not shown.

View File

@@ -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

View File

@@ -20,6 +20,7 @@ class UserModel:
otp_code: Optional[str] = None otp_code: Optional[str] = None
otp_expiration: Optional[str] = None otp_expiration: Optional[str] = None
active: Optional[int] = None active: Optional[int] = None
can_create_articles: Optional[int] = 0
class Users: class Users:
def __init__(self, db_path="instance/app_database.db"): def __init__(self, db_path="instance/app_database.db"):
@@ -46,10 +47,15 @@ class Users:
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
otp_code TEXT, otp_code TEXT,
otp_expiration TIMESTAMPTZ, 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() conn.commit()
def update_user_otp(self, user_id, otp_code, expiration): def update_user_otp(self, user_id, otp_code, expiration):
@@ -117,7 +123,8 @@ class Users:
created_at=row[11], created_at=row[11],
otp_code=row[12], otp_code=row[12],
otp_expiration=row[13], 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): def register_user(self, email, password, workspace_id):
@@ -141,10 +148,10 @@ class Users:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
""" """
INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic) INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic, can_create_articles)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 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() conn.commit()
return cursor.lastrowid return cursor.lastrowid
@@ -174,7 +181,8 @@ class Users:
created_at=row[11], created_at=row[11],
otp_code=row[12], otp_code=row[12],
otp_expiration=row[13], 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: def get_user_by_email(self, email: str) -> UserModel | None:
@@ -201,7 +209,8 @@ class Users:
created_at=row[11], created_at=row[11],
otp_code=row[12], otp_code=row[12],
otp_expiration=row[13], 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): def get_users_by_workspace_id(self, workspace_id):
@@ -225,7 +234,8 @@ class Users:
created_at=row[11], created_at=row[11],
otp_code=row[12], otp_code=row[12],
otp_expiration=row[13], 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 for row in rows
] ]
@@ -251,13 +261,14 @@ class Users:
created_at=row[11], created_at=row[11],
otp_code=row[12], otp_code=row[12],
otp_expiration=row[13], 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 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): 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: 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 return False
fields = [] fields = []
@@ -293,6 +304,9 @@ class Users:
if active is not None: if active is not None:
fields.append("active = ?") fields.append("active = ?")
params.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) params.append(user_id)
query = f"UPDATE users SET {', '.join(fields)} WHERE id = ?" query = f"UPDATE users SET {', '.join(fields)} WHERE id = ?"

111
server/routes/articles.py Normal file
View File

@@ -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("/<int:article_id>", 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/<int:article_id>", 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/<int:article_id>", 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

View File

@@ -94,9 +94,7 @@ def verify_code():
return jsonify({"error": "Missing email or verification code"}), 400 return jsonify({"error": "Missing email or verification code"}), 400
user = users.get_user_by_email(email) 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: 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!') entry = AuditModel(user_id=user.id, action=f"Attempt to verify code: {email}", status='401 - Invalid code!')
audit.new_entry(entry) audit.new_entry(entry)
@@ -235,7 +233,8 @@ def me():
'profile_pic': user.profile_pic, 'profile_pic': user.profile_pic,
'created_at': user.created_at, 'created_at': user.created_at,
'otp_code': user.otp_code, 'otp_code': user.otp_code,
'otp_expiration': user.otp_expiration 'otp_expiration': user.otp_expiration,
'can_create_articles': user.can_create_articles
}), 200 }), 200

View File

@@ -207,6 +207,8 @@ def add_standard():
# Sursa este folderul unde Flet salvează implicit upload-urile # Sursa este folderul unde Flet salvează implicit upload-urile
source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path))
dest_path = os.path.join(DOCUMENTS_ROOT, path) dest_path = os.path.join(DOCUMENTS_ROOT, path)
print(source_path)
print(dest_path)
if os.path.exists(source_path): if os.path.exists(source_path):
os.makedirs(os.path.dirname(dest_path), exist_ok=True) os.makedirs(os.path.dirname(dest_path), exist_ok=True)
@@ -265,8 +267,8 @@ def delete_standard(id):
def add_custom(): def add_custom():
user_id = get_jwt_identity() user_id = get_jwt_identity()
data = request.get_json() data = request.get_json()
name = data.get("name") name = data.get("name", "").strip()
path = data.get("path") path = data.get("path", "").strip()
access = data.get("access") access = data.get("access")
if not name or not path: if not name or not path:
@@ -274,17 +276,31 @@ def add_custom():
audit.new_entry(entry) audit.new_entry(entry)
return jsonify({"error": "Missing name or path"}), 400 return jsonify({"error": "Missing name or path"}), 400
# Physical file move from upload folder to documents root # Physical file move from upload folder to documents/Custom folder
source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path)) filename = os.path.basename(path)
dest_path = os.path.join(DOCUMENTS_ROOT, 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): 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(dest_dir, exist_ok=True) # Ensure the destination directory exists
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.move(source_path, dest_path) 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() 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) result = docs_custom.new_entry(custom)
if result: if result:
@@ -292,6 +308,10 @@ def add_custom():
audit.new_entry(entry) audit.new_entry(entry)
return jsonify({"message": "Custom document created successfully", "id": result}), 201 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 return jsonify({"error": "Failed to create custom document"}), 500
@documents_bp.route("/customs", methods=["GET"]) @documents_bp.route("/customs", methods=["GET"])

View File

@@ -86,7 +86,8 @@ def update_user(user_id):
role=data.get("role"), role=data.get("role"),
status=data.get("status"), status=data.get("status"),
profile_pic=data.get("profile_pic"), profile_pic=data.get("profile_pic"),
active=data.get("active") active=data.get("active"),
can_create_articles=data.get("can_create_articles")
) )
if success: if success:

View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor Publicație</title>
<!-- EasyMDE CSS -->
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f3f4f6;
padding: 30px 10px;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
.editor-card {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 35px;
border-radius: 16px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
border: 1px solid #e5e7eb;
}
.editor-toolbar {
border: 1px solid #d1d5db !important;
border-radius: 8px 8px 0 0 !important;
background-color: #f9fafb;
}
.CodeMirror {
border: 1px solid #d1d5db !important;
border-radius: 0 0 8px 8px !important;
min-height: 400px;
font-size: 15px;
}
.form-control:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
}
</style>
</head>
<body>
<div class="editor-card">
<div class="d-flex align-items-center mb-4">
<svg class="text-primary me-2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
</svg>
<h2 class="m-0 fw-bold text-dark" id="editor-title-label">Adaugă Articol Nou</h2>
</div>
<div class="mb-4">
<label for="title" class="form-label fw-bold text-secondary">Titlu Articol</label>
<input type="text" class="form-control form-control-lg border-2" id="title" placeholder="Introduceți un titlu captivant...">
</div>
<div class="mb-4">
<label for="content" class="form-label fw-bold text-secondary">Conținut Publicație</label>
<textarea id="content"></textarea>
</div>
<div class="d-flex justify-content-end gap-3">
<button class="btn btn-light border btn-lg px-4" onclick="window.close()">Renunță</button>
<button class="btn btn-primary btn-lg px-5" id="save-btn" onclick="saveArticle()">Salvează</button>
</div>
<div class="alert mt-4 d-none" id="status-alert" role="alert"></div>
</div>
<!-- EasyMDE JS -->
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script>
// Inițializare editor Markdown
const easyMDE = new EasyMDE({
element: document.getElementById('content'),
spellChecker: false,
placeholder: "Scrieți conținutul publicației folosind formatarea din bara de sus...",
autosave: {
enabled: true,
uniqueId: "article_editor_autosave_temp",
delay: 2000,
},
toolbar: [
"bold", "italic", "strikethrough", "|",
"heading-1", "heading-2", "heading-3", "|",
"quote", "unordered-list", "ordered-list", "|",
"link", "image", "table", "horizontal-rule", "|",
"preview", "side-by-side", "fullscreen", "|",
"guide"
]
});
// Preluare parametri din șablonul Jinja
const articleId = "{{ article_id }}";
const token = "{{ token }}";
// Dacă avem ID de articol, înseamnă că suntem în modul editare
if (articleId && articleId !== "None") {
document.getElementById('editor-title-label').innerText = "Editează Articolul";
// Încarcă datele articolului de pe server
fetch(`/articles/${articleId}`, {
headers: { 'Authorization': `Bearer ${token}` }
})
.then(res => {
if (!res.ok) throw new Error("Nu s-au putut încărca datele articolului.");
return res.json();
})
.then(data => {
if (data.title) document.getElementById('title').value = data.title;
if (data.content) easyMDE.value(data.content);
})
.catch(err => {
showStatus(err.message, "danger");
});
}
function showStatus(message, type) {
const alertDiv = document.getElementById('status-alert');
alertDiv.className = `alert alert-${type} mt-4`;
alertDiv.innerText = message;
alertDiv.classList.remove('d-none');
}
function saveArticle() {
const title = document.getElementById('title').value.trim();
const content = easyMDE.value().trim();
if (!title || !content) {
showStatus("Titlul și conținutul sunt obligatorii!", "danger");
return;
}
const payload = { title, content };
const url = (articleId && articleId !== "None") ? `/articles/update/${articleId}` : '/articles/add';
const method = (articleId && articleId !== "None") ? 'PUT' : 'POST';
const saveBtn = document.getElementById('save-btn');
saveBtn.disabled = true;
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
})
.then(async res => {
const data = await res.json();
if (res.status === 200 || res.status === 201) {
showStatus("Publicația a fost salvată cu succes! Această fereastră se va închide...", "success");
// Șterge salvările automate temporare
easyMDE.clearAutosavedValue();
setTimeout(() => {
window.close();
}, 1500);
} else {
throw new Error(data.error || "A apărut o eroare la salvare.");
}
})
.catch(err => {
showStatus(err.message, "danger");
saveBtn.disabled = false;
});
}
</script>
</body>
</html>