├── .dockerignore ├── .env ├── .gitignore ├── .node-version ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── README_assets.md ├── README_authentication.md ├── README_celery.md ├── README_css.md ├── README_db_migration.md ├── README_dynamic_tables.md ├── apps ├── __init__.py ├── authentication │ ├── __init__.py │ ├── forms.py │ ├── models.py │ ├── oauth.py │ ├── routes.py │ └── util.py ├── charts │ ├── __init__.py │ └── routes.py ├── config.py ├── db.sqlite3 ├── dyn_dt │ ├── __init__.py │ ├── routes.py │ └── utils.py ├── exceptions │ └── exception.py ├── helpers.py ├── home │ ├── __init__.py │ └── routes.py ├── messages.py ├── models.py └── tasks.py ├── build.sh ├── docker-compose.yml ├── env.sample ├── gunicorn-cfg.py ├── media └── .gitkeep ├── migrations ├── README ├── alembic.ini ├── env.py └── script.py.mako ├── nginx └── appseed-app.conf ├── package.json ├── postcss.config.js ├── render.yaml ├── requirements.txt ├── run.py ├── static └── assets │ ├── css │ ├── custom.css │ ├── custom.min.css │ ├── forms.css │ ├── forms.min.css │ ├── landing.css │ ├── landing.min.css │ ├── plugins │ │ ├── bootstrap.min.css │ │ └── jsvectormap.min.css │ ├── style-preset.css │ ├── style-preset.min.css │ ├── style.css │ ├── style.min.css │ ├── widgets.css │ └── widgets.min.css │ ├── fonts │ ├── feather.css │ ├── feather │ │ ├── feather.eot │ │ ├── feather.svg │ │ ├── feather.ttf │ │ └── feather.woff │ ├── fontawesome.css │ ├── fontawesome │ │ ├── fa-brands-400.eot │ │ ├── fa-brands-400.svg │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.eot │ │ ├── fa-regular-400.svg │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 │ ├── inter │ │ ├── Inter-italic.var.woff2 │ │ ├── Inter-roman.var.woff2 │ │ └── inter.css │ ├── material.css │ ├── material │ │ └── material.woff2 │ ├── phosphor │ │ └── duotone │ │ │ ├── Phosphor-Duotone.svg │ │ │ ├── Phosphor-Duotone.ttf │ │ │ ├── Phosphor-Duotone.woff │ │ │ ├── selection.json │ │ │ └── style.css │ ├── tabler-icons.min.css │ └── tabler │ │ ├── tabler-icons.eot │ │ ├── tabler-icons.svg │ │ ├── tabler-icons.ttf │ │ ├── tabler-icons.woff │ │ └── tabler-icons.woff2 │ ├── images │ ├── application │ │ └── img-coupon.png │ ├── error │ │ ├── 404.png │ │ └── generic.png │ ├── favicon.svg │ ├── icon-calendar.svg │ ├── icon-clock.svg │ ├── icon-unknown-alt.svg │ ├── icon-unknown.svg │ ├── landing │ │ ├── img-header-main.jpg │ │ └── img-wave.svg │ ├── logo-dark.svg │ ├── logo-white.svg │ ├── search.svg │ ├── selector-icons.svg │ └── user │ │ ├── avatar-1.jpg │ │ ├── avatar-10.jpg │ │ ├── avatar-2.jpg │ │ ├── avatar-3.jpg │ │ ├── avatar-4.jpg │ │ ├── avatar-5.jpg │ │ ├── avatar-6.jpg │ │ ├── avatar-7.jpg │ │ ├── avatar-8.jpg │ │ ├── avatar-9.jpg │ │ ├── avatar.jpg │ │ └── profile.jpg │ ├── img │ ├── csv.png │ └── export.png │ ├── js │ ├── fonts │ │ └── custom-font.js │ ├── pages │ │ └── dashboard-default.js │ ├── pcoded.js │ └── plugins │ │ ├── apexcharts.min.js │ │ ├── bootstrap.min.js │ │ ├── feather.min.js │ │ ├── jsvectormap.min.js │ │ ├── popper.min.js │ │ ├── simplebar.min.js │ │ ├── world-merc.js │ │ └── world.js │ └── scss │ └── custom.scss ├── templates ├── .gitkeep ├── authentication │ ├── login.html │ └── register.html ├── charts │ └── index.html ├── dyn_dt │ ├── index.html │ └── model.html ├── error │ ├── 403.html │ ├── 404.html │ └── 500.html ├── includes │ ├── footer.html │ ├── head.html │ ├── items-table.html │ ├── loader.html │ ├── navigation.html │ ├── scripts.html │ └── sidebar.html ├── layouts │ ├── base-auth.html │ └── base.html └── pages │ ├── color.html │ ├── icon-feather.html │ ├── index.html │ ├── profile.html │ ├── sample-page.html │ └── typography.html └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | __pycache__ 3 | *.pyc 4 | *.pyo 5 | *.pyd -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # True for development, False for production 2 | DEBUG=True 3 | 4 | # Flask ENV 5 | FLASK_APP=run.py 6 | FLASK_DEBUG=1 7 | 8 | # If not provided, a random one is generated 9 | # SECRET_KEY= 10 | 11 | # If DEBUG=False (production mode) 12 | # DB_ENGINE=mysql 13 | # DB_NAME=appseed_db 14 | # DB_HOST=localhost 15 | # DB_PORT=3306 16 | # DB_USERNAME=appseed_db_usr 17 | # DB_PASS= 18 | 19 | # SOCIAL AUTH Github 20 | # GITHUB_ID=YOUR_GITHUB_ID 21 | # GITHUB_SECRET=YOUR_GITHUB_SECRET 22 | 23 | # SOCIAL AUTH Google 24 | # GOOGLE_ID=YOUR_GOOGLE_ID 25 | # GOOGLE_SECRET=YOUR_GOOGLE_SECRET 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # tests and coverage 6 | *.pytest_cache 7 | .coverage 8 | 9 | # database & logs 10 | *.db 11 | #*.sqlite3 12 | *.log 13 | 14 | # venv 15 | env 16 | env_linux 17 | venv 18 | 19 | # other 20 | .DS_Store 21 | 22 | # sphinx docs 23 | _build 24 | _static 25 | _templates 26 | 27 | # javascript 28 | package-lock.json 29 | .vscode/symbols.json 30 | 31 | apps/static/assets/node_modules 32 | apps/static/assets/yarn.lock 33 | apps/static/assets/.temp 34 | 35 | 36 | #migrations 37 | 38 | node_modules/ 39 | yarn.lock 40 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v22.0.0 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.0.31] 2025-04-12 4 | ### Changes 5 | 6 | - Update RM Links 7 | - [Datta Able](https://app-generator.dev/product/datta-able/) Design 8 | - [CodedThemes](https://app-generator.dev/agency/codedthemes/) Agency 9 | 10 | ## [1.0.30] 2025-04-03 11 | ### Changes 12 | 13 | - Codebase Refactoring 14 | - Added Charts 15 | - Update Documentation 16 | 17 | ## [1.0.29] 2025-03-14 18 | ### Changes 19 | 20 | - RM - Update [Flask App Generator](https://app-generator.dev/tools/flask-generator/) Section 21 | 22 | ## [1.0.28] 2025-03-06 23 | ### Changes 24 | 25 | - Update RM - Added [Flask App Generator](https://app-generator.dev/tools/flask-generator/) Link 26 | - Select the preferred design 27 | - (Optional) Design Database: edit models and fields 28 | - (Optional) Edit the fields for the extended user model 29 | - (Optional) Enable OAuth for GitHub 30 | - (Optional) Add Celery (async tasks) 31 | - (Optional) Enable Dynamic Tables Module 32 | - Docker Scripts 33 | - Render CI/Cd Scripts 34 | 35 | ## [1.0.27] 2025-02-27 36 | ### Changes 37 | 38 | - User Profiles: Added Dynamic Management 39 | 40 | ## [1.0.26] 2025-02-26 41 | ### Changes 42 | 43 | - Added: 44 | - Celery Beat 45 | - OAuth: GitHub, Google 46 | - Flask-Minify 47 | 48 | ## [1.0.25] 2025-02-24 49 | ### Changes 50 | 51 | - Codebase Refactoring 52 | 53 | ## [1.0.24] 2025-02-21 54 | ### Changes 55 | 56 | - Update RM (minor) 57 | 58 | ## [1.0.23] 2025-02-20 59 | ### Changes 60 | 61 | - Update Dependencies 62 | - Added Error Pages 63 | 64 | ## [1.0.22] 2025-02-20 65 | ### Changes 66 | 67 | - Added Dynamic DataTables 68 | - Remove the Flask-RestX API 69 | 70 | ## [1.0.21] 2024-12-07 71 | ### Changes 72 | 73 | - Added Celery Support 74 | - a minimal core 75 | - Flask-RestX API 76 | 77 | ## [1.0.20] 2024-11-30 78 | ### Changes 79 | 80 | - Update Routes 81 | - Update Demo Link: 82 | - [Flask Datta Able](https://flask-datta-demo.onrender.com) 83 | 84 | ## [1.0.19] 2024-11-29 85 | ### Changes 86 | 87 | - Bump UI Version 88 | - Codebase Improvements 89 | - Update RM Links: 90 | - 👉 [Flask Datta Able](https://app-generator.dev/product/datta-able/flask/) - `Product Page` 91 | - 👉 [Flask Datta Able](https://flask-datta-demo.onrender.com) - `LIVE Demo` 92 | - 👉 [Flask Datta Able Documentation](https://app-generator.dev/docs/products/flask/datta-able/index.html) - `Complete Information` and Support Links 93 | - [Getting Started with Flask](https://app-generator.dev/docs/technologies/flask/index.html) - a `comprehensive tutorial` 94 | - `Configuration`: Install Tailwind/Flowbite, Prepare Environment, Setting up the Database 95 | - `Start with Docker` 96 | - `Manual Build` 97 | - `Start the project` 98 | - `Deploy on Render` 99 | 100 | ## [1.0.18] 2024-05-18 101 | ### Changes 102 | 103 | - Updated DOCS (readme) 104 | - [Custom Development](https://appseed.us/custom-development/) Section 105 | - [CI/CD Assistance for AWS, DO](https://appseed.us/terms/#section-ci-cd) 106 | 107 | ## [1.0.17] 2024-03-05 108 | ### Changes 109 | 110 | - Update [Custom Development](https://appseed.us/custom-development/) Section 111 | - New Pricing: `$3,999` 112 | 113 | ## [1.0.16] 2023-02-14 114 | ### Changes 115 | 116 | - Update [Custom Development](https://appseed.us/custom-development/) Section 117 | - Minor Changes (readme) 118 | 119 | ## [1.0.15] 2023-10-08 120 | ### Changes 121 | 122 | - Docs Update (readme) 123 | - Added infos for [Flask Datta PRO](https://appseed.us/product/datta-able-pro/flask/) 124 | 125 | ## [1.0.14] 2023-10-08 126 | ### Changes 127 | 128 | - Update Dependencies 129 | 130 | ## [1.0.13] 2023-01-02 131 | ### Changes 132 | 133 | - `DOCS Update` (readme) 134 | - [Flask Datta Able - Go LIVE](https://www.youtube.com/watch?v=ZpKy2j9UU84) (`video presentation`) 135 | 136 | ## [1.0.12] 2022-12-31 137 | ### Changes 138 | 139 | - Deployment-ready for Render (CI/CD) 140 | - `render.yaml` 141 | - `build.sh` 142 | - `DB Management` Improvement 143 | - `Silent fallback` to **SQLite** 144 | 145 | ## [1.0.11] 2022-09-07 146 | ### Improvements 147 | 148 | - Added OAuth via Github 149 | - Improved Auth Pages 150 | - Profile page (minor update) 151 | 152 | ## [1.0.10] 2022-06-28 153 | ### Improvements 154 | 155 | - Bump UI: `v1.0.0-enh1` 156 | - Added `dark-mode` 157 | - User profile page 158 | 159 | ## [1.0.9] 2022-06-23 160 | ### Improvements 161 | 162 | - Built with [Datta Able Generator](https://appseed.us/generator/datta-able/) 163 | - Timestamp: `2022-06-23 18:20` 164 | 165 | ## [1.0.8] 2022-06-13 166 | ### Improvements 167 | 168 | - Improved `Auth UX` 169 | - Built with [Datta Able Generator](https://appseed.us/generator/datta-able/) 170 | - Timestamp: `2022-05-30 21:10` 171 | 172 | ## [1.0.7] 2022-05-30 173 | ### Improvements 174 | 175 | - Built with [Datta Able Generator](https://appseed.us/generator/datta-able/) 176 | - Timestamp: `2022-05-30 21:10` 177 | 178 | ## [1.0.6] 2022-03-30 179 | ### Fixes 180 | 181 | - **Patch ImportError**: [cannot import name 'safe_str_cmp' from 'werkzeug.security'](https://docs.appseed.us/content/how-to-fix/importerror-cannot-import-name-safe_str_cmp-from-werkzeug.security) 182 | - `Werkzeug` deprecation of `safe_str_cmp` starting with version `2.1.0` 183 | - https://github.com/pallets/werkzeug/issues/2359 184 | 185 | ## [1.0.5] 2022-01-16 186 | ### Improvements 187 | 188 | - Bump Flask Codebase to [v2stable.0.1](https://github.com/app-generator/boilerplate-code-flask-dashboard/releases) 189 | - Dependencies update (all packages) 190 | - Flask==2.0.2 (latest stable version) 191 | - flask_wtf==1.0.0 192 | - jinja2==3.0.3 193 | - flask-restx==0.5.1 194 | - Forms Update: 195 | - Replace `TextField` (deprecated) with `StringField` 196 | 197 | ## Unreleased 198 | ### Fixes 199 | 200 | - 2021-11-08 - `v1.0.5-rc1` 201 | - ImportError: cannot import name 'TextField' from 'wtforms' 202 | - Problem caused by `WTForms-3.0.0` 203 | - Fix: use **WTForms==2.3.3** 204 | 205 | ## [1.0.4] 2021-11-06 206 | ### Improvements 207 | 208 | - Bump Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard) v2.0.0 209 | - Dependencies update (all packages) 210 | - Flask==2.0.1 (latest stable version) 211 | - Better Code formatting 212 | - Improved Files organization 213 | - Optimize imports 214 | - Docker Scripts Update 215 | 216 | ## [1.0.3] 2021-05-16 217 | ### Dependencies Update 218 | 219 | - Bump Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard) v1.0.6 220 | - Freeze used versions in `requirements.txt` 221 | - jinja2 = 2.11.3 222 | 223 | ## [1.0.2] 2021-03-18 224 | ### Improvements 225 | 226 | - Bump Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard) v1.0.5 227 | - Freeze used versions in `requirements.txt` 228 | - flask_sqlalchemy = 2.4.4 229 | - sqlalchemy = 1.3.23 230 | 231 | ## [1.0.1] 2020-01-17 232 | ### Improvements 233 | 234 | - Bump UI: [Jinja Datta Able](https://github.com/app-generator/jinja-datta-able/releases) v1.0.1 235 | - UI: [Datta Able](https://github.com/codedthemes/datta-able-bootstrap-dashboard) 2021-01-01 snapshot 236 | - Codebase: [Flask Dashboard](https://github.com/app-generator/boilerplate-code-flask-dashboard/releases) v1.0.3 237 | 238 | ## [1.0.0] 2020-02-07 239 | ### Initial Release 240 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | # set environment variables 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | ENV PYTHONUNBUFFERED 1 6 | ENV FLASK_APP run.py 7 | ENV DEBUG True 8 | 9 | COPY requirements.txt . 10 | 11 | # install python dependencies 12 | RUN pip install --upgrade pip 13 | RUN pip install --no-cache-dir -r requirements.txt 14 | 15 | COPY env.sample .env 16 | 17 | COPY . . 18 | 19 | # Init migration folder 20 | # RUN flask db init # to be executed only once 21 | RUN flask db migrate 22 | RUN flask db upgrade 23 | 24 | # gunicorn 25 | CMD ["gunicorn", "--config", "gunicorn-cfg.py", "run:app"] 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 - present [AppSeed](http://appseed.us/) 4 | 5 |
6 | 7 | ## Licensing Information 8 | 9 |
10 | 11 | | Item | - | 12 | | ---------------------------------- | --- | 13 | | License Type | MIT | 14 | | Use for print | **YES** | 15 | | Create single personal website/app | **YES** | 16 | | Create single website/app for client | **YES** | 17 | | Create multiple website/apps for clients | **YES** | 18 | | Create multiple SaaS applications | **YES** | 19 | | End-product paying users | **YES** | 20 | | Product sale | **YES** | 21 | | Remove footer credits | **YES** | 22 | | --- | --- | 23 | | Remove copyright mentions from source code | NO | 24 | | Production deployment assistance | NO | 25 | | Create HTML/CSS template for sale | NO | 26 | | Create Theme/Template for CMS for sale | NO | 27 | | Separate sale of our UI Elements | NO | 28 | 29 |
30 | 31 | --- 32 | For more information regarding licensing, please contact the AppSeed Service < *support@appseed.us* > 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Flask Datta Able](https://app-generator.dev/product/datta-able/flask/) 2 | 3 | **Open-source Flask Starter** crafted on top of **[Datta Able](https://app-generator.dev/product/datta-able/)**, an open-source `Bootstrap` UI Kit released by [CodedThemes](https://app-generator.dev/agency/codedthemes/). 4 | The product is designed to deliver the best possible user experience with highly customizable feature-rich pages. 5 | 6 | - 👉 [Flask Datta Able](https://app-generator.dev/product/datta-able/flask/) - `Product Page` 7 | - 👉 [Flask Datta Able](https://flask-datta-demo.onrender.com) - `LIVE Demo` 8 | - 👉 [Flask Datta Able Documentation](https://app-generator.dev/docs/products/flask/datta-able/index.html) - `Complete Information` and Support Links 9 | - [Getting Started with Flask](https://app-generator.dev/docs/technologies/flask/index.html) - a `comprehensive tutorial` 10 | - `Configuration`: Install Tailwind/Flowbite, Prepare Environment, Setting up the Database 11 | - `Start with Docker` 12 | - `Manual Build` 13 | - `Start the project` 14 | - `Deploy on Render` 15 | 16 |
17 | 18 | ## Features 19 | 20 | - Simple, Easy-to-Extend codebase, [Blueprint Pattern](https://app-generator.dev/blog/flask-blueprints-a-developers-guide/) 21 | - [Datta Able](https://app-generator.dev/product/datta-able/) Design Integration 22 | - [Bootstrap](https://app-generator.dev/docs/templates/bootstrap/index.html) 5 Styling 23 | - Session-based Authentication, GitHub, Google 24 | - DB Persistence: SQLite (default), can be used with MySql, PgSql 25 | - [Dynamic DataTables](https://flask-datta-demo.onrender.com/dynamic-dt) - manage data without coding 26 | - **Charts** by ApexCharts 27 | - Celery Beat 28 | - Docker, CI/CD for Render 29 | - [Vite](https://app-generator.dev/docs/technologies/vite/index.html) for assets management 30 | 31 | ![Flask Datta Able - Open-Source Flask Starter](https://user-images.githubusercontent.com/51070104/176118649-7233ffbc-6118-4f56-8cda-baa81d256877.png) 32 | 33 |
34 | 35 | ## Deploy LIVE 36 | 37 | > One-click deploy (requires already having an account). 38 | 39 | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) 40 | 41 |
42 | 43 | ## [Datta Able PRO Version](https://app-generator.dev/product/datta-able-pro/flask/) 44 | 45 | > The premium version provides more features, priority on support, and is more often updated - [Live Demo](https://flask-datta-pro.onrender.com/). 46 | 47 | - **Simple, Easy-to-Extend** Codebase 48 | - **Datta Able PRO** Design - Premium Version 49 | - **Extended User Profiles** 50 | - [Charts](https://flask-datta-pro.onrender.com/charts/) 51 | - [DataTables](https://flask-datta-pro.onrender.com/tables): Server-side Pagination, Search, Filters, Export 52 | - **File Manager** 53 | - **Celery** (async tasks) 54 | - **Docker** 55 | - **Deployment-Ready** for Render 56 | 57 | ![Datta Able PRO - Full-Stack Flask Starter provided by App-Generator.](https://user-images.githubusercontent.com/51070104/170474361-a58da82b-fff9-4a59-81a8-7ab99f478f48.png) 58 | 59 |
60 | 61 | ## `Customize` with [Flask Generator](https://app-generator.dev/tools/flask-generator/) 62 | 63 | - Access the [Flask Generator](https://app-generator.dev/tools/flask-generator/) 64 | - Select the preferred design 65 | - (Optional) Design Database: edit models and fields 66 | - (Optional) Edit the fields for the extended user model 67 | - (Optional) Enable OAuth for GitHub 68 | - (Optional) Add Celery (async tasks) 69 | - (Optional) Enable Dynamic Tables Module 70 | - Docker Scripts 71 | - Render CI/Cd Scripts 72 | 73 | **The generated Flask project is available as a ZIP Archive and also uploaded to GitHub.** 74 | 75 | ![Flask Generator - Flask App Generator - User Interface for choosing the Design](https://github.com/user-attachments/assets/fbf73fc0-e9a1-4f01-86a8-aa8be55413b5) 76 | 77 | ![Flask App Generator - User Interface for Edit the Extended User Model](https://github.com/user-attachments/assets/138b9816-4f2e-454f-84f2-7409969b8548) 78 | 79 |
80 | 81 | --- 82 | [Flask Datta Able](https://app-generator.dev/product/datta-able/flask/) - Open-Source **Flask** Starter provided by [App Generator](https://app-generator.dev) 83 | -------------------------------------------------------------------------------- /README_assets.md: -------------------------------------------------------------------------------- 1 | # Assets Management 2 | 3 | ## Flask-Minify 4 | 5 | @ToDo - compress css & pages 6 | 7 | ## Flask-CDN 8 | 9 | @ToDo - Enable CDN Support 10 | -------------------------------------------------------------------------------- /README_authentication.md: -------------------------------------------------------------------------------- 1 | # Flask-Dance Google OAuth Configuration 2 | 3 | This documentation outlines the setup and usage of Google OAuth authentication in a Flask application using Flask-Dance. 4 | 5 | ## Prerequisites 6 | 1. Install necessary dependencies: 7 | ```sh 8 | pip install flask-dance flask-login flask-sqlalchemy 9 | ``` 10 | 2. Set up Google OAuth credentials: 11 | - Go to [Google Cloud Console](https://console.developers.google.com/). 12 | - Create a new project and navigate to **APIs & Services > Credentials**. 13 | - Create OAuth 2.0 credentials and obtain the `Client ID` and `Client Secret`. 14 | - Add `http://127.0.0.1:5000/login/google/authorized` as an authorized redirect URI. 15 | 16 | ## Configuration 17 | 18 | ### `oauth.py` 19 | This file sets up Google OAuth using Flask-Dance and integrates it with Flask-Login. 20 | 21 | ```python 22 | from flask_dance.contrib.google import make_google_blueprint, google 23 | from flask_dance.consumer.storage.sqla import SQLAlchemyStorage 24 | from flask_login import login_user, current_user 25 | from sqlalchemy.orm.exc import NoResultFound 26 | from apps.config import Config 27 | from models import db, Users, OAuth 28 | 29 | # Google OAuth Blueprint 30 | google_blueprint = make_google_blueprint( 31 | client_id=Config.GOOGLE_ID, 32 | client_secret=Config.GOOGLE_SECRET, 33 | scope=[ 34 | "openid", 35 | "https://www.googleapis.com/auth/userinfo.email", 36 | "https://www.googleapis.com/auth/userinfo.profile", 37 | ], 38 | storage=SQLAlchemyStorage( 39 | OAuth, db.session, user=current_user, user_required=False, 40 | ), 41 | ) 42 | 43 | # OAuth Authorized Signal Handler 44 | @oauth_authorized.connect_via(google_blueprint) 45 | def google_logged_in(blueprint, token): 46 | info = google.get("/oauth2/v1/userinfo") 47 | if info.ok: 48 | account_info = info.json() 49 | username = account_info["given_name"] 50 | email = account_info["email"] 51 | 52 | query = Users.query.filter_by(oauth_google=username) 53 | try: 54 | user = query.one() 55 | login_user(user) 56 | except NoResultFound: 57 | user = Users() 58 | user.username = f"(google){username}" 59 | user.oauth_google = username 60 | user.email = email 61 | db.session.add(user) 62 | db.session.commit() 63 | login_user(user) 64 | ``` 65 | 66 | ### `routes.py` 67 | Defines the Google login route. 68 | 69 | ```python 70 | from flask import Blueprint, redirect, url_for 71 | from flask_dance.contrib.google import google 72 | from apps.authentication import blueprint 73 | 74 | @blueprint.route("/google") 75 | def login_google(): 76 | """Google login route.""" 77 | if not google.authorized: 78 | return redirect(url_for("google.login")) 79 | return redirect(url_for('home_blueprint.index')) 80 | ``` 81 | 82 | ### `login.html` 83 | Adds a Google login button to the template. 84 | 85 | ```html 86 | 93 | ``` 94 | 95 | -------------------------------------------------------------------------------- /README_celery.md: -------------------------------------------------------------------------------- 1 | # Celery Task Configuration 2 | 3 | ## 1. Celery Configuration (`tasks.py`) 4 | 5 | ### Initializing Celery 6 | 7 | ```python 8 | from celery import Celery 9 | from celery.utils.log import get_task_logger 10 | from celery.schedules import crontab 11 | import json, time 12 | from datetime import datetime 13 | from apps.config import Config 14 | 15 | logger = get_task_logger(__name__) 16 | 17 | celery_app = Celery( 18 | Config.CELERY_HOSTMACHINE, 19 | backend=Config.CELERY_RESULT_BACKEND, 20 | broker=Config.CELERY_BROKER_URL 21 | ) 22 | 23 | celery_app.conf.timezone = 'UTC' 24 | ``` 25 | 26 | ### Celery Beat Schedule 27 | A periodic task is scheduled to run every minute: 28 | 29 | ```python 30 | celery_app.conf.beat_schedule = { 31 | 'run_celery_beat_test_every_minute': { 32 | 'task': 'celery_beat_test', 33 | 'schedule': crontab(minute='*/1'), 34 | 'args': (json.dumps({'test': 'data'}),) 35 | }, 36 | } 37 | ``` 38 | 39 | ## 2. Task Definitions 40 | 41 | ### Regular Task (`celery_test`) 42 | 43 | ```python 44 | @celery_app.task(name="celery_test", bind=True) 45 | def celery_test(self, task_input): 46 | task_json = json.loads(task_input) 47 | 48 | logger.info('*** Started') 49 | logger.info(' > task_json:' + str(task_json)) 50 | 51 | task_json['state'] = 'STARTING' 52 | task_json['info'] = 'Task is starting' 53 | self.update_state(state='STARTING', meta={'info': 'Task is starting'}) 54 | time.sleep(1) 55 | 56 | task_json['state'] = 'RUNNING' 57 | task_json['info'] = 'Task is running' 58 | self.update_state(state='RUNNING', meta={'info': 'Task is running'}) 59 | time.sleep(1) 60 | 61 | task_json['state'] = 'CLOSING' 62 | task_json['info'] = 'Task is closing' 63 | self.update_state(state='CLOSING', meta={'info': 'Task is closing'}) 64 | time.sleep(1) 65 | 66 | task_json['state'] = 'FINISHED' 67 | task_json['info'] = 'Task is finished' 68 | task_json['result'] = 'SUCCESS' 69 | self.update_state(state='FINISHED', meta={'info': 'Task is finished'}) 70 | 71 | return task_json 72 | ``` 73 | 74 | ### Periodic Task (`celery_beat_test`) 75 | 76 | ```python 77 | @celery_app.task(name="celery_beat_test", bind=True) 78 | def celery_beat_test(self, task_input): 79 | task_json = {'info': 'Beat is running'} 80 | return task_json 81 | ``` 82 | 83 | ## 3. Flask Route for Testing Tasks (`routes.py`) 84 | 85 | A Flask route to trigger the Celery task and return the task ID: 86 | 87 | ```python 88 | @blueprint.route('/tasks-test') 89 | def tasks_test(): 90 | input_dict = {"data1": "04", "data2": "99"} 91 | input_json = json.dumps(input_dict) 92 | 93 | task = celery_test.delay(input_json) 94 | 95 | return f"TASK_ID: {task.id}, output: {task.get()}" 96 | ``` 97 | 98 | ## 4. Running Celery 99 | 100 | ### Start the Celery Worker 101 | Run the following command to start the worker: 102 | 103 | ```bash 104 | celery -A apps.tasks worker --loglevel=info 105 | ``` 106 | 107 | ### Start Celery Beat (for periodic tasks) 108 | Run the following command to start Celery Beat: 109 | 110 | ```bash 111 | celery -A apps.tasks beat --loglevel=info 112 | ``` 113 | 114 | -------------------------------------------------------------------------------- /README_css.md: -------------------------------------------------------------------------------- 1 | # Vite SCSS Compilation and Minification Setup 2 | 3 | This guide explains how to integrate Vite for SCSS compilation and CSS minification in a Flask project. It covers installation, configuration, and usage. 4 | 5 | ## Installation 6 | Ensure you have **Node.js** installed. Then, install the required dependencies: 7 | 8 | ```sh 9 | npm install vite sass autoprefixer cssnano --save-dev 10 | ``` 11 | 12 | 13 | ## Configuration 14 | 15 | ### `package.json` 16 | 17 | ```json 18 | { 19 | "name": "flask-datta-able", 20 | "version": "1.0.0", 21 | "description": "", 22 | "main": "index.js", 23 | "scripts": { 24 | "dev": "vite build --watch --mode development", 25 | "build": "vite build --mode production && npm run minify-css", 26 | "minify-css": "cssnano static/assets/css/*.css --dir static/assets/css --no-map --suffix .min" 27 | }, 28 | "keywords": [], 29 | "author": "", 30 | "license": "ISC", 31 | "devDependencies": { 32 | "autoprefixer": "^10.4.20", 33 | "cssnano": "^7.0.6", 34 | "sass": "^1.85.1", 35 | "vite": "^6.2.0" 36 | } 37 | } 38 | ``` 39 | 40 | ### `vite.config.js` 41 | 42 | ```javascript 43 | import { defineConfig } from "vite"; 44 | import autoprefixer from "autoprefixer"; 45 | import cssnano from "cssnano"; 46 | import path from "path"; 47 | 48 | export default defineConfig(({ mode }) => { 49 | const isProduction = mode === "production"; 50 | 51 | return { 52 | css: { 53 | postcss: { 54 | plugins: [ 55 | autoprefixer(), 56 | isProduction && cssnano(), 57 | ].filter(Boolean), 58 | }, 59 | }, 60 | build: { 61 | outDir: "static", 62 | emptyOutDir: false, 63 | rollupOptions: { 64 | input: path.resolve(__dirname, "static/assets/scss/custom.scss"), 65 | output: { 66 | assetFileNames: (assetInfo) => { 67 | if (assetInfo.name === "custom.css") { 68 | return "assets/css/custom.css"; 69 | } 70 | return "assets/css/[name].[ext]"; 71 | }, 72 | }, 73 | }, 74 | }, 75 | }; 76 | }); 77 | ``` 78 | 79 | ## Usage 80 | 81 | ### **Development Mode (Auto-Compile on Changes)** 82 | 83 | ```sh 84 | npm run dev 85 | ``` 86 | 87 | ### **Production Build (Minify CSS & Compile SCSS)** 88 | 89 | ```sh 90 | npm run build 91 | ``` -------------------------------------------------------------------------------- /README_db_migration.md: -------------------------------------------------------------------------------- 1 | # DB Migration 2 | 3 | @Todo 4 | -------------------------------------------------------------------------------- /README_dynamic_tables.md: -------------------------------------------------------------------------------- 1 | # Dynamic Tables 2 | 3 | @Todo 4 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os 7 | from flask import Flask 8 | from flask_login import LoginManager 9 | from flask_sqlalchemy import SQLAlchemy 10 | from importlib import import_module 11 | 12 | db = SQLAlchemy() 13 | login_manager = LoginManager() 14 | 15 | def register_extensions(app): 16 | db.init_app(app) 17 | login_manager.init_app(app) 18 | 19 | def register_blueprints(app): 20 | for module_name in ('authentication', 'home', 'dyn_dt', 'charts', ): 21 | module = import_module('apps.{}.routes'.format(module_name)) 22 | app.register_blueprint(module.blueprint) 23 | 24 | from apps.authentication.oauth import github_blueprint, google_blueprint 25 | 26 | def create_app(config): 27 | 28 | # Contextual 29 | static_prefix = '/static' 30 | templates_dir = os.path.dirname(config.BASE_DIR) 31 | 32 | TEMPLATES_FOLDER = os.path.join(templates_dir,'templates') 33 | STATIC_FOLDER = os.path.join(templates_dir,'static') 34 | 35 | print(' > TEMPLATES_FOLDER: ' + TEMPLATES_FOLDER) 36 | print(' > STATIC_FOLDER: ' + STATIC_FOLDER) 37 | 38 | app = Flask(__name__, static_url_path=static_prefix, template_folder=TEMPLATES_FOLDER, static_folder=STATIC_FOLDER) 39 | 40 | app.config.from_object(config) 41 | register_extensions(app) 42 | register_blueprints(app) 43 | app.register_blueprint(github_blueprint, url_prefix="/login") 44 | app.register_blueprint(google_blueprint, url_prefix="/login") 45 | return app 46 | -------------------------------------------------------------------------------- /apps/authentication/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask import Blueprint 7 | 8 | blueprint = Blueprint( 9 | 'authentication_blueprint', 10 | __name__, 11 | url_prefix='' 12 | ) 13 | -------------------------------------------------------------------------------- /apps/authentication/forms.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask_wtf import FlaskForm 7 | from wtforms import StringField, PasswordField 8 | from wtforms.validators import Email, DataRequired 9 | 10 | # login and registration 11 | 12 | class LoginForm(FlaskForm): 13 | username = StringField('Username', 14 | id='username_login', 15 | validators=[DataRequired()]) 16 | password = PasswordField('Password', 17 | id='pwd_login', 18 | validators=[DataRequired()]) 19 | 20 | class CreateAccountForm(FlaskForm): 21 | username = StringField('Username', 22 | id='username_create', 23 | validators=[DataRequired()]) 24 | email = StringField('Email', 25 | id='email_create', 26 | validators=[DataRequired(), Email()]) 27 | password = PasswordField('Password', 28 | id='pwd_create', 29 | validators=[DataRequired()]) 30 | -------------------------------------------------------------------------------- /apps/authentication/models.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask_login import UserMixin 7 | 8 | from sqlalchemy.exc import SQLAlchemyError, IntegrityError 9 | from flask_dance.consumer.storage.sqla import OAuthConsumerMixin 10 | 11 | from apps import db, login_manager 12 | from apps.authentication.util import hash_pass 13 | 14 | class Users(db.Model, UserMixin): 15 | 16 | __tablename__ = 'users' 17 | 18 | id = db.Column(db.Integer, primary_key=True) 19 | username = db.Column(db.String(64), unique=True) 20 | email = db.Column(db.String(64), unique=True) 21 | password = db.Column(db.LargeBinary) 22 | bio = db.Column(db.Text(), nullable=True) 23 | 24 | oauth_github = db.Column(db.String(100), nullable=True) 25 | oauth_google = db.Column(db.String(100), nullable=True) 26 | 27 | readonly_fields = ["id", "username", "email", "oauth_github", "oauth_google"] 28 | 29 | def __init__(self, **kwargs): 30 | for property, value in kwargs.items(): 31 | # depending on whether value is an iterable or not, we must 32 | # unpack it's value (when **kwargs is request.form, some values 33 | # will be a 1-element list) 34 | if hasattr(value, '__iter__') and not isinstance(value, str): 35 | # the ,= unpack of a singleton fails PEP8 (travis flake8 test) 36 | value = value[0] 37 | 38 | if property == 'password': 39 | value = hash_pass(value) # we need bytes here (not plain str) 40 | 41 | setattr(self, property, value) 42 | 43 | def __repr__(self): 44 | return str(self.username) 45 | 46 | @classmethod 47 | def find_by_email(cls, email: str) -> "Users": 48 | return cls.query.filter_by(email=email).first() 49 | 50 | @classmethod 51 | def find_by_username(cls, username: str) -> "Users": 52 | return cls.query.filter_by(username=username).first() 53 | 54 | @classmethod 55 | def find_by_id(cls, _id: int) -> "Users": 56 | return cls.query.filter_by(id=_id).first() 57 | 58 | def save(self) -> None: 59 | try: 60 | db.session.add(self) 61 | db.session.commit() 62 | 63 | except SQLAlchemyError as e: 64 | db.session.rollback() 65 | db.session.close() 66 | error = str(e.__dict__['orig']) 67 | raise IntegrityError(error, 422) 68 | 69 | def delete_from_db(self) -> None: 70 | try: 71 | db.session.delete(self) 72 | db.session.commit() 73 | except SQLAlchemyError as e: 74 | db.session.rollback() 75 | db.session.close() 76 | error = str(e.__dict__['orig']) 77 | raise IntegrityError(error, 422) 78 | return 79 | 80 | @login_manager.user_loader 81 | def user_loader(id): 82 | return Users.query.filter_by(id=id).first() 83 | 84 | @login_manager.request_loader 85 | def request_loader(request): 86 | username = request.form.get('username') 87 | user = Users.query.filter_by(username=username).first() 88 | return user if user else None 89 | 90 | class OAuth(OAuthConsumerMixin, db.Model): 91 | user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="cascade"), nullable=False) 92 | user = db.relationship(Users) 93 | -------------------------------------------------------------------------------- /apps/authentication/oauth.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os 7 | from flask import current_app as app 8 | from flask_login import current_user, login_user 9 | from flask_dance.consumer import oauth_authorized 10 | from flask_dance.contrib.github import github, make_github_blueprint 11 | from flask_dance.contrib.google import google, make_google_blueprint 12 | from flask_dance.consumer.storage.sqla import SQLAlchemyStorage 13 | from sqlalchemy.orm.exc import NoResultFound 14 | from apps.config import Config 15 | from .models import Users, db, OAuth 16 | from flask import redirect, url_for 17 | from flask import flash 18 | 19 | github_blueprint = make_github_blueprint( 20 | client_id=Config.GITHUB_ID, 21 | client_secret=Config.GITHUB_SECRET, 22 | scope = 'user', 23 | storage=SQLAlchemyStorage( 24 | OAuth, 25 | db.session, 26 | user=current_user, 27 | user_required=False, 28 | ), 29 | ) 30 | 31 | @oauth_authorized.connect_via(github_blueprint) 32 | def github_logged_in(blueprint, token): 33 | info = github.get("/user") 34 | 35 | if info.ok: 36 | 37 | account_info = info.json() 38 | username = account_info["login"] 39 | 40 | query = Users.query.filter_by(oauth_github=username) 41 | try: 42 | 43 | user = query.one() 44 | login_user(user) 45 | 46 | except NoResultFound: 47 | 48 | # Save to db 49 | user = Users() 50 | user.username = '(gh)' + username 51 | user.oauth_github = username 52 | 53 | # Save current user 54 | db.session.add(user) 55 | db.session.commit() 56 | 57 | login_user(user) 58 | 59 | # Google 60 | 61 | google_blueprint = make_google_blueprint( 62 | client_id=Config.GOOGLE_ID, 63 | client_secret=Config.GOOGLE_SECRET, 64 | scope=[ 65 | "openid", 66 | "https://www.googleapis.com/auth/userinfo.email", 67 | "https://www.googleapis.com/auth/userinfo.profile", 68 | ], 69 | storage=SQLAlchemyStorage( 70 | OAuth, 71 | db.session, 72 | user=current_user, 73 | user_required=False, 74 | ), 75 | ) 76 | 77 | @oauth_authorized.connect_via(google_blueprint) 78 | def google_logged_in(blueprint, token): 79 | info = google.get("/oauth2/v1/userinfo") 80 | 81 | if info.ok: 82 | account_info = info.json() 83 | username = account_info["given_name"] 84 | email = account_info["email"] 85 | 86 | query = Users.query.filter_by(oauth_google=username) 87 | try: 88 | 89 | user = query.one() 90 | login_user(user) 91 | 92 | except NoResultFound: 93 | # Save to db 94 | user = Users() 95 | user.username = '(google)' + username 96 | user.oauth_google = username 97 | user.email = email 98 | 99 | # Save current user 100 | db.session.add(user) 101 | db.session.commit() 102 | 103 | login_user(user) 104 | -------------------------------------------------------------------------------- /apps/authentication/routes.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask import render_template, redirect, request, url_for 7 | from flask_login import ( 8 | current_user, 9 | login_user, 10 | logout_user 11 | ) 12 | from flask_dance.contrib.github import github 13 | from flask_dance.contrib.google import google 14 | 15 | from apps import db, login_manager 16 | from apps.authentication import blueprint 17 | from apps.authentication.forms import LoginForm, CreateAccountForm 18 | from apps.authentication.models import Users 19 | from apps.config import Config 20 | 21 | from apps.authentication.util import verify_pass 22 | 23 | # Login & Registration 24 | 25 | @blueprint.route("/github") 26 | def login_github(): 27 | """ Github login """ 28 | if not github.authorized: 29 | return redirect(url_for("github.login")) 30 | 31 | res = github.get("/user") 32 | return redirect(url_for('home_blueprint.index')) 33 | 34 | 35 | @blueprint.route("/google") 36 | def login_google(): 37 | """ Google login """ 38 | if not google.authorized: 39 | return redirect(url_for("google.login")) 40 | 41 | res = google.get("/oauth2/v1/userinfo") 42 | return redirect(url_for('home_blueprint.index')) 43 | 44 | @blueprint.route('/login', methods=['GET', 'POST']) 45 | def login(): 46 | login_form = LoginForm(request.form) 47 | if 'login' in request.form: 48 | 49 | # read form data 50 | user_id = request.form['username'] # we can have here username OR email 51 | password = request.form['password'] 52 | 53 | # Locate user 54 | user = Users.find_by_username(user_id) 55 | 56 | # if user not found 57 | if not user: 58 | 59 | user = Users.find_by_email(user_id) 60 | 61 | if not user: 62 | return render_template( 'authentication/login.html', 63 | msg='Unknown User or Email', 64 | form=login_form) 65 | 66 | # Check the password 67 | if verify_pass(password, user.password): 68 | 69 | login_user(user) 70 | return redirect(url_for('home_blueprint.index')) 71 | 72 | # Something (user or pass) is not ok 73 | return render_template('authentication/login.html', 74 | msg='Wrong user or password', 75 | form=login_form) 76 | 77 | if not current_user.is_authenticated: 78 | return render_template('authentication/login.html', 79 | form=login_form) 80 | return redirect(url_for('home_blueprint.index')) 81 | 82 | 83 | @blueprint.route('/register', methods=['GET', 'POST']) 84 | def register(): 85 | create_account_form = CreateAccountForm(request.form) 86 | if 'register' in request.form: 87 | 88 | username = request.form['username'] 89 | email = request.form['email'] 90 | 91 | # Check usename exists 92 | user = Users.query.filter_by(username=username).first() 93 | if user: 94 | return render_template('authentication/register.html', 95 | msg='Username already registered', 96 | success=False, 97 | form=create_account_form) 98 | 99 | # Check email exists 100 | user = Users.query.filter_by(email=email).first() 101 | if user: 102 | return render_template('authentication/register.html', 103 | msg='Email already registered', 104 | success=False, 105 | form=create_account_form) 106 | 107 | # else we can create the user 108 | user = Users(**request.form) 109 | db.session.add(user) 110 | db.session.commit() 111 | 112 | # Delete user from session 113 | logout_user() 114 | 115 | return render_template('authentication/register.html', 116 | msg='User created successfully.', 117 | success=True, 118 | form=create_account_form) 119 | 120 | else: 121 | return render_template('authentication/register.html', form=create_account_form) 122 | 123 | 124 | @blueprint.route('/logout') 125 | def logout(): 126 | logout_user() 127 | return redirect(url_for('home_blueprint.index')) 128 | 129 | # Errors 130 | 131 | @blueprint.context_processor 132 | def has_github(): 133 | return {'has_github': bool(Config.GITHUB_ID) and bool(Config.GITHUB_SECRET)} 134 | 135 | @blueprint.context_processor 136 | def has_google(): 137 | return {'has_google': bool(Config.GOOGLE_ID) and bool(Config.GOOGLE_SECRET)} 138 | 139 | @login_manager.unauthorized_handler 140 | def unauthorized_handler(): 141 | return redirect('/login') 142 | -------------------------------------------------------------------------------- /apps/authentication/util.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os 7 | import hashlib 8 | import binascii 9 | 10 | # Inspiration -> https://www.vitoshacademy.com/hashing-passwords-in-python/ 11 | 12 | 13 | def hash_pass(password): 14 | """Hash a password for storing.""" 15 | 16 | salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii') 17 | pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), 18 | salt, 100000) 19 | pwdhash = binascii.hexlify(pwdhash) 20 | return (salt + pwdhash) # return bytes 21 | 22 | 23 | def verify_pass(provided_password, stored_password): 24 | """Verify a stored password against one provided by user""" 25 | 26 | stored_password = stored_password.decode('ascii') 27 | salt = stored_password[:64] 28 | stored_password = stored_password[64:] 29 | pwdhash = hashlib.pbkdf2_hmac('sha512', 30 | provided_password.encode('utf-8'), 31 | salt.encode('ascii'), 32 | 100000) 33 | pwdhash = binascii.hexlify(pwdhash).decode('ascii') 34 | return pwdhash == stored_password 35 | -------------------------------------------------------------------------------- /apps/charts/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask import Blueprint 7 | 8 | blueprint = Blueprint( 9 | 'charts_blueprint', 10 | __name__, 11 | url_prefix='' 12 | ) 13 | -------------------------------------------------------------------------------- /apps/charts/routes.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from apps.charts import blueprint 7 | from flask import render_template 8 | from apps.models import Product 9 | 10 | @blueprint.route('/charts') 11 | def charts(): 12 | products = [{'name': product.name, 'price': product.price} for product in Product.get_list()] 13 | return render_template('charts/index.html', segment='charts', products=products) -------------------------------------------------------------------------------- /apps/config.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os 7 | from pathlib import Path 8 | 9 | class Config(object): 10 | 11 | BASE_DIR = Path(__file__).resolve().parent 12 | 13 | USERS_ROLES = { 'ADMIN' :1 , 'USER' : 2 } 14 | USERS_STATUS = { 'ACTIVE' :1 , 'SUSPENDED' : 2 } 15 | 16 | # celery 17 | CELERY_BROKER_URL = "redis://localhost:6379" 18 | CELERY_RESULT_BACKEND = "redis://localhost:6379" 19 | CELERY_HOSTMACHINE = "celery@app-generator" 20 | 21 | # Set up the App SECRET_KEY 22 | SECRET_KEY = os.getenv('SECRET_KEY', 'S3cret_999') 23 | 24 | # Social AUTH context 25 | SOCIAL_AUTH_GITHUB = False 26 | 27 | GITHUB_ID = os.getenv('GITHUB_ID' , None) 28 | GITHUB_SECRET = os.getenv('GITHUB_SECRET', None) 29 | 30 | # Enable/Disable Github Social Login 31 | if GITHUB_ID and GITHUB_SECRET: 32 | SOCIAL_AUTH_GITHUB = True 33 | 34 | GOOGLE_ID = os.getenv('GOOGLE_ID' , None) 35 | GOOGLE_SECRET = os.getenv('GOOGLE_SECRET', None) 36 | 37 | # Enable/Disable Google Social Login 38 | if GOOGLE_ID and GOOGLE_SECRET: 39 | SOCIAL_AUTH_GOOGLE = True 40 | 41 | SQLALCHEMY_TRACK_MODIFICATIONS = False 42 | 43 | DB_ENGINE = os.getenv('DB_ENGINE' , None) 44 | DB_USERNAME = os.getenv('DB_USERNAME' , None) 45 | DB_PASS = os.getenv('DB_PASS' , None) 46 | DB_HOST = os.getenv('DB_HOST' , None) 47 | DB_PORT = os.getenv('DB_PORT' , None) 48 | DB_NAME = os.getenv('DB_NAME' , None) 49 | 50 | USE_SQLITE = True 51 | 52 | # try to set up a Relational DBMS 53 | if DB_ENGINE and DB_NAME and DB_USERNAME: 54 | 55 | try: 56 | 57 | # Relational DBMS: PSQL, MySql 58 | SQLALCHEMY_DATABASE_URI = '{}://{}:{}@{}:{}/{}'.format( 59 | DB_ENGINE, 60 | DB_USERNAME, 61 | DB_PASS, 62 | DB_HOST, 63 | DB_PORT, 64 | DB_NAME 65 | ) 66 | 67 | USE_SQLITE = False 68 | 69 | except Exception as e: 70 | 71 | print('> Error: DBMS Exception: ' + str(e) ) 72 | print('> Fallback to SQLite ') 73 | 74 | if USE_SQLITE: 75 | 76 | # This will create a file in FOLDER 77 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3') 78 | 79 | DYNAMIC_DATATB = { 80 | "products": "apps.models.Product" 81 | } 82 | 83 | CDN_DOMAIN = os.getenv('CDN_DOMAIN') 84 | CDN_HTTPS = os.getenv('CDN_HTTPS', True) 85 | 86 | class ProductionConfig(Config): 87 | DEBUG = False 88 | 89 | # Security 90 | SESSION_COOKIE_HTTPONLY = True 91 | REMEMBER_COOKIE_HTTPONLY = True 92 | REMEMBER_COOKIE_DURATION = 3600 93 | 94 | class DebugConfig(Config): 95 | DEBUG = True 96 | 97 | # Load all possible configurations 98 | config_dict = { 99 | 'Production': ProductionConfig, 100 | 'Debug' : DebugConfig 101 | } 102 | -------------------------------------------------------------------------------- /apps/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/apps/db.sqlite3 -------------------------------------------------------------------------------- /apps/dyn_dt/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask import Blueprint 7 | 8 | blueprint = Blueprint( 9 | 'table_blueprint', 10 | __name__, 11 | url_prefix='' 12 | ) 13 | -------------------------------------------------------------------------------- /apps/dyn_dt/utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import importlib 7 | from sqlalchemy import or_ 8 | from sqlalchemy import DateTime, func 9 | from apps import db 10 | 11 | class PageItems(db.Model): 12 | __tablename__ = 'page_items' 13 | id = db.Column(db.Integer, primary_key=True) 14 | parent = db.Column(db.String(255), nullable=True) 15 | items_per_page = db.Column(db.Integer, default=25) 16 | 17 | 18 | class HideShowFilter(db.Model): 19 | __tablename__ = 'hide_show_filter' 20 | id = db.Column(db.Integer, primary_key=True) 21 | parent = db.Column(db.String(255), nullable=True) 22 | key = db.Column(db.String(255), nullable=False) 23 | value = db.Column(db.Boolean, default=False) 24 | 25 | 26 | class ModelFilter(db.Model): 27 | __tablename__ = 'model_filter' 28 | id = db.Column(db.Integer, primary_key=True) 29 | parent = db.Column(db.String(255), nullable=True) 30 | key = db.Column(db.String(255), nullable=False) 31 | value = db.Column(db.String(255), nullable=False) 32 | 33 | 34 | def get_model_fk_values(aModelClass): 35 | fk_values = {} 36 | 37 | current_table_name = aModelClass.__tablename__ 38 | 39 | for relationship in aModelClass.__mapper__.relationships: 40 | if relationship.direction.name == 'MANYTOONE': 41 | related_model = relationship.mapper.class_ 42 | foreign_key_column = list(relationship.local_columns)[0] 43 | referenced_table_name = list(foreign_key_column.foreign_keys)[0].column.table.name 44 | 45 | if referenced_table_name != current_table_name: 46 | field_name = relationship.key 47 | related_instances = related_model.query.all() 48 | fk_values[field_name] = related_instances 49 | 50 | return fk_values 51 | 52 | 53 | def get_model_field_names(model, field_type): 54 | """Returns a list of field names based on the given field type in SQLAlchemy.""" 55 | return [ 56 | column.name for column in model.__table__.columns 57 | if isinstance(column.type, field_type) 58 | ] 59 | 60 | 61 | def name_to_class(name: str): 62 | try: 63 | module_name = '.'.join(name.split('.')[:-1]) 64 | class_name = name.split('.')[-1] 65 | 66 | module = importlib.import_module(module_name) 67 | return getattr(module, class_name) 68 | except Exception as e: 69 | print(f"Error importing {name}: {e}") 70 | return None 71 | 72 | 73 | def user_filter(request, query, fields, fk_fields=[]): 74 | value = request.args.get('search') 75 | 76 | if value: 77 | dynamic_filter = [] 78 | 79 | for field in fields: 80 | if field not in fk_fields: 81 | dynamic_filter.append(getattr(query.column_descriptions[0]['entity'], field).ilike(f"%{value}%")) 82 | 83 | query = query.filter(or_(*dynamic_filter)) 84 | 85 | return query 86 | 87 | 88 | def exclude_auto_gen_fields(aModelClass): 89 | exclude_fields = [ 90 | field.name for field in aModelClass.__table__.columns 91 | if isinstance(field.type, DateTime) and ( 92 | field.default is not None or 93 | field.server_default is not None or 94 | field.onupdate is not None or 95 | isinstance(field.default, func) or 96 | isinstance(field.onupdate, func) 97 | ) 98 | ] 99 | return exclude_fields 100 | -------------------------------------------------------------------------------- /apps/exceptions/exception.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | class InvalidUsage(Exception): 7 | status_code = 400 8 | 9 | def __init__(self, message, status_code=None, payload=None): 10 | Exception.__init__(self) 11 | self.message = message 12 | if status_code is not None: 13 | self.status_code = status_code 14 | self.payload = payload 15 | 16 | def to_dict(self): 17 | rv = dict(self.payload or ()) 18 | rv['message'] = self.message 19 | 20 | return rv -------------------------------------------------------------------------------- /apps/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os, re, uuid 7 | from colorama import Fore, Style 8 | from apps.authentication.models import Users 9 | from apps.config import Config 10 | from marshmallow import ValidationError 11 | from apps.messages import Messages 12 | from functools import wraps 13 | from flask import request 14 | from uuid import uuid4 15 | import datetime, time 16 | message = Messages.message 17 | 18 | Currency = Config.CURRENCY 19 | PAYMENT_TYPE = Config.PAYMENT_TYPE 20 | STATE = Config.STATE 21 | 22 | 23 | regex = re.compile(r'([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+') 24 | 25 | def get_ts(): 26 | return int(time.time()) 27 | 28 | def password_validate(password): 29 | """ password validate """ 30 | msg = '' 31 | while True: 32 | if len(password) < 6: 33 | msg = "Make sure your password is at lest 6 letters" 34 | return msg 35 | elif re.search('[0-9]',password) is None: 36 | msg = "Make sure your password has a number in it" 37 | return msg 38 | elif re.search('[A-Z]',password) is None: 39 | msg = "Make sure your password has a capital letter in it" 40 | return msg 41 | else: 42 | msg = True 43 | break 44 | 45 | return True 46 | 47 | def emailValidate(email): 48 | """ validate email """ 49 | if re.fullmatch(regex, email): 50 | return True 51 | else: 52 | return False 53 | 54 | # santise file name 55 | def sanitise_fille_name(value): 56 | """ remove special char """ 57 | return value.strip().lower().replace(' ', '_').replace('(', '').replace(')', '').replace(',', '').replace('=','_').replace('-', '_').replace('#', '') 58 | 59 | def createFolder(folder_name): 60 | """ create folder for save csv """ 61 | if not os.path.exists(f'{folder_name}'): 62 | os.makedirs(f'{folder_name}') 63 | 64 | return folder_name 65 | 66 | 67 | def uniqueFileName(file_name): 68 | """ for Unique file name""" 69 | file_uuid = uuid.uuid4() 70 | IMAGE_NAME = f'{file_uuid}-{file_name}' 71 | return IMAGE_NAME 72 | 73 | def serverImageUrl(file_name): 74 | """ for Unique file name""" 75 | url = f'{FTP_IMAGE_URL}{file_name}' 76 | return url 77 | 78 | def errorColor(error): 79 | """ for terminal input error color """ 80 | print(Fore.RED + f'{error}') 81 | print(Style.RESET_ALL) 82 | return True 83 | 84 | def splitUrlGetFilename(url): 85 | """ image url split and get file name """ 86 | return url.split('/')[-1] 87 | 88 | def validateCurrency(currency): 89 | """ check currency """ 90 | # if check currency validate or not 91 | if currency not in list(Currency.keys()): 92 | raise ValidationError( 93 | f"{message['invalid_currency']}, expected {','.join(Currency.keys())}", 422) 94 | 95 | def validatePaymentMethod(payment): 96 | """ check valid payment methods """ 97 | # if check PAYMENT_TYPE validate or not 98 | if payment not in list(PAYMENT_TYPE.keys()): 99 | raise ValidationError( 100 | f"{message['invalid_payment_method']}, expected {expectedValue(PAYMENT_TYPE)}", 422) 101 | 102 | else: 103 | value = 0 104 | if payment == "cc": 105 | value = 1 106 | elif payment == "paypal": 107 | value = 2 108 | else: 109 | value = 3 110 | 111 | return value 112 | 113 | def validateState(state): 114 | """ check valid state methods """ 115 | # if check state validate or not 116 | if state not in list(STATE.keys()): 117 | raise ValidationError( 118 | f"{message['invalid_state']}, expected {expectedValue(STATE)}", 422) 119 | 120 | else: 121 | value = 0 122 | if state == "completed": 123 | value = 1 124 | elif state == "pending": 125 | value = 2 126 | else: 127 | value = 3 128 | 129 | return value 130 | 131 | 132 | def expectedValue(data): 133 | """ key get values """ 134 | values = [] 135 | for k,v in data.items(): 136 | values.append(f'{v}.({k})') 137 | 138 | return ",".join(values) 139 | 140 | 141 | def createAccessToken(): 142 | """ create access token w""" 143 | rand_token = uuid4() 144 | 145 | return f"{str(rand_token)}" 146 | 147 | 148 | # token validate 149 | def token_required(f): 150 | """ check token """ 151 | @wraps(f) 152 | def decorated(*args, **kwargs): 153 | token = None 154 | if "Authorization" in request.headers: 155 | token = request.headers["Authorization"] 156 | if not token: 157 | return { 158 | "message": "Authentication Token is missing!", 159 | "error": "Unauthorized" 160 | }, 401 161 | try: 162 | current_user = Users.find_by_api_token(token) 163 | if current_user is None: 164 | return { 165 | "message": "Invalid Authentication token!", 166 | "error": "Unauthorized" 167 | }, 401 168 | # if not current_user["active"]: 169 | # abort(403) 170 | except Exception as e: 171 | return { 172 | "message": "Something went wrong", 173 | "error": str(e) 174 | }, 500 175 | 176 | return f(current_user, **kwargs) 177 | 178 | return decorated 179 | -------------------------------------------------------------------------------- /apps/home/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from flask import Blueprint 7 | 8 | blueprint = Blueprint( 9 | 'home_blueprint', 10 | __name__, 11 | url_prefix='' 12 | ) 13 | -------------------------------------------------------------------------------- /apps/home/routes.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os, json, pprint 7 | import wtforms 8 | 9 | from apps.home import blueprint 10 | from flask import render_template, request, redirect, url_for 11 | from flask_login import login_required 12 | from jinja2 import TemplateNotFound 13 | from flask_login import login_required, current_user 14 | from apps import db, config 15 | from apps.models import * 16 | from apps.tasks import * 17 | from apps.authentication.models import Users 18 | from flask_wtf import FlaskForm 19 | 20 | @blueprint.route('/') 21 | @blueprint.route('/index') 22 | def index(): 23 | return render_template('pages/index.html', segment='index') 24 | 25 | @blueprint.route('/icon_feather') 26 | def icon_feather(): 27 | return render_template('pages/icon-feather.html', segment='icon_feather') 28 | 29 | @blueprint.route('/color') 30 | def color(): 31 | return render_template('pages/color.html', segment='color') 32 | 33 | @blueprint.route('/sample_page') 34 | def sample_page(): 35 | return render_template('pages/sample-page.html', segment='sample_page') 36 | 37 | @blueprint.route('/typography') 38 | def typography(): 39 | return render_template('pages/typography.html', segment='typography') 40 | 41 | def getField(column): 42 | if isinstance(column.type, db.Text): 43 | return wtforms.TextAreaField(column.name.title()) 44 | if isinstance(column.type, db.String): 45 | return wtforms.StringField(column.name.title()) 46 | if isinstance(column.type, db.Boolean): 47 | return wtforms.BooleanField(column.name.title()) 48 | if isinstance(column.type, db.Integer): 49 | return wtforms.IntegerField(column.name.title()) 50 | if isinstance(column.type, db.Float): 51 | return wtforms.DecimalField(column.name.title()) 52 | if isinstance(column.type, db.LargeBinary): 53 | return wtforms.HiddenField(column.name.title()) 54 | return wtforms.StringField(column.name.title()) 55 | 56 | 57 | @blueprint.route('/profile', methods=['GET', 'POST']) 58 | @login_required 59 | def profile(): 60 | 61 | class ProfileForm(FlaskForm): 62 | pass 63 | 64 | readonly_fields = Users.readonly_fields 65 | full_width_fields = {"bio"} 66 | 67 | for column in Users.__table__.columns: 68 | if column.name == "id": 69 | continue 70 | 71 | field_name = column.name 72 | if field_name in full_width_fields: 73 | continue 74 | 75 | field = getField(column) 76 | setattr(ProfileForm, field_name, field) 77 | 78 | for field_name in full_width_fields: 79 | if field_name in Users.__table__.columns: 80 | column = Users.__table__.columns[field_name] 81 | field = getField(column) 82 | setattr(ProfileForm, field_name, field) 83 | 84 | form = ProfileForm(obj=current_user) 85 | 86 | if form.validate_on_submit(): 87 | readonly_fields.append("password") 88 | excluded_fields = readonly_fields 89 | for field_name, field_value in form.data.items(): 90 | if field_name not in excluded_fields: 91 | setattr(current_user, field_name, field_value) 92 | 93 | db.session.commit() 94 | return redirect(url_for('home_blueprint.profile')) 95 | 96 | context = { 97 | 'segment': 'profile', 98 | 'form': form, 99 | 'readonly_fields': readonly_fields, 100 | 'full_width_fields': full_width_fields, 101 | } 102 | return render_template('pages/profile.html', **context) 103 | 104 | 105 | # Helper - Extract current page name from request 106 | def get_segment(request): 107 | 108 | try: 109 | 110 | segment = request.path.split('/')[-1] 111 | 112 | if segment == '': 113 | segment = 'index' 114 | 115 | return segment 116 | 117 | except: 118 | return None 119 | 120 | @blueprint.route('/error-403') 121 | def error_403(): 122 | return render_template('error/403.html'), 403 123 | 124 | @blueprint.errorhandler(403) 125 | def not_found_error(error): 126 | return redirect(url_for('error-403')) 127 | 128 | @blueprint.route('/error-404') 129 | def error_404(): 130 | return render_template('error/404.html'), 404 131 | 132 | @blueprint.errorhandler(404) 133 | def not_found_error(error): 134 | return redirect(url_for('error-404')) 135 | 136 | @blueprint.route('/error-500') 137 | def error_500(): 138 | return render_template('error/500.html'), 500 139 | 140 | @blueprint.errorhandler(500) 141 | def not_found_error(error): 142 | return redirect(url_for('error-500')) 143 | 144 | # Celery (to be refactored) 145 | @blueprint.route('/tasks-test') 146 | def tasks_test(): 147 | 148 | input_dict = { "data1": "04", "data2": "99" } 149 | input_json = json.dumps(input_dict) 150 | 151 | task = celery_test.delay( input_json ) 152 | 153 | return f"TASK_ID: {task.id}, output: { task.get() }" 154 | 155 | 156 | # Custom template filter 157 | 158 | @blueprint.app_template_filter("replace_value") 159 | def replace_value(value, arg): 160 | return value.replace(arg, " ").title() -------------------------------------------------------------------------------- /apps/messages.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | class Messages: 7 | message = { 8 | "wrong_user_or_password" : "Wrong user or password", 9 | "suspended_account_please_contact_support" : "Suspended Account - Please contact support.", 10 | "suspended_account_maximum_nb_of_tries_exceeded" : "Suspended Account - Maximum NB of tries exceeded.", 11 | "incorrect_password" : "Incorrect password", 12 | "username_already_registered" : "Username already registered", 13 | "email_already_registered" : "Email already registered", 14 | "account_created_successfully" : "Account created successfully.", 15 | "record_not_found" : "Record not found.", 16 | "user_updated_successfully" : "User updated successfully.", 17 | "successfully_updated" : "Successfully updated.", 18 | "email_already_registered" : "Email already registered.", 19 | "valid_email" : "Valid email.", 20 | "deleted_successfully" : "Deleted successfully.", 21 | "product_created_successfully" : "Product created successfully", 22 | "not_exists" : "Record does not exist.", 23 | "record_updated" : "Record updated successfully.", 24 | "record_created_successfully" : "Record created successfully.", 25 | "required_field" : "This field is required.", 26 | "product_not_exists" : "Product does not exists.", 27 | "invalid_currency" : "Invalid currency", 28 | "invalid_payment_method" : "Invalid payment method", 29 | "invalid_state" : "Invalid state", 30 | "user_not_found" : "User not found.", 31 | "pwd_not_match" : "Password don\'t match.", 32 | "email_not_found" : "Email not found.", 33 | "pwd_not_match" : "Password don\'t match.", 34 | "email_has_been_sent_via_email" : "A confirmation email has been sent via email. Please check your email", 35 | "link_is_invalid_or_has_expired" : "The confirmation link is invalid or has expired.", 36 | "account_already_confirmed" : "Account already confirmed. Please login", 37 | "password_has_been_updated" : "Your password has been updated.", 38 | "old_password_not_match" : "Old password doesnt match!", 39 | "new_password_should_be_different" : "New Password should be different than the OLD one." 40 | } 41 | -------------------------------------------------------------------------------- /apps/models.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | from apps import db 7 | from sqlalchemy.exc import SQLAlchemyError 8 | from apps.exceptions.exception import InvalidUsage 9 | 10 | class Product(db.Model): 11 | 12 | __tablename__ = 'products' 13 | 14 | id = db.Column(db.Integer, primary_key=True) 15 | name = db.Column(db.String(128), nullable=False) 16 | info = db.Column(db.Text, nullable=True) 17 | price = db.Column(db.Integer, nullable=False) 18 | 19 | def __init__(self, **kwargs): 20 | super(Product, self).__init__(**kwargs) 21 | 22 | def __repr__(self): 23 | return f"{self.name} / ${self.price}" 24 | 25 | @classmethod 26 | def find_by_id(cls, _id: int) -> "Product": 27 | return cls.query.filter_by(id=_id).first() 28 | 29 | @classmethod 30 | def get_list(cls): 31 | return cls.query.all() 32 | 33 | def save(self) -> None: 34 | try: 35 | db.session.add(self) 36 | db.session.commit() 37 | except SQLAlchemyError as e: 38 | db.session.rollback() 39 | db.session.close() 40 | error = str(e.__dict__['orig']) 41 | raise InvalidUsage(error, 422) 42 | 43 | def delete(self) -> None: 44 | try: 45 | db.session.delete(self) 46 | db.session.commit() 47 | except SQLAlchemyError as e: 48 | db.session.rollback() 49 | db.session.close() 50 | error = str(e.__dict__['orig']) 51 | raise InvalidUsage(error, 422) 52 | return 53 | -------------------------------------------------------------------------------- /apps/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import json, time 7 | from datetime import datetime 8 | 9 | from apps.config import * 10 | 11 | from celery import Celery 12 | from celery.utils.log import get_task_logger 13 | from celery.schedules import crontab 14 | 15 | logger = get_task_logger(__name__) 16 | 17 | celery_app = Celery(Config.CELERY_HOSTMACHINE, backend=Config.CELERY_RESULT_BACKEND, broker=Config.CELERY_BROKER_URL) 18 | 19 | celery_app.conf.beat_schedule = { 20 | 'run_celery_beat_test_every_minute': { 21 | 'task': 'celery_beat_test', 22 | 'schedule': crontab(minute='*/1'), # Runs every 1 minute 23 | 'args': (json.dumps({'test': 'data'}),) 24 | }, 25 | } 26 | celery_app.conf.timezone = 'UTC' 27 | 28 | 29 | # task used for tests 30 | @celery_app.task(name="celery_test", bind=True) 31 | def celery_test( self, task_input ): 32 | 33 | task_json = json.loads( task_input ) 34 | 35 | logger.info( '*** Started' ) 36 | logger.info( ' > task_json:' + str( task_json ) ) 37 | 38 | task_json['result'] = 'NA' 39 | task_json['ts_start'] = datetime.now() 40 | 41 | # get current task id 42 | task_id = celery_app.current_task.request.id 43 | 44 | # ###################################################### 45 | # Task is STARTING (prepare the task) 46 | 47 | # Update Output JSON 48 | task_json['state'] = 'STARTING' 49 | task_json['info'] = 'Task is starting' 50 | 51 | self.update_state(state='STARTING', 52 | meta={ 'info':'Task is starting' }) 53 | 54 | time.sleep(1) 55 | 56 | # ###################################################### 57 | # Task is RUNNING (execute MAIN stuff) 58 | 59 | # Update Output JSON 60 | task_json['state'] = 'RUNNING' 61 | task_json['info'] = 'Task is running' 62 | 63 | self.update_state(state='RUNNING', 64 | meta={ 'info':'Task is running' }) 65 | 66 | time.sleep(1) 67 | 68 | # ###################################################### 69 | # Task is CLOSING (task cleanUP) 70 | 71 | # Update Output JSON 72 | task_json['state'] = 'CLOSING' 73 | task_json['info'] = 'Task is closing' 74 | 75 | self.update_state(state='CLOSING', 76 | meta={ 'info':'Task is running the cleanUP' }) 77 | 78 | task_json['ts_end'] = datetime.now() 79 | 80 | time.sleep(1) 81 | 82 | # ###################################################### 83 | # Task is FINISHED (task cleanUP) 84 | 85 | # Update Output JSON 86 | task_json['state'] = 'FINISHED' 87 | task_json['info'] = 'Task is finished' 88 | task_json['result'] = 'SUCCESS' 89 | 90 | self.update_state(state='FINISHED', 91 | meta={ 'info':'Task is finisled' }) 92 | 93 | return task_json 94 | 95 | 96 | @celery_app.task(name="celery_beat_test", bind=True) 97 | def celery_beat_test( self, task_input ): 98 | task_json = {'info': 'Beat is running'} 99 | return task_json 100 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # exit on error 3 | set -o errexit 4 | 5 | python -m pip install --upgrade pip 6 | 7 | pip install -r requirements.txt 8 | 9 | # DB Migration 10 | 11 | # Init migration folder 12 | # flask db init # to be executed only once 13 | 14 | flask db migrate # Generate migration SQL 15 | flask db upgrade # Apply changes 16 | 17 | # Compile SCSS 18 | # yarn 19 | # yarn build 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | appseed-app: 4 | container_name: appseed_app 5 | restart: always 6 | build: . 7 | networks: 8 | - db_network 9 | - web_network 10 | nginx: 11 | container_name: nginx 12 | restart: always 13 | image: "nginx:latest" 14 | ports: 15 | - "5085:5085" 16 | volumes: 17 | - ./nginx:/etc/nginx/conf.d 18 | networks: 19 | - web_network 20 | depends_on: 21 | - appseed-app 22 | networks: 23 | db_network: 24 | driver: bridge 25 | web_network: 26 | driver: bridge 27 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | # True in development, False in production 2 | DEBUG=True 3 | 4 | FLASK_APP=run.py 5 | FLASK_DEBUG=1 6 | 7 | # If not provided, a random one is generated 8 | # SECRET_KEY= 9 | 10 | # If DB credentials (if NOT provided, or wrong values SQLite is used) 11 | # DB_ENGINE=mysql 12 | # DB_HOST=localhost 13 | # DB_NAME=appseed_db 14 | # DB_USERNAME=appseed_db_usr 15 | # DB_PASS=pass 16 | # DB_PORT=3306 17 | 18 | # SOCIAL AUTH Github 19 | # GITHUB_ID=YOUR_GITHUB_ID 20 | # GITHUB_SECRET=YOUR_GITHUB_SECRET 21 | 22 | # SOCIAL AUTH Google 23 | # GOOGLE_ID=YOUR_GOOGLE_ID 24 | # GOOGLE_SECRET=YOUR_GOOGLE_SECRET 25 | -------------------------------------------------------------------------------- /gunicorn-cfg.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | bind = '0.0.0.0:5005' 7 | workers = 1 8 | accesslog = '-' 9 | loglevel = 'debug' 10 | capture_output = True 11 | enable_stdio_inheritance = True 12 | -------------------------------------------------------------------------------- /media/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/media/.gitkeep -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.config import fileConfig 3 | 4 | from flask import current_app 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | logger = logging.getLogger('alembic.env') 16 | 17 | 18 | def get_engine(): 19 | try: 20 | # this works with Flask-SQLAlchemy<3 and Alchemical 21 | return current_app.extensions['migrate'].db.get_engine() 22 | except (TypeError, AttributeError): 23 | # this works with Flask-SQLAlchemy>=3 24 | return current_app.extensions['migrate'].db.engine 25 | 26 | 27 | def get_engine_url(): 28 | try: 29 | return get_engine().url.render_as_string(hide_password=False).replace( 30 | '%', '%%') 31 | except AttributeError: 32 | return str(get_engine().url).replace('%', '%%') 33 | 34 | 35 | # add your model's MetaData object here 36 | # for 'autogenerate' support 37 | # from myapp import mymodel 38 | # target_metadata = mymodel.Base.metadata 39 | config.set_main_option('sqlalchemy.url', get_engine_url()) 40 | target_db = current_app.extensions['migrate'].db 41 | 42 | # other values from the config, defined by the needs of env.py, 43 | # can be acquired: 44 | # my_important_option = config.get_main_option("my_important_option") 45 | # ... etc. 46 | 47 | 48 | def get_metadata(): 49 | if hasattr(target_db, 'metadatas'): 50 | return target_db.metadatas[None] 51 | return target_db.metadata 52 | 53 | 54 | def run_migrations_offline(): 55 | """Run migrations in 'offline' mode. 56 | 57 | This configures the context with just a URL 58 | and not an Engine, though an Engine is acceptable 59 | here as well. By skipping the Engine creation 60 | we don't even need a DBAPI to be available. 61 | 62 | Calls to context.execute() here emit the given string to the 63 | script output. 64 | 65 | """ 66 | url = config.get_main_option("sqlalchemy.url") 67 | context.configure( 68 | url=url, target_metadata=get_metadata(), literal_binds=True 69 | ) 70 | 71 | with context.begin_transaction(): 72 | context.run_migrations() 73 | 74 | 75 | def run_migrations_online(): 76 | """Run migrations in 'online' mode. 77 | 78 | In this scenario we need to create an Engine 79 | and associate a connection with the context. 80 | 81 | """ 82 | 83 | # this callback is used to prevent an auto-migration from being generated 84 | # when there are no changes to the schema 85 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 86 | def process_revision_directives(context, revision, directives): 87 | if getattr(config.cmd_opts, 'autogenerate', False): 88 | script = directives[0] 89 | if script.upgrade_ops.is_empty(): 90 | directives[:] = [] 91 | logger.info('No changes in schema detected.') 92 | 93 | conf_args = current_app.extensions['migrate'].configure_args 94 | if conf_args.get("process_revision_directives") is None: 95 | conf_args["process_revision_directives"] = process_revision_directives 96 | 97 | connectable = get_engine() 98 | 99 | with connectable.connect() as connection: 100 | context.configure( 101 | connection=connection, 102 | target_metadata=get_metadata(), 103 | **conf_args 104 | ) 105 | 106 | with context.begin_transaction(): 107 | context.run_migrations() 108 | 109 | 110 | if context.is_offline_mode(): 111 | run_migrations_offline() 112 | else: 113 | run_migrations_online() 114 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /nginx/appseed-app.conf: -------------------------------------------------------------------------------- 1 | upstream webapp { 2 | server appseed_app:5005; 3 | } 4 | 5 | server { 6 | listen 5085; 7 | server_name localhost; 8 | 9 | location / { 10 | proxy_pass http://webapp; 11 | proxy_set_header Host $host:$server_port; 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flask-datta-able", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vite build --watch --mode development", 8 | "build": "vite build --mode production && npm run minify-css", 9 | "minify-css": "postcss static/assets/css/*.css --dir static/assets/css --no-map --ext .min.css" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "autoprefixer": "^10.4.20", 16 | "cssnano": "^7.0.6", 17 | "postcss": "^8.5.3", 18 | "postcss-cli": "^11.0.0", 19 | "sass": "^1.85.1", 20 | "vite": "^6.2.0" 21 | } 22 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('cssnano')({ 4 | preset: 'default', 5 | }), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: flask-datta-latest 4 | plan: starter 5 | env: python 6 | region: frankfurt # region should be same as your database region. 7 | buildCommand: "./build.sh" 8 | startCommand: "gunicorn run:app" 9 | envVars: 10 | - key: SECRET_KEY 11 | generateValue: true 12 | - key: WEB_CONCURRENCY 13 | value: 4 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # core 2 | flask==3.1.0 3 | Werkzeug==3.1.3 4 | jinja2==3.1.6 5 | WTForms==3.2.1 6 | flask_wtf==1.2.2 7 | 8 | # DB 9 | flask_migrate==4.1.0 10 | flask_sqlalchemy==3.1.1 11 | sqlalchemy==2.0.38 12 | 13 | # tools 14 | flask_login==0.6.3 15 | flask-dance==7.1.0 16 | celery==5.4.0 17 | redis==5.2.1 18 | colorama==0.4.6 19 | PyJWT~=2.10.1 20 | WTForms-Alchemy==0.19.0 21 | 22 | # utils 23 | email_validator==2.2.0 24 | blinker==1.9.0 25 | 26 | # env 27 | python-dotenv==1.0.1 28 | 29 | # deployment 30 | gunicorn==23.0.0 31 | Flask-Minify==0.49 32 | Flask-CDN==1.5.3 33 | 34 | # flask_mysqldb 35 | # psycopg2-binary 36 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Copyright (c) 2019 - present AppSeed.us 4 | """ 5 | 6 | import os 7 | from flask_migrate import Migrate 8 | from flask_minify import Minify 9 | from sys import exit 10 | 11 | from apps.config import config_dict 12 | from apps import create_app, db 13 | 14 | # WARNING: Don't run with debug turned on in production! 15 | DEBUG = (os.getenv('DEBUG', 'False') == 'True') 16 | 17 | # The configuration 18 | get_config_mode = 'Debug' if DEBUG else 'Production' 19 | 20 | try: 21 | 22 | # Load the configuration using the default values 23 | app_config = config_dict[get_config_mode.capitalize()] 24 | 25 | except KeyError: 26 | exit('Error: Invalid . Expected values [Debug, Production] ') 27 | 28 | app = create_app(app_config) 29 | 30 | # Create tables & Fallback to SQLite 31 | with app.app_context(): 32 | 33 | try: 34 | db.create_all() 35 | except Exception as e: 36 | 37 | print('> Error: DBMS Exception: ' + str(e) ) 38 | 39 | # fallback to SQLite 40 | basedir = os.path.abspath(os.path.dirname(__file__)) 41 | app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'db.sqlite3') 42 | 43 | print('> Fallback to SQLite ') 44 | db.create_all() 45 | 46 | # Apply all changes 47 | Migrate(app, db) 48 | 49 | if not DEBUG: 50 | Minify(app=app, html=True, js=False, cssless=False) 51 | 52 | if DEBUG: 53 | app.logger.info('DEBUG = ' + str(DEBUG) ) 54 | app.logger.info('Page Compression = ' + 'FALSE' if DEBUG else 'TRUE' ) 55 | app.logger.info('DBMS = ' + app_config.SQLALCHEMY_DATABASE_URI) 56 | 57 | if __name__ == "__main__": 58 | app.run() 59 | -------------------------------------------------------------------------------- /static/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Custom SCSS 4 | 5 | */ 6 | -------------------------------------------------------------------------------- /static/assets/css/custom.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Custom SCSS 4 | 5 | */ -------------------------------------------------------------------------------- /static/assets/css/forms.css: -------------------------------------------------------------------------------- 1 | @import url('widgets.css'); 2 | 3 | /* FORM ROWS */ 4 | 5 | .form-row { 6 | overflow: hidden; 7 | padding: 10px; 8 | font-size: 13px; 9 | border-bottom: 1px solid #eee; 10 | display: table-row !important; 11 | } 12 | .form-row img, .form-row input { 13 | vertical-align: middle; 14 | } 15 | .form-row .field-title { 16 | padding-left: 0 !important; 17 | } 18 | 19 | .form-row label input[type="checkbox"] { 20 | margin-top: 0; 21 | vertical-align: 0; 22 | } 23 | 24 | form .form-row p { 25 | padding-left: 0; 26 | } 27 | 28 | .hidden { 29 | display: none; 30 | } 31 | 32 | /* FORM LABELS */ 33 | 34 | label { 35 | font-weight: normal; 36 | color: #666; 37 | font-size: 13px; 38 | } 39 | 40 | .required label, label.required { 41 | font-weight: bold; 42 | /*color: #333;*/ 43 | } 44 | 45 | /* RADIO BUTTONS */ 46 | 47 | form ul.radiolist li { 48 | list-style-type: none; 49 | } 50 | 51 | form ul.radiolist label { 52 | float: none; 53 | display: inline; 54 | } 55 | 56 | form ul.radiolist input[type="radio"] { 57 | margin: -2px 4px 0 0; 58 | padding: 0; 59 | } 60 | 61 | form ul.inline { 62 | margin-left: 0; 63 | padding: 0; 64 | } 65 | 66 | form ul.inline li { 67 | float: left; 68 | padding-right: 7px; 69 | } 70 | 71 | /* ALIGNED FIELDSETS */ 72 | 73 | .aligned label { 74 | display: block; 75 | /*padding: 4px 10px 0 0;*/ 76 | /*float: left;*/ 77 | /*width: 160px;*/ 78 | word-wrap: break-word; 79 | /*line-height: 1;*/ 80 | } 81 | 82 | .aligned label:not(.vCheckboxLabel):after { 83 | /*content: '';*/ 84 | display: inline-block; 85 | vertical-align: middle; 86 | height: 26px; 87 | } 88 | 89 | .aligned label + p, .aligned label + div.help, .aligned label + div.readonly { 90 | padding: 6px 0; 91 | margin-top: 0; 92 | margin-bottom: 0; 93 | /*margin-left: 170px;*/ 94 | } 95 | 96 | .aligned ul label { 97 | display: inline; 98 | float: none; 99 | width: auto; 100 | } 101 | 102 | .aligned .form-row input { 103 | margin-bottom: 0; 104 | } 105 | 106 | .colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { 107 | width: 350px; 108 | } 109 | 110 | form .aligned ul { 111 | margin-left: 160px; 112 | padding-left: 10px; 113 | } 114 | 115 | form .aligned ul.radiolist { 116 | display: inline-block; 117 | margin: 0; 118 | padding: 0; 119 | } 120 | 121 | form .aligned p.help, 122 | form .aligned div.help { 123 | clear: left; 124 | /*margin-top: 0;*/ 125 | /*margin-left: 130px;*/ 126 | /*padding-left: 3px;*/ 127 | } 128 | 129 | form .aligned label + p.help, 130 | form .aligned label + div.help { 131 | margin-left: 0; 132 | padding-left: 0; 133 | } 134 | 135 | form .aligned p.help:last-child, 136 | form .aligned div.help:last-child { 137 | margin-bottom: 0; 138 | padding-bottom: 0; 139 | } 140 | 141 | form .aligned input + p.help, 142 | form .aligned textarea + p.help, 143 | form .aligned select + p.help, 144 | form .aligned input + div.help, 145 | form .aligned textarea + div.help, 146 | form .aligned select + div.help { 147 | /*margin-left: 160px;*/ 148 | /*padding-left: 10px;*/ 149 | } 150 | 151 | form .aligned ul li { 152 | /*list-style: none;*/ 153 | } 154 | input[type="text"], 155 | input[type="email"], 156 | input[type="password"], 157 | input[type="number"], 158 | select, 159 | textarea { 160 | display: block; 161 | width: 100%; 162 | padding: 0.5rem 20px; 163 | font-size: 0.875rem; 164 | font-weight: 400; 165 | line-height: 1.5rem; 166 | color: #495057; 167 | background-color: transparent; 168 | background-clip: padding-box; 169 | border: 1px solid #d2d6da; 170 | appearance: none; 171 | border-radius: 0.375rem; 172 | transition: 0.2s ease; 173 | } 174 | 175 | form .aligned table p { 176 | margin-left: 0; 177 | padding-left: 0; 178 | } 179 | 180 | .aligned .vCheckboxLabel { 181 | float: none; 182 | width: auto; 183 | display: inline-block; 184 | /*vertical-align: -3px;*/ 185 | /*padding: 0 0 5px 5px;*/ 186 | } 187 | 188 | .aligned .vCheckboxLabel + p.help, 189 | .aligned .vCheckboxLabel + div.help { 190 | margin-top: -4px; 191 | } 192 | 193 | .colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { 194 | width: 610px; 195 | } 196 | 197 | .checkbox-row p.help, 198 | .checkbox-row div.help { 199 | margin-left: 0; 200 | padding-left: 0; 201 | } 202 | 203 | fieldset .fieldBox { 204 | float: left; 205 | margin-right: 20px; 206 | } 207 | 208 | /* WIDE FIELDSETS */ 209 | 210 | .wide label { 211 | width: 200px; 212 | } 213 | 214 | form .wide p, 215 | form .wide input + p.help, 216 | form .wide input + div.help { 217 | /*margin-left: 200px;*/ 218 | } 219 | 220 | form .wide p.help, 221 | form .wide div.help { 222 | /*padding-left: 38px;*/ 223 | } 224 | 225 | form div.help ul { 226 | padding-left: 0; 227 | margin-left: 0; 228 | } 229 | 230 | .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { 231 | width: 450px; 232 | } 233 | 234 | /* COLLAPSED FIELDSETS */ 235 | 236 | fieldset.collapsed * { 237 | display: none; 238 | } 239 | 240 | fieldset.collapsed h2, fieldset.collapsed { 241 | display: block; 242 | } 243 | 244 | fieldset.collapsed { 245 | border: 1px solid #eee; 246 | border-radius: 4px; 247 | overflow: hidden; 248 | } 249 | 250 | fieldset.collapsed h2 { 251 | background: #f8f8f8; 252 | color: #666; 253 | } 254 | 255 | fieldset .collapse-toggle { 256 | color: #fff; 257 | } 258 | 259 | fieldset.collapsed .collapse-toggle { 260 | background: transparent; 261 | display: inline; 262 | color: #447e9b; 263 | } 264 | 265 | /* MONOSPACE TEXTAREAS */ 266 | 267 | fieldset.monospace textarea { 268 | font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; 269 | } 270 | 271 | /* SUBMIT ROW */ 272 | 273 | .submit-row { 274 | padding: 12px 14px; 275 | margin: 0 0 20px; 276 | background: #f8f8f8; 277 | border: 1px solid #eee; 278 | border-radius: 4px; 279 | text-align: right; 280 | overflow: hidden; 281 | } 282 | 283 | body.popup .submit-row { 284 | overflow: auto; 285 | } 286 | 287 | .submit-row input { 288 | height: 35px; 289 | line-height: 15px; 290 | margin: 0 0 0 5px; 291 | } 292 | 293 | .submit-row input.default { 294 | margin: 0 0 0 8px; 295 | text-transform: uppercase; 296 | } 297 | 298 | .submit-row p { 299 | margin: 0.3em; 300 | } 301 | 302 | .submit-row p.deletelink-box { 303 | float: left; 304 | margin: 0; 305 | } 306 | 307 | .submit-row a.deletelink { 308 | display: block; 309 | background: #ba2121; 310 | border-radius: 4px; 311 | padding: 10px 15px; 312 | height: 15px; 313 | line-height: 15px; 314 | color: #fff; 315 | } 316 | 317 | .submit-row a.closelink { 318 | display: inline-block; 319 | background: #bbbbbb; 320 | border-radius: 4px; 321 | padding: 10px 15px; 322 | height: 15px; 323 | line-height: 15px; 324 | margin: 0 0 0 5px; 325 | color: #fff; 326 | } 327 | 328 | .submit-row a.deletelink:focus, 329 | .submit-row a.deletelink:hover, 330 | .submit-row a.deletelink:active { 331 | background: #a41515; 332 | } 333 | 334 | .submit-row a.closelink:focus, 335 | .submit-row a.closelink:hover, 336 | .submit-row a.closelink:active { 337 | background: #aaaaaa; 338 | } 339 | 340 | /* CUSTOM FORM FIELDS */ 341 | 342 | .vSelectMultipleField { 343 | vertical-align: top; 344 | } 345 | 346 | .vCheckboxField { 347 | border: none; 348 | } 349 | 350 | .vDateField, .vTimeField { 351 | margin-right: 2px; 352 | margin-bottom: 4px; 353 | } 354 | 355 | .vDateField { 356 | min-width: 6.85em; 357 | } 358 | 359 | .vTimeField { 360 | min-width: 4.7em; 361 | } 362 | 363 | .vURLField { 364 | width: 30em; 365 | } 366 | 367 | .vLargeTextField, .vXMLLargeTextField { 368 | width: 48em; 369 | } 370 | 371 | .flatpages-flatpage #id_content { 372 | height: 40.2em; 373 | } 374 | 375 | .module table .vPositiveSmallIntegerField { 376 | width: 2.2em; 377 | } 378 | 379 | .vTextField, .vUUIDField { 380 | width: 20em; 381 | } 382 | 383 | .vIntegerField { 384 | width: 5em; 385 | } 386 | 387 | .vBigIntegerField { 388 | width: 10em; 389 | } 390 | 391 | .vForeignKeyRawIdAdminField { 392 | width: 5em; 393 | } 394 | 395 | /* INLINES */ 396 | 397 | .inline-group { 398 | padding: 0; 399 | margin: 0 0 30px; 400 | } 401 | 402 | .inline-group thead th { 403 | padding: 8px 10px; 404 | } 405 | 406 | .inline-group .aligned label { 407 | width: 160px; 408 | } 409 | 410 | .inline-related { 411 | position: relative; 412 | } 413 | 414 | .inline-related h3 { 415 | margin: 0; 416 | /*color: #666;*/ 417 | padding: 5px; 418 | font-size: 13px; 419 | /*background: #f8f8f8;*/ 420 | /*border-top: 1px solid #eee;*/ 421 | /*border-bottom: 1px solid #eee;*/ 422 | } 423 | 424 | .inline-related h3 span.delete { 425 | float: right; 426 | } 427 | 428 | .inline-related h3 span.delete label { 429 | margin-left: 2px; 430 | font-size: 11px; 431 | } 432 | 433 | .inline-related fieldset { 434 | margin: 0; 435 | /*background: #fff;*/ 436 | border: none; 437 | width: 100%; 438 | } 439 | 440 | .inline-related fieldset.module h3 { 441 | margin: 0; 442 | padding: 2px 5px 3px 5px; 443 | font-size: 11px; 444 | text-align: left; 445 | font-weight: bold; 446 | background: #bcd; 447 | color: #fff; 448 | } 449 | 450 | .inline-group .tabular fieldset.module { 451 | border: none; 452 | } 453 | 454 | .inline-related.tabular fieldset.module table { 455 | width: 100%; 456 | } 457 | 458 | .last-related fieldset { 459 | border: none; 460 | } 461 | 462 | .inline-group .tabular tr.has_original td { 463 | padding-top: 2em; 464 | } 465 | 466 | .inline-group .tabular tr td.original { 467 | padding: 2px 0 0 0; 468 | width: 0; 469 | _position: relative; 470 | } 471 | 472 | .inline-group .tabular th.original { 473 | width: 0px !important; 474 | padding: 0 !important; 475 | } 476 | 477 | .inline-group .tabular td.original p { 478 | position: absolute; 479 | left: 5px; 480 | /*height: 1.1em;*/ 481 | /*padding: 2px 9px;*/ 482 | overflow: hidden; 483 | font-size: 12px; 484 | font-weight: bold; 485 | /*color: #666;*/ 486 | _width: 700px; 487 | } 488 | 489 | .inline-group ul.tools { 490 | padding: 0; 491 | margin: 0; 492 | list-style: none; 493 | } 494 | 495 | .inline-group ul.tools li { 496 | display: inline; 497 | padding: 0 5px; 498 | } 499 | 500 | .inline-group div.add-row, 501 | .inline-group .tabular tr.add-row td { 502 | color: #666; 503 | background: #f2f4f6; 504 | padding: 8px 10px; 505 | /*border-bottom: 1px solid #eee;*/ 506 | } 507 | 508 | .inline-group .tabular tr.add-row td { 509 | padding: 8px 10px; 510 | border-bottom: 1px solid #eee; 511 | } 512 | 513 | .inline-group ul.tools a.add, 514 | .inline-group div.add-row a, 515 | .inline-group .tabular tr.add-row td a { 516 | background: url(../images/icon-addlink.svg) 0 1px no-repeat; 517 | padding-left: 16px; 518 | font-size: 12px; 519 | } 520 | 521 | .empty-form { 522 | display: none !important; 523 | } 524 | 525 | /* RELATED FIELD ADD ONE / LOOKUP */ 526 | 527 | .add-another, .related-lookup { 528 | margin-left: 5px; 529 | display: inline-block; 530 | vertical-align: middle; 531 | background-repeat: no-repeat; 532 | background-size: 14px; 533 | } 534 | 535 | .add-another { 536 | width: 16px; 537 | height: 16px; 538 | background-image: url(../images/icon-addlink.svg); 539 | } 540 | 541 | .related-lookup { 542 | width: 16px; 543 | height: 16px; 544 | background-image: url(../images/search.svg); 545 | } 546 | 547 | form .related-widget-wrapper ul { 548 | display: inline-block; 549 | margin-left: 0; 550 | padding-left: 0; 551 | } 552 | 553 | .clearable-file-input input { 554 | margin-top: 0; 555 | } 556 | -------------------------------------------------------------------------------- /static/assets/css/forms.min.css: -------------------------------------------------------------------------------- 1 | @import url("widgets.css");.form-row{border-bottom:1px solid #eee;display:table-row!important;font-size:13px;overflow:hidden;padding:10px}.form-row img,.form-row input{vertical-align:middle}.form-row .field-title{padding-left:0!important}.form-row label input[type=checkbox]{margin-top:0;vertical-align:0}form .form-row p{padding-left:0}.hidden{display:none}label{color:#666;font-size:13px;font-weight:400}.required label,label.required{font-weight:700}form ul.radiolist li{list-style-type:none}form ul.radiolist label{display:inline;float:none}form ul.radiolist input[type=radio]{margin:-2px 4px 0 0;padding:0}form ul.inline{margin-left:0;padding:0}form ul.inline li{float:left;padding-right:7px}.aligned label{display:block;word-wrap:break-word}.aligned label:not(.vCheckboxLabel):after{display:inline-block;height:26px;vertical-align:middle}.aligned label+div.help,.aligned label+div.readonly,.aligned label+p{margin-bottom:0;margin-top:0;padding:6px 0}.aligned ul label{display:inline;float:none;width:auto}.aligned .form-row input{margin-bottom:0}.colMS .aligned .vLargeTextField,.colMS .aligned .vXMLLargeTextField{width:350px}form .aligned ul{margin-left:160px;padding-left:10px}form .aligned ul.radiolist{display:inline-block;margin:0;padding:0}form .aligned div.help,form .aligned p.help{clear:left}form .aligned label+div.help,form .aligned label+p.help{margin-left:0;padding-left:0}form .aligned div.help:last-child,form .aligned p.help:last-child{margin-bottom:0;padding-bottom:0}input[type=email],input[type=number],input[type=password],input[type=text],select,textarea{appearance:none;background-clip:padding-box;background-color:transparent;border:1px solid #d2d6da;border-radius:.375rem;color:#495057;display:block;font-size:.875rem;font-weight:400;line-height:1.5rem;padding:.5rem 20px;transition:.2s ease;width:100%}form .aligned table p{margin-left:0;padding-left:0}.aligned .vCheckboxLabel{display:inline-block;float:none;width:auto}.aligned .vCheckboxLabel+div.help,.aligned .vCheckboxLabel+p.help{margin-top:-4px}.colM .aligned .vLargeTextField,.colM .aligned .vXMLLargeTextField{width:610px}.checkbox-row div.help,.checkbox-row p.help{margin-left:0;padding-left:0}fieldset .fieldBox{float:left;margin-right:20px}.wide label{width:200px}form div.help ul{margin-left:0;padding-left:0}.colM fieldset.wide .vLargeTextField,.colM fieldset.wide .vXMLLargeTextField{width:450px}fieldset.collapsed *{display:none}fieldset.collapsed,fieldset.collapsed h2{display:block}fieldset.collapsed{border:1px solid #eee;border-radius:4px;overflow:hidden}fieldset.collapsed h2{background:#f8f8f8;color:#666}fieldset .collapse-toggle{color:#fff}fieldset.collapsed .collapse-toggle{background:transparent;color:#447e9b;display:inline}fieldset.monospace textarea{font-family:Bitstream Vera Sans Mono,Monaco,Courier New,Courier,monospace}.submit-row{background:#f8f8f8;border:1px solid #eee;border-radius:4px;margin:0 0 20px;overflow:hidden;padding:12px 14px;text-align:right}body.popup .submit-row{overflow:auto}.submit-row input{height:35px;line-height:15px;margin:0 0 0 5px}.submit-row input.default{margin:0 0 0 8px;text-transform:uppercase}.submit-row p{margin:.3em}.submit-row p.deletelink-box{float:left;margin:0}.submit-row a.deletelink{background:#ba2121;display:block}.submit-row a.closelink,.submit-row a.deletelink{border-radius:4px;color:#fff;height:15px;line-height:15px;padding:10px 15px}.submit-row a.closelink{background:#bbb;display:inline-block;margin:0 0 0 5px}.submit-row a.deletelink:active,.submit-row a.deletelink:focus,.submit-row a.deletelink:hover{background:#a41515}.submit-row a.closelink:active,.submit-row a.closelink:focus,.submit-row a.closelink:hover{background:#aaa}.vSelectMultipleField{vertical-align:top}.vCheckboxField{border:none}.vDateField,.vTimeField{margin-bottom:4px;margin-right:2px}.vDateField{min-width:6.85em}.vTimeField{min-width:4.7em}.vURLField{width:30em}.vLargeTextField,.vXMLLargeTextField{width:48em}.flatpages-flatpage #id_content{height:40.2em}.module table .vPositiveSmallIntegerField{width:2.2em}.vTextField,.vUUIDField{width:20em}.vIntegerField{width:5em}.vBigIntegerField{width:10em}.vForeignKeyRawIdAdminField{width:5em}.inline-group{margin:0 0 30px;padding:0}.inline-group thead th{padding:8px 10px}.inline-group .aligned label{width:160px}.inline-related{position:relative}.inline-related h3{font-size:13px;margin:0;padding:5px}.inline-related h3 span.delete{float:right}.inline-related h3 span.delete label{font-size:11px;margin-left:2px}.inline-related fieldset{border:none;margin:0;width:100%}.inline-related fieldset.module h3{background:#bcd;color:#fff;font-size:11px;font-weight:700;margin:0;padding:2px 5px 3px;text-align:left}.inline-group .tabular fieldset.module{border:none}.inline-related.tabular fieldset.module table{width:100%}.last-related fieldset{border:none}.inline-group .tabular tr.has_original td{padding-top:2em}.inline-group .tabular tr td.original{padding:2px 0 0;_position:relative;width:0}.inline-group .tabular th.original{padding:0!important;width:0!important}.inline-group .tabular td.original p{font-size:12px;font-weight:700;left:5px;overflow:hidden;position:absolute;_width:700px}.inline-group ul.tools{list-style:none;margin:0;padding:0}.inline-group ul.tools li{display:inline;padding:0 5px}.inline-group .tabular tr.add-row td,.inline-group div.add-row{background:#f2f4f6;color:#666;padding:8px 10px}.inline-group .tabular tr.add-row td{border-bottom:1px solid #eee;padding:8px 10px}.inline-group .tabular tr.add-row td a,.inline-group div.add-row a,.inline-group ul.tools a.add{background:url(../images/icon-addlink.svg) 0 1px no-repeat;font-size:12px;padding-left:16px}.empty-form{display:none!important}.add-another,.related-lookup{background-repeat:no-repeat;background-size:14px;display:inline-block;margin-left:5px;vertical-align:middle}.add-another{background-image:url(../images/icon-addlink.svg);height:16px;width:16px}.related-lookup{background-image:url(../images/search.svg);height:16px;width:16px}form .related-widget-wrapper ul{display:inline-block;margin-left:0;padding-left:0}.clearable-file-input input{margin-top:0} -------------------------------------------------------------------------------- /static/assets/css/plugins/jsvectormap.min.css: -------------------------------------------------------------------------------- 1 | svg{-ms-touch-action:none;touch-action:none}image,text,.jvm-zoomin,.jvm-zoomout{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jvm-container{-ms-touch-action:none;touch-action:none;position:relative;overflow:hidden;height:100%;width:100%}.jvm-tooltip{border-radius:3px;background-color:#5c5cff;font-family:sans-serif,Verdana;font-size:smaller;box-shadow:1px 2px 12px rgba(0,0,0,0.2);padding:3px 5px;white-space:nowrap;position:absolute;display:none;color:#FFF}.jvm-tooltip.active{display:block}.jvm-zoom-btn{border-radius:3px;background-color:#292929;padding:3px;box-sizing:border-box;position:absolute;line-height:10px;cursor:pointer;color:#FFF;height:15px;width:15px;left:10px}.jvm-zoom-btn.jvm-zoomout{top:30px}.jvm-zoom-btn.jvm-zoomin{top:10px}.jvm-series-container{right:15px;position:absolute}.jvm-series-container.jvm-series-h{bottom:15px}.jvm-series-container.jvm-series-v{top:15px}.jvm-series-container .jvm-legend{background-color:#fff;border:1px solid #e5e7eb;margin-left:.75rem;border-radius:.25rem;border-color:#e5e7eb;padding:.6rem;box-shadow:0 1px 2px 0 rgba(0,0,0,0.05);float:left}.jvm-series-container .jvm-legend .jvm-legend-title{line-height:1;border-bottom:1px solid #e5e7eb;padding-bottom:.5rem;margin-bottom:.575rem;text-align:left}.jvm-series-container .jvm-legend .jvm-legend-inner{overflow:hidden}.jvm-series-container .jvm-legend .jvm-legend-inner .jvm-legend-tick{overflow:hidden;min-width:40px}.jvm-series-container .jvm-legend .jvm-legend-inner .jvm-legend-tick:not(:first-child){margin-top:.575rem}.jvm-series-container .jvm-legend .jvm-legend-inner .jvm-legend-tick .jvm-legend-tick-sample{border-radius:4px;margin-right:.65rem;height:16px;width:16px;float:left}.jvm-series-container .jvm-legend .jvm-legend-inner .jvm-legend-tick .jvm-legend-tick-text{font-size:12px;text-align:center;float:left}.jvm-line[animation="true"]{-webkit-animation:jvm-line-animation 10s linear forwards infinite;animation:jvm-line-animation 10s linear forwards infinite}@-webkit-keyframes jvm-line-animation{from{stroke-dashoffset:250}}@keyframes jvm-line-animation{from{stroke-dashoffset:250}} 2 | -------------------------------------------------------------------------------- /static/assets/css/widgets.min.css: -------------------------------------------------------------------------------- 1 | .selector{float:left;width:800px}.selector select{background-color:#fff!important;height:17.2em;width:380px}.selector-available,.selector-chosen{float:left;margin-bottom:5px;text-align:center;width:380px}.selector-chosen select{border-top:none}.selector-available h2,.selector-chosen h2{border:1px solid #ccc;border-radius:4px 4px 0 0}.selector-chosen h2{background:#1f2937}.selector .selector-available h2,.selector-chosen h2{color:#fff;font-size:20px;margin-bottom:0;padding:8px}.selector .selector-available h2{background:#644f97}.selector .selector-filter{background:#fff;border:1px solid #ccc;border-width:0 1px;color:#999;font-size:10px;margin:0;padding:8px;text-align:left}.inline-group .aligned .selector .selector-filter label,.selector .selector-filter label{float:left;height:25px;line-height:1;margin:10px 3px 0;overflow:hidden;padding:0;width:25px}.selector .selector-available input{background-clip:padding-box;background-color:transparent;border:1px solid #9e9e9e;border-radius:.25rem;box-shadow:none;color:#272424;display:block;font-size:.875rem;font-weight:400;height:calc(2.25rem + 2px);line-height:1.428571;margin-left:8px;padding:.5rem .7rem;transition:all .2s cubic-bezier(.68,-.55,.265,1.55);width:320px}.selector ul.selector-chooser{background-color:#eee;border-radius:10px;float:left;margin:10em 5px 0;padding:0;width:22px}.selector-chooser li{list-style-type:none;margin:0;padding:3px}.selector select{border-radius:0 0 4px 4px;margin:0 0 10px;padding:0 10px}.selector-add,.selector-remove{cursor:default;display:block;height:16px;opacity:.3;overflow:hidden;text-indent:-3000px;width:16px}.active.selector-add,.active.selector-remove{opacity:1}.active.selector-add:hover,.active.selector-remove:hover{cursor:pointer}.selector-add{background:url(../images/selector-icons.svg) 0 -96px no-repeat}.active.selector-add:focus,.active.selector-add:hover{background-position:0 -112px}.selector-remove{background:url(../images/selector-icons.svg) 0 -64px no-repeat}.active.selector-remove:focus,.active.selector-remove:hover{background-position:0 -80px}a.selector-chooseall,a.selector-clearall{display:inline-block;font-weight:700;height:16px;line-height:16px;margin:1px auto 3px;opacity:.3;overflow:hidden;text-align:left;text-decoration:none}a.active.selector-chooseall:focus,a.active.selector-chooseall:hover,a.active.selector-clearall:focus,a.active.selector-clearall:hover{color:#447e9b}a.active.selector-chooseall,a.active.selector-clearall{opacity:1}a.active.selector-chooseall:hover,a.active.selector-clearall:hover{cursor:pointer}a.selector-chooseall{background:url(../images/selector-icons.svg) right -160px no-repeat;cursor:default;padding:0 18px 0 0}a.active.selector-chooseall:focus,a.active.selector-chooseall:hover{background-position:100% -176px}a.selector-clearall{background:url(../images/selector-icons.svg) 0 -128px no-repeat;cursor:default;padding:0 0 0 18px}a.active.selector-clearall:focus,a.active.selector-clearall:hover{background-position:0 -144px}.stacked{float:left;width:490px}.stacked select{height:10.1em;width:480px}.stacked .selector-available,.stacked .selector-chosen{width:480px}.stacked .selector-available{margin-bottom:0}.stacked .selector-available input{width:422px}.stacked ul.selector-chooser{background-color:#eee;border-radius:10px;height:22px;margin:0 0 10px 40%;width:50px}.stacked .selector-chooser li{float:left;padding:3px 3px 3px 5px}.stacked .selector-chooseall,.stacked .selector-clearall{display:none}.stacked .selector-add{background:url(../images/selector-icons.svg) 0 -32px no-repeat;cursor:default}.stacked .active.selector-add{background-position:0 -48px;cursor:pointer}.stacked .selector-remove{background:url(../images/selector-icons.svg) 0 0 no-repeat;cursor:default}.stacked .active.selector-remove{background-position:0 -16px;cursor:pointer}.selector .help-icon{background:url(../images/icon-unknown.svg) 0 0 no-repeat;display:inline-block;height:13px;margin:-2px 0 0 2px;vertical-align:middle;width:13px}.selector .selector-chosen .help-icon{background:url(../images/icon-unknown-alt.svg) 0 0 no-repeat}.selector .search-label-icon{background:url(../images/search.svg) 0 0 no-repeat;display:inline-block;height:25px;width:25px}p.datetime{color:#666;font-weight:700;line-height:20px;margin:0;padding:0}.datetime span{color:#ccc;float:right;font-size:11px;font-weight:400;white-space:nowrap}.datetime input,.form-row .datetime input.vDateField,.form-row .datetime input.vTimeField{margin-bottom:4px;margin-left:5px;min-width:0}table p.datetime{font-size:11px;margin-left:0;padding-left:0}.datetimeshortcuts .clock-icon,.datetimeshortcuts .date-icon{display:inline-block;height:16px;overflow:hidden;position:relative;vertical-align:middle;width:16px}.datetimeshortcuts .clock-icon{background:url(../images/icon-clock.svg) 0 0 no-repeat}.datetimeshortcuts a:focus .clock-icon,.datetimeshortcuts a:hover .clock-icon{background-position:0 -16px}.datetimeshortcuts .date-icon{background:url(../images/icon-calendar.svg) 0 0 no-repeat;top:-1px}.datetimeshortcuts a:focus .date-icon,.datetimeshortcuts a:hover .date-icon{background-position:0 -16px}.timezonewarning{color:#999;font-size:11px}p.url{color:#666;font-size:11px;font-weight:700;line-height:20px;margin:0;padding:0}.url a{font-weight:400}p.file-upload{color:#666;font-size:11px;font-weight:700;line-height:20px;margin:0;padding:0}.aligned p.file-upload{margin-left:170px}.file-upload a{font-weight:400}.file-upload .deletelink{margin-left:5px}span.clearable-file-input label{color:#333;display:inline;float:none;font-size:11px}.calendarbox,.clockbox{background:#fff;border:1px solid #ddd;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,.15);font-size:12px;margin:5px auto;overflow:hidden;text-align:center;width:19em}.clockbox{width:auto}.calendar{margin:10px;padding:10px}.calendar table{background:#fff;border-collapse:collapse;margin:0;padding:0;width:100%}.calendar caption,.calendarbox h2{background:#f5dd5d;border-top:none;color:#333;font-size:12px;font-weight:700;margin:0;text-align:center}.calendar th{background:#f8f8f8;border-bottom:1px solid #ddd;color:#666;padding:8px 5px}.calendar td,.calendar th{font-size:12px;font-weight:400;text-align:center}.calendar td{border-bottom:none;border-top:1px solid #eee;padding:0}.calendar td.selected a{background:#79aec8;color:#fff}.calendar td.nonday{background:#f8f8f8}.calendar td.today a{font-weight:700}.calendar td a,.timelist a{color:#444;display:block;font-weight:400;padding:6px;text-decoration:none}.calendar td a:focus,.calendar td a:hover,.timelist a:focus,.timelist a:hover{background:#79aec8;color:#fff}.calendar td a:active,.timelist a:active{background:#417690;color:#fff}.calendarnav{color:#ccc;font-size:10px;margin:0;padding:1px 3px;text-align:center}#calendarnav a:focus,#calendarnav a:hover,#calendarnav a:visited,.calendarnav a:link{color:#999}.calendar-shortcuts{background:#fff;border-top:1px solid #eee;color:#ccc;font-size:11px;line-height:11px;padding:8px 0}.calendarbox .calendarnav-next,.calendarbox .calendarnav-previous{display:block;height:15px;padding:0;position:absolute;text-indent:-9999px;top:8px;width:15px}.calendarnav-previous{background:url(../images/calendar-icons.svg) 0 0 no-repeat;left:10px}.calendarbox .calendarnav-previous:focus,.calendarbox .calendarnav-previous:hover{background-position:0 -15px}.calendarnav-next{background:url(../images/calendar-icons.svg) 0 -30px no-repeat;right:10px}.calendarbox .calendarnav-next:focus,.calendarbox .calendarnav-next:hover{background-position:0 -45px}.calendar-cancel{background:#eee;border-top:1px solid #ddd;color:#333;font-size:12px;margin:0;padding:4px 0}.calendar-cancel:focus,.calendar-cancel:hover{background:#ddd}.calendar-cancel a{color:#000;display:block}.timelist li,ul.timelist{list-style-type:none;margin:0;padding:0}.timelist a{padding:2px}.inline-deletelink{background:url(../images/inline-delete.svg) 0 0 no-repeat;border:0;float:right;height:16px;text-indent:-9999px;width:16px}.inline-deletelink:focus,.inline-deletelink:hover{cursor:pointer}.related-widget-wrapper{float:left;overflow:hidden}.related-widget-wrapper-link{opacity:.3}.related-widget-wrapper-link:link{opacity:.8}.related-widget-wrapper-link:link:focus,.related-widget-wrapper-link:link:hover{opacity:1}.related-widget-wrapper-link+.related-widget-wrapper-link,select+.related-widget-wrapper-link{margin-left:7px}.related-widget-wrapper .related-widget-wrapper-link{float:right}.help{color:#0000007a;font-size:13px;margin:5px 0 0 5px!important}.help a{color:#6464ff}.help ul{margin-left:15px!important}.help li{font-size:13px!important}.clockbox h2{background-color:#1f2937;color:#fff;font-size:23px;padding:6px}.submit_btn{background-color:#f2f4f6;direction:rtl;margin-top:40px}ul.errorlist{color:#d96060;list-style-type:none;margin:-25px 0 25px}.stacked_volt_card{margin-bottom:15px}.stacked_volt_card fieldset{padding:17px}.stacked_volt_card h3{background-color:#f2f4f6}.original p.tabular_p_volt{background-color:#f2f4f6;border-radius:5px 0 0 5px;padding:3px} -------------------------------------------------------------------------------- /static/assets/fonts/feather/feather.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/feather/feather.eot -------------------------------------------------------------------------------- /static/assets/fonts/feather/feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/feather/feather.ttf -------------------------------------------------------------------------------- /static/assets/fonts/feather/feather.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/feather/feather.woff -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-brands-400.eot -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-brands-400.ttf -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-brands-400.woff -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-brands-400.woff2 -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-regular-400.eot -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-regular-400.ttf -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-regular-400.woff -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-regular-400.woff2 -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-solid-900.eot -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-solid-900.ttf -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-solid-900.woff -------------------------------------------------------------------------------- /static/assets/fonts/fontawesome/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/fontawesome/fa-solid-900.woff2 -------------------------------------------------------------------------------- /static/assets/fonts/inter/Inter-italic.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/inter/Inter-italic.var.woff2 -------------------------------------------------------------------------------- /static/assets/fonts/inter/Inter-roman.var.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/inter/Inter-roman.var.woff2 -------------------------------------------------------------------------------- /static/assets/fonts/inter/inter.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter var'; 3 | font-weight: 100 900; 4 | font-display: swap; 5 | font-style: normal; 6 | font-named-instance: 'Regular'; 7 | src: url('Inter-roman.var.woff2?v=3.18') format('woff2'); 8 | } 9 | 10 | @font-face { 11 | font-family: 'Inter var'; 12 | font-weight: 100 900; 13 | font-display: swap; 14 | font-style: italic; 15 | font-named-instance: 'Italic'; 16 | src: url('Inter-italic.var.woff2?v=3.18') format('woff2'); 17 | } 18 | -------------------------------------------------------------------------------- /static/assets/fonts/material.css: -------------------------------------------------------------------------------- 1 | /* fallback */ 2 | @font-face { 3 | font-family: 'Material Icons Two Tone'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url(material/material.woff2) format('woff2'); 7 | } 8 | 9 | .material-icons-two-tone { 10 | font-family: 'Material Icons Two Tone'; 11 | font-weight: normal; 12 | font-style: normal; 13 | font-size: 24px; 14 | line-height: 1; 15 | letter-spacing: normal; 16 | text-transform: none; 17 | display: inline-block; 18 | white-space: nowrap; 19 | word-wrap: normal; 20 | direction: ltr; 21 | -webkit-font-feature-settings: 'liga'; 22 | -webkit-font-smoothing: antialiased; 23 | } 24 | -------------------------------------------------------------------------------- /static/assets/fonts/material/material.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/material/material.woff2 -------------------------------------------------------------------------------- /static/assets/fonts/phosphor/duotone/Phosphor-Duotone.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/phosphor/duotone/Phosphor-Duotone.ttf -------------------------------------------------------------------------------- /static/assets/fonts/phosphor/duotone/Phosphor-Duotone.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/phosphor/duotone/Phosphor-Duotone.woff -------------------------------------------------------------------------------- /static/assets/fonts/tabler/tabler-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/tabler/tabler-icons.eot -------------------------------------------------------------------------------- /static/assets/fonts/tabler/tabler-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/tabler/tabler-icons.ttf -------------------------------------------------------------------------------- /static/assets/fonts/tabler/tabler-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/tabler/tabler-icons.woff -------------------------------------------------------------------------------- /static/assets/fonts/tabler/tabler-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/fonts/tabler/tabler-icons.woff2 -------------------------------------------------------------------------------- /static/assets/images/application/img-coupon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/application/img-coupon.png -------------------------------------------------------------------------------- /static/assets/images/error/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/error/404.png -------------------------------------------------------------------------------- /static/assets/images/error/generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/error/generic.png -------------------------------------------------------------------------------- /static/assets/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /static/assets/images/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/assets/images/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/assets/images/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/assets/images/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/assets/images/landing/img-header-main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/landing/img-header-main.jpg -------------------------------------------------------------------------------- /static/assets/images/landing/img-wave.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /static/assets/images/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /static/assets/images/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /static/assets/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /static/assets/images/selector-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /static/assets/images/user/avatar-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-1.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-10.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-2.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-3.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-4.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-5.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-6.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-7.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-8.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar-9.jpg -------------------------------------------------------------------------------- /static/assets/images/user/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/avatar.jpg -------------------------------------------------------------------------------- /static/assets/images/user/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/images/user/profile.jpg -------------------------------------------------------------------------------- /static/assets/img/csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/img/csv.png -------------------------------------------------------------------------------- /static/assets/img/export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/static/assets/img/export.png -------------------------------------------------------------------------------- /static/assets/js/pages/dashboard-default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // [ world-low chart ] start 3 | (function () { 4 | var map = new jsVectorMap({ 5 | selector: "#world-low", 6 | map: "world", 7 | markersSelectable: true, 8 | markers: [{ 9 | coords: [-14.2350, -51.9253] 10 | }, 11 | { 12 | coords: [35.8617, 104.1954] 13 | }, 14 | { 15 | coords: [61, 105] 16 | }, 17 | { 18 | coords: [26.8206, 30.8025] 19 | } 20 | ], 21 | markerStyle: { 22 | initial: { 23 | fill: '#3f4d67', 24 | 25 | }, 26 | hover: { 27 | fill: '#04a9f5', 28 | }, 29 | }, 30 | markerLabelStyle: { 31 | initial: { 32 | fontFamily: "'Inter', sans-serif", 33 | fontSize: 13, 34 | fontWeight: 500, 35 | fill: '#3f4d67', 36 | }, 37 | }, 38 | }); 39 | })(); 40 | // [ world-low chart ] end 41 | 42 | // [ Widget-line-chart ] start 43 | var options = { 44 | chart: { 45 | type: 'line', 46 | height: 210, 47 | zoom: { 48 | enabled: false 49 | }, 50 | toolbar: { 51 | show: false, 52 | }, 53 | }, 54 | dataLabels: { 55 | enabled: false, 56 | }, 57 | colors: ["#fff"], 58 | fill: { 59 | type: 'solid', 60 | }, 61 | plotOptions: { 62 | bar: { 63 | columnWidth: '30%', 64 | } 65 | }, 66 | series: [{ 67 | data: [10, 60, 45, 72, 45, 86] 68 | }], 69 | xaxis: { 70 | categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], 71 | axisBorder: { 72 | show: false, 73 | }, 74 | axisTicks: { 75 | show: false, 76 | }, 77 | labels: { 78 | style: { 79 | colors: "#fff" 80 | } 81 | }, 82 | }, 83 | yaxis: { 84 | axisBorder: { 85 | show: false, 86 | }, 87 | axisTicks: { 88 | show: false, 89 | }, 90 | crosshairs: { 91 | width: 0 92 | }, 93 | labels: { 94 | show: false, 95 | }, 96 | }, 97 | grid: { 98 | padding: { 99 | bottom: 0, 100 | left: 10, 101 | }, 102 | xaxis: { 103 | lines: { 104 | show: false 105 | } 106 | }, 107 | yaxis: { 108 | lines: { 109 | show: false 110 | } 111 | }, 112 | }, 113 | markers: { 114 | size: 5, 115 | colors: '#fff', 116 | opacity: 0.9, 117 | strokeWidth: 2, 118 | hover: { 119 | size: 7, 120 | } 121 | }, 122 | tooltip: { 123 | fixed: { 124 | enabled: false 125 | }, 126 | x: { 127 | show: false 128 | }, 129 | y: { 130 | title: { 131 | formatter: function (seriesName) { 132 | return 'Statistics :' 133 | } 134 | } 135 | }, 136 | marker: { 137 | show: false 138 | } 139 | } 140 | }; 141 | var chart = new ApexCharts(document.querySelector("#Widget-line-chart"), options); 142 | chart.render(); 143 | // [ Widget-line-chart ] end -------------------------------------------------------------------------------- /static/assets/scss/custom.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Custom SCSS 4 | 5 | */ -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-generator/flask-datta-able/52f76dbf6dc141b729ccee0bcc2a0b61d9becb3e/templates/.gitkeep -------------------------------------------------------------------------------- /templates/authentication/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base-auth.html" %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 | {{ form.hidden_tag() }} 20 | 21 | 28 | 29 |
30 | {% if has_github %} 31 | 38 | {% endif %} 39 | 40 | {% if has_google %} 41 | 48 | {% endif %} 49 |
50 | 51 |

Login

52 |

53 | Default account: test / pass 54 |

55 | 56 | {% if msg %} 57 | {{ msg | safe }} 58 | {% endif %} 59 | 60 |
61 | {{ form.username(placeholder="Username or eMail", class="form-control") }} 62 |
63 |
64 | {{ form.password(placeholder="Password", class="form-control", type="password") }} 65 |
66 | 67 |
68 |
69 | 70 | 71 |
72 | Forgot Password? 73 |
74 |
75 | 76 |
77 |
78 | Create Account 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 | {% endblock content %} -------------------------------------------------------------------------------- /templates/authentication/register.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base-auth.html" %} 2 | 3 | {% block title %}Register{% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | {% if success %} 20 |
21 | {{ msg | safe }} 22 |
23 | Sign IN 24 |
25 | {% else %} 26 | 27 |
28 | {{ form.hidden_tag() }} 29 | 30 | 37 | 38 | {% if has_github %} 39 | 46 | {% endif %} 47 | 48 |

Sign up

49 | 50 | {% if msg %} 51 |
52 | {{ msg | safe }} 53 |
54 | {% endif %} 55 | 56 |
57 | {{ form.username(placeholder="Username", class="form-control") }} 58 |
59 |
60 | {{ form.email(placeholder="Email", class="input form-control", type="email") }} 61 |
62 |
63 | {{ form.password(placeholder="Password", class="form-control", type="password") }} 64 |
65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 | 74 |
75 |
76 | Login 77 |
78 |
79 | {% endif %} 80 |
81 |
82 |
83 |
84 |
85 | 86 | {% endblock content %} -------------------------------------------------------------------------------- /templates/charts/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %}Charts{% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 | 10 | 27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 |
Bar Chart
36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 |
48 |
Pie Chart
49 |
50 |
51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 |
59 |
60 | {% endblock content %} 61 | 62 | {% block extra_js %} 63 | 64 | 87 | {% endblock extra_js %} 88 | -------------------------------------------------------------------------------- /templates/dyn_dt/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %} Profile {% endblock %} 4 | 5 | 6 | {% block stylesheets %}{% endblock stylesheets %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 | 13 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 | Available Routes - defined in config.DYNAMIC_DATATB - Read Documentation. 39 |
40 |
41 |
42 |
    43 | {% for link in routes %} 44 |
  • 45 | {{ link }} 46 |
  • 47 | {% endfor %} 48 |
49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 | {% endblock content %} 57 | 58 | 59 | {% block javascripts %}{% endblock javascripts %} -------------------------------------------------------------------------------- /templates/error/403.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base-auth.html' %} 2 | 3 | {% block title %} Error 403 - Access denied {% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 | Access denied. Please contact support or authenticate. 15 |
16 |
17 | 18 | Sign IN 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | {% endblock content %} -------------------------------------------------------------------------------- /templates/error/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base-auth.html' %} 2 | 3 | {% block title %} Error 404 - Page not found {% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 |
Oops! Page not found!
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 | {% endblock content %} -------------------------------------------------------------------------------- /templates/error/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base-auth.html' %} 2 | 3 | {% block title %} Error 500 - Server Error {% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 | Server Error. Please contact support. 15 |
16 |
17 | 18 | HOME 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | {% endblock content %} -------------------------------------------------------------------------------- /templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/includes/items-table.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | {% for field in db_field_names %} 7 | 8 | {% endfor %} 9 | 10 | 11 | 12 | {% for item in items %} 13 | 14 | {% for field_name in db_field_names %} 15 | 16 | {% endfor %} 17 | 18 | {% endfor %} 19 | 20 |
{{ field }}
{{ item|getattribute(field_name) }}
21 |
-------------------------------------------------------------------------------- /templates/includes/loader.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /templates/includes/navigation.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 34 |
35 | 36 |
37 |
    38 | 145 | 214 |
215 |
216 |
217 |
-------------------------------------------------------------------------------- /templates/includes/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/includes/sidebar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/layouts/base-auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock title %} | Datta able Dashboard Template 5 | {% include "includes/head.html" %} 6 | {% block extrastyle %}{% endblock extrastyle %} 7 | 8 | 9 | 10 | {% block loader %} 11 | {% include "includes/loader.html" %} 12 | {% endblock loader %} 13 | 14 | {% block content %}{% endblock content %} 15 | 16 | {% include "includes/scripts.html" %} 17 | {% block extra_js %}{% endblock extra_js %} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/layouts/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock title %} | Flask Datta Able 6 | 7 | 8 | 9 | {% include "includes/head.html" %} 10 | {% block extrastyle %}{% endblock extrastyle %} 11 | 12 | 13 | 14 | 15 | {% block loader %} 16 | {% include "includes/loader.html" %} 17 | {% endblock loader %} 18 | 19 | {% block sidebar %} 20 | {% include "includes/sidebar.html" %} 21 | {% endblock sidebar %} 22 | 23 | {% block navigation %} 24 | {% include "includes/navigation.html" %} 25 | {% endblock navigation %} 26 | 27 | {% block content %}{% endblock content %} 28 | 29 | {% block footer %} 30 | {% include "includes/footer.html" %} 31 | {% endblock footer %} 32 | 33 | {% include "includes/scripts.html" %} 34 | {% block extra_js %}{% endblock extra_js %} 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /templates/pages/icon-feather.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %}Icon Feather{% endblock title %} 4 | 5 | {% block content %} 6 | 7 | 8 |
9 |
10 | 11 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 |
Feather Icon
38 |

