335 lines
13 KiB
Python
335 lines
13 KiB
Python
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
|
|
)
|