Ver Fonte

Merge pull request #44 from sleepytaco/untube-react

Re-structure Untube project to run on both prod and locally
Mohammed Khan há 1 ano atrás
pai
commit
9dadeee3d4
100 ficheiros alterados com 453 adições e 251 exclusões
  1. 70 10
      .gitignore
  2. 31 0
      Makefile
  3. 0 50
      UnTube/production.py
  4. 0 6
      UnTube/secrets.py
  5. 0 5
      apps/main/static/fontawesome-free-5.15.3-web/css/brands.min.css
  6. 0 5
      apps/main/static/fontawesome-free-5.15.3-web/css/regular.min.css
  7. 0 5
      apps/main/static/fontawesome-free-5.15.3-web/css/solid.min.css
  8. 0 3
      apps/users/admin.py
  9. 0 0
      backend/UnTube/__init__.py
  10. 4 4
      backend/UnTube/asgi.py
  11. 33 0
      backend/UnTube/settings/__init__.py
  12. 34 0
      backend/UnTube/settings/allauth.py
  13. 23 61
      backend/UnTube/settings/base.py
  14. 17 0
      backend/UnTube/settings/custom.py
  15. 12 0
      backend/UnTube/settings/docker.py
  16. 14 0
      backend/UnTube/settings/envvars.py
  17. 48 0
      backend/UnTube/settings/pythonanywhere.py
  18. 11 0
      backend/UnTube/settings/templates/settings.dev.py
  19. 6 5
      backend/UnTube/urls.py
  20. 4 4
      backend/UnTube/wsgi.py
  21. 0 0
      backend/__init__.py
  22. 0 0
      backend/charts/__init__.py
  23. 0 0
      backend/charts/admin.py
  24. 1 1
      backend/charts/apps.py
  25. 0 0
      backend/charts/migrations/__init__.py
  26. 0 0
      backend/charts/models.py
  27. 0 0
      backend/charts/tests.py
  28. 1 1
      backend/charts/urls.py
  29. 0 0
      backend/charts/views.py
  30. 0 0
      backend/general/__init__.py
  31. 0 0
      backend/general/utils/__init__.py
  32. 20 0
      backend/general/utils/collections.py
  33. 20 0
      backend/general/utils/misc.py
  34. 17 0
      backend/general/utils/settings.py
  35. 0 0
      backend/main/__init__.py
  36. 0 0
      backend/main/admin.py
  37. 1 1
      backend/main/apps.py
  38. 0 0
      backend/main/migrations/0001_initial.py
  39. 0 0
      backend/main/migrations/0002_auto_20211204_0521.py
  40. 0 0
      backend/main/migrations/__init__.py
  41. 78 82
      backend/main/models.py
  42. 0 0
      backend/main/static/assets/imgs/dashboard.gif
  43. 0 0
      backend/main/static/assets/imgs/features.gif
  44. 0 0
      backend/main/static/assets/imgs/import.gif
  45. 0 0
      backend/main/static/assets/imgs/organize.gif
  46. 0 0
      backend/main/static/assets/imgs/playlist_stats.gif
  47. 0 0
      backend/main/static/assets/imgs/watching.gif
  48. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css
  49. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css.map
  50. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css
  51. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css.map
  52. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css
  53. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css.map
  54. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css
  55. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css.map
  56. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css
  57. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css.map
  58. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css
  59. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css.map
  60. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css
  61. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css.map
  62. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css
  63. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css.map
  64. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css
  65. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css.map
  66. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css
  67. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css.map
  68. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css
  69. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css.map
  70. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css
  71. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css.map
  72. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.css
  73. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.css.map
  74. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.min.css
  75. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.min.css.map
  76. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css
  77. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css.map
  78. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css
  79. 0 0
      backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css.map
  80. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js
  81. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js.map
  82. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js
  83. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js.map
  84. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js
  85. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js.map
  86. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js
  87. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js.map
  88. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.js
  89. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.js.map
  90. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.min.js
  91. 0 0
      backend/main/static/bootstrap5.0.1/js/bootstrap.min.js.map
  92. 0 0
      backend/main/static/choices.js/choices.min.css
  93. 0 0
      backend/main/static/choices.js/choices.min.js
  94. 0 0
      backend/main/static/clipboard.js/clipboard.min.js
  95. 0 0
      backend/main/static/css/carousel.css
  96. 0 0
      backend/main/static/fontawesome-free-5.15.3-web/LICENSE.txt
  97. 0 0
      backend/main/static/fontawesome-free-5.15.3-web/attribution.js
  98. 6 6
      backend/main/static/fontawesome-free-5.15.3-web/css/all.css
  99. 0 0
      backend/main/static/fontawesome-free-5.15.3-web/css/all.min.css
  100. 2 2
      backend/main/static/fontawesome-free-5.15.3-web/css/brands.css

+ 70 - 10
.gitignore

@@ -1,15 +1,75 @@
-/venv/
-.azure
-/UnTube/client_secrets.json
-/db.sqlite3
-/modules
-/.idea/
-__pycache__/
+# Backup files
+*.bak
+
+# Distribution / packaging
+*.egg
+*.egg-info/
+*.manifest
+*.spec
+.Python build/
+.eggs/
+.installed.cfg
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+
+# Django
 *.log
 *.log
 *.pot
 *.pot
 *.pyc
 *.pyc
-/static/
-.staticfiles
-.env
+/staticfiles
+__pycache__
+db.sqlite3
+media
+
+# Environments
+.venv
+/.env
+/local
+ENV/
+env.bak/
+env/
+venv
+venv.bak/
+venv/
+
+# IDEs
+.DS_Store
+.idea
+.vscode
+
+# Installer logs
+pip-delete-this-directory.txt
+pip-log.txt
+
+# mypy
+.mypy_cache
+
+# pyenv
+.python-version
+
+# pytest
+.pytest_cache
 
 
+# Python
+*$py.class
+*.py[cod]
 
 
+# Unit test / coverage reports
+*.cover
+.cache
+.coverage
+.coverage.*
+.hypothesis/
+.pytest_cache/
+.tox/
+coverage.xml
+htmlcov/
+nosetests.xml

+ 31 - 0
Makefile

@@ -0,0 +1,31 @@
+.PHONY: install
+install:
+	poetry install
+
+.PHONY: migrations
+migrations:
+	poetry run python3 -m backend.manage makemigrations
+
+.PHONY: migrate
+migrate:
+	poetry run python3 -m backend.manage migrate
+
+.PHONY: run-server
+run-server:
+	poetry run python3 -m backend.manage runserver
+
+.PHONY: shell
+shell:
+	poetry run python -m backend.manage shell
+
+.PHONY: superuser
+superuser:
+	poetry run python3 -m backend.manage createsuperuser
+
+.PHONY: update
+update: install migrate ;
+
+.PHONY: local-settings
+local-settings:
+	mkdir -p local
+	cp ./backend/UnTube/settings/templates/settings.dev.py ./local/settings.dev.py

+ 0 - 50
UnTube/production.py

