mirror of
https://github.com/lucaspalomodevelop/sqlite-analyzer.git
synced 2026-03-13 00:07:27 +00:00
second commit
This commit is contained in:
parent
f5d4f2940f
commit
e779470d41
50
README.md
50
README.md
@ -1,2 +1,48 @@
|
||||
# sqlite-analyzer
|
||||
A web sqlite analyzer
|
||||
# SQLite Analyzer
|
||||
|
||||
This project is a simple web service that allows generating and displaying an ER model for a SQLite database. The ER model is created as a PNG image to visualize the relationships between tables in the database.
|
||||
|
||||
## Requirements
|
||||
|
||||
To run this project, you need:
|
||||
|
||||
- Python 3 installed on your system.
|
||||
- The Python libraries Flask and Graphviz. You can install them via pip:
|
||||
|
||||
```bash
|
||||
pip install Flask graphviz
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
1. Clone or download the project to your local computer.
|
||||
2. Navigate to the project directory.
|
||||
3. Start the application by running the `app.py` file:
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
4. Open a web browser and go to the URL [http://127.0.0.1:5000/](http://127.0.0.1:5000/).
|
||||
5. Upload a SQLite database file (`.db` file extension).
|
||||
6. The ER model will be automatically generated and displayed along with table data.
|
||||
|
||||
## Features
|
||||
|
||||
- **ER Model Generation**: The ER model is automatically generated from the uploaded SQLite database and displayed as a PNG image.
|
||||
- **Display of Table Data**: The data of each table in the uploaded SQLite database is displayed to facilitate quick data inspection.
|
||||
|
||||
## File Structure
|
||||
|
||||
- `app.py`: The main application that creates the web service using Flask.
|
||||
- `uploads/`: The directory where uploaded SQLite database files are stored.
|
||||
- `templates/`: The directory containing HTML templates for the web pages.
|
||||
- `static/`: The directory containing static files such as CSS or images.
|
||||
|
||||
## Contributors
|
||||
|
||||
- **Author**: fingadumbledore
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
||||
|
||||
|
||||
BIN
demo_tables/1000.db
Normal file
BIN
demo_tables/1000.db
Normal file
Binary file not shown.
BIN
demo_tables/20x.db
Normal file
BIN
demo_tables/20x.db
Normal file
Binary file not shown.
BIN
demo_tables/firma.db
Normal file
BIN
demo_tables/firma.db
Normal file
Binary file not shown.
BIN
er_model.png
Normal file
BIN
er_model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
116
main.py
Normal file
116
main.py
Normal file
@ -0,0 +1,116 @@
|
||||
from flask import Flask, render_template, request, send_file
|
||||
import sqlite3
|
||||
import os
|
||||
from graphviz import Digraph
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Verzeichnis für hochgeladene Dateien
|
||||
UPLOAD_FOLDER = 'uploads'
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
|
||||
def create_er_model(database_file, output_file='er_model.png'):
|
||||
"""Erstellt ein ER-Modell für die angegebene SQLite-Datenbank und speichert es als Bild."""
|
||||
conn = sqlite3.connect(database_file)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = c.fetchall()
|
||||
|
||||
# Graphviz-Diagramm erstellen
|
||||
dot = Digraph()
|
||||
|
||||
# Dictionary zur Speicherung der Fremdschlüsselbeziehungen
|
||||
foreign_keys = {}
|
||||
|
||||
# Für jede Tabelle Informationen über Spalten abrufen und dem Diagramm hinzufügen
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
dot.node(table_name, shape='rectangle', color='lightblue2', style='filled')
|
||||
|
||||
# Spalten der Tabelle abrufen
|
||||
c.execute(f"PRAGMA table_info({table_name});")
|
||||
columns = c.fetchall()
|
||||
|
||||
# Jede Spalte als Knoten im Diagramm hinzufügen
|
||||
for column in columns:
|
||||
column_name = column[1]
|
||||
dot.node(f"{table_name}.{column_name}", label=column_name, shape='ellipse')
|
||||
|
||||
# Verbindung von Tabelle zu Spalte erstellen
|
||||
dot.edge(table_name, f"{table_name}.{column_name}")
|
||||
|
||||
# Fremdschlüsselbeziehungen abrufen
|
||||
c.execute(f"PRAGMA foreign_key_list({table_name});")
|
||||
foreign_keys[table_name] = c.fetchall()
|
||||
|
||||
# Fremdschlüsselbeziehungen als Kanten im Diagramm hinzufügen
|
||||
for table, fks in foreign_keys.items():
|
||||
for fk in fks:
|
||||
parent_table = fk[2]
|
||||
parent_column = fk[3]
|
||||
child_table = table
|
||||
child_column = fk[4]
|
||||
dot.edge(f"{parent_table}.{parent_column}", f"{child_table}.{child_column}", label='1..n')
|
||||
|
||||
# ER-Modell als Graphviz-Dot-Datei speichern
|
||||
dot.render(output_file, format='png', cleanup=True)
|
||||
|
||||
# Verbindung schließen
|
||||
conn.close()
|
||||
|
||||
return output_file
|
||||
|
||||
# Route für den Index
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
error = None
|
||||
er_model = None
|
||||
table_data = None
|
||||
|
||||
if request.method == 'POST':
|
||||
# Überprüfen, ob eine Datei hochgeladen wurde
|
||||
if 'file' not in request.files:
|
||||
error = 'Keine Datei hochgeladen'
|
||||
else:
|
||||
file = request.files['file']
|
||||
|
||||
# Überprüfen, ob eine Datei ausgewählt wurde
|
||||
if file.filename == '':
|
||||
error = 'Keine Datei ausgewählt'
|
||||
|
||||
# Überprüfen, ob die Datei eine SQLite-Datenbank ist
|
||||
elif file.filename.endswith('.db'):
|
||||
# Datei speichern
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
|
||||
file.save(file_path)
|
||||
|
||||
# ER-Modell erstellen
|
||||
er_model_file = create_er_model(file_path)
|
||||
os.rename("er_model.png.png", "er_model.png")
|
||||
er_model = er_model_file
|
||||
|
||||
# Tabellendaten abrufen
|
||||
conn = sqlite3.connect(file_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = cursor.fetchall()
|
||||
table_data = {}
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
cursor.execute(f"SELECT * FROM {table_name};")
|
||||
table_data[table_name] = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
else:
|
||||
error = 'Die hochgeladene Datei muss eine SQLite-Datenbankdatei sein'
|
||||
|
||||
# HTML-Seite mit den Daten anzeigen
|
||||
return render_template('index.html', error=error, er_model=er_model, table_data=table_data)
|
||||
|
||||
# Route zum Anzeigen des ER-Modells
|
||||
@app.route('/er_model/<filename>')
|
||||
def show_er_model(filename):
|
||||
return send_file(os.path.abspath(filename), mimetype='image/png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
197
static/css/style.css
Normal file
197
static/css/style.css
Normal file
@ -0,0 +1,197 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f9f9f9;
|
||||
color: #333333;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.dark-mode {
|
||||
background-color: #333333;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: inherit;
|
||||
}
|
||||
body.dark-mode table {
|
||||
background-color: #222222;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
body.dark-mode th {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
body.dark-mode tr:nth-child(even) {
|
||||
background-color: #444444;
|
||||
}
|
||||
|
||||
body.dark-mode tr:nth-child(odd) {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
body.dark-mode td {
|
||||
border-color: #555555;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
animation: fadeIn 0.5s ease;
|
||||
transition: max-height 0.5s ease;
|
||||
max-height: 500px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.closed {
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.dark-mode-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background-color: transparent;
|
||||
font-size: 44px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark-mode-button img {
|
||||
width: 20px;
|
||||
height: auto;
|
||||
}
|
||||
#dropArea {
|
||||
border: 2px dashed #007bff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin: 20px auto;
|
||||
width: 80%;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#dropArea.highlight {
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
#sql_input{
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
background-color: #d4d4d4;
|
||||
color: #201d1d;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#execute_button{
|
||||
background-color:#007bff ;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s ease;
|
||||
margin-bottom: 10px;
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
#execute_button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
43
static/js/index.js
Normal file
43
static/js/index.js
Normal file
@ -0,0 +1,43 @@
|
||||
function handleDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
document.getElementById('dropArea').classList.add('highlight');
|
||||
}
|
||||
|
||||
function handleDragLeave(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById('dropArea').classList.remove('highlight');
|
||||
}
|
||||
|
||||
function handleDrop(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById('dropArea').classList.remove('highlight');
|
||||
const files = event.dataTransfer.files;
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
function handleFiles(files) {
|
||||
document.getElementById('uploadForm').file.files = files;
|
||||
submitForm();
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
document.getElementById('uploadForm').submit();
|
||||
}
|
||||
|
||||
function toggleForm() {
|
||||
var form = document.getElementById('uploadForm');
|
||||
form.classList.toggle('closed');
|
||||
document.getElementById('toggleFormButton').textContent = form.classList.contains('closed') ? 'DB Hochladen' : 'ausblenden';
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
var body = document.body;
|
||||
var button = document.querySelector('.dark-mode-button');
|
||||
body.classList.toggle('dark-mode');
|
||||
if (body.classList.contains('dark-mode')) {
|
||||
button.textContent = '🌞';
|
||||
} else {
|
||||
button.textContent = '🌙';
|
||||
}
|
||||
}
|
||||
66
templates/index.html
Normal file
66
templates/index.html
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='../static/css/style.css') }}">
|
||||
<title>SQL Analyzer</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>SQLITE Analyzer</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="dropArea" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)" ondrop="handleDrop(event)">
|
||||
Drag & Drop Datei hierhin oder <input type="file" id="fileInput" style="display: none;" onchange="handleFiles(this.files)"> klicken, um hochzuladen.
|
||||
</div>
|
||||
<center><button class="button" id="toggleFormButton" onclick="toggleForm()">DB Hochladen</button></center>
|
||||
|
||||
<form id="uploadForm" class="closed" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="file" id="fileInput" onchange="submitForm()">
|
||||
</form>
|
||||
|
||||
{% if error %}
|
||||
<p>{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if table_data %}
|
||||
{% for table_name, data in table_data.items() %}
|
||||
<h2>{{ table_name }}</h2>
|
||||
<table>
|
||||
<tr>
|
||||
{% for column_name in data[0] %}
|
||||
<th>{{ column_name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for row in data %}
|
||||
<tr>
|
||||
{% for value in row %}
|
||||
<td>{{ value }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endfor %}
|
||||
<script>
|
||||
document.getElementById('toggleFormButton').click();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if er_model %}
|
||||
<h2>ER-Modell</h2>
|
||||
<img src="{{ url_for('show_er_model', filename=er_model) }}" alt="ER-Modell">
|
||||
{% endif %}
|
||||
|
||||
<input id="sql_input" type="text" placeholder="SELECT * FROM test;">
|
||||
<center><button id="execute_button">Ausühren</button></center>
|
||||
<footer style="background-color: #007bff; color: #ffffff; text-align: center; width: 100%;">
|
||||
© fingadumbledore 2024 Version 0.1
|
||||
</footer>
|
||||
<button class="dark-mode-button" onclick="toggleDarkMode()">🌙</button>
|
||||
<script src="/static/js/index.js"></script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
124
test.py
Normal file
124
test.py
Normal file
@ -0,0 +1,124 @@
|
||||
from flask import Flask, render_template, request, send_file
|
||||
import sqlite3
|
||||
import os
|
||||
from graphviz import Digraph
|
||||
import threading
|
||||
from werkzeug.serving import WSGIRequestHandler, ThreadedMixIn
|
||||
from werkzeug.debug import DebuggedApplication
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Verzeichnis für hochgeladene Dateien
|
||||
UPLOAD_FOLDER = 'uploads'
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
|
||||
class ThreadedWSGIRequestHandler(WSGIRequestHandler, ThreadedMixIn):
|
||||
pass
|
||||
|
||||
def create_er_model(database_file, output_file='er_model.png'):
|
||||
"""Erstellt ein ER-Modell für die angegebene SQLite-Datenbank und speichert es als Bild."""
|
||||
conn = sqlite3.connect(database_file)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = c.fetchall()
|
||||
|
||||
# Graphviz-Diagramm erstellen
|
||||
dot = Digraph()
|
||||
|
||||
# Dictionary zur Speicherung der Fremdschlüsselbeziehungen
|
||||
foreign_keys = {}
|
||||
|
||||
# Für jede Tabelle Informationen über Spalten abrufen und dem Diagramm hinzufügen
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
dot.node(table_name, shape='rectangle', color='lightblue2', style='filled')
|
||||
|
||||
# Spalten der Tabelle abrufen
|
||||
c.execute(f"PRAGMA table_info({table_name});")
|
||||
columns = c.fetchall()
|
||||
|
||||
# Jede Spalte als Knoten im Diagramm hinzufügen
|
||||
for column in columns:
|
||||
column_name = column[1]
|
||||
dot.node(f"{table_name}.{column_name}", label=column_name, shape='ellipse')
|
||||
|
||||
# Verbindung von Tabelle zu Spalte erstellen
|
||||
dot.edge(table_name, f"{table_name}.{column_name}")
|
||||
|
||||
# Fremdschlüsselbeziehungen abrufen
|
||||
c.execute(f"PRAGMA foreign_key_list({table_name});")
|
||||
foreign_keys[table_name] = c.fetchall()
|
||||
|
||||
# Fremdschlüsselbeziehungen als Kanten im Diagramm hinzufügen
|
||||
for table, fks in foreign_keys.items():
|
||||
for fk in fks:
|
||||
parent_table = fk[2]
|
||||
parent_column = fk[3]
|
||||
child_table = table
|
||||
child_column = fk[4]
|
||||
dot.edge(f"{parent_table}.{parent_column}", f"{child_table}.{child_column}", label='1..n')
|
||||
|
||||
# ER-Modell als Graphviz-Dot-Datei speichern
|
||||
dot.render(output_file, format='png', cleanup=True)
|
||||
|
||||
# Verbindung schließen
|
||||
conn.close()
|
||||
|
||||
return output_file
|
||||
|
||||
def create_er_model_threaded(database_file, output_file='er_model.png'):
|
||||
"""Funktion zum Erstellen des ER-Modells in einem separaten Thread."""
|
||||
threading.Thread(target=create_er_model, args=(database_file, output_file)).start()
|
||||
|
||||
# Route für den Index
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
error = None
|
||||
er_model = None
|
||||
table_data = None
|
||||
|
||||
if request.method == 'POST':
|
||||
# Überprüfen, ob eine Datei hochgeladen wurde
|
||||
if 'file' not in request.files:
|
||||
error = 'Keine Datei hochgeladen'
|
||||
else:
|
||||
file = request.files['file']
|
||||
|
||||
# Überprüfen, ob eine Datei ausgewählt wurde
|
||||
if file.filename == '':
|
||||
error = 'Keine Datei ausgewählt'
|
||||
|
||||
# Überprüfen, ob die Datei eine SQLite-Datenbank ist
|
||||
elif file.filename.endswith('.db'):
|
||||
# Datei speichern
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
|
||||
file.save(file_path)
|
||||
|
||||
# ER-Modell erstellen im separaten Thread
|
||||
create_er_model_threaded(file_path)
|
||||
|
||||
# Tabellendaten abrufen
|
||||
conn = sqlite3.connect(file_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
||||
tables = cursor.fetchall()
|
||||
table_data = {}
|
||||
for table in tables:
|
||||
table_name = table[0]
|
||||
cursor.execute(f"SELECT * FROM {table_name};")
|
||||
table_data[table_name] = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
else:
|
||||
error = 'Die hochgeladene Datei muss eine SQLite-Datenbankdatei sein'
|
||||
|
||||
# HTML-Seite mit den Daten anzeigen
|
||||
return render_template('index.html', error=error, er_model=er_model, table_data=table_data)
|
||||
|
||||
# Route zum Anzeigen des ER-Modells
|
||||
@app.route('/er_model/<filename>')
|
||||
def show_er_model(filename):
|
||||
return send_file(os.path.abspath(filename), mimetype='image/png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, threaded=True, request_handler=ThreadedWSGIRequestHandler)
|
||||
BIN
uploads/1000.db
Normal file
BIN
uploads/1000.db
Normal file
Binary file not shown.
BIN
uploads/20x.db
Normal file
BIN
uploads/20x.db
Normal file
Binary file not shown.
BIN
uploads/example.db
Normal file
BIN
uploads/example.db
Normal file
Binary file not shown.
BIN
uploads/firma.db
Normal file
BIN
uploads/firma.db
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user