diff --git a/.dockerignore b/.dockerignore
index d7d4b6b..cd49c72 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -7,6 +7,7 @@ db/
*.pyc
__pycache__
django/importer/uploadeddata/*
+django/attachments/attachment/*
_scratches
exports.sh
.last_git_commit
diff --git a/.gitignore b/.gitignore
index 0c41d79..9ca9901 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,9 +5,11 @@ labelbase.log
labelbase.log.*
bgt.log
db/
+backup_*
*.pyc
__pycache__
django/importer/uploadeddata/*
+django/attachments/attachment/*
#_scratches
exports.sh
.last_git_commit
diff --git a/BACKUP_AND_MIGRATION_GUIDE.md b/BACKUP_AND_MIGRATION_GUIDE.md
new file mode 100644
index 0000000..59b71b8
--- /dev/null
+++ b/BACKUP_AND_MIGRATION_GUIDE.md
@@ -0,0 +1,896 @@
+# Labelbase Backup and Migration Guide
+
+A comprehensive guide for safely backing up your Labelbase database and running Django migrations.
+
+## Table of Contents
+- [Why Backup?](#why-backup)
+- [Quick Backup](#quick-backup)
+- [Automated Backup Script](#automated-backup-script)
+- [Running Migrations Safely](#running-migrations-safely)
+- [Upgrading Labelbase](#upgrading-labelbase)
+- [Restoring from Backup](#restoring-from-backup)
+- [Scheduled Backups](#scheduled-backups)
+- [Best Practices](#best-practices)
+
+---
+
+## Why Backup?
+
+**Always backup before running migrations!** Migrations can:
+- Alter database table structures in irreversible ways
+- Delete data if there are bugs in the migration code
+- Fail mid-execution, leaving your database inconsistent
+- Introduce conflicts with existing data
+
+A backup takes 30 seconds. Recovery without one could take hours or days.
+
+---
+
+## Quick Backup
+
+From your Labelbase directory:
+
+```bash
+# Navigate to Labelbase directory
+cd Labelbase
+
+# Source environment variables
+source exports.sh
+
+# Create backup with timestamp
+docker-compose exec -T labelbase_mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" labelbase > backup_$(date +%Y%m%d_%H%M%S).sql
+```
+
+This creates a backup file like `backup_20250119_143022.sql`.
+
+### Backup config.ini (Important!)
+
+The `config.ini` file contains encryption keys and other critical settings. Always back it up too:
+
+```bash
+# Navigate to Labelbase directory
+cd Labelbase
+
+# Source environment variables
+source exports.sh
+
+# Create backup with timestamp
+docker-compose exec -T labelbase_django cat /app/config.ini > backup_$(date +%Y%m%d_%H%M%S)_config.ini
+```
+
+This creates a backup file like `backup_20250119_143022_config.ini`.
+
+---
+
+## Automated Backup Script
+
+Create a reusable backup script that handles everything automatically.
+
+### Create the Script
+
+Save this as `backup-labelbase.sh` in your Labelbase directory:
+
+```bash
+#!/bin/bash
+
+# Labelbase Database Backup Script
+# Usage: ./backup-labelbase.sh
+
+# Configuration
+LABELBASE_DIR="/path/to/Labelbase" # CHANGE THIS to your actual path
+BACKUP_DIR="$LABELBASE_DIR/backups"
+KEEP_BACKUPS=10 # Number of backups to keep
+
+# Colors for output
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Change to Labelbase directory
+cd "$LABELBASE_DIR" || exit 1
+
+# Source environment variables for MySQL passwords
+if [ ! -f "exports.sh" ]; then
+ echo -e "${RED}✗ Error: exports.sh not found!${NC}"
+ echo "Make sure you're in the Labelbase directory and exports.sh exists."
+ exit 1
+fi
+
+source exports.sh
+
+# Check if MySQL password is set
+if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
+ echo -e "${RED}✗ Error: MYSQL_ROOT_PASSWORD not set!${NC}"
+ echo "Make sure exports.sh contains the MySQL password."
+ exit 1
+fi
+
+# Create backup directory if it doesn't exist
+mkdir -p "$BACKUP_DIR"
+
+# Generate backup filename with timestamp
+TIMESTAMP=$(date +%Y%m%d_%H%M%S)
+BACKUP_FILE="$BACKUP_DIR/labelbase_backup_$TIMESTAMP.sql"
+CONFIG_BACKUP_FILE="$BACKUP_DIR/config_backup_$TIMESTAMP.ini"
+
+echo -e "${YELLOW}Starting backup...${NC}"
+echo "Database backup: $BACKUP_FILE"
+echo "Config backup: $CONFIG_BACKUP_FILE"
+
+# Backup config.ini first (contains encryption keys!)
+echo "Backing up config.ini..."
+docker-compose exec -T labelbase_django cat /app/config.ini > "$CONFIG_BACKUP_FILE"
+
+if [ $? -eq 0 ] && [ -s "$CONFIG_BACKUP_FILE" ]; then
+ echo -e "${GREEN}✓ Config backup successful${NC}"
+else
+ echo -e "${YELLOW}⚠ Config backup failed or file is empty${NC}"
+fi
+
+# Create database backup
+docker-compose exec -T labelbase_mysql mysqldump \
+ -u root \
+ -p"${MYSQL_ROOT_PASSWORD}" \
+ --single-transaction \
+ --quick \
+ --lock-tables=false \
+ labelbase > "$BACKUP_FILE"
+
+# Check if backup was successful
+if [ $? -eq 0 ] && [ -s "$BACKUP_FILE" ]; then
+ echo -e "${GREEN}✓ Backup successful!${NC}"
+
+ # Get file size
+ SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
+ echo "Backup size: $SIZE"
+
+ # Compress backup to save space
+ echo "Compressing backup..."
+ gzip "$BACKUP_FILE"
+
+ if [ $? -eq 0 ]; then
+ COMPRESSED_SIZE=$(du -h "${BACKUP_FILE}.gz" | cut -f1)
+ echo -e "${GREEN}✓ Compressed to: $COMPRESSED_SIZE${NC}"
+ echo "Backup location: ${BACKUP_FILE}.gz"
+ else
+ echo -e "${YELLOW}⚠ Compression failed, keeping uncompressed backup${NC}"
+ fi
+
+ # Clean up old backups (keep only last N backups)
+ echo "Cleaning up old backups (keeping last $KEEP_BACKUPS)..."
+ BACKUP_COUNT=$(ls -1 "$BACKUP_DIR"/labelbase_backup_*.sql.gz 2>/dev/null | wc -l)
+
+ if [ "$BACKUP_COUNT" -gt "$KEEP_BACKUPS" ]; then
+ ls -t "$BACKUP_DIR"/labelbase_backup_*.sql.gz | tail -n +$((KEEP_BACKUPS + 1)) | xargs -r rm
+ echo -e "${GREEN}✓ Cleaned up old backups${NC}"
+ else
+ echo "No cleanup needed ($BACKUP_COUNT backups exist)"
+ fi
+
+ echo ""
+ echo -e "${GREEN}=== Backup Complete ===${NC}"
+ echo "Database: ${BACKUP_FILE}.gz"
+ echo "Config: ${CONFIG_BACKUP_FILE}"
+
+else
+ echo -e "${RED}✗ Backup failed!${NC}"
+
+ # Remove empty or failed backup file
+ [ -f "$BACKUP_FILE" ] && rm "$BACKUP_FILE"
+
+ echo "Troubleshooting:"
+ echo "1. Check if MySQL container is running: docker-compose ps"
+ echo "2. Check MySQL logs: docker-compose logs labelbase_mysql"
+ echo "3. Verify password in exports.sh"
+ exit 1
+fi
+```
+
+### Make Script Executable
+
+```bash
+chmod +x backup-labelbase.sh
+```
+
+### Edit Configuration
+
+Open `backup-labelbase.sh` and change this line to your actual Labelbase path:
+
+```bash
+LABELBASE_DIR="/path/to/Labelbase" # CHANGE THIS!
+```
+
+For example:
+```bash
+LABELBASE_DIR="/root/Labelbase"
+# or
+LABELBASE_DIR="/home/username/Labelbase"
+```
+
+### Run the Backup
+
+```bash
+./backup-labelbase.sh
+```
+
+You'll see output like:
+```
+Starting backup...
+Backup file: /path/to/Labelbase/backups/labelbase_backup_20250119_143022.sql
+✓ Backup successful!
+Backup size: 15M
+Compressing backup...
+✓ Compressed to: 3.2M
+Backup location: /path/to/Labelbase/backups/labelbase_backup_20250119_143022.sql.gz
+Cleaning up old backups (keeping last 10)...
+No cleanup needed (3 backups exist)
+
+=== Backup Complete ===
+Location: /path/to/Labelbase/backups/labelbase_backup_20250119_143022.sql.gz
+```
+
+---
+
+## Running Migrations Safely
+
+**Always follow this order:**
+
+### Step 1: Create a Backup
+
+```bash
+./backup-labelbase.sh
+```
+
+### Step 2: Check Migration Status
+
+```bash
+source exports.sh
+docker-compose exec labelbase_django python manage.py showmigrations
+```
+
+This shows which migrations are applied (marked with `[X]`) and pending (marked with `[ ]`).
+
+### Step 3: Review Pending Migrations
+
+Look for any unapplied migrations. If you see pending migrations for critical apps, review them carefully.
+
+### Step 4: Apply Migrations
+
+```bash
+# If you've modified models, create new migrations first
+docker-compose exec labelbase_django python manage.py makemigrations
+
+# Apply all pending migrations
+docker-compose exec labelbase_django python manage.py migrate
+```
+
+### Step 5: Verify Application
+
+After migrations complete:
+1. Check for any error messages
+2. Visit your Labelbase site
+3. Test critical functionality
+4. Check Django logs: `docker-compose logs labelbase_django`
+
+### Step 6: If Something Goes Wrong
+
+If migrations fail or break functionality, restore from backup (see below).
+
+---
+
+## Upgrading Labelbase
+
+When new versions of Labelbase are released, follow this workflow to safely upgrade.
+
+### Complete Upgrade Workflow
+
+**Step 1: Backup First (Critical!)**
+
+```bash
+cd Labelbase
+./backup-labelbase.sh
+```
+
+**Step 2: Pull Latest Code**
+
+```bash
+git pull origin master
+```
+
+**Step 3: Rebuild Containers (if needed)**
+
+If dependencies or Docker configuration changed:
+
+```bash
+source exports.sh && docker-compose up --build -d
+```
+
+Or use the main script:
+
+```bash
+source exports.sh && ./build-and-run-labelbase.sh
+```
+
+**⚠️ IMPORTANT**: These commands are SAFE - they rebuild containers but preserve your data in Docker volumes. Your database and uploaded files are NOT deleted.
+
+**❌ DANGER ZONE - Commands that DELETE data:**
+```bash
+# NEVER run these unless you want to lose ALL data:
+docker-compose down -v # The -v flag deletes volumes = data loss!
+docker volume prune # Deletes unused volumes
+docker system prune -a # Nuclear option - deletes everything
+```
+
+**Step 4: Apply Migrations and collect static files **
+
+```bash
+source exports.sh
+docker-compose exec labelbase_django python manage.py showmigrations
+docker-compose exec labelbase_django python manage.py migrate
+docker-compose exec labelbase_django python manage.py collectstatic --noinput
+
+```
+
+**Step 5: Restart Services**
+
+```bash
+docker-compose restart labelbase_django
+```
+
+**Step 6: Verify Everything Works**
+
+1. Visit your Labelbase site
+2. Test critical functionality
+3. Check logs: `docker-compose logs -f labelbase_django`
+
+### Quick Upgrade Script
+
+Create `update-and-migrate.sh` for a streamlined upgrade process:
+
+```bash
+#!/bin/bash
+
+# Labelbase Quick Update & Migration Script
+# Usage: source exports.sh && ./update-and-migrate.sh
+
+# Colors
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+echo -e "${YELLOW}=== Labelbase Update & Migrate ===${NC}"
+
+# Check if we're in the right directory
+if [ ! -f "docker-compose.yml" ]; then
+ echo -e "${RED}Error: Not in Labelbase directory (docker-compose.yml not found)${NC}"
+ exit 1
+fi
+
+# Check env vars
+if [[ -z "${MYSQL_ROOT_PASSWORD}" ]]; then
+ echo -e "${RED}Error: Run 'source exports.sh' first!${NC}"
+ exit 1
+fi
+
+# Step 1: Backup
+echo -e "${YELLOW}Step 1: Creating backup...${NC}"
+if [ -f "backup-labelbase.sh" ]; then
+ ./backup-labelbase.sh
+ if [ $? -ne 0 ]; then
+ echo -e "${RED}Backup failed! Aborting upgrade.${NC}"
+ exit 1
+ fi
+else
+ echo -e "${YELLOW}⚠ Warning: backup-labelbase.sh not found, skipping backup${NC}"
+ read -p "Continue without backup? (yes/no): " -r
+ if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
+ echo "Upgrade cancelled."
+ exit 0
+ fi
+fi
+
+# Step 2: Pull latest code
+echo ""
+echo -e "${YELLOW}Step 2: Pulling latest changes...${NC}"
+git pull origin master
+
+if [ $? -ne 0 ]; then
+ echo -e "${RED}Git pull failed!${NC}"
+ exit 1
+fi
+
+# Step 3: Check for pending migrations
+echo ""
+echo -e "${YELLOW}Step 3: Checking for migrations...${NC}"
+PENDING=$(docker-compose exec -T labelbase_django python manage.py showmigrations --plan 2>/dev/null | grep "\[ \]" | wc -l)
+
+if [ $PENDING -gt 0 ]; then
+ echo -e "${YELLOW}Found $PENDING pending migration(s)${NC}"
+
+ # Show what will be migrated
+ echo "Pending migrations:"
+ docker-compose exec -T labelbase_django python manage.py showmigrations | grep "\[ \]"
+
+ echo ""
+ read -p "Apply migrations now? (y/n): " -n 1 -r
+ echo
+
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ echo "Applying migrations..."
+ docker-compose exec -T labelbase_django python manage.py migrate --noinput
+
+ if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✓ Migrations applied successfully${NC}"
+ else
+ echo -e "${RED}✗ Migrations failed!${NC}"
+ echo "Check logs: docker-compose logs labelbase_django"
+ exit 1
+ fi
+ else
+ echo -e "${YELLOW}⚠ Skipping migrations${NC}"
+ echo "Run manually later: docker-compose exec labelbase_django python manage.py migrate"
+ fi
+else
+ echo -e "${GREEN}✓ No pending migrations${NC}"
+fi
+
+# Step 4: Restart Django
+echo ""
+echo -e "${YELLOW}Step 4: Restarting Django...${NC}"
+docker-compose restart labelbase_django
+
+echo ""
+echo -e "${GREEN}=== Update Complete! ===${NC}"
+echo "Check logs: docker-compose logs -f labelbase_django"
+echo "Visit your site to verify everything works"
+```
+
+Make it executable:
+
+```bash
+chmod +x update-and-migrate.sh
+```
+
+### Using the Quick Upgrade Script
+
+```bash
+# Navigate to Labelbase
+cd Labelbase
+
+# Source environment and run update
+source exports.sh && ./update-and-migrate.sh
+```
+
+The script will:
+1. ✓ Create automatic backup
+2. ✓ Pull latest code from git
+3. ✓ Detect pending migrations
+4. ✓ Ask for confirmation before applying
+5. ✓ Restart services
+6. ✓ Provide verification steps
+
+### When to Rebuild vs. Restart
+
+**Just restart** (`docker-compose restart`) when:
+- Only Django code changed (Python files)
+- No dependency updates
+- No Dockerfile changes
+- Fastest option
+
+**Full rebuild** (`docker-compose up --build -d`) when:
+- requirements.txt changed
+- Dockerfile modified
+- New system packages needed
+- Docker configuration changed
+
+**Data Safety Note**: Both `restart` and `--build` are SAFE - they preserve your data. Docker stores your database and files in **volumes** that persist across rebuilds.
+
+If unsure, rebuild - it's safer and only takes a minute longer.
+
+### What Actually Deletes Data
+
+Only these commands delete data (requires `-v` flag):
+
+```bash
+# DANGER: This deletes ALL data including database!
+docker-compose down -v
+
+# To safely stop without deleting data, use:
+docker-compose down # Safe - keeps volumes
+docker-compose stop # Safe - just stops containers
+```
+
+**Rule of thumb**: If you see `-v` flag, your data is at risk!
+
+### Rollback After Failed Upgrade
+
+If something goes wrong:
+
+```bash
+# 1. Stop services
+docker-compose down
+
+# 2. Restore previous code
+git reset --hard HEAD~1
+
+# 3. Restore database
+./restore-labelbase.sh backups/labelbase_backup_TIMESTAMP.sql.gz
+
+# 4. Restart
+source exports.sh && docker-compose up -d
+```
+
+---
+
+## Restoring from Backup
+
+If something goes wrong, you can restore your database from a backup.
+
+### Quick Restore
+
+```bash
+# Navigate to Labelbase directory
+cd Labelbase
+
+# Source environment variables
+source exports.sh
+
+# Stop Django to prevent conflicts
+docker-compose stop labelbase_django
+
+# Decompress and restore backup
+gunzip -c backups/labelbase_backup_20250119_143022.sql.gz | \
+ docker-compose exec -T labelbase_mysql mysql -u root -p"${MYSQL_ROOT_PASSWORD}" labelbase
+
+# Restart all services
+docker-compose up -d
+
+# Check logs
+docker-compose logs -f labelbase_django
+```
+
+### Restore Script (Optional)
+
+Create `restore-labelbase.sh`:
+
+```bash
+#!/bin/bash
+
+# Labelbase Database Restore Script
+# Usage: ./restore-labelbase.sh
+
+LABELBASE_DIR="/path/to/Labelbase" # CHANGE THIS
+BACKUP_FILE="$1"
+
+# Colors
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+# Check if backup file provided
+if [ -z "$BACKUP_FILE" ]; then
+ echo -e "${RED}✗ Error: No backup file specified${NC}"
+ echo "Usage: ./restore-labelbase.sh "
+ echo ""
+ echo "Available backups:"
+ ls -lh "$LABELBASE_DIR/backups/"*.sql.gz 2>/dev/null || echo "No backups found"
+ exit 1
+fi
+
+# Check if backup file exists
+if [ ! -f "$BACKUP_FILE" ]; then
+ echo -e "${RED}✗ Error: Backup file not found: $BACKUP_FILE${NC}"
+ exit 1
+fi
+
+# Change to Labelbase directory
+cd "$LABELBASE_DIR" || exit 1
+
+# Source environment variables
+source exports.sh
+
+echo -e "${YELLOW}⚠ WARNING: This will overwrite your current database!${NC}"
+echo "Backup file: $BACKUP_FILE"
+read -p "Are you sure you want to continue? (yes/no): " -r
+echo
+
+if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then
+ echo "Restore cancelled."
+ exit 0
+fi
+
+echo "Stopping Django container..."
+docker-compose stop labelbase_django
+
+echo "Restoring database..."
+
+# Check if file is compressed
+if [[ "$BACKUP_FILE" == *.gz ]]; then
+ gunzip -c "$BACKUP_FILE" | \
+ docker-compose exec -T labelbase_mysql mysql -u root -p"${MYSQL_ROOT_PASSWORD}" labelbase
+else
+ cat "$BACKUP_FILE" | \
+ docker-compose exec -T labelbase_mysql mysql -u root -p"${MYSQL_ROOT_PASSWORD}" labelbase
+fi
+
+if [ $? -eq 0 ]; then
+ echo -e "${GREEN}✓ Database restored successfully${NC}"
+else
+ echo -e "${RED}✗ Restore failed${NC}"
+ exit 1
+fi
+
+echo "Restarting services..."
+docker-compose up -d
+
+# Optional: Restore config.ini if you have a backup from the same time
+# CONFIG_FILE="${BACKUP_FILE%_backup_*}_config_backup_${BACKUP_FILE##*_backup_}"
+# CONFIG_FILE="${CONFIG_FILE%.sql.gz}.ini"
+# if [ -f "$CONFIG_FILE" ]; then
+# echo "Found config backup: $CONFIG_FILE"
+# read -p "Restore config.ini too? (y/n) " -n 1 -r
+# echo
+# if [[ $REPLY =~ ^[Yy]$ ]]; then
+# cat "$CONFIG_FILE" | docker-compose exec -T labelbase_django bash -c "cat > /app/config.ini"
+# echo -e "${GREEN}✓ Config restored${NC}"
+# docker-compose restart labelbase_django
+# fi
+# fi
+
+echo ""
+echo -e "${GREEN}=== Restore Complete ===${NC}"
+echo "Check logs with: docker-compose logs -f labelbase_django"
+```
+
+Make executable and configure:
+```bash
+chmod +x restore-labelbase.sh
+# Edit LABELBASE_DIR in the script
+```
+
+Usage:
+```bash
+./restore-labelbase.sh backups/labelbase_backup_20250119_143022.sql.gz
+```
+
+---
+
+## Scheduled Backups
+
+### Using Cron (Linux/Unix)
+
+Automate daily backups at 2 AM:
+
+```bash
+# Edit crontab
+crontab -e
+
+# Add this line (adjust path to your Labelbase directory)
+0 2 * * * cd /path/to/Labelbase && ./backup-labelbase.sh >> /var/log/labelbase-backup.log 2>&1
+```
+
+This runs the backup script daily at 2:00 AM and logs output to `/var/log/labelbase-backup.log`.
+
+### Verify Cron Job
+
+```bash
+# List current cron jobs
+crontab -l
+
+# Check backup log
+tail -f /var/log/labelbase-backup.log
+```
+
+### Alternative: Weekly Backups
+
+```bash
+# Every Sunday at 3 AM
+0 3 * * 0 cd /path/to/Labelbase && ./backup-labelbase.sh >> /var/log/labelbase-backup.log 2>&1
+```
+
+---
+
+## Best Practices
+
+### Before Migrations
+1. ✓ **Always create a backup first**
+2. ✓ Review what migrations will be applied
+3. ✓ Have a rollback plan ready
+4. ✓ Test migrations on a development copy if possible
+5. ✓ Schedule migrations during low-traffic periods
+
+### Backup Storage
+1. ✓ Keep backups in multiple locations
+2. ✓ Regularly test your restore process
+3. ✓ Keep at least 7-10 recent backups
+4. ✓ Store critical backups off-server (external drive, cloud storage)
+5. ✓ Monitor backup script success/failure
+
+### Security
+1. ✓ Protect `exports.sh` - it contains database passwords
+2. ✓ Secure backup files - they contain all your data
+3. ✓ Protect `config.ini` backups - they contain encryption keys
+4. ✓ Use appropriate file permissions:
+ ```bash
+ chmod 600 exports.sh
+ chmod 700 backups/
+ chmod 600 backups/*.sql.gz
+ chmod 600 backups/*_config.ini
+ ```
+
+### Regular Maintenance
+1. ✓ Run backups before any system updates
+2. ✓ Test restore process quarterly
+3. ✓ Monitor backup file sizes (unexpected changes may indicate issues)
+4. ✓ Keep backup logs for troubleshooting
+
+### Docker Data Safety
+1. ✓ **SAFE commands** (preserve data):
+ - `docker-compose up --build -d` - Rebuild containers
+ - `docker-compose restart` - Restart services
+ - `docker-compose down` - Stop without deleting volumes
+ - `docker-compose stop` - Pause containers
+2. ✓ **DANGEROUS commands** (delete data):
+ - `docker-compose down -v` - ⚠️ Deletes ALL volumes/data
+ - `docker volume prune` - ⚠️ Removes unused volumes
+ - `docker system prune -a` - ⚠️ Nuclear option
+3. ✓ **Remember**: The `-v` flag means "delete volumes" = data loss!
+
+---
+
+## Troubleshooting
+
+### "MYSQL_ROOT_PASSWORD not set"
+
+**Cause**: `exports.sh` not sourced or doesn't contain password
+
+**Solution**:
+```bash
+# Check if exports.sh exists
+ls -la exports.sh
+
+# Source it
+source exports.sh
+
+# Verify password is set
+echo $MYSQL_ROOT_PASSWORD
+```
+
+### "Access denied for user 'root'"
+
+**Cause**: Wrong password in `exports.sh`
+
+**Solution**:
+1. Check MySQL container logs: `docker-compose logs labelbase_mysql`
+2. Verify password in `exports.sh` matches what MySQL expects
+3. If lost, you may need to reset MySQL root password
+
+### Backup File is Empty or Very Small
+
+**Cause**: MySQL container not running or database empty
+
+**Solution**:
+```bash
+# Check container status
+docker-compose ps
+
+# Check MySQL logs
+docker-compose logs labelbase_mysql
+
+# Verify database exists
+docker-compose exec labelbase_mysql mysql -u root -p -e "SHOW DATABASES;"
+```
+
+### Restore Fails with "Unknown Database"
+
+**Cause**: Database doesn't exist in MySQL
+
+**Solution**:
+```bash
+# Create database first
+docker-compose exec labelbase_mysql mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS labelbase;"
+
+# Then restore
+./restore-labelbase.sh backups/your_backup.sql.gz
+```
+
+---
+
+## Complete Safe Migration Workflow
+
+Here's the complete workflow combining backup and migration:
+
+```bash
+# 1. Navigate to Labelbase
+cd Labelbase
+
+# 2. Create backup
+./backup-labelbase.sh
+
+# 3. Source environment
+source exports.sh
+
+# 4. Check what migrations will run
+docker-compose exec labelbase_django python manage.py showmigrations
+
+# 5. Apply migrations
+docker-compose exec labelbase_django python manage.py makemigrations
+docker-compose exec labelbase_django python manage.py migrate
+
+# 6. Check for errors
+docker-compose logs labelbase_django | tail -50
+
+# 7. Test your application
+# Visit site and verify functionality
+
+# 8. If problems occur, restore:
+# ./restore-labelbase.sh backups/labelbase_backup_TIMESTAMP.sql.gz
+```
+
+---
+
+## Quick Command Reference
+
+```bash
+# Quick upgrade (recommended)
+source exports.sh && ./update-and-migrate.sh
+
+# Manual upgrade workflow
+./backup-labelbase.sh
+git pull origin master
+source exports.sh && docker-compose up --build -d
+docker-compose exec labelbase_django python manage.py migrate
+docker-compose restart labelbase_django
+
+# Create backup (database + config.ini)
+./backup-labelbase.sh
+
+# Manual database backup
+docker-compose exec -T labelbase_mysql mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" labelbase > backup_$(date +%Y%m%d_%H%M%S).sql
+
+# Manual config.ini backup
+docker-compose exec -T labelbase_django cat /app/config.ini > backup_$(date +%Y%m%d_%H%M%S)_config.ini
+
+# List backups
+ls -lh backups/
+
+# Check migration status
+docker-compose exec labelbase_django python manage.py showmigrations
+
+# Apply migrations
+docker-compose exec labelbase_django python manage.py migrate
+
+# Restore backup
+./restore-labelbase.sh backups/labelbase_backup_20250119_143022.sql.gz
+
+# View recent Django logs
+docker-compose logs labelbase_django | tail -100
+
+# Access MySQL directly
+docker-compose exec labelbase_mysql mysql -u root -p labelbase
+```
+
+---
+
+## Additional Resources
+
+- [Labelbase Development Guide](DEVELOPMENT_GUIDE.md)
+- [Django Migrations Documentation](https://docs.djangoproject.com/en/stable/topics/migrations/)
+- [mysqldump Documentation](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html)
+
+---
+
+## Support
+
+If you encounter issues:
+1. Check the troubleshooting section above
+2. Review Docker logs: `docker-compose logs -f`
+3. Verify all services are running: `docker-compose ps`
+4. Check Labelbase GitHub issues: https://github.com/Labelbase/Labelbase/issues
+
+---
+
+**Remember: A backup today saves recovery tomorrow. Always backup before migrations!**
diff --git a/BARE_METALL_INSTALL.md b/BARE_METALL_INSTALL.md
new file mode 100644
index 0000000..ddc2c0c
--- /dev/null
+++ b/BARE_METALL_INSTALL.md
@@ -0,0 +1,357 @@
+# Labelbase Bare Metal Installation Guide
+
+This guide will help you install and run Labelbase directly on macOS and Linux systems, based on the RaspiBlitz installation script.
+
+## Prerequisites
+
+### Common Requirements
+- Git
+- Python 3.8 or higher
+- pip (Python package manager)
+- virtualenv
+- MySQL/MariaDB server
+
+### System-Specific Requirements
+
+#### macOS
+```bash
+# Install Homebrew if not already installed
+/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+
+# Install required packages
+brew install python3 mysql git
+brew install pkg-config mysql-client
+```
+
+#### Linux (Ubuntu/Debian)
+```bash
+# Update package list
+sudo apt update
+
+# Install required packages
+sudo apt install -y \
+ mariadb-server \
+ mariadb-client \
+ default-libmysqlclient-dev \
+ build-essential \
+ python3 \
+ python3-pip \
+ python3-venv \
+ git \
+ libpcre3-dev
+```
+
+#### Linux (CentOS/RHEL/Fedora)
+```bash
+# For CentOS/RHEL 8+
+sudo dnf install -y \
+ mariadb-server \
+ mariadb-devel \
+ python3 \
+ python3-pip \
+ python3-virtualenv \
+ git \
+ gcc \
+ gcc-c++ \
+ make
+
+# For older versions, use yum instead of dnf
+```
+
+## Installation Steps
+
+### 1. Create Application User (Optional but Recommended)
+
+#### macOS
+```bash
+# Create a new user (optional on macOS for development)
+sudo dscl . -create /Users/labelbase
+sudo dscl . -create /Users/labelbase UserShell /bin/bash
+sudo dscl . -create /Users/labelbase RealName "Labelbase User"
+sudo dscl . -create /Users/labelbase UniqueID 1001
+sudo dscl . -create /Users/labelbase PrimaryGroupID 20
+sudo dscl . -create /Users/labelbase NFSHomeDirectory /Users/labelbase
+sudo createhomedir -c -u labelbase
+```
+
+#### Linux
+```bash
+# Create system user
+sudo adduser --system --group --shell /bin/bash --home /home/labelbase labelbase
+sudo -u labelbase cp -r /etc/skel/. /home/labelbase/
+```
+
+### 2. Start Database Service
+
+#### macOS
+```bash
+# Start MySQL service
+brew services start mysql
+
+# Secure the installation
+mysql_secure_installation
+```
+
+#### Linux
+```bash
+# Start MariaDB service
+sudo systemctl enable mariadb
+sudo systemctl start mariadb
+
+# Secure the installation
+sudo mysql_secure_installation
+```
+
+### 3. Download and Setup Labelbase
+
+```bash
+# Switch to labelbase user (if created) or use your regular user
+# sudo su - labelbase # (if using dedicated user)
+
+# Set variables
+LABELBASE_HOME="$HOME" # or /home/labelbase if using dedicated user
+LABELBASE_REPO="https://github.com/Labelbase/Labelbase/"
+LABELBASE_VERSION="2.2.1"
+
+# Clone the repository
+git clone $LABELBASE_REPO $LABELBASE_HOME/labelbase
+cd $LABELBASE_HOME/labelbase
+
+# Checkout specific version
+git checkout $LABELBASE_VERSION
+
+# Create virtual environment
+python3 -m venv $LABELBASE_HOME/ENV
+
+# Activate virtual environment
+source $LABELBASE_HOME/ENV/bin/activate
+
+# Install Python dependencies
+pip install --upgrade pip
+pip install --no-cache-dir -r $LABELBASE_HOME/labelbase/django/requirements.txt
+```
+
+### 4. Database Configuration
+
+```bash
+# Generate a secure password
+MYSQL_PASSWORD=$(openssl rand -base64 32 | tr -d '+/' | fold -w 32 | head -n 1)
+
+# Create exports file
+cat > $LABELBASE_HOME/exports.sh << EOF
+export MYSQL_PASSWORD=$MYSQL_PASSWORD
+export DATABASE_URL=mysql://ulabelbase:$MYSQL_PASSWORD@localhost:3306/labelbase
+EOF
+
+chmod 755 $LABELBASE_HOME/exports.sh
+```
+
+#### Create Database and User
+
+##### macOS
+```bash
+# Connect to MySQL
+mysql -u root -p
+
+# In MySQL prompt:
+CREATE DATABASE labelbase;
+CREATE USER 'ulabelbase'@'localhost' IDENTIFIED BY 'YOUR_GENERATED_PASSWORD';
+GRANT ALL PRIVILEGES ON labelbase.* TO 'ulabelbase'@'localhost';
+FLUSH PRIVILEGES;
+EXIT;
+```
+
+##### Linux
+```bash
+# Connect to MariaDB
+sudo mysql
+
+# In MariaDB prompt:
+CREATE DATABASE labelbase;
+CREATE USER 'ulabelbase'@'localhost' IDENTIFIED BY 'YOUR_GENERATED_PASSWORD';
+GRANT ALL PRIVILEGES ON labelbase.* TO 'ulabelbase'@'localhost';
+FLUSH PRIVILEGES;
+EXIT;
+```
+
+**Note:** Replace `YOUR_GENERATED_PASSWORD` with the password generated in the previous step.
+
+### 5. Django Setup
+
+```bash
+# Navigate to Django directory
+cd $LABELBASE_HOME/labelbase/django
+
+# Activate virtual environment and load environment variables
+source $LABELBASE_HOME/ENV/bin/activate
+source $LABELBASE_HOME/exports.sh
+
+# Run Django migrations
+python manage.py makemigrations --noinput
+python manage.py migrate --noinput
+python manage.py collectstatic --noinput
+
+# Create a superuser (optional)
+python manage.py createsuperuser
+```
+
+### 6. Running Labelbase
+
+#### Development Mode
+```bash
+# Activate environment
+source $LABELBASE_HOME/ENV/bin/activate
+source $LABELBASE_HOME/exports.sh
+
+# Navigate to Django directory
+cd $LABELBASE_HOME/labelbase/django
+
+# Run development server
+python manage.py runserver 0.0.0.0:8089
+
+# Access at: http://localhost:8089
+```
+
+#### Production Mode (using Gunicorn)
+```bash
+# Install Gunicorn if not already installed
+pip install gunicorn
+
+# Run with Gunicorn
+source $LABELBASE_HOME/ENV/bin/activate
+source $LABELBASE_HOME/exports.sh
+cd $LABELBASE_HOME/labelbase/django
+
+gunicorn labellabor.wsgi:application -b 0.0.0.0:8089 --reload
+```
+
+## Creating System Services (Optional)
+
+### macOS (using LaunchAgent)
+
+Create a plist file at `~/Library/LaunchAgents/com.labelbase.app.plist`:
+
+```xml
+
+
+
+
+ Label
+ com.labelbase.app
+ ProgramArguments
+
+ /bin/bash
+ -c
+ cd /Users/labelbase/labelbase/django && source /Users/labelbase/ENV/bin/activate && source /Users/labelbase/exports.sh && gunicorn labellabor.wsgi:application -b 0.0.0.0:8089
+
+ RunAtLoad
+
+ KeepAlive
+
+ StandardOutPath
+ /Users/labelbase/labelbase.out
+ StandardErrorPath
+ /Users/labelbase/labelbase.err
+
+
+```
+
+Load the service:
+```bash
+launchctl load ~/Library/LaunchAgents/com.labelbase.app.plist
+```
+
+### Linux (using systemd)
+
+Create `/etc/systemd/system/labelbase.service`:
+
+```ini
+[Unit]
+Description=Labelbase Application
+After=mariadb.service
+
+[Service]
+Type=simple
+User=labelbase
+WorkingDirectory=/home/labelbase/labelbase/django
+Environment="HOME_PATH=/home/labelbase"
+ExecStart=/bin/bash -c 'source /home/labelbase/ENV/bin/activate && source /home/labelbase/exports.sh && gunicorn labellabor.wsgi:application -b 0.0.0.0:8089'
+Restart=always
+TimeoutSec=120
+RestartSec=30
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Enable and start the service:
+```bash
+sudo systemctl enable labelbase
+sudo systemctl start labelbase
+sudo systemctl status labelbase
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Database Connection Errors**
+ - Ensure MySQL/MariaDB is running
+ - Verify database credentials in `exports.sh`
+ - Check if the database user has proper permissions
+
+2. **Python Package Issues**
+ - Make sure virtual environment is activated
+ - Try upgrading pip: `pip install --upgrade pip`
+ - Install packages one by one if requirements.txt fails
+
+3. **Port Already in Use**
+ - Change the port in the run command: `gunicorn ... -b 0.0.0.0:8090`
+ - Find and kill processes using the port: `lsof -ti:8089 | xargs kill -9`
+
+4. **Permission Errors**
+ - Ensure proper ownership of files: `sudo chown -R labelbase:labelbase /home/labelbase`
+ - Check file permissions: `chmod 755 /home/labelbase/exports.sh`
+
+### Logs and Debugging
+
+- **Development server logs**: Displayed in terminal
+- **Gunicorn logs**: Use `--log-file` flag for logging
+- **System service logs** (Linux): `sudo journalctl -u labelbase -f`
+- **Database logs**: Check MySQL/MariaDB error logs
+
+### Stopping the Application
+
+```bash
+# If running in development mode
+Ctrl+C
+
+# If running as system service
+# macOS:
+launchctl unload ~/Library/LaunchAgents/com.labelbase.app.plist
+
+# Linux:
+sudo systemctl stop labelbase
+```
+
+## Security Considerations
+
+1. **Firewall Configuration**
+ - Only expose necessary ports
+ - Consider using a reverse proxy (nginx/apache) for production
+
+2. **Database Security**
+ - Use strong passwords
+ - Limit database user permissions
+ - Consider encrypting database connections
+
+3. **Application Security**
+ - Keep dependencies updated
+ - Use HTTPS in production
+ - Regular security updates
+
+4. **File Permissions**
+ - Ensure proper file ownership and permissions
+ - Limit access to configuration files
+
+This guide provides a foundation for running Labelbase on bare metal systems. Adjust paths, ports, and configurations according to your specific needs and security requirements.
diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md
new file mode 100644
index 0000000..18baa1a
--- /dev/null
+++ b/DEVELOPMENT_GUIDE.md
@@ -0,0 +1,312 @@
+# Labelbase Development Guide
+
+Quick reference for working with and developing Labelbase in Docker.
+
+## Initial Setup
+
+### 1. Clone and Setup
+```bash
+git clone https://github.com/Labelbase/Labelbase/
+cd Labelbase
+```
+
+### 2. Generate MySQL Passwords
+```bash
+./make-exports.sh
+```
+
+This creates `exports.sh` with random passwords. **Backup this file!**
+
+### 3. Build and Run
+```bash
+source exports.sh && ./build-and-run-labelbase.sh
+```
+
+The `source exports.sh` loads the passwords into your shell, then the script uses them.
+
+Access at: http://127.0.0.1:8080
+
+---
+
+## Daily Development Workflow
+
+### Start/Stop Services
+```bash
+# Start (always source exports.sh first!)
+source exports.sh && docker-compose up -d
+
+# Stop
+docker-compose down
+
+# Rebuild and restart (after code changes)
+source exports.sh && docker-compose up --build -d
+
+# Or use the main script
+source exports.sh && ./build-and-run-labelbase.sh
+```
+
+### View Logs
+```bash
+# All services
+docker-compose logs -f
+
+# Specific service
+docker-compose logs -f labelbase_django
+docker-compose logs -f labelbase_mysql
+docker-compose logs -f labelbase_nginx
+
+# Search logs
+docker-compose logs labelbase_django | grep -i error
+```
+
+### Access Container Shell
+```bash
+# Django container (most common)
+docker-compose exec labelbase_django bash
+
+# MySQL container
+docker-compose exec labelbase_mysql bash
+
+# Nginx container
+docker-compose exec labelbase_nginx sh
+```
+
+### Django Management Commands
+```bash
+# From host
+docker-compose exec labelbase_django python manage.py
+
+# Or from inside container
+docker-compose exec labelbase_django bash
+python manage.py makemigrations
+python manage.py migrate
+python manage.py createsuperuser
+python manage.py shell
+```
+
+### Database Operations
+```bash
+# Access MySQL CLI
+docker-compose exec labelbase_mysql mysql -u ulabelbase -p labelbase
+
+# Backup database
+docker-compose exec labelbase_mysql mysqldump -u root -p labelbase > backup.sql
+
+# Restore database
+docker-compose exec -T labelbase_mysql mysql -u root -p labelbase < backup.sql
+```
+
+---
+
+## Common Tasks
+
+### Reset config.ini (if passwords change)
+```bash
+docker-compose exec labelbase_django bash
+rm /app/config.ini
+python manage.py make_config
+exit
+docker-compose restart labelbase_django
+```
+
+### Clean Rebuild (fresh start, deletes volumes!!)
+
+❌ DANGER ZONE
+
+```bash
+docker-compose down -v
+source exports.sh && docker-compose up --build -d
+```
+
+⚠️ **Warning**: `-v` deletes volumes including database data!
+
+### Check What Django Sees
+```bash
+# Check environment variables
+docker-compose exec labelbase_django env | grep MYSQL
+
+# Check config.ini
+docker-compose exec labelbase_django cat /app/config.ini
+
+# Check database connection
+docker-compose exec labelbase_django python manage.py dbshell
+```
+
+### Update from Git
+```bash
+git pull origin master
+source exports.sh && ./build-and-run-labelbase.sh
+```
+
+The script automatically detects updates and rebuilds if needed.
+
+---
+
+## Configuration Files
+
+### exports.sh (IMPORTANT - backup this!)
+```bash
+#!/bin/bash
+export MYSQL_ROOT_PASSWORD="your_password_here"
+export MYSQL_PASSWORD="your_password_here"
+```
+- Generated by `./make-exports.sh`
+- Contains MySQL passwords
+- Source before running docker-compose: `source exports.sh`
+- **Add to `.gitignore`** - contains secrets!
+
+### config.ini (auto-generated, persisted)
+- Located at `/app/config.ini` inside Django container
+- Generated from environment variables on first run by `python manage.py make_config`
+- **Not overwritten** on subsequent runs (preserves user settings)
+- Force regenerate: `rm /app/config.ini` then restart
+
+### docker-compose.yml
+Uses environment variables like `${MYSQL_PASSWORD}` from your shell (after sourcing exports.sh).
+
+---
+
+## Troubleshooting
+
+### "Access denied for user 'ulabelbase'@'localhost'"
+**Cause**: Old `config.ini` with wrong password from previous build
+
+**Solution**:
+```bash
+docker-compose exec labelbase_django bash
+rm /app/config.ini
+python manage.py make_config
+exit
+docker-compose restart labelbase_django
+```
+
+### "MYSQL_PASSWORD variable is not set" warnings
+**Cause**: Forgot to source exports.sh
+
+**Solution**: Always use `source exports.sh && docker-compose up`
+
+These warnings appear at parse time but are harmless if you source exports.sh before running the command.
+
+### "Can't connect to MySQL server"
+**Cause**: Database not ready yet
+
+**Solution**: Wait 15 seconds (run.sh has a built-in delay) or check logs:
+```bash
+docker-compose logs labelbase_mysql
+```
+
+### Django can't see code changes
+- Volume mounting issue
+- Solution: `docker-compose restart labelbase_django`
+- Or use `--reload` in gunicorn (already enabled)
+
+### Port 8080 already in use
+```bash
+# Find what's using it
+lsof -i :8080
+
+# Change port in docker-compose.yml
+ports:
+ - "127.0.0.1:8081:8080" # Use 8081 instead
+```
+
+---
+
+## Development Tips
+
+### Live Code Reloading
+Django container mounts `./django:/app`, so changes are live. Gunicorn runs with `--reload` flag.
+
+### Static Files
+After changing CSS/JS:
+```bash
+docker-compose exec labelbase_django python manage.py collectstatic --noinput
+```
+
+### Running Tests
+```bash
+docker-compose exec labelbase_django python manage.py test
+```
+
+### Python Dependencies
+Add to `requirements.txt`, then:
+```bash
+docker-compose up --build -d
+```
+
+### Database Migrations
+```bash
+# Create migrations
+docker-compose exec labelbase_django python manage.py makemigrations
+
+# Apply migrations
+docker-compose exec labelbase_django python manage.py migrate
+
+# Show migration status
+docker-compose exec labelbase_django python manage.py showmigrations
+```
+
+---
+
+## Production Deployment
+
+See main README for:
+- Setting up nginx reverse proxy
+- SSL certificates with certbot
+- Domain configuration
+- Firewall rules
+
+---
+
+## Quick Command Reference
+
+```bash
+# Build and run
+source exports.sh && ./build-and-run-labelbase.sh
+
+# Stop
+docker-compose down
+
+# Logs
+docker-compose logs -f
+
+# Shell
+docker-compose exec labelbase_django bash
+or
+source exports.sh && docker-compose exec labelbase_django bash
+
+# Reset everything (DANGER: deletes data!)
+docker-compose down -v && source exports.sh && docker-compose up --build -d
+
+# Reset config.ini (if passwords changed)
+docker-compose exec labelbase_django rm /app/config.ini
+docker-compose restart labelbase_django
+```
+
+---
+
+## Environment Variables Reference
+
+| Variable | Purpose | Default |
+|----------|---------|---------|
+| `MYSQL_ROOT_PASSWORD` | MySQL root password | (required) |
+| `MYSQL_PASSWORD` | MySQL user password | (required) |
+| `MYSQL_DATABASE` | Database name | `labelbase` |
+| `MYSQL_USER` | Database user | `ulabelbase` |
+| `MYSQL_HOST` | Database hostname | `labelbase_mysql` |
+| `MYSQL_PORT` | Database port | `3306` |
+
+Set in `exports.sh` file and load with `source exports.sh` before running docker-compose.
+
+**Note I**: You may see warnings like "variable is not set" when running docker-compose commands. These appear at parse time but are harmless - the variables are properly set when you source exports.sh before the command.
+
+**NOTE II**: Some of the variables are hard coded or may need to be changed in the docker-compose.yml file manually.
+
+---
+
+## Need Help?
+
+- Check logs: `docker-compose logs -f`
+- View settings: `docker-compose exec labelbase_django cat /app/config.ini`
+- Check environment: `docker-compose exec labelbase_django env | grep MYSQL`
+- Access Django shell: `docker-compose exec labelbase_django python manage.py shell`
diff --git a/build-and-run-labelbase.sh b/build-and-run-labelbase.sh
index bfa4ec3..b00af70 100755
--- a/build-and-run-labelbase.sh
+++ b/build-and-run-labelbase.sh
@@ -7,17 +7,19 @@ docker-compose down # make sure Labelbase is terminated.
if [[ -z "${MYSQL_ROOT_PASSWORD}" ]]; then
echo "Error: MYSQL_ROOT_PASSWORD environment variable is not set"
exit 1
-else
- export $MYSQL_ROOT_PASSWORD
fi
if [[ -z "${MYSQL_PASSWORD}" ]]; then
echo "Error: MYSQL_PASSWORD environment variable is not set"
exit 1
-else
- export $MYSQL_PASSWORD
fi
+
+export MYSQL_ROOT_PASSWORD
+export MYSQL_PASSWORD
+
+
+
# Check git next
LAST_GIT_COMMIT_FILE=".last_git_commit"
diff --git a/django/Dockerfile b/django/Dockerfile
index 2e9f23c..5b90b98 100644
--- a/django/Dockerfile
+++ b/django/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.9
+FROM python:3.11
ENV PYTHONUNBUFFERED 1
@@ -11,7 +11,7 @@ RUN apt-get update && \
default-libmysqlclient-dev \
build-essential \
cron vim logrotate \
- libpcre3-dev \
+ libpcre2-dev \
default-mysql-client \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --upgrade pip \
diff --git a/django/attachments/README.md b/django/attachments/README.md
new file mode 100644
index 0000000..e9cd013
--- /dev/null
+++ b/django/attachments/README.md
@@ -0,0 +1,3 @@
+Friendly-forked at commit https://github.com/bartTC/django-attachments/commit/d0c3e7b366691f2a57329c804b0dd002064396a6 (version 1.11) and extended for Labelbase.
+
+BSD 3-Clause "New" or "Revised" License https://github.com/bartTC/django-attachments/blob/master/LICENSE
diff --git a/django/attachments/__init__.py b/django/attachments/__init__.py
new file mode 100644
index 0000000..0bb3635
--- /dev/null
+++ b/django/attachments/__init__.py
@@ -0,0 +1 @@
+default_app_config = "attachments.apps.AttachmentsConfig"
diff --git a/django/attachments/admin.py b/django/attachments/admin.py
new file mode 100644
index 0000000..9e9672d
--- /dev/null
+++ b/django/attachments/admin.py
@@ -0,0 +1,21 @@
+from __future__ import unicode_literals
+
+from django.contrib.contenttypes.admin import GenericStackedInline
+
+
+
+from django.contrib import admin
+from .models import LabelAttachment
+from .models import Attachment
+
+class AttachmentInlines(GenericStackedInline):
+ model = Attachment
+ exclude = ()
+ extra = 1
+
+
+
+
+
+
+admin.site.register(LabelAttachment)
diff --git a/django/attachments/apps.py b/django/attachments/apps.py
new file mode 100644
index 0000000..1c56402
--- /dev/null
+++ b/django/attachments/apps.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class AttachmentsConfig(AppConfig):
+ default_auto_field = 'django.db.models.AutoField'
+ name = "attachments"
+ verbose_name = _("Attachments")
diff --git a/django/attachments/forms.py b/django/attachments/forms.py
new file mode 100644
index 0000000..a2d84e7
--- /dev/null
+++ b/django/attachments/forms.py
@@ -0,0 +1,46 @@
+from __future__ import unicode_literals
+
+from django import forms
+from django.apps import apps
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.template.defaultfilters import filesizeformat
+from django.utils.translation import gettext_lazy as _
+
+from .models import Attachment
+
+config = apps.get_app_config("attachments")
+
+
+def validate_max_size(data):
+ if (
+ hasattr(settings, "FILE_UPLOAD_MAX_SIZE")
+ and data.size > settings.FILE_UPLOAD_MAX_SIZE
+ ):
+ raise forms.ValidationError(
+ _("File exceeds maximum size of {size}").format(
+ size=filesizeformat(settings.FILE_UPLOAD_MAX_SIZE)
+ )
+ )
+
+
+def custom_attachment_validators(uploaded_file):
+ for validator in getattr(config, "attachment_validators", ()):
+ validator(uploaded_file)
+
+
+class AttachmentForm(forms.ModelForm):
+ attachment_file = forms.FileField(
+ label=_("Upload attachment"),
+ validators=[validate_max_size, custom_attachment_validators]
+ )
+
+ class Meta:
+ model = Attachment
+ fields = ("attachment_file",)
+
+ def save(self, request, obj, *args, **kwargs):
+ self.instance.creator = request.user
+ self.instance.content_type = ContentType.objects.get_for_model(obj)
+ self.instance.object_id = obj.pk
+ super(AttachmentForm, self).save(*args, **kwargs)
diff --git a/django/attachments/locale/README.transifex b/django/attachments/locale/README.transifex
new file mode 100644
index 0000000..90789a0
--- /dev/null
+++ b/django/attachments/locale/README.transifex
@@ -0,0 +1,11 @@
+Transifex.net Token Verification
+=================================
+
+The list of tokens bellow guarantee the respective users to be able to enable
+submission on components using the following repository url:
+
+git://github.com/bartTC/django-attachments.git
+
+Tokens:
+
+24u9WhbEZQbCGDn3ruWJ6c7YpDpSxCCf / bartTC
diff --git a/django/attachments/locale/da/LC_MESSAGES/django.mo b/django/attachments/locale/da/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..cae96e1
Binary files /dev/null and b/django/attachments/locale/da/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/da/LC_MESSAGES/django.po b/django/attachments/locale/da/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8aeb328
--- /dev/null
+++ b/django/attachments/locale/da/LC_MESSAGES/django.po
@@ -0,0 +1,54 @@
+# django-attachments in Danish
+# django-attachments på Dansk
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Michael Lind Mortensen , 2009.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-08-27 23:52+0200\n"
+"PO-Revision-Date: 2010-04-04 00:24+0100\n"
+"Last-Translator: Martin Mahner \n"
+"Language-Team: da \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Upload vedhæftning"
+
+#: models.py:29
+msgid "creator"
+msgstr "skaber"
+
+#: models.py:30
+msgid "attachment"
+msgstr "vedhæftning"
+
+#: models.py:31
+msgid "created"
+msgstr "skabt"
+
+#: models.py:32
+msgid "modified"
+msgstr "ændret"
+
+#: views.py:33
+msgid "Your attachment was uploaded."
+msgstr "Din vedhæftning blev uploadet."
+
+#: views.py:51
+msgid "Your attachment was deleted."
+msgstr "Din vedhæftning blev slettet."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Tilføj vedhæftning"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Slet vedhæftning"
+
diff --git a/django/attachments/locale/de/LC_MESSAGES/django.mo b/django/attachments/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..979dd37
Binary files /dev/null and b/django/attachments/locale/de/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/de/LC_MESSAGES/django.po b/django/attachments/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..22a4f6d
--- /dev/null
+++ b/django/attachments/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,53 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-04-04 00:19+0200\n"
+"PO-Revision-Date: 2010-04-04 00:29+0100\n"
+"Last-Translator: Martin Mahner \n"
+"Language-Team: de \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Anhang hochladen"
+
+#: models.py:29
+msgid "creator"
+msgstr "Autor"
+
+#: models.py:30
+msgid "attachment"
+msgstr "Anhang"
+
+#: models.py:31
+msgid "created"
+msgstr "Erstellt"
+
+#: models.py:32
+msgid "modified"
+msgstr "Geändert"
+
+#: views.py:33
+msgid "Your attachment was uploaded."
+msgstr "Dein Anhang wurde hochgeladen."
+
+#: views.py:51
+msgid "Your attachment was deleted."
+msgstr "Dein Anhang wurde gelöscht."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Anhang hinzufügen"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Anhang löschen"
+
diff --git a/django/attachments/locale/el/LC_MESSAGES/django.mo b/django/attachments/locale/el/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..ce0354e
Binary files /dev/null and b/django/attachments/locale/el/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/el/LC_MESSAGES/django.po b/django/attachments/locale/el/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e2452ee
--- /dev/null
+++ b/django/attachments/locale/el/LC_MESSAGES/django.po
@@ -0,0 +1,53 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Panos Laganakos , 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-04-04 02:52+0300\n"
+"PO-Revision-Date: 2010-04-04 15:07+0100\n"
+"Last-Translator: Martin Mahner \n"
+"Language-Team: gr \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Φώρτωση συνημμένου"
+
+#: models.py:29
+msgid "creator"
+msgstr "δημιουργός"
+
+#: models.py:30
+msgid "attachment"
+msgstr "συνημμένο"
+
+#: models.py:31
+msgid "created"
+msgstr "δημιουργήθηκε"
+
+#: models.py:32
+msgid "modified"
+msgstr "τροποποιήθηκε"
+
+#: views.py:33
+msgid "Your attachment was uploaded."
+msgstr "Η επισύναψη σας ολοκληρώθηκε."
+
+#: views.py:51
+msgid "Your attachment was deleted."
+msgstr "Το συνήμμενο σας διεγράφει."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Προσθέστε ένα συνημμένο"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Διαγράψτε το συνημμένο"
+
diff --git a/django/attachments/locale/en/LC_MESSAGES/django.mo b/django/attachments/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..d50a24a
Binary files /dev/null and b/django/attachments/locale/en/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/en/LC_MESSAGES/django.po b/django/attachments/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..0b3ccbc
--- /dev/null
+++ b/django/attachments/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,53 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-04-04 00:18+0200\n"
+"PO-Revision-Date: 2010-04-04 00:28+0100\n"
+"Last-Translator: Martin Mahner \n"
+"Language-Team: en \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Upload attachment"
+
+#: models.py:29
+msgid "creator"
+msgstr "creator"
+
+#: models.py:30
+msgid "attachment"
+msgstr "attachment"
+
+#: models.py:31
+msgid "created"
+msgstr "created"
+
+#: models.py:32
+msgid "modified"
+msgstr "modified"
+
+#: views.py:33
+msgid "Your attachment was uploaded."
+msgstr "Your attachment was uploaded."
+
+#: views.py:51
+msgid "Your attachment was deleted."
+msgstr "Your attachment was deleted."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Add attachment"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Delete attachment"
+
diff --git a/django/attachments/locale/es_AR/LC_MESSAGES/django.mo b/django/attachments/locale/es_AR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a8b2ce5
Binary files /dev/null and b/django/attachments/locale/es_AR/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/es_AR/LC_MESSAGES/django.po b/django/attachments/locale/es_AR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..12ec32e
--- /dev/null
+++ b/django/attachments/locale/es_AR/LC_MESSAGES/django.po
@@ -0,0 +1,54 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Gonzalo Bustos, 2015.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-05-31 17:20-0300\n"
+"PO-Revision-Date: 2015-10-11 22:04-0300\n"
+"Last-Translator: Gonzalo Bustos\n"
+"Language-Team: Spanish (Argentina)\n"
+"Language: es_AR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 1.6.10\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Subir adjunto"
+
+#: models.py:32
+msgid "creator"
+msgstr "creador"
+
+#: models.py:33 models.py:38
+msgid "attachment"
+msgstr "adjunto"
+
+#: models.py:34
+msgid "created"
+msgstr "creado"
+
+#: models.py:35
+msgid "modified"
+msgstr "modificado"
+
+#: views.py:34
+msgid "Your attachment was uploaded."
+msgstr "Su adjunto fue subido."
+
+#: views.py:52
+msgid "Your attachment was deleted."
+msgstr "Su adjunto fue eliminado."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Agregar adjunto"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Eliminar adjunto"
diff --git a/django/attachments/locale/fi/LC_MESSAGES/django.mo b/django/attachments/locale/fi/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..599e3dd
Binary files /dev/null and b/django/attachments/locale/fi/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/fi/LC_MESSAGES/django.po b/django/attachments/locale/fi/LC_MESSAGES/django.po
new file mode 100644
index 0000000..a57f0de
--- /dev/null
+++ b/django/attachments/locale/fi/LC_MESSAGES/django.po
@@ -0,0 +1,54 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-06-11 15:55+0300\n"
+"PO-Revision-Date: 2016-06-11 16:03+0300\n"
+"Last-Translator: Aleksi Häkli \n"
+"Language-Team: fi \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"X-Generator: Poedit 1.8.8\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Lähetä liite"
+
+#: models.py:29
+msgid "creator"
+msgstr "luoja"
+
+#: models.py:30
+msgid "attachment"
+msgstr "liite"
+
+#: models.py:31
+msgid "created"
+msgstr "luotu"
+
+#: models.py:32
+msgid "modified"
+msgstr "muokattu"
+
+#: views.py:33
+msgid "Your attachment was uploaded."
+msgstr "Liitteesi ladattiin palvelimelle."
+
+#: views.py:51
+msgid "Your attachment was deleted."
+msgstr "Liitteesi poistettiin palvelimelta."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Lisää liite"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Poista liite"
diff --git a/django/attachments/locale/fr/LC_MESSAGES/django.mo b/django/attachments/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..22abd79
Binary files /dev/null and b/django/attachments/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/fr/LC_MESSAGES/django.po b/django/attachments/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..4f71bc1
--- /dev/null
+++ b/django/attachments/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,53 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-04-04 00:18+0200\n"
+"PO-Revision-Date: 2010-04-04 00:28+0100\n"
+"Last-Translator: AERT \n"
+"Language-Team: en \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Ajouter une pièce jointe"
+
+#: models.py:29
+msgid "creator"
+msgstr "créateur"
+
+#: models.py:30
+msgid "attachment"
+msgstr "pièce jointe"
+
+#: models.py:31
+msgid "created"
+msgstr "créé"
+
+#: models.py:32
+msgid "modified"
+msgstr "modifié"
+
+#: views.py:33
+msgid "Your attachment was uploaded."
+msgstr "Votre pièce jointe a été ajoutée."
+
+#: views.py:51
+msgid "Your attachment was deleted."
+msgstr "Votre pièce jointe a été supprimée."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Ajouter une pièce jointe"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Supprimer la pièce jointe"
+
diff --git a/django/attachments/locale/it/LC_MESSAGES/django.mo b/django/attachments/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..a8b50f4
Binary files /dev/null and b/django/attachments/locale/it/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/it/LC_MESSAGES/django.po b/django/attachments/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 0000000..b782b75
--- /dev/null
+++ b/django/attachments/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,77 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-05-29 16:35+0200\n"
+"PO-Revision-Date: 2018-05-29 16:39+0200\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Last-Translator: Mario Orlandi \n"
+"Language-Team: \n"
+"X-Generator: Poedit 2.0.7\n"
+
+#: apps.py:7
+msgid "Attachments"
+msgstr "Allegati"
+
+#: forms.py:13
+#, python-brace-format
+msgid "File exceeds maximum size of {size}"
+msgstr "Il file supera le dimensioni massime di {size}"
+
+#: forms.py:18
+msgid "Upload attachment"
+msgstr "Invia allegato"
+
+#: models.py:35
+msgid "creator"
+msgstr "autore"
+
+#: models.py:36 models.py:41
+msgid "attachment"
+msgstr "allegato"
+
+#: models.py:37
+msgid "created"
+msgstr "creato il"
+
+#: models.py:38
+msgid "modified"
+msgstr "modificato il"
+
+#: models.py:42
+msgid "attachments"
+msgstr "allegati"
+
+#: models.py:45
+msgid "Can delete foreign attachments"
+msgstr "Può eliminare gli allegati esterni"
+
+#: models.py:49
+#, python-brace-format
+msgid "{username} attached {filename}"
+msgstr "{username} ha allegato {filename}"
+
+#: templates/attachments/add_form.html:8
+msgid "Add attachment"
+msgstr "Aggiungi allegato"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Elimina allegato"
+
+#: views.py:51
+msgid "Your attachment was uploaded."
+msgstr "L'allegato è stato inviato."
+
+#: views.py:75
+msgid "Your attachment was deleted."
+msgstr "L'allegato è stato eliminato."
diff --git a/django/attachments/locale/pt_BR/LC_MESSAGES/django.mo b/django/attachments/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..f1b6105
Binary files /dev/null and b/django/attachments/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/django/attachments/locale/pt_BR/LC_MESSAGES/django.po b/django/attachments/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 0000000..8b7af3a
--- /dev/null
+++ b/django/attachments/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,54 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-05-31 17:20-0300\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:7
+msgid "Upload attachment"
+msgstr "Subir anexo"
+
+#: models.py:32
+msgid "creator"
+msgstr "criador"
+
+#: models.py:33 models.py:38
+msgid "attachment"
+msgstr "anexo"
+
+#: models.py:34
+msgid "created"
+msgstr "criado"
+
+#: models.py:35
+msgid "modified"
+msgstr "modificado"
+
+#: views.py:34
+msgid "Your attachment was uploaded."
+msgstr "Seu anexo foi enviado."
+
+#: views.py:52
+msgid "Your attachment was deleted."
+msgstr "Seu anexo foi excluido."
+
+#: templates/attachments/add_form.html:7
+msgid "Add attachment"
+msgstr "Adicionar anexo"
+
+#: templates/attachments/delete_link.html:2
+msgid "Delete attachment"
+msgstr "Excluir anexo"
diff --git a/django/attachments/locale/ru/LC_MESSAGES/django.po b/django/attachments/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000..5c74ab9
--- /dev/null
+++ b/django/attachments/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,61 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: django-attachments\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-04-07 23:35+0300\n"
+"PO-Revision-Date: 2017-04-07 23:26+0300:MI+ZONE\n"
+"Last-Translator: Maxim Barabanov \n"
+"Language-Team: RU \n"
+"Language: Russian\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
+
+#: .\attachments\forms.py:9
+msgid "Upload attachment"
+msgstr "Загрузить вложение"
+
+#: .\attachments\models.py:35
+msgid "creator"
+msgstr "создатель"
+
+#: .\attachments\models.py:36
+msgid "attachment"
+msgstr "вложение"
+
+#: .\attachments\models.py:37
+msgid "created"
+msgstr "создано"
+
+#: .\attachments\models.py:38
+msgid "modified"
+msgstr "изменено"
+
+#: .\attachments\models.py:43
+msgid "Can delete foreign attachments"
+msgstr "Может удалять чужие вложения"
+
+#: .\attachments\templates\attachments\add_form.html:8
+msgid "Add attachment"
+msgstr "Добавить вложение"
+
+#: .\attachments\templates\attachments\delete_link.html:2
+msgid "Delete attachment"
+msgstr "Удалить вложение"
+
+#: .\attachments\views.py:40
+msgid "Your attachment was uploaded."
+msgstr "Ваше вложение было загружено."
+
+#: .\attachments\views.py:62
+msgid "Your attachment was deleted."
+msgstr "Ваше вложение было удалено"
diff --git a/django/attachments/management/__init__.py b/django/attachments/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/attachments/management/commands/__init__.py b/django/attachments/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/attachments/management/commands/delete_stale_attachments.py b/django/attachments/management/commands/delete_stale_attachments.py
new file mode 100644
index 0000000..7f77ad3
--- /dev/null
+++ b/django/attachments/management/commands/delete_stale_attachments.py
@@ -0,0 +1,44 @@
+from django.core.management.base import BaseCommand
+
+from attachments.models import Attachment
+from attachments.views import remove_file_from_disk
+
+
+class Command(BaseCommand):
+ help = ("Remove attachments for which the related objects "
+ "don't exist anymore!")
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-y', '--yes', default='x', action='store_const', const='y',
+ dest='answer', help='Automatically confirm deletion',
+ )
+
+ def handle(self, *args, **kwargs):
+ verbose = kwargs['verbosity'] >= 1
+ answer = kwargs['answer']
+
+ # -v0 sets --yes
+ if not verbose:
+ answer = 'y'
+
+ for att in Attachment.objects.all():
+ if att.content_object is None:
+ if verbose:
+ self.stdout.write(
+ "Attachment `%s' to non-existing `%s' with PK `%s'" %
+ (att, att.content_type.model, att.object_id))
+
+ while answer not in 'yn':
+ answer = input("Do you wish to delete? [yN] ")
+ if not answer:
+ answer = 'x'
+ continue
+ answer = answer[0].lower()
+
+ if answer == 'y' :
+ remove_file_from_disk(att.attachment_file)
+ att.delete()
+
+ if verbose:
+ self.stdout.write("Deleted attachment `%s'" % att)
diff --git a/django/attachments/migrations/0001_initial.py b/django/attachments/migrations/0001_initial.py
new file mode 100644
index 0000000..3057bbd
--- /dev/null
+++ b/django/attachments/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+import attachments.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('contenttypes', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Attachment',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('object_id', models.PositiveIntegerField()),
+ ('attachment_file', models.FileField(upload_to=attachments.models.attachment_upload, verbose_name='attachment')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
+ ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
+ ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)),
+ ('creator', models.ForeignKey(related_name='created_attachments', verbose_name='creator', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
+ ],
+ options={
+ 'ordering': ['-created'],
+ 'permissions': (('delete_foreign_attachments', 'Can delete foreign attachments'),),
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/django/attachments/migrations/0002_auto_20180104_1247.py b/django/attachments/migrations/0002_auto_20180104_1247.py
new file mode 100644
index 0000000..707fa43
--- /dev/null
+++ b/django/attachments/migrations/0002_auto_20180104_1247.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11 on 2018-01-04 12:47
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='attachment',
+ options={'ordering': ['-created'], 'permissions': (('delete_foreign_attachments', 'Can delete foreign attachments'),), 'verbose_name': 'attachment', 'verbose_name_plural': 'attachments'},
+ ),
+ ]
diff --git a/django/attachments/migrations/0003_auto_20190722_1216.py b/django/attachments/migrations/0003_auto_20190722_1216.py
new file mode 100644
index 0000000..a3820bd
--- /dev/null
+++ b/django/attachments/migrations/0003_auto_20190722_1216.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2019-07-22 12:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0002_auto_20180104_1247'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='attachment',
+ name='object_id',
+ field=models.TextField(),
+ ),
+ ]
diff --git a/django/attachments/migrations/0004_db_index.py b/django/attachments/migrations/0004_db_index.py
new file mode 100644
index 0000000..852a5b5
--- /dev/null
+++ b/django/attachments/migrations/0004_db_index.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.0.9 on 2020-08-17 13:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0003_auto_20190722_1216'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='attachment',
+ name='created',
+ field=models.DateTimeField(auto_now_add=True, db_index=True,
+ verbose_name='created'),
+ ),
+ migrations.AlterField(
+ model_name='attachment',
+ name='modified',
+ field=models.DateTimeField(auto_now=True, db_index=True,
+ verbose_name='modified'),
+ ),
+ ]
diff --git a/django/attachments/migrations/0005_object_id_charfield.py b/django/attachments/migrations/0005_object_id_charfield.py
new file mode 100644
index 0000000..303e51f
--- /dev/null
+++ b/django/attachments/migrations/0005_object_id_charfield.py
@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0004_db_index'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='attachment',
+ name='object_id',
+ field=models.CharField(db_index=True, max_length=64),
+ ),
+ ]
diff --git a/django/attachments/migrations/0006_alter_attachment_attachment_file.py b/django/attachments/migrations/0006_alter_attachment_attachment_file.py
new file mode 100644
index 0000000..7a398af
--- /dev/null
+++ b/django/attachments/migrations/0006_alter_attachment_attachment_file.py
@@ -0,0 +1,19 @@
+# Generated by Django 3.2.25 on 2024-04-11 14:35
+
+from django.db import migrations, models
+import uuid_upload_path.storage
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('attachments', '0005_object_id_charfield'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='attachment',
+ name='attachment_file',
+ field=models.FileField(upload_to=uuid_upload_path.storage.upload_to, verbose_name='attachment'),
+ ),
+ ]
diff --git a/django/attachments/migrations/0007_labelattachment.py b/django/attachments/migrations/0007_labelattachment.py
new file mode 100644
index 0000000..326e73f
--- /dev/null
+++ b/django/attachments/migrations/0007_labelattachment.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.2.25 on 2024-04-11 15:06
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('attachments', '0006_alter_attachment_attachment_file'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='LabelAttachment',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('type_ref_hash', models.CharField(blank=True, help_text='Reflects type + ref, where type could be any bip-329 defined type', max_length=64)),
+ ('network', models.CharField(choices=[('mainnet', 'Mainnet'), ('testnet', 'Testnet')], default='mainnet', help_text="Choose the network for this labelbase's label attachement.", max_length=10)),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'network', 'type_ref_hash')},
+ },
+ ),
+ ]
diff --git a/django/attachments/migrations/__init__.py b/django/attachments/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/attachments/models.py b/django/attachments/models.py
new file mode 100644
index 0000000..2f107cf
--- /dev/null
+++ b/django/attachments/models.py
@@ -0,0 +1,111 @@
+from __future__ import unicode_literals
+
+import os
+
+from django.conf import settings
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+from six import python_2_unicode_compatible
+from uuid_upload_path import upload_to
+from django.contrib.auth.models import User
+import logging
+
+logger = logging.getLogger('labelbase')
+
+def attachment_upload(instance, filename):
+ pass # used for compatibility / migrations only.
+
+
+class AttachmentManager(models.Manager):
+ def attachments_for_object(self, obj):
+ object_type = ContentType.objects.get_for_model(obj)
+ return self.filter(content_type__pk=object_type.id, object_id=obj.pk)
+
+
+@python_2_unicode_compatible
+class Attachment(models.Model):
+ objects = AttachmentManager()
+
+
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+ object_id = models.CharField(db_index=True, max_length=64)
+ content_object = GenericForeignKey("content_type", "object_id")
+ creator = models.ForeignKey(
+ settings.AUTH_USER_MODEL,
+ related_name="created_attachments",
+ verbose_name=_("creator"),
+ on_delete=models.CASCADE,
+ )
+ attachment_file = models.FileField(
+ _("attachment"), upload_to=upload_to
+ )
+ created = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
+ modified = models.DateTimeField(_("modified"), auto_now=True, db_index=True)
+
+ class Meta:
+ verbose_name = _("attachment")
+ verbose_name_plural = _("attachments")
+ ordering = ["-created"]
+ permissions = (
+ ("delete_foreign_attachments", _("Can delete foreign attachments")),
+ )
+
+ def __str__(self):
+ return _("{username} attached {filename}").format(
+ username=self.creator.get_username(),
+ filename=self.attachment_file.name,
+ )
+
+ @property
+ def filename(self):
+ return os.path.split(self.attachment_file.name)[1]
+
+ def attach_to(self, new_object, update_path=False):
+ """
+ Attach to a new object and possibly move the actual file on disk!
+
+ .. important::
+
+ As long as path names are valid you can continue serving
+ the files from their original path and not change it!
+ """
+ self.object_id = new_object.pk
+ self.content_type = ContentType.objects.get_for_model(new_object)
+ self.save()
+
+ if update_path:
+ old_path = self.attachment_file.path
+ self.attachment_file.name = upload_to
+ self.attachment_file.save()
+
+ os.makedirs(
+ os.path.dirname(self.attachment_file.path), exist_ok=True)
+ os.rename(old_path, self.attachment_file.path)
+
+
+class LabelAttachment(models.Model):
+
+ user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
+ type_ref_hash = models.CharField(max_length=64,
+ blank=True,
+ help_text="Reflects type + ref, where type could be any bip-329 defined type")
+
+ MAINNET = 'mainnet'
+ TESTNET = 'testnet'
+
+ NETWORK_CHOICES = [
+ (MAINNET, 'Mainnet'),
+ (TESTNET, 'Testnet'),
+ ]
+
+ network = models.CharField(
+ max_length=10,
+ choices=NETWORK_CHOICES,
+ default='mainnet',
+ help_text="Choose the network for this labelbase's label attachement."
+ )
+
+ class Meta:
+ unique_together = (("user", "network", "type_ref_hash"),)
diff --git a/django/attachments/templatetags/__init__.py b/django/attachments/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/attachments/templatetags/attachments_tags.py b/django/attachments/templatetags/attachments_tags.py
new file mode 100644
index 0000000..6580fef
--- /dev/null
+++ b/django/attachments/templatetags/attachments_tags.py
@@ -0,0 +1,69 @@
+from django.template import Library
+from django.urls import reverse
+
+from ..forms import AttachmentForm
+from ..models import Attachment
+from ..views import add_url_for_obj
+
+register = Library()
+
+@register.filter(name='endswith')
+def endswith(value, arg):
+ """Checks if the value ends with a certain string."""
+ if isinstance(value, str):
+ return value.endswith(arg)
+ return False
+
+@register.inclusion_tag("attachments/add_form.html", takes_context=True)
+def attachment_form(context, obj, **kwargs):
+ """
+ Renders a "upload attachment" form.
+
+ The user must own ``attachments.add_attachment permission`` to add
+ attachments.
+ """
+
+ return {
+ "form": AttachmentForm(),
+ "form_url": add_url_for_obj(obj),
+ "next": context.request.path,
+ }
+
+
+@register.inclusion_tag("attachments/delete_link.html", takes_context=True)
+def attachment_delete_link(context, attachment, **kwargs):
+ if context["user"] == attachment.creator:
+ return {
+ "next": context.request.path,
+ "delete_url": reverse(
+ "attachments:delete", kwargs={"attachment_pk": attachment.pk}
+ ),
+ }
+
+
+
+@register.simple_tag
+def attachments_count(obj):
+ """
+ Counts attachments that are attached to a given object::
+
+ {% attachments_count obj %}
+ """
+ attachments_count = Attachment.objects.attachments_for_object(obj).count()
+ print("obj: {}, attachments_count: {}".format(obj.id, attachments_count))
+
+ return attachments_count
+
+
+@register.simple_tag
+def get_attachments_for(obj, *args, **kwargs):
+ """
+ Resolves attachments that are attached to a given object. You can specify
+ the variable name in the context the attachments are stored using the `as`
+ argument.
+
+ Syntax::
+
+ {% get_attachments_for obj as "my_attachments" %}
+ """
+ return Attachment.objects.attachments_for_object(obj)
diff --git a/django/attachments/tests/__init__.py b/django/attachments/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django/attachments/tests/base.py b/django/attachments/tests/base.py
new file mode 100644
index 0000000..32d6945
--- /dev/null
+++ b/django/attachments/tests/base.py
@@ -0,0 +1,68 @@
+# -*- encoding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.contrib.auth.models import Permission, User
+from django.contrib.contenttypes.models import ContentType
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.test import TestCase
+from django.urls import reverse
+
+from ..models import Attachment
+from .testapp.models import TestModel
+
+
+class BaseTestCase(TestCase):
+ target_model_class = TestModel
+
+ def setUp(self):
+ """
+ Create two users with `attachments.add_attachment` permission
+ and one object to attach files to.
+ """
+ content_type = ContentType.objects.get_for_model(Attachment)
+ self.add_permission = Permission.objects.get(
+ content_type=content_type, codename="add_attachment"
+ )
+ self.del_permission = Permission.objects.get(
+ content_type=content_type, codename="delete_attachment"
+ )
+
+ self.del_foreign_permission = Permission.objects.get(
+ content_type=content_type, codename="delete_foreign_attachments"
+ )
+
+ self.cred_jon = {"username": "jon", "password": "foobar"}
+ self.cred_jane = {"username": "jane", "password": "foobar"}
+ self.jon = User.objects.create_user(**self.cred_jon)
+ self.jon.user_permissions.add(self.add_permission)
+ self.jon.user_permissions.add(self.del_permission)
+
+ self.jane = User.objects.create_user(**self.cred_jane)
+ self.jane.user_permissions.add(self.add_permission)
+ self.jane.user_permissions.add(self.del_permission)
+
+ self.obj = self.target_model_class.objects.create(title="My first test item")
+
+ def _upload_testfile(self, file_obj=None, file_content=b"file content", **extra):
+ """
+ Uploads a sample file for the given user.
+ """
+ add_url = reverse(
+ "attachments:add",
+ kwargs={
+ "app_label": "testapp",
+ "model_name": self.target_model_class.__name__.lower(),
+ "pk": self.obj.pk,
+ },
+ )
+
+ if not file_obj:
+ file_obj = SimpleUploadedFile(
+ "Ünicode Filename 🙂.jpg",
+ file_content,
+ content_type="image/jpeg",
+ )
+ return self.client.post(
+ add_url, {"attachment_file": file_obj}, follow=True,
+ **extra
+ )
diff --git a/django/attachments/tests/test_integrity.py b/django/attachments/tests/test_integrity.py
new file mode 100644
index 0000000..68ac80c
--- /dev/null
+++ b/django/attachments/tests/test_integrity.py
@@ -0,0 +1,19 @@
+from django.core.management import call_command
+from django.test import TestCase
+from six import StringIO
+
+
+class IntegrityTestCase(TestCase):
+ """
+ Very basic tests around the app itself, not the code.
+ """
+
+ def test_no_pending_migrations(self):
+ """
+ Make sure all model changes are reflected with Django migrations.
+ """
+ output = StringIO()
+ call_command(
+ "makemigrations", "--dry-run", interactive=False, stdout=output
+ )
+ self.assertTrue("No changes detected" in output.getvalue())
diff --git a/django/attachments/tests/test_template.py b/django/attachments/tests/test_template.py
new file mode 100644
index 0000000..1cac854
--- /dev/null
+++ b/django/attachments/tests/test_template.py
@@ -0,0 +1,69 @@
+from django.urls import reverse
+
+from ..models import Attachment
+from .base import BaseTestCase
+
+
+class ViewTestCase(BaseTestCase):
+ def setUp(self):
+ super(ViewTestCase, self).setUp()
+ self.item_url = reverse("testapp-detail", kwargs={"pk": self.obj.pk})
+
+ def test_uploaded_attachment_urls_are_listed(self):
+ self.client.login(**self.cred_jon)
+ self._upload_testfile()
+ response = self.client.get(self.item_url)
+ attachment = Attachment.objects.attachments_for_object(self.obj)[0]
+ self.assertTrue(attachment.attachment_file.url in str(response.content))
+
+ def test_attachment_count_is_listed(self):
+ self.client.login(**self.cred_jon)
+ self._upload_testfile()
+ self._upload_testfile()
+ response = self.client.get(self.item_url)
+ attachment_count = Attachment.objects.attachments_for_object(
+ self.obj
+ ).count()
+ self.assertTrue(
+ "Object has %d attachments" % attachment_count
+ in str(response.content)
+ )
+
+ def test_upload_form_is_listed_with_add_permission(self):
+ self.client.login(**self.cred_jon)
+ response = self.client.get(self.item_url)
+ self.assertTrue("
+
+
+
+
Community-trusted Electrum servers
+
+
+ Mainnet:
+
+ fulcrum.sethforprivacy.com / s50002
+ electrum.emzy.de / s50002
+ electrum.blockstream.info / s50002
+
+
+
+
+
+ Testnet:
+
+ testnet.qtornado.com / s51002
+ testnet.aranguren.org / s51002
+
+
+
+
+
+
+
+
+ {% csrf_token %}
+
+
+ {{ form|bootstrap }}
+
+
+
+
+
+
+ Update Electrum Server
+
+
+
+
+
+
+
+
+
+
+
+
+
{% endblock %}
diff --git a/django/templates/encryption.html b/django/templates/encryption.html
index be512f6..c925c51 100644
--- a/django/templates/encryption.html
+++ b/django/templates/encryption.html
@@ -5,7 +5,7 @@
{% block nav_home %}active{% endblock %}
{% block content %}
-
+
L belbase
All your labels in one place.
@@ -18,11 +18,8 @@
-
-
+
+
Your labels are encrypted.
diff --git a/django/templates/fill_missing_data.html b/django/templates/fill_missing_data.html
new file mode 100644
index 0000000..b1d3f93
--- /dev/null
+++ b/django/templates/fill_missing_data.html
@@ -0,0 +1,128 @@
+{% extends "_base.html" %}
+{% load i18n %}
+{% load labelbase_tags %}
+{% load sekizai_tags %}
+
+{% block content %}
+
+{% if labelbase %}
+
+
+
Fill Missing Data - {{ labelbase.name }}
+ {% if labelbase.fingerprint %}
{{ labelbase.fingerprint }} {% endif %}
+ {% if labelbase.about %}
+
{{ labelbase.about }}
+ {% endif %}
+
+
+
+
+
+ Auto-populate BIP-329 additional fields (height, time, value) for outputs from OutputStat data.
+
+
+ {% if can_fill_from_outputstat %}
+
+ {{ can_fill_from_outputstat|length }} output labels can be filled!
+
+ {% else %}
+
+ All good! All {{ already_complete|length }} output labels have their fields populated.
+
+ {% endif %}
+
+
+ {% if can_fill_from_outputstat %}
+
+
+
+
+
+ {% for item in can_fill_from_outputstat %}
+
+
+
+ {{ item.label.get_type_display }} : {{ item.label.ref }}
+ Label: {{ item.label.label|default:"(empty)" }}
+ Missing fields:
+ {{ item.missing_fields|join:", " }}
+ Available data:
+
+ height={{ item.output_stat.confirmed_at_block_height }},
+ time={{ item.output_stat.confirmed_at_block_time }},
+ value={{ item.output_stat.value }} sats
+
+
+
+
+ {% csrf_token %}
+
+
+ Fill Now
+
+
Edit
+
+
+
+ {% endfor %}
+
+
+ {% csrf_token %}
+
+ Fill All from OutputStat
+
+
+
+
+ {% endif %}
+
+
+ {% if already_complete %}
+
+
+
+
+
+ {% for label in already_complete %}
+
+
+
+ {{ label.get_type_display }} : {{ label.ref }}
+ {{ label.label }}
+
+
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+
+
+{% else %}
+
Labelbase not found.
+{% endif %}
+
+
+
+{% endblock %}
diff --git a/django/templates/home.html b/django/templates/home.html
index b69639f..e49663e 100644
--- a/django/templates/home.html
+++ b/django/templates/home.html
@@ -268,27 +268,11 @@
-
Cloud-Hosted
-
Experience the flexibility and accessibility of Labelbase's cloud-hosted solution, providing users with a reliable and efficient label management service hosted on secure and scalable cloud infrastructure.
+
Attachments
+
Enhance your labels with attachments. Attach images and files directly to your labels, streamlining your documentation workflow and enriching the data associated with each label.
- {% comment %}
-
-
-
Customized Email Sending
-
Personalize email sending with custom server settings. Labelbase enables users to configure preferred SMTP and IMAP options, enhancing control, privacy and security.
-
-
- {% endcomment %}
-
-
-
-
On-demand Support Chat
-
We are happy to assist you with the integrated on-demand support chat by Chatwoot.
- Privacy is ensured as the chat script loads only when you request it, which is totally optional.
-
-
-
+
Automated Output Management
@@ -303,6 +287,47 @@
+
+
+
BIP-329 Extended Fields
+
Full support for BIP-329 additional fields including transaction height, timestamp, fees, values, exchange rates, and derivation paths. Enrich your labels with comprehensive transaction metadata.
+
+
+
+
+
+
Currency Sync Tool
+
Seamlessly synchronize between legacy currency annotations in label text and structured FMV fields. Automatically detect and resolve conflicts to maintain data consistency across formats.
+
+
+
+
+
+
Bulk Auto-Fill Missing Data
+
Automatically bulk populate transaction metadata from OutputStat records. Fill block heights, timestamps, and values for your outputs with one click, saving time and ensuring accuracy.
+
+
+
+
+
+
One-Click BIP-329 Field Population
+
Automatically populate transaction metadata like block height, timestamp, and value directly from OutputStat records. View missing fields at a glance and fill them with a single click from the output details page.
+
+
+
+
+
+
+
Fee Health Monitoring
+
Instantly visualize the cost-effectiveness of spending your UTXOs. Labelbase calculates and displays fee health status for each spendable output, color-coded to show if transaction fees would consume a healthy percentage of the output value.
+
+
+
+
+
UTXO Fee Efficiency Visualization
+
Visualize the cost-effectiveness of spending your UTXOs with color-coded treemaps. See at a glance which outputs have healthy fee-to-value ratios and which would be expensive to spend, helping you make informed decisions about consolidation and transaction planning.
+
+
diff --git a/django/templates/knowledge_base/article_detail.html b/django/templates/knowledge_base/article_detail.html
index 4ae51cc..8f660bc 100644
--- a/django/templates/knowledge_base/article_detail.html
+++ b/django/templates/knowledge_base/article_detail.html
@@ -53,7 +53,8 @@ code {
{% endfor %}
+
{{ article.content | markdown | safe }}
-
+
{% endblock %}
diff --git a/django/templates/knowledge_base/category_detail.html b/django/templates/knowledge_base/category_detail.html
index febd64f..3618cc4 100644
--- a/django/templates/knowledge_base/category_detail.html
+++ b/django/templates/knowledge_base/category_detail.html
@@ -8,8 +8,7 @@
{#% block nav_home %}active{% endblock %#}
{% block content %}
-
-
+
{% breadcrumbs_category category as current_breadcrumbs %}
@@ -31,5 +30,5 @@
{{ article.title }}
{% endfor %}
-
+
{% endblock %}
diff --git a/django/templates/knowledge_base/index.html b/django/templates/knowledge_base/index.html
index 2cedc8e..5dc6140 100644
--- a/django/templates/knowledge_base/index.html
+++ b/django/templates/knowledge_base/index.html
@@ -5,7 +5,7 @@
{% load static %}
{% load sekizai_tags %}
{% block title %}Knowledge Base Categories | Labelbase{% endblock %}
-{#% block nav_home %}active{% endblock %#}
+
{% block content %}
diff --git a/django/templates/label_derive_addresses.html b/django/templates/label_derive_addresses.html
new file mode 100644
index 0000000..76be5ef
--- /dev/null
+++ b/django/templates/label_derive_addresses.html
@@ -0,0 +1,171 @@
+{% extends "label_edit.html" %}
+{% load bootstrap %}
+{% load sekizai_tags %}
+{% load i18n %}
+{% load labelbase_tags %}
+
+
+{% block label_edit_content %}
+
+{% include "_modal_add_label.html" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/django/templates/label_edit.html b/django/templates/label_edit.html
index aa2f9fd..5830f6f 100644
--- a/django/templates/label_edit.html
+++ b/django/templates/label_edit.html
@@ -2,76 +2,74 @@
{% load i18n %}
{% load sekizai_tags %}
{% load labelbase_tags %}
+{% load backgroundtask_tags %}
+{% load attachments_tags %}
+
+{% block title %}{{ object.type }} {{ object.ref }}{% endblock %}
{% block content %}
-
+ {% endif %}
+
+
@@ -90,58 +88,51 @@
Ouch! Got "{{ output.last_error.message }}{% if output.last_error.code %} (code {{ output.last_error.code }}){% endif %}" from Electrum.
{% endif %}
- {% if object.type == "output" %}
- {% switch output.get_spent_status %}
- {% case "spent" %}
-
- Output spent! Blockchain records indicate that this output has been spent in another transaction.
-
- {% case "unspent" %}
-
- Output unspent! Blockchain records indicate that this output has not been spent yet.
-
- {% case "unconfirmed" %}
-
-
-
- Actions
-
-
-
-
Output unconfirmed! Blockchain records indicate that this output has not been confirmed yet.
-
- {% endswitch %}
- {% endif %}
-
-
-
-{% comment %}
-{% if object.type == "addr" %}
-
-
Heads up! There are multiple transaction outputs sent to this address. For privacy reasons, do not reuse addresses.
+{% is_label_id_in_queue object.id as is_in_queue %}
+{% if is_in_queue %}
+
+ Output in queue! This output is currently in the processing queue. It will be checked shortly.
+{% else %}
+ {% if object.type == "output" %}
+ {% if output.get_spent_status == "spent" %}
+
+ Output spent! Blockchain records indicate that this output has been spent in another transaction.
+
+ {% elif output.get_spent_status == "unspent" %}
+
+ Output unspent! Blockchain records indicate that this output has not been spent yet.
+
+ {% elif output.get_spent_status == "unconfirmed" %}
+
+
+
+ Actions
+
+
+
+
Output unconfirmed! Blockchain records indicate that this output has not been confirmed yet.
+
+ {% else %}
+
+ Unknown status: The status of this output could not be determined.
+
+ {% endif %}
+ {% endif %}
{% endif %}
-{% endcomment %}
+
{% block label_edit_content %}
{% endblock %}
-
-
-
-
-
-
-
-
-
+
+
-
{% endblock %}
diff --git a/django/templates/label_edit_attachments.html b/django/templates/label_edit_attachments.html
new file mode 100644
index 0000000..6e1ee3e
--- /dev/null
+++ b/django/templates/label_edit_attachments.html
@@ -0,0 +1,99 @@
+{% extends "label_edit.html" %}
+{% load bootstrap %}
+{% load i18n %}
+{% load static %}
+{% load attachments_tags %}
+{% load sekizai_tags %}
+
+
+{% block extra_media %}
+
+
+{% endblock %}
+
+
+{% block label_edit_content %}
+
+
+
+
+{% if my_attachments %}
+
+
+
+
+ #
+ Attachment
+ Action
+
+
+
+ {% for attachment in my_attachments %}
+
+ {{ attachment.id }}
+
+ {% if attachment.attachment_file.url|lower|endswith:".pdf" %}
+
+
+ {% else %}
+
+
+
+
+ {% endif %}
+
+ {% attachment_delete_link attachment %}
+
+ {% empty %}
+
+ No attachments found.
+
+ {% endfor %}
+
+
+
+{% else %}
+
+
+Label has {% attachments_count object.get_label_attachment %} attachments.
+
+
+{% endif %}
+
+
+{% attachment_form form.instance %}
+
+
+
+{% endblock %}
diff --git a/django/templates/label_edit_labeling.html b/django/templates/label_edit_labeling.html
index 034cbcc..35416fc 100644
--- a/django/templates/label_edit_labeling.html
+++ b/django/templates/label_edit_labeling.html
@@ -168,6 +168,8 @@
-
-
+
{% endif %}
-
-
{% endblock %}
diff --git a/django/templates/labelbase_tree_maps.html b/django/templates/labelbase_tree_maps.html
index 36e80e1..b18d500 100644
--- a/django/templates/labelbase_tree_maps.html
+++ b/django/templates/labelbase_tree_maps.html
@@ -16,6 +16,12 @@
Unspent Spendable Outputs
+
+
+ Fee Efficiency (VTER)
+
+
diff --git a/django/templates/labelbase_tree_maps_unspent_outputs.html b/django/templates/labelbase_tree_maps_unspent_outputs.html
index ceeb33a..326ceb2 100644
--- a/django/templates/labelbase_tree_maps_unspent_outputs.html
+++ b/django/templates/labelbase_tree_maps_unspent_outputs.html
@@ -11,14 +11,26 @@
-
-{% if labelbase.is_testnet %}
+{% if action == "fee-efficiency" %}
+
+
+
+
Fee Efficiency (VTER): Color shows how efficient it is to spend each output based on the fee-to-value ratio.
+
+ 🟢
Healthy: Fee < {{ request.user.profile.my_fee_threshold_healthy }}% of value
+ 🟡
Warning: Fee {{ request.user.profile.my_fee_threshold_healthy }}-{{ request.user.profile.my_fee_threshold_warning }}% of value
+ 🔴
High: Fee > {{ request.user.profile.my_fee_threshold_warning }}% of value
+
+
Box size represents output value in sats. Thresholds based on your fee settings. Adjust in Profile → Fees
+
+
+{% elif labelbase.is_testnet %}
Important Notice for Testnet Transactions: Testnet coins hold no real-world value and are solely intended for testing purposes.
-
{% else %}
@@ -28,6 +40,7 @@ If the UTXO value is not provided, Labelbase will estimate it based on historica
{% endif %}
+