@@ -1,50 +0,0 @@
-from .settings import *
-import os
-
-SECRET_KEY = os.environ['SECRET_KEY']
-YOUTUBE_V3_API_KEY = os.environ['YOUTUBE_V3_API_KEY']
-
-# configure the domain name using the environment variable found on pythonanywhere
-ALLOWED_HOSTS = ['bakaabu.pythonanywhere.com', '127.0.0.1', 'untube.it'] if 'UNTUBE' in os.environ else ['bakaabu.pythonanywhere.com', 'untube.it']
-SITE_ID = 10
-
-DEBUG = False
-CSRF_COOKIE_SECURE = True
-SESSION_COOKIE_SECURE = True
-SECURE_SSL_REDIRECT = True
-
-# WhiteNoise configuration
-MIDDLEWARE = [
-    'django.middleware.security.SecurityMiddleware',
-    # Add whitenoise middleware after the security middleware
-    'whitenoise.middleware.WhiteNoiseMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-]
-
-STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
-
-# DBHOST is only the server name
-hostname = os.environ['DBHOST']
-
-# Configure MySQL database on pythonanywhere
-# See https://django-mysql.readthedocs.io/en/latest/checks.html for options
-DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.mysql',
-        'NAME': f'{os.environ["DBUSER"]}${os.environ["DBNAME"]}',
-        'USER': f'{os.environ["DBUSER"]}',
-        'PASSWORD': f'{os.environ["DBPASS"]}',
-        'HOST': hostname,
-        'OPTIONS': {
-            'init_command': "SET sql_mode='STRICT_TRANS_TABLES', innodb_strict_mode=1",
-            'charset': 'utf8mb4',
-            "autocommit": True,
-        }
-    }
-}

+ 0 - 6
UnTube/secrets.py

@@ -1,6 +0,0 @@
-# Make sure you change these before production or to run this project on your own machine ;)
-SECRETS = {"SECRET_KEY": 'django-insecure-ycs22y+20sq67y(6dm6ynqw=dlhg!)%vuqpd@$p6rf3!#1h$u=',
-           "YOUTUBE_V3_API_KEY": 'AIzaSyCBOucAIJ5PdLeqzTfkTQ_6twsjNaMecS8',
-           "GOOGLE_OAUTH_CLIENT_ID": "901333803283-1lscbdmukcjj3qp0t3relmla63h6l9k6.apps.googleusercontent.com",
-           "GOOGLE_OAUTH_CLIENT_SECRET": "ekdBniL-_mAnNPwCmugfIL2q",
-           "GOOGLE_OAUTH_SCOPES": ['https://www.googleapis.com/auth/youtube']}

+ 0 - 5
apps/main/static/fontawesome-free-5.15.3-web/css/brands.min.css

@@ -1,5 +0,0 @@
-/*!
- * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- */
-@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400}

+ 0 - 5
apps/main/static/fontawesome-free-5.15.3-web/css/regular.min.css

@@ -1,5 +0,0 @@
-/*!
- * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- */
-@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400}

+ 0 - 5
apps/main/static/fontawesome-free-5.15.3-web/css/solid.min.css

@@ -1,5 +0,0 @@
-/*!
- * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com
- * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- */
-@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}

+ 0 - 3
apps/users/admin.py

@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.

+ 0 - 0
UnTube/__init__.py → backend/UnTube/__init__.py


+ 4 - 4
UnTube/asgi.py → backend/UnTube/asgi.py

@@ -8,14 +8,14 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
 """
 """
 
 
 import os
 import os
-from dotenv import load_dotenv
+# from dotenv import load_dotenv
 from django.core.asgi import get_asgi_application
 from django.core.asgi import get_asgi_application
 
 
-settings_module = "UnTube.production" if 'UNTUBE' in os.environ else 'UnTube.settings'
+settings_module = 'backend.UnTube.settings'
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
 
 
 # to use env variables on pythonanywhere
 # to use env variables on pythonanywhere
-project_folder = os.path.expanduser('/home/bakaabu')
-load_dotenv(os.path.join(project_folder, '.env'))
+# project_folder = os.path.expanduser('/home/bakaabu')
+# load_dotenv(os.path.join(project_folder, '.env'))
 
 
 application = get_asgi_application()
 application = get_asgi_application()

+ 33 - 0
backend/UnTube/settings/__init__.py

@@ -0,0 +1,33 @@
+import os
+from pathlib import Path
+from split_settings.tools import include, optional
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
+# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Namespacing our own custom environment variables
+ENVVAR_SETTINGS_PREFIX = 'UNTUBE_SETTINGS_'
+
+LOCAL_SETTINGS_PATH = os.getenv(f'{ENVVAR_SETTINGS_PREFIX}LOCAL_SETTINGS_PATH')
+
+# if local settings not specified in the environment
+if not LOCAL_SETTINGS_PATH:  # default to development mode - use local dev settings
+    LOCAL_SETTINGS_PATH = 'local/settings.dev.py'
+
+if not os.path.isabs(LOCAL_SETTINGS_PATH):  # always make sure to have the absolute path
+    LOCAL_SETTINGS_PATH = str(BASE_DIR / LOCAL_SETTINGS_PATH)
+
+include(
+    'base.py',
+    # 'logging.py',
+    # 'rest_framework.py',
+    # 'channels.py',
+    # 'aws.py',
+    'custom.py',
+    'allauth.py',
+    optional(LOCAL_SETTINGS_PATH),  # `optional` means the file may or may not exist - it is fine if it does not
+    'envvars.py',
+    'docker.py',
+    'pythonanywhere.py',
+)

+ 34 - 0
backend/UnTube/settings/allauth.py

@@ -0,0 +1,34 @@
+"""
+Django AllAuth package related settings
+"""
+
+SITE_ID = 2  # increment/decrement site ID as necessary
+LOGIN_REDIRECT_URL = '/home/'
+LOGOUT_REDIRECT_URL = '/'
+
+# Additional configuration settings
+# ACCOUNT_LOGOUT_ON_GET= True
+SOCIALACCOUNT_LOGIN_ON_GET = True
+SOCIALACCOUNT_STORE_TOKENS = True
+ACCOUNT_UNIQUE_EMAIL = True
+ACCOUNT_EMAIL_REQUIRED = True
+
+SOCIALACCOUNT_PROVIDERS = {
+    'google': {
+        # 'APP': {
+        #     'client_id': GOOGLE_OAUTH_CLIENT_ID,  # type: ignore
+        #     'secret': GOOGLE_OAUTH_CLIENT_SECRET,  # type: ignore
+        #     'key': ''
+        # },
+        # 'OAUTH_PKCE_ENABLED': True,  # valid in allauth ver > 0.47.0
+        'SCOPE': [
+            'profile',
+            'email',
+            'https://www.googleapis.com/auth/youtube',
+        ],
+        'AUTH_PARAMS': {
+            # To refresh authentication in the background, set AUTH_PARAMS['access_type'] to offline.
+            'access_type': 'offline',
+        }
+    }
+}

+ 23 - 61
UnTube/settings.py → backend/UnTube/settings/base.py

@@ -9,25 +9,8 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
 For the full list of settings and their values, see
 For the full list of settings and their values, see
 https://docs.djangoproject.com/en/3.2/ref/settings/
 https://docs.djangoproject.com/en/3.2/ref/settings/
 """
 """
-import os
-from pathlib import Path
-from UnTube.secrets import SECRETS
-
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
-
-# PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
-BASE_DIR = Path(__file__).resolve().parent.parent
-# BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-
-# Quick-start development settings - unsuitable for production
-# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
-
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = SECRETS['SECRET_KEY']
-YOUTUBE_V3_API_KEY = SECRETS['YOUTUBE_V3_API_KEY']
-
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = False
+SECRET_KEY = NotImplemented
 
 
 ALLOWED_HOSTS = ['127.0.0.1']
 ALLOWED_HOSTS = ['127.0.0.1']
 
 