Use svg icon with <i data-feather="<< Copied code >>"> in you html code

39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 | 56 | {% endblock content %} 57 | 58 | {% block extra_js %} 59 | 60 | 61 | 62 | 63 | 363 | 364 | 365 | {% endblock extra_js %} 366 | -------------------------------------------------------------------------------- /templates/pages/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %} Profile {% endblock %} 4 | 5 | 6 | {% block stylesheets %}{% endblock stylesheets %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 | 13 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
Edit Info
38 |
39 |
40 |
41 | {{ form.hidden_tag() }} 42 | 43 | {% for field in form %} 44 | {% if field.type in ['CSRFTokenField', 'HiddenField'] %} 45 | {{ field() }} 46 | {% else %} 47 |
48 |
49 | 50 | {{ field(class_="form-control", readonly=True if field.name in readonly_fields else False) }} 51 |
52 |
53 | {% endif %} 54 | {% endfor %} 55 | 56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | profile image 71 |
72 |
73 |

74 | {{ current_user.username }} 75 |

76 |

77 | {% if current_user.email %} {{ current_user.email }} {% endif %} 78 |

79 |
80 |
81 | 82 |
83 |

84 | This page is your private space. 85 |
86 |

87 |
88 |
89 | 90 | 94 | 95 | 99 | 100 | 104 | 105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | {% endblock content %} 113 | 114 | 115 | {% block javascripts %}{% endblock javascripts %} -------------------------------------------------------------------------------- /templates/pages/sample-page.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 |
8 | 9 | 27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 |
Hello card
36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 | {% endblock content %} -------------------------------------------------------------------------------- /templates/pages/typography.html: -------------------------------------------------------------------------------- 1 | {% extends "layouts/base.html" %} 2 | 3 | {% block title %}Typography{% endblock title %} 4 | 5 | {% block content %} 6 | 7 | 8 |
9 |
10 | 11 | 29 | 30 | 31 | 32 |
33 | 34 |
35 |
36 |
37 |
Headings
38 |

.h1 through .h6 classes are also available, for when you want to match 39 | the font styling of a heading 40 | but cannot use the associated HTML element.

41 |
42 |
43 |

h1. Heading

44 |
45 |

h2. Heading

46 |
47 |

This is a H3

48 |
49 |

This is a H4

50 |
51 |
This is a H5
52 |
53 |
This is a H6
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Display Headings
61 |
62 |
63 |

Display 1

64 |

Display 2

65 |

Display 3

66 |

Display 4

67 |

Display 5

68 |

Display 6

69 |
70 |
71 |
72 |
73 |
74 |
75 |
Inline Text Elements
76 |
77 |
78 |

Your title goes here

79 | You can use the mark tag to 80 | highlight text. 81 |
82 | This line of text is meant to be treated as deleted text. 83 |
84 | This line of text is meant to be treated as an addition to the document. 85 |
86 | rendered as bold text 87 |
88 | rendered as italicized text 89 |
90 |
91 |
92 |
93 |
94 |
95 |
Contextual Text Colors
96 |
97 |
98 |

Fusce dapibus, tellus ac cursus commodo, tortor mauris nibh.

99 |

Nullam id dolor id nibh ultricies vehicula ut id elit.

100 |

Duis mollis, est non commodo luctus, nisi erat porttitor ligula.

101 |

Maecenas sed diam eget risus varius blandit sit amet non magna.

102 |

Etiam porta sem malesuada magna mollis euismod.

103 |

Donec ullamcorper nulla non metus auctor fringilla.

104 |

Nullam id dolor id nibh ultricies vehicula ut id elit.

105 |
106 |
107 |
108 |
109 |
110 |
111 |
Unordered
112 |
113 |
114 |
    115 |
  • Lorem ipsum dolor sit amet
  • 116 |
  • Consectetur adipiscing elit
  • 117 |
  • Integer molestie lorem at massa
  • 118 |
  • Facilisis in pretium nisl aliquet
  • 119 |
  • Nulla volutpat aliquam velit 120 |
      121 |
    • Phasellus iaculis neque
    • 122 |
    • Purus sodales ultricies
    • 123 |
    • Vestibulum laoreet porttitor sem
    • 124 |
    • Ac tristique libero volutpat at
    • 125 |
    126 |
  • 127 |
  • Faucibus porta lacus fringilla vel
  • 128 |
  • Aenean sit amet erat nunc
  • 129 |
  • Eget porttitor lorem
  • 130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
Ordered
138 |
139 |
140 |
    141 |
  1. Lorem ipsum dolor sit amet
  2. 142 |
  3. Consectetur adipiscing elit
  4. 143 |
  5. Integer molestie lorem at massa
  6. 144 |
  7. Facilisis in pretium nisl aliquet
  8. 145 |
  9. Nulla volutpat aliquam velit 146 |
      147 |
    • Phasellus iaculis neque
    • 148 |
    • Purus sodales ultricies
    • 149 |
    • Vestibulum laoreet porttitor sem
    • 150 |
    • Ac tristique libero volutpat at
    • 151 |
    152 |
  10. 153 |
  11. Faucibus porta lacus fringilla vel
  12. 154 |
  13. Aenean sit amet erat nunc
  14. 155 |
  15. Eget porttitor lorem
  16. 156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
Unstyled
164 |
165 |
166 |
    167 |
  • Lorem ipsum dolor sit amet
  • 168 |
  • Integer molestie lorem at massa 169 |
      170 |
    • Phasellus iaculis neque
    • 171 |
    172 |
  • 173 |
  • Faucibus porta lacus fringilla vel
  • 174 |
  • Eget porttitor lorem
  • 175 |
176 |
Inline
177 |
178 |
    179 |
  • Lorem ipsum
  • 180 |
  • Phasellus iaculis
  • 181 |
  • Nulla volutpat
  • 182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
Blockquotes
190 |
191 |
192 |

Your awesome text goes here.

193 |
194 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a 195 | ante.

196 |
Someone famous in Source Title 197 |
198 |
199 |

Add .text-end for a blockquote with right-aligned 200 | content.

201 |
202 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer posuere erat a 203 | ante.

204 |
Someone famous in Source Title 205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
Horizontal Description
214 |
215 |
216 |
217 |
Description lists
218 |
A description list is perfect for defining terms.
219 | 220 |
Euismod
221 |
Vestibulum id ligula porta felis euismod semper eget lacinia odio sem nec elit. 222 |
223 |
Donec id elit non mi porta gravida at eget metus.
224 | 225 |
Malesuada porta
226 |
Etiam porta sem malesuada magna mollis euismod.
227 | 228 |
Truncated term is truncated
229 |
Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut 230 | fermentum massa justo sit amet risus.
231 |
232 |
233 |
234 |
235 | 236 |
237 | 238 |
239 |
240 | 241 | {% endblock content %} -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import autoprefixer from "autoprefixer"; 3 | import cssnano from "cssnano"; 4 | import path from "path"; 5 | 6 | export default defineConfig(({ mode }) => { 7 | const isProduction = mode === "production"; 8 | 9 | return { 10 | css: { 11 | postcss: { 12 | plugins: [ 13 | autoprefixer(), 14 | isProduction && cssnano(), 15 | ].filter(Boolean), 16 | }, 17 | }, 18 | build: { 19 | outDir: "static", 20 | emptyOutDir: false, 21 | rollupOptions: { 22 | input: path.resolve(__dirname, "static/assets/scss/custom.scss"), 23 | output: { 24 | assetFileNames: (assetInfo) => { 25 | if (assetInfo.name === "custom.css") { 26 | return "assets/css/custom.css"; 27 | } 28 | return "assets/css/[name].[ext]"; 29 | }, 30 | }, 31 | }, 32 | }, 33 | }; 34 | }); --------------------------------------------------------------------------------