delete old files
This commit is contained in:
@@ -1,274 +0,0 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Nginx Proxy Manager API",
|
||||
"version": "2.x.x"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://127.0.0.1:81/api"
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/": {
|
||||
"get": {
|
||||
"$ref": "./paths/get.json"
|
||||
}
|
||||
},
|
||||
"/audit-log": {
|
||||
"get": {
|
||||
"$ref": "./paths/audit-log/get.json"
|
||||
}
|
||||
},
|
||||
"/nginx/access-lists": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/access-lists/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/access-lists/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/access-lists/{listID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/access-lists/listID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/nginx/access-lists/listID/put.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/nginx/access-lists/listID/delete.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/certificates/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/certificates/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates/validate": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/certificates/validate/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates/test-http": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/certificates/test-http/get.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates/{certID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/certificates/certID/get.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/nginx/certificates/certID/delete.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates/{certID}/download": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/certificates/certID/download/get.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates/{certID}/renew": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/certificates/certID/renew/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/certificates/{certID}/upload": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/certificates/certID/upload/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/proxy-hosts": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/proxy-hosts/{hostID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/hostID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/hostID/put.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/hostID/delete.json"
|
||||
}
|
||||
},
|
||||
"/nginx/proxy-hosts/{hostID}/enable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/proxy-hosts/{hostID}/disable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/redirection-hosts": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/redirection-hosts/{hostID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/hostID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/hostID/put.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/hostID/delete.json"
|
||||
}
|
||||
},
|
||||
"/nginx/redirection-hosts/{hostID}/enable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/redirection-hosts/{hostID}/disable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/dead-hosts": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/dead-hosts/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/dead-hosts/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/dead-hosts/{hostID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/dead-hosts/hostID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/nginx/dead-hosts/hostID/put.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/nginx/dead-hosts/hostID/delete.json"
|
||||
}
|
||||
},
|
||||
"/nginx/dead-hosts/{hostID}/enable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/dead-hosts/{hostID}/disable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/streams": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/streams/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/streams/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/streams/{streamID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/nginx/streams/streamID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/nginx/streams/streamID/put.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/nginx/streams/streamID/delete.json"
|
||||
}
|
||||
},
|
||||
"/nginx/streams/{streamID}/enable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/streams/streamID/enable/post.json"
|
||||
}
|
||||
},
|
||||
"/nginx/streams/{streamID}/disable": {
|
||||
"post": {
|
||||
"$ref": "./paths/nginx/streams/streamID/disable/post.json"
|
||||
}
|
||||
},
|
||||
"/reports/hosts": {
|
||||
"get": {
|
||||
"$ref": "./paths/reports/hosts/get.json"
|
||||
}
|
||||
},
|
||||
"/schema": {
|
||||
"get": {
|
||||
"$ref": "./paths/schema/get.json"
|
||||
}
|
||||
},
|
||||
"/settings": {
|
||||
"get": {
|
||||
"$ref": "./paths/settings/get.json"
|
||||
}
|
||||
},
|
||||
"/settings/{settingID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/settings/settingID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/settings/settingID/put.json"
|
||||
}
|
||||
},
|
||||
"/tokens": {
|
||||
"get": {
|
||||
"$ref": "./paths/tokens/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/tokens/post.json"
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"$ref": "./paths/users/get.json"
|
||||
},
|
||||
"post": {
|
||||
"$ref": "./paths/users/post.json"
|
||||
}
|
||||
},
|
||||
"/users/{userID}": {
|
||||
"get": {
|
||||
"$ref": "./paths/users/userID/get.json"
|
||||
},
|
||||
"put": {
|
||||
"$ref": "./paths/users/userID/put.json"
|
||||
},
|
||||
"delete": {
|
||||
"$ref": "./paths/users/userID/delete.json"
|
||||
}
|
||||
},
|
||||
"/users/{userID}/auth": {
|
||||
"put": {
|
||||
"$ref": "./paths/users/userID/auth/put.json"
|
||||
}
|
||||
},
|
||||
"/users/{userID}/permissions": {
|
||||
"put": {
|
||||
"$ref": "./paths/users/userID/permissions/put.json"
|
||||
}
|
||||
},
|
||||
"/users/{userID}/login": {
|
||||
"post": {
|
||||
"$ref": "./paths/users/userID/login/post.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
# Pricing Configuration Feature
|
||||
|
||||
This document describes the new configurable pricing feature that allows MASTER instances to manage pricing plans through the admin interface.
|
||||
|
||||
## Overview
|
||||
|
||||
The pricing configuration feature allows administrators on MASTER instances to:
|
||||
- Create, edit, and delete pricing plans
|
||||
- Configure plan features, prices, and settings
|
||||
- Set resource quotas for rooms, conversations, storage, and users
|
||||
- Mark plans as "Most Popular" or "Custom"
|
||||
- Control plan visibility and ordering
|
||||
- Update pricing without code changes
|
||||
|
||||
## Features
|
||||
|
||||
### Pricing Plan Management
|
||||
- **Plan Name**: Display name for the pricing plan
|
||||
- **Description**: Optional description shown below the plan name
|
||||
- **Pricing**: Monthly and annual prices (annual prices are typically 20% lower)
|
||||
- **Features**: Dynamic list of features with checkmarks
|
||||
- **Button Configuration**: Customizable button text and URL
|
||||
- **Plan Types**: Regular plans with prices or custom plans
|
||||
- **Popular Plans**: Mark one plan as "Most Popular" with special styling
|
||||
- **Active/Inactive**: Toggle plan visibility
|
||||
- **Ordering**: Control the display order of plans
|
||||
|
||||
### Resource Quotas
|
||||
- **Room Quota**: Maximum number of rooms allowed (0 = unlimited)
|
||||
- **Conversation Quota**: Maximum number of conversations allowed (0 = unlimited)
|
||||
- **Storage Quota**: Maximum storage in gigabytes (0 = unlimited)
|
||||
- **Manager Quota**: Maximum number of manager users allowed (0 = unlimited)
|
||||
- **Admin Quota**: Maximum number of admin users allowed (0 = unlimited)
|
||||
|
||||
### Admin Interface
|
||||
- **Pricing Tab**: New tab in admin settings (MASTER instances only)
|
||||
- **Add/Edit Modals**: User-friendly forms for plan management
|
||||
- **Real-time Updates**: Changes are reflected immediately
|
||||
- **Feature Management**: Add/remove features dynamically
|
||||
- **Quota Configuration**: Set resource limits for each plan
|
||||
- **Status Toggles**: Quick switches for plan properties
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Database Migration
|
||||
Run the migrations to create the pricing_plans table and add quota fields:
|
||||
|
||||
```bash
|
||||
# Apply the migrations
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### 2. Initialize Default Plans (Optional)
|
||||
Run the initialization script to create default pricing plans:
|
||||
|
||||
```bash
|
||||
# Set MASTER environment variable
|
||||
export MASTER=true
|
||||
|
||||
# Run the initialization script
|
||||
python init_pricing_plans.py
|
||||
```
|
||||
|
||||
This will create four default plans with quotas:
|
||||
- **Starter**: 5 rooms, 10 conversations, 10GB storage, 10 managers, 1 admin
|
||||
- **Professional**: 25 rooms, 50 conversations, 100GB storage, 50 managers, 3 admins
|
||||
- **Enterprise**: 100 rooms, 200 conversations, 500GB storage, 200 managers, 10 admins
|
||||
- **Custom**: Unlimited everything
|
||||
|
||||
### 3. Access Admin Interface
|
||||
1. Log in as an admin user on a MASTER instance
|
||||
2. Go to Settings
|
||||
3. Click on the "Pricing" tab
|
||||
4. Configure your pricing plans
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a New Plan
|
||||
1. Click "Add New Plan" in the pricing tab
|
||||
2. Fill in the plan details:
|
||||
- **Name**: Plan display name
|
||||
- **Description**: Optional description
|
||||
- **Monthly Price**: Price per month
|
||||
- **Annual Price**: Price per month when billed annually
|
||||
- **Quotas**: Set resource limits (0 = unlimited)
|
||||
- **Features**: Add features using the "Add Feature" button
|
||||
- **Button Text**: Text for the call-to-action button
|
||||
- **Button URL**: URL the button should link to
|
||||
- **Options**: Check "Most Popular", "Custom Plan", or "Active" as needed
|
||||
3. Click "Create Plan"
|
||||
|
||||
### Editing a Plan
|
||||
1. Click the "Edit" button on any plan card
|
||||
2. Modify the plan details in the modal
|
||||
3. Click "Update Plan"
|
||||
|
||||
### Managing Plan Status
|
||||
- **Active/Inactive**: Use the toggle switch in the plan header
|
||||
- **Most Popular**: Check the "Most Popular" checkbox (only one plan can be popular)
|
||||
- **Custom Plan**: Check "Custom Plan" for plans without fixed pricing
|
||||
|
||||
### Deleting a Plan
|
||||
1. Click the "Delete" button on a plan card
|
||||
2. Confirm the deletion in the modal
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Database Schema
|
||||
The `pricing_plans` table includes:
|
||||
- `id`: Primary key
|
||||
- `name`: Plan name (required)
|
||||
- `description`: Optional description
|
||||
- `monthly_price`: Monthly price (float)
|
||||
- `annual_price`: Annual price (float)
|
||||
- `features`: JSON array of feature strings
|
||||
- `button_text`: Button display text
|
||||
- `button_url`: Button link URL
|
||||
- `is_popular`: Boolean for "Most Popular" styling
|
||||
- `is_custom`: Boolean for custom plans
|
||||
- `is_active`: Boolean for plan visibility
|
||||
- `order_index`: Integer for display ordering
|
||||
- `room_quota`: Maximum rooms (0 = unlimited)
|
||||
- `conversation_quota`: Maximum conversations (0 = unlimited)
|
||||
- `storage_quota_gb`: Maximum storage in GB (0 = unlimited)
|
||||
- `manager_quota`: Maximum managers (0 = unlimited)
|
||||
- `admin_quota`: Maximum admins (0 = unlimited)
|
||||
- `created_by`: Foreign key to user who created the plan
|
||||
- `created_at`/`updated_at`: Timestamps
|
||||
|
||||
### API Endpoints
|
||||
- `POST /api/admin/pricing-plans` - Create new plan
|
||||
- `GET /api/admin/pricing-plans/<id>` - Get plan details
|
||||
- `PUT /api/admin/pricing-plans/<id>` - Update plan
|
||||
- `DELETE /api/admin/pricing-plans/<id>` - Delete plan
|
||||
- `PATCH /api/admin/pricing-plans/<id>/status` - Update plan status
|
||||
|
||||
### Template Integration
|
||||
The pricing page automatically uses configured plans:
|
||||
- Falls back to hardcoded plans if no plans are configured
|
||||
- Supports dynamic feature lists
|
||||
- Handles custom plans without pricing
|
||||
- Shows/hides billing toggle based on plan types
|
||||
- Displays quota information in plan cards
|
||||
|
||||
### Quota Enforcement
|
||||
The PricingPlan model includes utility methods for quota checking:
|
||||
- `check_quota(quota_type, current_count)`: Returns True if quota allows the operation
|
||||
- `get_quota_remaining(quota_type, current_count)`: Returns remaining quota
|
||||
- `format_quota_display(quota_type)`: Formats quota for display
|
||||
- `get_storage_quota_bytes()`: Converts GB to bytes for storage calculations
|
||||
|
||||
Example usage in your application:
|
||||
```python
|
||||
# Check if user can create a new room
|
||||
plan = PricingPlan.query.get(user_plan_id)
|
||||
current_rooms = Room.query.filter_by(created_by=user.id).count()
|
||||
if plan.check_quota('room_quota', current_rooms):
|
||||
# Allow room creation
|
||||
pass
|
||||
else:
|
||||
# Show upgrade message
|
||||
pass
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Only admin users can access pricing configuration
|
||||
- Only MASTER instances can configure pricing
|
||||
- All API endpoints require authentication and admin privileges
|
||||
- CSRF protection is enabled for all forms
|
||||
|
||||
## Customization
|
||||
|
||||
### Styling
|
||||
The pricing plans use the existing CSS variables:
|
||||
- `--primary-color`: Main brand color
|
||||
- `--secondary-color`: Secondary brand color
|
||||
- `--shadow-color`: Card shadows
|
||||
|
||||
### Button URLs
|
||||
Configure button URLs to point to:
|
||||
- Contact forms
|
||||
- Payment processors
|
||||
- Sales pages
|
||||
- Custom landing pages
|
||||
|
||||
### Features
|
||||
Features can include:
|
||||
- Storage limits
|
||||
- User limits
|
||||
- Feature availability
|
||||
- Support levels
|
||||
- Integration options
|
||||
|
||||
### Quota Integration
|
||||
To integrate quotas into your application:
|
||||
|
||||
1. **User Plan Assignment**: Associate users with pricing plans
|
||||
2. **Quota Checking**: Use the `check_quota()` method before operations
|
||||
3. **Upgrade Prompts**: Show upgrade messages when quotas are exceeded
|
||||
4. **Usage Tracking**: Track current usage for quota calculations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Pricing tab not visible**
|
||||
- Ensure you're on a MASTER instance (`MASTER=true`)
|
||||
- Ensure you're logged in as an admin user
|
||||
|
||||
2. **Plans not showing on pricing page**
|
||||
- Check that plans are marked as "Active"
|
||||
- Verify the database migration was applied
|
||||
- Check for JavaScript errors in browser console
|
||||
|
||||
3. **Features not saving**
|
||||
- Ensure at least one feature is provided
|
||||
- Check that feature text is not empty
|
||||
|
||||
4. **Quota fields not working**
|
||||
- Verify the quota migration was applied
|
||||
- Check that quota values are integers
|
||||
- Ensure quota fields are included in form submissions
|
||||
|
||||
5. **API errors**
|
||||
- Verify CSRF token is included in requests
|
||||
- Check that all required fields are provided
|
||||
- Ensure proper JSON formatting for features
|
||||
|
||||
### Debugging
|
||||
- Check browser console for JavaScript errors
|
||||
- Review server logs for API errors
|
||||
- Verify database connectivity
|
||||
- Test with default plans first
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements:
|
||||
- Plan categories/tiers
|
||||
- Regional pricing
|
||||
- Currency support
|
||||
- Promotional pricing
|
||||
- Plan comparison features
|
||||
- Analytics and usage tracking
|
||||
- Automatic quota enforcement middleware
|
||||
- Usage dashboard for quota monitoring
|
||||
@@ -1,59 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from app import create_app
|
||||
from models import db, RoomFile, Room, RoomMemberPermission
|
||||
from sqlalchemy import text
|
||||
|
||||
app = create_app()
|
||||
|
||||
def clear_all_data():
|
||||
with app.app_context():
|
||||
# Delete records in the correct order to handle foreign key constraints
|
||||
# 1. Delete all RoomFile records from the database
|
||||
RoomFile.query.delete()
|
||||
print("All RoomFile records deleted.")
|
||||
|
||||
# 2. Delete all RoomMemberPermission records
|
||||
RoomMemberPermission.query.delete()
|
||||
print("All RoomMemberPermission records deleted.")
|
||||
|
||||
# 3. Delete all room_members associations
|
||||
db.session.execute(text('DELETE FROM room_members'))
|
||||
print("All room_members associations deleted.")
|
||||
|
||||
# 4. Delete all Room records
|
||||
Room.query.delete()
|
||||
print("All Room records deleted.")
|
||||
|
||||
# Commit the database changes
|
||||
db.session.commit()
|
||||
print("Database cleanup completed.")
|
||||
|
||||
def clear_filesystem():
|
||||
# 1. Clear the data/rooms directory
|
||||
data_root = os.path.join(os.path.dirname(__file__), 'data', 'rooms')
|
||||
if os.path.exists(data_root):
|
||||
for item in os.listdir(data_root):
|
||||
item_path = os.path.join(data_root, item)
|
||||
if os.path.isfile(item_path):
|
||||
os.remove(item_path)
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
print("Cleared data/rooms directory")
|
||||
|
||||
# 2. Clear the uploads directory except for profile_pics
|
||||
uploads_dir = os.path.join(os.path.dirname(__file__), 'uploads')
|
||||
if os.path.exists(uploads_dir):
|
||||
for item in os.listdir(uploads_dir):
|
||||
if item != 'profile_pics':
|
||||
item_path = os.path.join(uploads_dir, item)
|
||||
if os.path.isfile(item_path):
|
||||
os.remove(item_path)
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
print("Cleared uploads directory")
|
||||
|
||||
if __name__ == '__main__':
|
||||
clear_all_data()
|
||||
clear_filesystem()
|
||||
print("Cleanup completed successfully!")
|
||||
@@ -1,27 +0,0 @@
|
||||
from app import create_app, db
|
||||
from app.models import RoomFile, Room
|
||||
import os
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# Get the Test room
|
||||
room = Room.query.filter_by(name='Test').first()
|
||||
if not room:
|
||||
print("Test room not found")
|
||||
exit(1)
|
||||
|
||||
# Delete from database
|
||||
files = ['Screenshot_2025-03-19_100338.png', 'Screenshot_2025-03-19_100419.png']
|
||||
deleted = RoomFile.query.filter_by(room_id=room.id, name__in=files).delete()
|
||||
db.session.commit()
|
||||
print(f"Deleted {deleted} records from database")
|
||||
|
||||
# Delete from filesystem
|
||||
room_path = os.path.join('data', 'rooms', str(room.id))
|
||||
for file in files:
|
||||
file_path = os.path.join(room_path, file)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Deleted file: {file_path}")
|
||||
else:
|
||||
print(f"File not found: {file_path}")
|
||||
@@ -1,11 +0,0 @@
|
||||
from app import app, db
|
||||
from models import Notif
|
||||
|
||||
def create_notifs_table():
|
||||
with app.app_context():
|
||||
# Create the table
|
||||
Notif.__table__.create(db.engine)
|
||||
print("Notifications table created successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_notifs_table()
|
||||
Reference in New Issue
Block a user