@@ -39,25 +22,23 @@ INSTALLED_APPS = [
     'django.contrib.sessions',
     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django.contrib.staticfiles',
-
     'django.contrib.humanize',  # A set of Django template filters useful for adding a “human touch” to data.
     'django.contrib.humanize',  # A set of Django template filters useful for adding a “human touch” to data.
-
     'django.contrib.sites',
     'django.contrib.sites',
+
+    'crispy_forms',
+    'import_export',
     'allauth',
     'allauth',
     'allauth.account',
     'allauth.account',
     'allauth.socialaccount',
     'allauth.socialaccount',
     'allauth.socialaccount.providers.google',  # specifies google as OAuth provider
     'allauth.socialaccount.providers.google',  # specifies google as OAuth provider
 
 
-    'crispy_forms',
-    'apps.users',  # has stuff related to user management in it (login, signup, show homepage, import)
-    'apps.main',  # main app, shows user their homepage
-    'apps.manage_playlists',
-    'apps.charts',
-    'apps.search',
+    'backend.users.apps.UsersConfig',  # has stuff related to user management in it (login, signup, show homepage, import)
+    'backend.main.apps.MainConfig',  # main app, shows user their homepage
+    'backend.manage_playlists.apps.ManagePlaylistsConfig',
+    'backend.charts.apps.ChartsConfig',
+    'backend.search.apps.SearchConfig',
 ]
 ]
 
 
-CRISPY_TEMPLATE_PACK = 'bootstrap4'
-
 MIDDLEWARE = [
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -66,15 +47,15 @@ MIDDLEWARE = [
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
-
 ]
 ]
 
 
-ROOT_URLCONF = 'UnTube.urls'
+ROOT_URLCONF = 'backend.UnTube.urls'  # path to the urls.py file in root UnTube app folder
 
 
 TEMPLATES = [
 TEMPLATES = [
     {
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [BASE_DIR / 'templates'],
+        'DIRS': [BASE_DIR / 'backend' / 'templates'],  # type: ignore
+        # 'DIRS': [BASE_DIR / 'backend', BASE_DIR / 'backend' / 'templates'],
         # 'DIRS': [os.path.join(BASE_DIR, "templates")],
         # 'DIRS': [os.path.join(BASE_DIR, "templates")],
         'APP_DIRS': True,
         'APP_DIRS': True,
         'OPTIONS': {
         'OPTIONS': {
@@ -93,44 +74,19 @@ AUTHENTICATION_BACKENDS = [
     'allauth.account.auth_backends.AuthenticationBackend'
     'allauth.account.auth_backends.AuthenticationBackend'
 ]
 ]
 
 
-SOCIALACCOUNT_PROVIDERS = {
-    'google': {
-        'SCOPE': [
-            'profile',
-            'email',
-            'https://www.googleapis.com/auth/youtube',
-        ],
-        'AUTH_PARAMS': {
-            # To refresh authentication in the background, set AUTH_PARAMS['access_type'] to offline.
-            'access_type': 'offline',
-        }
-    }
-}
-
-SITE_ID = 10
-
 LOGIN_URL = '/'
 LOGIN_URL = '/'
 
 
-LOGIN_REDIRECT_URL = '/home/'
-LOGOUT_REDIRECT_URL = '/home/'
-
-WSGI_APPLICATION = 'UnTube.wsgi.application'
+WSGI_APPLICATION = 'backend.UnTube.wsgi.application'  # path to the wsgi.py file in root UnTube app folder
 
 
 # Database
 # Database
 # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
 # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
 DATABASES = {
 DATABASES = {
     'default': {
     'default': {
         'ENGINE': 'django.db.backends.sqlite3',
         'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': BASE_DIR / 'db.sqlite3',
+        'NAME': BASE_DIR / 'db.sqlite3',  # type: ignore
     }
     }
 }
 }
 
 
-# DATABASES = {}
-# DATABASES['default'] = dj_database_url.config(conn_max_age=600)
-
-# DATABASE_URL = os.environ['DATABASE_URL']
-# conn = psycopg2.connect(DATABASE_URL, sslmode='require')
-
 # Password validation
 # Password validation
 # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
 # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
 AUTH_PASSWORD_VALIDATORS = [
 AUTH_PASSWORD_VALIDATORS = [
@@ -163,12 +119,18 @@ USE_TZ = True
 # Static files (CSS, JavaScript, Images)
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/3.2/howto/static-files/
 # https://docs.djangoproject.com/en/3.2/howto/static-files/
 STATIC_URL = '/static/'
 STATIC_URL = '/static/'
-STATIC_ROOT = os.path.join(BASE_DIR, 'static')
+STATIC_ROOT = BASE_DIR / 'static'  # type: ignore
 # STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
 # STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+# STATICFILES_DIRS = (
+#     BASE_DIR / 'backend' / 'main',  # type: ignore
+# )
+STATICFILES_DIRS = (
+    os.path.join(BASE_DIR, 'backend', 'main', 'static'),  # type: ignore
+)
 
 
 # Default primary key field type
 # Default primary key field type
 # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
 # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 
 
 MEDIA_URL = '/media/'
 MEDIA_URL = '/media/'
-MEDIA_ROOT = Path(BASE_DIR / 'media')
+MEDIA_ROOT = BASE_DIR / 'media'  # type: ignore

+ 17 - 0
backend/UnTube/settings/custom.py

@@ -0,0 +1,17 @@
+"""
+Settings specific to this application only (no Django or third party settings)
+"""
+
+YOUTUBE_V3_API_KEY = NotImplemented
+GOOGLE_OAUTH_URI = NotImplemented
+GOOGLE_OAUTH_CLIENT_ID = NotImplemented
+GOOGLE_OAUTH_CLIENT_SECRET = NotImplemented
+CRISPY_TEMPLATE_PACK = 'bootstrap4'
+
+# hosting environments
+IN_PYTHONANYWHERE = False  # PA has PYTHONANYWHERE_SITE in its env
+IN_DOCKER = False
+
+ENABLE_PRINT_STATEMENTS = False  # runs the custom print_() statement that will log outputs to terminal
+STOKEN_EXPIRATION_SECONDS = 10
+USE_ON_COMMIT_HOOK = True

+ 12 - 0
backend/UnTube/settings/docker.py

@@ -0,0 +1,12 @@
+import os
+
+if IN_DOCKER or os.path.isfile('/.dockerenv'):  # type: ignore # noqa: F821
+    # We need it to serve static files with DEBUG=False
+    assert MIDDLEWARE[:1] == [  # type: ignore # noqa: F821
+        'django.middleware.security.SecurityMiddleware'
+    ]
+    MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')  # type: ignore # noqa: F821
+    DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+    STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
+
+    print("Using Docker settings...")

+ 14 - 0
backend/UnTube/settings/envvars.py

@@ -0,0 +1,14 @@
+from backend.general.utils.collections import deep_update
+from backend.general.utils.settings import get_settings_from_environment
+
+"""
+This takes env variables with a matching prefix (set by you), strips out the prefix, and adds it to globals
+
+Eg. 
+export UNTUBE_SETTINGS_IN_DOCKER=true (environment variable)
+
+could be then referenced in the globals() dictionary as
+IN_DOCKER (where the value will be set to Pythonic True)
+"""
+# globals() is a dictionary of global variables
+deep_update(globals(), get_settings_from_environment(ENVVAR_SETTINGS_PREFIX))  # type: ignore

+ 48 - 0
backend/UnTube/settings/pythonanywhere.py

@@ -0,0 +1,48 @@
+if IN_PYTHONANYWHERE:  # type: ignore
+    # to use env variables on pythonanywhere
+    # from dotenv import load_dotenv
+    # project_folder = os.path.expanduser('/home/bakaabu')
+    # load_dotenv(os.path.join(project_folder, '.env'))
+    GOOGLE_OAUTH_URI = os.environ['GOOGLE_OAUTH_URI']  # type: ignore #  "bakaabu.pythonanywhere.com"
+    SECRET_KEY = os.environ['SECRET_KEY']  # type: ignore
+    YOUTUBE_V3_API_KEY = os.environ['YOUTUBE_V3_API_KEY']   # type: ignore
+
+    # WhiteNoise configuration
+    assert MIDDLEWARE[:1] == [  # type: ignore # noqa: F821
+        'django.middleware.security.SecurityMiddleware'
+    ] and not IN_DOCKER  # type: ignore # PA does not support dockerized apps
+    # Add whitenoise middleware after the security middleware
+    MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')  # type: ignore # noqa: F821
+
+    # configure the domain name using the environment variable found on pythonanywhere
+    ALLOWED_HOSTS = ['bakaabu.pythonanywhere.com', '127.0.0.1', 'untube.it']
+    SITE_ID = 10
+
+    CSRF_COOKIE_SECURE = True
+    SESSION_COOKIE_SECURE = True
+    SECURE_SSL_REDIRECT = True
+
+    STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
+    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')  # type: ignore
+
+    # DBHOST is only the server name
+    hostname = os.environ['DBHOST']  # type: ignore
+
+    # Configure MySQL database on pythonanywhere
+    # See https://django-mysql.readthedocs.io/en/latest/checks.html for options
+    DATABASES = {
+        'default': {
+            'ENGINE': 'django.db.backends.mysql',
+            'NAME': f'{os.environ["DBUSER"]}${os.environ["DBNAME"]}',  # type: ignore
+            'USER': f'{os.environ["DBUSER"]}',  # type: ignore
+            'PASSWORD': f'{os.environ["DBPASS"]}',  # type: ignore
+            'HOST': hostname,
+            'OPTIONS': {
+                'init_command': "SET sql_mode='STRICT_TRANS_TABLES', innodb_strict_mode=1",
+                'charset': 'utf8mb4',
+                "autocommit": True,
+            }
+        }
+    }
+
+    print("Using Pythonanywhere settings...")

+ 11 - 0
backend/UnTube/settings/templates/settings.dev.py

@@ -0,0 +1,11 @@
+DEBUG = True
+SECRET_KEY = "django-insecure-ycs22y+20sq67y(6dm6ynqw=dlhg!)%vuqpd@$p6rf3!#1h$u="
+ENABLE_PRINT_STATEMENTS = False
+
+SITE_ID = 2  # increment/decrement site ID as necessary
+
+# please fill these in with your own Google OAuth credentials for the app to run properly!
+YOUTUBE_V3_API_KEY = NotImplemented
+GOOGLE_OAUTH_URI = "127.0.0.1:8000"
+GOOGLE_OAUTH_CLIENT_ID = NotImplemented
+GOOGLE_OAUTH_CLIENT_SECRET = NotImplemented

+ 6 - 5
UnTube/urls.py → backend/UnTube/urls.py

@@ -18,9 +18,10 @@ from django.urls import path, include
 
 
 urlpatterns = [
 urlpatterns = [
     path('admin/', admin.site.urls),
     path('admin/', admin.site.urls),
-    path('', include("apps.users.urls")),
-    path('', include("apps.main.urls")),
-    path('manage/', include("apps.manage_playlists.urls")),
-    path('search/', include("apps.search.urls")),
-    path('charts/', include("apps.charts.urls")),
+    path('accounts/', include('allauth.urls')),
+    path('', include("backend.users.urls")),
+    path('', include("backend.main.urls")),
+    path('manage/', include("backend.manage_playlists.urls")),
+    path('search/', include("backend.search.urls")),
+    path('charts/', include("backend.charts.urls")),
 ]
 ]

+ 4 - 4
UnTube/wsgi.py → backend/UnTube/wsgi.py

@@ -8,14 +8,14 @@ https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
 """
 """
 
 
 import os
 import os
-from dotenv import load_dotenv
 from django.core.wsgi import get_wsgi_application
 from django.core.wsgi import get_wsgi_application
 
 
-settings_module = "UnTube.production" if 'UNTUBE' in os.environ else 'UnTube.settings'
+settings_module = 'backend.UnTube.settings'
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
 os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module)
 
 
 # to use env variables on pythonanywhere
 # to use env variables on pythonanywhere
-project_folder = os.path.expanduser('/home/bakaabu')
-load_dotenv(os.path.join(project_folder, '.env'))
+# from dotenv import load_dotenv
+# project_folder = os.path.expanduser('/home/bakaabu')
+# load_dotenv(os.path.join(project_folder, '.env'))
 
 
 application = get_wsgi_application()
 application = get_wsgi_application()

+ 0 - 0
apps/__init__.py → backend/__init__.py


+ 0 - 0
apps/charts/__init__.py → backend/charts/__init__.py


+ 0 - 0
apps/charts/admin.py → backend/charts/admin.py


+ 1 - 1
apps/charts/apps.py → backend/charts/apps.py

@@ -3,4 +3,4 @@ from django.apps import AppConfig
 
 
 class ChartsConfig(AppConfig):
 class ChartsConfig(AppConfig):
     default_auto_field = 'django.db.models.BigAutoField'
     default_auto_field = 'django.db.models.BigAutoField'
-    name = 'apps.charts'
+    name = 'backend.charts'

+ 0 - 0
apps/charts/migrations/__init__.py → backend/charts/migrations/__init__.py


+ 0 - 0
apps/charts/models.py → backend/charts/models.py


+ 0 - 0
apps/charts/tests.py → backend/charts/tests.py


+ 1 - 1
apps/charts/urls.py → backend/charts/urls.py

@@ -1,5 +1,5 @@
 from django.urls import path
 from django.urls import path
-from apps.charts import views
+from backend.charts import views
 
 
 urlpatterns = [
 urlpatterns = [
     path('channel-videos-distribution/<slug:playlist_id>', views.channel_videos_distribution, name='channel_videos_distribution'),
     path('channel-videos-distribution/<slug:playlist_id>', views.channel_videos_distribution, name='channel_videos_distribution'),

+ 0 - 0
apps/charts/views.py → backend/charts/views.py


+ 0 - 0
apps/main/__init__.py → backend/general/__init__.py


+ 0 - 0
apps/main/migrations/__init__.py → backend/general/utils/__init__.py


+ 20 - 0
backend/general/utils/collections.py

@@ -0,0 +1,20 @@
+def deep_update(base_dict, update_with):
+    """
+    Updates the base_dict (eg. settings/base.py settings) with the values present
+    in the update_with dict
+    """
+    # iterate over items in the update_with dict
+    for key, value in update_with.items():
+        if isinstance(value, dict):  # update_with dict value is a dict, i.e. update_with[key] = {}
+            base_dict_value = base_dict.get(key)
+
+            # check if base_dict value is also a dict
+            if isinstance(base_dict_value, dict):  # check if base_dict[key] = {}
+                deep_update(base_dict_value, value)  # recurse
+            else:
+                base_dict[key] = value  # else update the base dict with whatever dict value in update_with
+        else:
+            base_dict[key] = value
+
+    # return the updated base_dict
+    return base_dict

+ 20 - 0
backend/general/utils/misc.py

@@ -0,0 +1,20 @@
+import yaml
+from django.conf import settings
+
+
+def yaml_coerce(value):
+    """
+    Pass in a string dictionary and convert it into proper python dictionary
+    """
+    if isinstance(value, str):
+        # create a tiny snippet of YAML, with key=dummy, and val=value, then load the YAML into
+        # a pure Pythonic dictionary. Then, we read off the dummy key from the coversion to get our
+        # final result
+        return yaml.load(f'dummy: {value}', Loader=yaml.SafeLoader)['dummy']
+
+    return value
+
+
+def print_(*args, **kwargs):
+    if settings.ENABLE_PRINT_STATEMENTS:
+        print(*args, **kwargs)

+ 17 - 0
backend/general/utils/settings.py

@@ -0,0 +1,17 @@
+import os
+from .misc import yaml_coerce
+
+
+def get_settings_from_environment(prefix):
+    """
+    Django settings specific to the environment (eg. production) will be stored as environment variables
+    prefixed with "PREFIX_", eg. prefix="UNTUBESETTINGS_"
+    E.G. environment variables like UNTUBESETTINGS_SECRET_KEY=123, UNTUBESETTINGS_DATABASE="{'DB': {'NAME': 'db'}}"
+    will be converted to pure Python dictionary with the prefix "UNTUBESETTINGS_" removed from the keys
+    {
+       "SECRET_KEY": 123,
+       "DB": {'NAME': 'db'}
+    }
+    """
+    prefix_len = len(prefix)
+    return {key[prefix_len:]: yaml_coerce(value) for key, value in os.environ.items() if key.startswith(prefix)}

+ 0 - 0
apps/manage_playlists/__init__.py → backend/main/__init__.py


+ 0 - 0
apps/main/admin.py → backend/main/admin.py


+ 1 - 1
apps/main/apps.py → backend/main/apps.py

@@ -3,4 +3,4 @@ from django.apps import AppConfig
 
 
 class MainConfig(AppConfig):
 class MainConfig(AppConfig):
     default_auto_field = 'django.db.models.BigAutoField'
     default_auto_field = 'django.db.models.BigAutoField'
-    name = 'apps.main'
+    name = 'backend.main'

+ 0 - 0
apps/main/migrations/0001_initial.py → backend/main/migrations/0001_initial.py


+ 0 - 0
apps/main/migrations/0002_auto_20211204_0521.py → backend/main/migrations/0002_auto_20211204_0521.py


+ 0 - 0
apps/manage_playlists/migrations/__init__.py → backend/main/migrations/__init__.py


+ 78 - 82
apps/main/models.py → backend/main/models.py

@@ -1,19 +1,11 @@
-import datetime
-
-import requests
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from allauth.socialaccount.models import SocialAccount, SocialApp, SocialToken
-from apps.users.models import Profile
 from .util import *
 from .util import *
 import pytz
 import pytz
-from UnTube.secrets import SECRETS
 from django.db import models
 from django.db import models
-from google.oauth2.credentials import Credentials
-from google.auth.transport.requests import Request
-from datetime import timedelta
 from googleapiclient.discovery import build
 from googleapiclient.discovery import build
 import googleapiclient.errors
 import googleapiclient.errors
 from django.db.models import Q, Sum
 from django.db.models import Q, Sum
+from ..general.utils.misc import print_
 
 
 
 
 def get_message_from_httperror(e):
 def get_message_from_httperror(e):
@@ -21,24 +13,24 @@ def get_message_from_httperror(e):
 
 
 
 
 class PlaylistManager(models.Manager):
 class PlaylistManager(models.Manager):
-    def getCredentials(self, user):
-        app = SocialApp.objects.get(provider='google')
-        credentials = Credentials(
-            token=user.profile.access_token,
-            refresh_token=user.profile.refresh_token,
-            token_uri="https://oauth2.googleapis.com/token",
-            client_id=app.client_id,
-            client_secret=app.secret,
-            scopes=['https://www.googleapis.com/auth/youtube']
-        )
-
-        if not credentials.valid:
-            credentials.refresh(Request())
-            user.profile.access_token = credentials.token
-            user.profile.refresh_token = credentials.refresh_token
-            user.save()
-
-        return credentials
+    # def getCredentials(self, user):
+    #     app = SocialApp.objects.get(provider='google')
+    #     credentials = Credentials(
+    #         token=user.profile.access_token,
+    #         refresh_token=user.profile.refresh_token,
+    #         token_uri="https://oauth2.googleapis.com/token",
+    #         client_id=app.client_id,
+    #         client_secret=app.secret,
+    #         scopes=['https://www.googleapis.com/auth/youtube']
+    #     )
+    #
+    #     if not credentials.valid:
+    #         credentials.refresh(Request())
+    #         user.profile.access_token = credentials.token
+    #         user.profile.refresh_token = credentials.refresh_token
+    #         user.save()
+    #
+    #     return credentials
 
 
     def getPlaylistId(self, playlist_link):
     def getPlaylistId(self, playlist_link):
         if "?" not in playlist_link:
         if "?" not in playlist_link:
@@ -53,7 +45,11 @@ class PlaylistManager(models.Manager):
     # Used to check if the user has a vaild YouTube channel
     # Used to check if the user has a vaild YouTube channel
     # Will return -1 if user does not have a YouTube channel
     # Will return -1 if user does not have a YouTube channel
     def getUserYTChannelID(self, user):
     def getUserYTChannelID(self, user):
-        credentials = self.getCredentials(user)
+        user_profile = user.profile
+        if user_profile.yt_channel_id != "":
+            return 0
+
+        credentials = user_profile.get_credentials()
 
 
         with build('youtube', 'v3', credentials=credentials) as youtube:
         with build('youtube', 'v3', credentials=credentials) as youtube:
             pl_request = youtube.channels().list(
             pl_request = youtube.channels().list(
@@ -63,14 +59,14 @@ class PlaylistManager(models.Manager):
 
 
             pl_response = pl_request.execute()
             pl_response = pl_request.execute()
 
 
-            print(pl_response)
+            print_(pl_response)
 
 
             if pl_response['pageInfo']['totalResults'] == 0:
             if pl_response['pageInfo']['totalResults'] == 0:
-                print("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
+                print_("Looks like do not have a channel on youtube. Create one to import all of your playlists. Retry?")
                 return -1
                 return -1
             else:
             else:
-                user.profile.yt_channel_id = pl_response['items'][0]['id']
-                user.save()
+                user_profile.yt_channel_id = pl_response['items'][0]['id']
+                user_profile.save(update_fields=['yt_channel_id'])
 
 
         return 0
         return 0
 
 
@@ -91,7 +87,7 @@ class PlaylistManager(models.Manager):
                   "error_message": "",
                   "error_message": "",
                   "playlist_ids": []}
                   "playlist_ids": []}
 
 
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
 
         playlist_ids = []
         playlist_ids = []
         with build('youtube', 'v3', credentials=credentials) as youtube:
         with build('youtube', 'v3', credentials=credentials) as youtube:
@@ -102,7 +98,7 @@ class PlaylistManager(models.Manager):
                     maxResults=50
                     maxResults=50
                 )
                 )
             else:
             else:
-                print("GETTING ALL USER AUTH PLAYLISTS")
+                print_("GETTING ALL USER AUTH PLAYLISTS")
                 pl_request = youtube.playlists().list(
                 pl_request = youtube.playlists().list(
                     part='contentDetails, snippet, id, player, status',
                     part='contentDetails, snippet, id, player, status',
                     mine=True,  # get playlist details for this playlist id
                     mine=True,  # get playlist details for this playlist id
@@ -113,15 +109,15 @@ class PlaylistManager(models.Manager):
             try:
             try:
                 pl_response = pl_request.execute()
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:
             except googleapiclient.errors.HttpError as e:
-                print("YouTube channel not found if mine=True")
-                print("YouTube playlist not found if id=playlist_id")
+                print_("YouTube channel not found if mine=True")
+                print_("YouTube playlist not found if id=playlist_id")
                 result["status"] = -1
                 result["status"] = -1
                 result["error_message"] = get_message_from_httperror(e)
                 result["error_message"] = get_message_from_httperror(e)
                 return result
                 return result
 
 
-            print(pl_response)
+            print_(pl_response)
             if pl_response["pageInfo"]["totalResults"] == 0:
             if pl_response["pageInfo"]["totalResults"] == 0:
-                print("No playlists created yet on youtube.")
+                print_("No playlists created yet on youtube.")
                 result["status"] = -2
                 result["status"] = -2
                 return result
                 return result
 
 
@@ -150,7 +146,7 @@ class PlaylistManager(models.Manager):
             # check if this playlist already exists in user's untube collection
             # check if this playlist already exists in user's untube collection
             if user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=True)).exists():
             if user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=True)).exists():
                 playlist = user.playlists.get(playlist_id=playlist_id)
                 playlist = user.playlists.get(playlist_id=playlist_id)
-                print(f"PLAYLIST {playlist.name} ({playlist_id}) ALREADY EXISTS IN DB")
+                print_(f"PLAYLIST {playlist.name} ({playlist_id}) ALREADY EXISTS IN DB")
 
 
                 # POSSIBLE CASES:
                 # POSSIBLE CASES:
                 # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
                 # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
@@ -164,7 +160,7 @@ class PlaylistManager(models.Manager):
                     result["status"] = -3
                     result["status"] = -3
                     return result
                     return result
             else:  # no such playlist in database
             else:  # no such playlist in database
-                print(f"CREATING {item['snippet']['title']} ({playlist_id})")
+                print_(f"CREATING {item['snippet']['title']} ({playlist_id})")
                 if user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=False)).exists():
                 if user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=False)).exists():
                     unimported_playlist = user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=False)).first()
                     unimported_playlist = user.playlists.filter(Q(playlist_id=playlist_id) & Q(is_in_db=False)).first()
                     unimported_playlist.delete()
                     unimported_playlist.delete()
@@ -196,7 +192,7 @@ class PlaylistManager(models.Manager):
         return result
         return result
 
 
     def getAllVideosForPlaylist(self, user, playlist_id):
     def getAllVideosForPlaylist(self, user, playlist_id):
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
 
         playlist = user.playlists.get(playlist_id=playlist_id)
         playlist = user.playlists.get(playlist_id=playlist_id)
 
 
@@ -265,7 +261,7 @@ class PlaylistManager(models.Manager):
 
 
                 else:  # video found in user's db
                 else:  # video found in user's db
                     if playlist.playlist_items.filter(playlist_item_id=playlist_item_id).exists():
                     if playlist.playlist_items.filter(playlist_item_id=playlist_item_id).exists():
-                        print("PLAYLIST ITEM ALREADY EXISTS")
+                        print_("PLAYLIST ITEM ALREADY EXISTS")
                         continue
                         continue
 
 
                     video = user.videos.get(video_id=video_id)
                     video = user.videos.get(video_id=video_id)
@@ -463,14 +459,14 @@ class PlaylistManager(models.Manager):
         If full_scan is False, only the playlist count difference on YT and UT is checked on every visit
         If full_scan is False, only the playlist count difference on YT and UT is checked on every visit
         to the playlist page. This is done everytime.
         to the playlist page. This is done everytime.
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
 
         playlist = user.playlists.get(playlist_id=pl_id)
         playlist = user.playlists.get(playlist_id=pl_id)
 
 
         # if its been a week since the last full scan, do a full playlist scan
         # if its been a week since the last full scan, do a full playlist scan
         # basically checks all the playlist video for any updates
         # basically checks all the playlist video for any updates
         if playlist.last_full_scan_at + datetime.timedelta(minutes=1) < datetime.datetime.now(pytz.utc):
         if playlist.last_full_scan_at + datetime.timedelta(minutes=1) < datetime.datetime.now(pytz.utc):
-            print("DOING A FULL SCAN")
+            print_("DOING A FULL SCAN")
             current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in
             current_playlist_item_ids = [playlist_item.playlist_item_id for playlist_item in
                                          playlist.playlist_items.all()]
                                          playlist.playlist_items.all()]
 
 
@@ -551,10 +547,10 @@ class PlaylistManager(models.Manager):
 
 
             return [1, deleted_videos, unavailable_videos, added_videos]
             return [1, deleted_videos, unavailable_videos, added_videos]
         else:
         else:
-            print("YOU CAN DO A FULL SCAN AGAIN IN",
+            print_("YOU CAN DO A FULL SCAN AGAIN IN",
                   str(datetime.datetime.now(pytz.utc) - (playlist.last_full_scan_at + datetime.timedelta(minutes=1))))
                   str(datetime.datetime.now(pytz.utc) - (playlist.last_full_scan_at + datetime.timedelta(minutes=1))))
         """
         """
-        print("DOING A SMOL SCAN")
+        print_("DOING A SMOL SCAN")
 
 
         with build('youtube', 'v3', credentials=credentials) as youtube:
         with build('youtube', 'v3', credentials=credentials) as youtube:
             pl_request = youtube.playlists().list(
             pl_request = youtube.playlists().list(
@@ -567,11 +563,11 @@ class PlaylistManager(models.Manager):
             try:
             try:
                 pl_response = pl_request.execute()
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError:
             except googleapiclient.errors.HttpError:
-                print("YouTube channel not found if mine=True")
-                print("YouTube playlist not found if id=playlist_id")
+                print_("YouTube channel not found if mine=True")
+                print_("YouTube playlist not found if id=playlist_id")
                 return -1
                 return -1
 
 
-            print("PLAYLIST", pl_response)
+            print_("PLAYLIST", pl_response)
 
 
             playlist_items = []
             playlist_items = []
 
 
@@ -593,7 +589,7 @@ class PlaylistManager(models.Manager):
             # check if this playlist already exists in database
             # check if this playlist already exists in database
             if user.playlists.filter(playlist_id=playlist_id).exists():
             if user.playlists.filter(playlist_id=playlist_id).exists():
                 playlist = user.playlists.get(playlist_id__exact=playlist_id)
                 playlist = user.playlists.get(playlist_id__exact=playlist_id)
-                print(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
+                print_(f"PLAYLIST {playlist.name} ALREADY EXISTS IN DB")
 
 
                 # POSSIBLE CASES:
                 # POSSIBLE CASES:
                 # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
                 # 1. PLAYLIST HAS DUPLICATE VIDEOS, DELETED VIDS, UNAVAILABLE VIDS
@@ -608,7 +604,7 @@ class PlaylistManager(models.Manager):
         return [0, "no change"]
         return [0, "no change"]
 
 
     def updatePlaylist(self, user, playlist_id):
     def updatePlaylist(self, user, playlist_id):
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
 
         playlist = user.playlists.get(playlist_id__exact=playlist_id)
         playlist = user.playlists.get(playlist_id__exact=playlist_id)
 
 
@@ -632,10 +628,10 @@ class PlaylistManager(models.Manager):
             try:
             try:
                 pl_response = pl_request.execute()
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError:
             except googleapiclient.errors.HttpError:
-                print("Playist was deleted on YouTube")
+                print_("Playist was deleted on YouTube")
                 return [-1, [], [], []]
                 return [-1, [], [], []]
 
 
-            print("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
+            print_("ESTIMATED VIDEO IDS FROM RESPONSE", len(pl_response["items"]))
             updated_playlist_video_count += len(pl_response["items"])
             updated_playlist_video_count += len(pl_response["items"])
             for item in pl_response['items']:
             for item in pl_response['items']:
                 playlist_item_id = item["id"]
                 playlist_item_id = item["id"]
@@ -914,7 +910,7 @@ class PlaylistManager(models.Manager):
         """
         """
         Takes in playlist itemids for the videos in a particular playlist
         Takes in playlist itemids for the videos in a particular playlist
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist = user.playlists.get(playlist_id=playlist_id)
         playlist = user.playlists.get(playlist_id=playlist_id)
 
 
         # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
         # new_playlist_duration_in_seconds = playlist.playlist_duration_in_seconds
@@ -925,13 +921,13 @@ class PlaylistManager(models.Manager):
             )
             )
             try:
             try:
                 pl_response = pl_request.execute()
                 pl_response = pl_request.execute()
-                print(pl_response)
+                print_(pl_response)
             except googleapiclient.errors.HttpError as e:  # failed to delete playlist
             except googleapiclient.errors.HttpError as e:  # failed to delete playlist
                 # possible causes:
                 # possible causes:
                 # playlistForbidden (403)
                 # playlistForbidden (403)
                 # playlistNotFound  (404)
                 # playlistNotFound  (404)
                 # playlistOperationUnsupported (400)
                 # playlistOperationUnsupported (400)
-                print(e.error_details, e.status_code)
+                print_(e.error_details, e.status_code)
                 return [-1, get_message_from_httperror(e), e.status_code]
                 return [-1, get_message_from_httperror(e), e.status_code]
 
 
             # playlistItem was successfully deleted if no HttpError, so delete it from db
             # playlistItem was successfully deleted if no HttpError, so delete it from db
@@ -948,7 +944,7 @@ class PlaylistManager(models.Manager):
         """
         """
         Takes in playlist itemids for the videos in a particular playlist
         Takes in playlist itemids for the videos in a particular playlist
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist = user.playlists.get(playlist_id=playlist_id)
         playlist = user.playlists.get(playlist_id=playlist_id)
         playlist_items = user.playlists.get(playlist_id=playlist_id).playlist_items.select_related('video').filter(
         playlist_items = user.playlists.get(playlist_id=playlist_id).playlist_items.select_related('video').filter(
             playlist_item_id__in=playlist_item_ids)
             playlist_item_id__in=playlist_item_ids)
@@ -960,16 +956,16 @@ class PlaylistManager(models.Manager):
                 pl_request = youtube.playlistItems().delete(
                 pl_request = youtube.playlistItems().delete(
                     id=playlist_item.playlist_item_id
                     id=playlist_item.playlist_item_id
                 )
                 )
-                print(pl_request)
+                print_(pl_request)
                 try:
                 try:
                     pl_response = pl_request.execute()
                     pl_response = pl_request.execute()
-                    print(pl_response)
+                    print_(pl_response)
                 except googleapiclient.errors.HttpError as e:  # failed to delete playlist item
                 except googleapiclient.errors.HttpError as e:  # failed to delete playlist item
                     # possible causes:
                     # possible causes:
                     # playlistItemsNotAccessible (403)
                     # playlistItemsNotAccessible (403)
                     # playlistItemNotFound (404)
                     # playlistItemNotFound (404)
                     # playlistOperationUnsupported (400)
                     # playlistOperationUnsupported (400)
-                    print(e, e.error_details, e.status_code)
+                    print_(e, e.error_details, e.status_code)
                     continue
                     continue
 
 
                 # playlistItem was successfully deleted if no HttpError, so delete it from db
                 # playlistItem was successfully deleted if no HttpError, so delete it from db
@@ -1032,7 +1028,7 @@ class PlaylistManager(models.Manager):
         """
         """
         Takes in playlist details and creates a new private playlist in the user's account
         Takes in playlist details and creates a new private playlist in the user's account
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         result = {
         result = {
             "status": 0,
             "status": 0,
             "playlist_id": None
             "playlist_id": None
@@ -1054,7 +1050,7 @@ class PlaylistManager(models.Manager):
             try:
             try:
                 pl_response = pl_request.execute()
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:  # failed to create playlist
             except googleapiclient.errors.HttpError as e:  # failed to create playlist
-                print(e.status_code, e.error_details)
+                print_(e.status_code, e.error_details)
                 if e.status_code == 400:  # maxPlaylistExceeded
                 if e.status_code == 400:  # maxPlaylistExceeded
                     result["status"] = 400
                     result["status"] = 400
                 result["status"] = -1
                 result["status"] = -1
@@ -1066,7 +1062,7 @@ class PlaylistManager(models.Manager):
         """
         """
         Takes in playlist itemids for the videos in a particular playlist
         Takes in playlist itemids for the videos in a particular playlist
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist = user.playlists.get(playlist_id=playlist_id)
         playlist = user.playlists.get(playlist_id=playlist_id)
 
 
         with build('youtube', 'v3', credentials=credentials) as youtube:
         with build('youtube', 'v3', credentials=credentials) as youtube:
@@ -1084,7 +1080,7 @@ class PlaylistManager(models.Manager):
                 },
                 },
             )
             )
 
 
-            print(details["description"])
+            print_(details["description"])
             try:
             try:
                 pl_response = pl_request.execute()
                 pl_response = pl_request.execute()
             except googleapiclient.errors.HttpError as e:  # failed to update playlist details
             except googleapiclient.errors.HttpError as e:  # failed to update playlist details
@@ -1094,10 +1090,10 @@ class PlaylistManager(models.Manager):
                 # playlistOperationUnsupported (400)
                 # playlistOperationUnsupported (400)
                 # errors i ran into:
                 # errors i ran into:
                 # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
                 # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
-                print("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
+                print_("ERROR UPDATING PLAYLIST DETAILS", e, e.status_code, e.error_details)
                 return -1
                 return -1
 
 
-            print(pl_response)
+            print_(pl_response)
             playlist.name = pl_response['snippet']['title']
             playlist.name = pl_response['snippet']['title']
             playlist.description = pl_response['snippet']['description']
             playlist.description = pl_response['snippet']['description']
             playlist.is_private_on_yt = True if pl_response['status']['privacyStatus'] == "private" else False
             playlist.is_private_on_yt = True if pl_response['status']['privacyStatus'] == "private" else False
@@ -1109,7 +1105,7 @@ class PlaylistManager(models.Manager):
         """
         """
         Takes in playlist itemids for the videos in a particular playlist
         Takes in playlist itemids for the videos in a particular playlist
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
         playlist_items = user.playlists.get(playlist_id=from_playlist_id).playlist_items.select_related('video').filter(
         playlist_items = user.playlists.get(playlist_id=from_playlist_id).playlist_items.select_related('video').filter(
             playlist_item_id__in=playlist_item_ids)
             playlist_item_id__in=playlist_item_ids)
 
 
@@ -1145,7 +1141,7 @@ class PlaylistManager(models.Manager):
                         # playlistOperationUnsupported (400)
                         # playlistOperationUnsupported (400)
                         # errors i ran into:
                         # errors i ran into:
                         # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
                         # runs into HttpError 400 "Invalid playlist snippet." when the description contains <, >
-                        print("ERROR UPDATING PLAYLIST DETAILS", e.status_code, e.error_details)
+                        print_("ERROR UPDATING PLAYLIST DETAILS", e.status_code, e.error_details)
                         if e.status_code == 400:
                         if e.status_code == 400:
                             pl_request = youtube.playlistItems().insert(
                             pl_request = youtube.playlistItems().insert(
                                 part="snippet",
                                 part="snippet",
@@ -1179,7 +1175,7 @@ class PlaylistManager(models.Manager):
         """
         """
         Takes in playlist itemids for the videos in a particular playlist
         Takes in playlist itemids for the videos in a particular playlist
         """
         """
-        credentials = self.getCredentials(user)
+        credentials = user.profile.get_credentials()
 
 
         result = {
         result = {
             "num_added": 0,
             "num_added": 0,
@@ -1207,7 +1203,7 @@ class PlaylistManager(models.Manager):
                 try:
                 try:
                     pl_response = pl_request.execute()
                     pl_response = pl_request.execute()
                 except googleapiclient.errors.HttpError as e:  # failed to update add video to playlis
                 except googleapiclient.errors.HttpError as e:  # failed to update add video to playlis
-                    print("ERROR ADDDING VIDEOS TO PLAYLIST", e.status_code, e.error_details)
+                    print_("ERROR ADDDING VIDEOS TO PLAYLIST", e.status_code, e.error_details)
                     if e.status_code == 400:  # manualSortRequired - see errors https://developers.google.com/youtube/v3/docs/playlistItems/insert
                     if e.status_code == 400:  # manualSortRequired - see errors https://developers.google.com/youtube/v3/docs/playlistItems/insert
                         pl_request = youtube.playlistItems().insert(
                         pl_request = youtube.playlistItems().insert(
                             part="snippet",
                             part="snippet",
@@ -1398,18 +1394,18 @@ class Playlist(models.Model):
 
 
         return [num_channels, channels_list]
         return [num_channels, channels_list]
 
 
-    def generate_playlist_thumbnail_url(self):
-        """
-        Generates a playlist thumnail url based on the playlist name
-        """
-        pl_name = self.name
-        response = requests.get(
-            f'https://api.unsplash.com/search/photos/?client_id={SECRETS["UNSPLASH_API_ACCESS_KEY"]}&page=1&query={pl_name}')
-        image = response.json()["results"][0]["urls"]["small"]
-
-        print(image)
-
-        return image
+    # def generate_playlist_thumbnail_url(self):
+    #     """
+    #     Generates a playlist thumnail url based on the playlist name
+    #     """
+    #     pl_name = self.name
+    #     response = requests.get(
+    #         f'https://api.unsplash.com/search/photos/?client_id={SECRETS["UNSPLASH_API_ACCESS_KEY"]}&page=1&query={pl_name}')
+    #     image = response.json()["results"][0]["urls"]["small"]
+    #
+    #     print_(image)
+    #
+    #     return image
 
 
     def get_playlist_thumbnail_url(self):
     def get_playlist_thumbnail_url(self):
         playlist_items = self.playlist_items.filter(
         playlist_items = self.playlist_items.filter(

+ 0 - 0
apps/main/static/assets/imgs/dashboard.gif → backend/main/static/assets/imgs/dashboard.gif


+ 0 - 0
apps/main/static/assets/imgs/features.gif → backend/main/static/assets/imgs/features.gif


+ 0 - 0
apps/main/static/assets/imgs/import.gif → backend/main/static/assets/imgs/import.gif


+ 0 - 0
apps/main/static/assets/imgs/organize.gif → backend/main/static/assets/imgs/organize.gif


+ 0 - 0
apps/main/static/assets/imgs/playlist_stats.gif → backend/main/static/assets/imgs/playlist_stats.gif


+ 0 - 0
apps/main/static/assets/imgs/watching.gif → backend/main/static/assets/imgs/watching.gif


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-grid.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-reboot.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap-utilities.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.css → backend/main/static/bootstrap5.0.1/css/bootstrap.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.css → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css


+ 0 - 0
apps/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css.map → backend/main/static/bootstrap5.0.1/css/bootstrap.rtl.min.css.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.js → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.bundle.min.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.js → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.esm.min.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.js → backend/main/static/bootstrap5.0.1/js/bootstrap.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.js.map


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.min.js → backend/main/static/bootstrap5.0.1/js/bootstrap.min.js


+ 0 - 0
apps/main/static/bootstrap5.0.1/js/bootstrap.min.js.map → backend/main/static/bootstrap5.0.1/js/bootstrap.min.js.map


+ 0 - 0
apps/main/static/choices.js/choices.min.css → backend/main/static/choices.js/choices.min.css


+ 0 - 0
apps/main/static/choices.js/choices.min.js → backend/main/static/choices.js/choices.min.js


+ 0 - 0
apps/main/static/clipboard.js/clipboard.min.js → backend/main/static/clipboard.js/clipboard.min.js


+ 0 - 0
apps/main/static/css/carousel.css → backend/main/static/css/carousel.css


+ 0 - 0
apps/main/static/fontawesome-free-5.15.3-web/LICENSE.txt → backend/main/static/fontawesome-free-5.15.3-web/LICENSE.txt


+ 0 - 0
apps/main/static/fontawesome-free-5.15.3-web/attribution.js → backend/main/static/fontawesome-free-5.15.3-web/attribution.js


+ 6 - 6
apps/main/static/fontawesome-free-5.15.3-web/css/all.css → backend/main/static/fontawesome-free-5.15.3-web/css/all.css

@@ -4588,8 +4588,8 @@ readers do not read off random characters that represent icons */
   font-style: normal;
   font-style: normal;
   font-weight: 400;
   font-weight: 400;
   font-display: block;
   font-display: block;
-  src: url("../webfonts/fa-brands-400.eot");
-  src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
+  src: url("fa-brands-400.eot");
+  src: url("fa-brands-400.eot?#iefix") format("embedded-opentype"), url("fa-brands-400.woff2") format("woff2"), url("fa-brands-400.woff") format("woff"), url("fa-brands-400.ttf") format("truetype"), url("fa-brands-400.svg#fontawesome") format("svg"); }
 
 
 .fab {
 .fab {
   font-family: 'Font Awesome 5 Brands';
   font-family: 'Font Awesome 5 Brands';
@@ -4599,8 +4599,8 @@ readers do not read off random characters that represent icons */
   font-style: normal;
   font-style: normal;
   font-weight: 400;
   font-weight: 400;
   font-display: block;
   font-display: block;
-  src: url("../webfonts/fa-regular-400.eot");
-  src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
+  src: url("fa-regular-400.eot");
+  src: url("fa-regular-400.eot?#iefix") format("embedded-opentype"), url("fa-regular-400.woff2") format("woff2"), url("fa-regular-400.woff") format("woff"), url("fa-regular-400.ttf") format("truetype"), url("fa-regular-400.svg#fontawesome") format("svg"); }
 
 
 .far {
 .far {
   font-family: 'Font Awesome 5 Free';
   font-family: 'Font Awesome 5 Free';
@@ -4610,8 +4610,8 @@ readers do not read off random characters that represent icons */
   font-style: normal;
   font-style: normal;
   font-weight: 900;
   font-weight: 900;
   font-display: block;
   font-display: block;
-  src: url("../webfonts/fa-solid-900.eot");
-  src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
+  src: url("fa-solid-900.eot");
+  src: url("fa-solid-900.eot?#iefix") format("embedded-opentype"), url("fa-solid-900.woff2") format("woff2"), url("fa-solid-900.woff") format("woff"), url("fa-solid-900.ttf") format("truetype"), url("fa-solid-900.svg#fontawesome") format("svg"); }
 
 
 .fa,
 .fa,
 .fas {
 .fas {

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
backend/main/static/fontawesome-free-5.15.3-web/css/all.min.css


+ 2 - 2
apps/main/static/fontawesome-free-5.15.3-web/css/brands.css → backend/main/static/fontawesome-free-5.15.3-web/css/brands.css

@@ -7,8 +7,8 @@
   font-style: normal;
   font-style: normal;
   font-weight: 400;
   font-weight: 400;
   font-display: block;
   font-display: block;
-  src: url("../webfonts/fa-brands-400.eot");
-  src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
+  src: url("fa-brands-400.eot");
+  src: url("fa-brands-400.eot?#iefix") format("embedded-opentype"), url("fa-brands-400.woff2") format("woff2"), url("fa-brands-400.woff") format("woff"), url("fa-brands-400.ttf") format("truetype"), url("fa-brands-400.svg#fontawesome") format("svg"); }
 
 
 .fab {
 .fab {
   font-family: 'Font Awesome 5 Brands';
   font-family: 'Font Awesome 5 Brands';

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff