Browse Source

add linting

Mohammed Khan 1 năm trước cách đây
mục cha
commit
8a2bf076b2
42 tập tin đã thay đổi với 1408 bổ sung1020 xóa
  1. 7 0
      .editorconfig
  2. 16 0
      .flake8
  3. 14 0
      .pre-commit-config.yaml
  4. 10 3
      Makefile
  5. 1 1
      backend/UnTube/settings/allauth.py
  6. 6 9
      backend/UnTube/settings/base.py
  7. 1 1
      backend/UnTube/settings/docker.py
  8. 2 3
      backend/UnTube/settings/envvars.py
  9. 13 13
      backend/UnTube/settings/pythonanywhere.py
  10. 2 2
      backend/UnTube/settings/templates/settings.dev.py
  11. 6 6
      backend/UnTube/urls.py
  12. 1 1
      backend/charts/admin.py
  13. 1 1
      backend/charts/models.py
  14. 1 1
      backend/charts/tests.py
  15. 16 9
      backend/charts/urls.py
  16. 14 21
      backend/charts/views.py
  17. 1 1
      backend/main/admin.py
  18. 46 6
      backend/main/migrations/0001_initial.py
  19. 241 236
      backend/main/models.py
  20. 1 1
      backend/main/tests.py
  21. 90 51
      backend/main/urls.py
  22. 20 24
      backend/main/util.py
  23. 359 295
      backend/main/views.py
  24. 2 2
      backend/manage.py
  25. 1 1
      backend/manage_playlists/admin.py
  26. 1 1
      backend/manage_playlists/models.py
  27. 1 1
      backend/manage_playlists/tests.py
  28. 10 7
      backend/manage_playlists/urls.py
  29. 30 25
      backend/manage_playlists/views.py
  30. 1 1
      backend/search/admin.py
  31. 1 1
      backend/search/models.py
  32. 1 1
      backend/search/tests.py
  33. 5 4
      backend/search/urls.py
  34. 115 97
      backend/search/views.py
  35. 2 1
      backend/users/admin.py
  36. 5 2
      backend/users/migrations/0001_initial.py
  37. 22 21
      backend/users/models.py
  38. 1 1
      backend/users/tests.py
  39. 16 18
      backend/users/urls.py
  40. 178 150
      backend/users/views.py
  41. 144 1
      poetry.lock
  42. 3 0
      pyproject.toml

+ 7 - 0
.editorconfig

@@ -0,0 +1,7 @@
+root = true  # signifies that this is the top-level editor config file
+
+[*.{html,py}]
+charset = utf-8
+indent_size = 4
+indent_style = space
+max_line_length = 150

+ 16 - 0
.flake8

@@ -0,0 +1,16 @@
+[flake8]
+ignore = C101,D403,E126,E251,F403,F405,I100,I201,W291,W503,W504
+exclude =
+    **/migrations/*,
+    .git,
+    .mypy_cache,
+    __pycache__,
+    venv
+filename = *.py
+accept-encodings = utf-8
+inline-quotes = single
+max-linenumber = 500
+max-line-length = 150
+multiline-quotes = double
+per-file-ignores =
+    backend/UnTube/settings/templates/settings.dev.py:F821

+ 14 - 0
.pre-commit-config.yaml

@@ -0,0 +1,14 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+  - repo: https://github.com/PyCQA/flake8
+    rev: 4.0.1
+    hooks:
+      - id: flake8
+        additional_dependencies:
+          - flake8-bugbear
+          - flake8-builtins
+          - flake8-coding
+          - flake8-import-order
+          - flake8-polyfill
+          - flake8-quotes

+ 10 - 3
Makefile

@@ -2,6 +2,14 @@
 install:
 	poetry install
 
+.PHONY: install-pre-commit
+install-pre-commit:
+	poetry run pre-commit uninstall; poetry run pre-commit install
+
+.PHONY: lint
+lint:
+	poetry run pre-commit run --all-files
+
 .PHONY: migrations
 migrations:
 	poetry run python3 -m backend.manage makemigrations
@@ -23,9 +31,8 @@ superuser:
 	poetry run python3 -m backend.manage createsuperuser
 
 .PHONY: update
-update: install migrate ;
+update: install migrate install-pre-commit ;
 
 .PHONY: local-settings
 local-settings:
-	mkdir -p local
-	cp ./backend/UnTube/settings/templates/settings.dev.py ./local/settings.dev.py
+	mkdir -p local; cp ./backend/UnTube/settings/templates/settings.dev.py ./local/settings.dev.py

+ 1 - 1
backend/UnTube/settings/allauth.py

@@ -30,4 +30,4 @@ SOCIALACCOUNT_PROVIDERS = {
             'access_type': 'offline',
         }
     }
-}
+}

+ 6 - 9
backend/UnTube/settings/base.py

@@ -25,14 +25,12 @@ INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'django.contrib.humanize',  # A set of Django template filters useful for adding a “human touch” to data.
     'django.contrib.sites',
-
     'crispy_forms',
     'import_export',
     'allauth',
     'allauth.account',
     'allauth.socialaccount',
     'allauth.socialaccount.providers.google',  # specifies google as OAuth provider
-
     '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',
@@ -55,7 +53,7 @@ ROOT_URLCONF = 'backend.UnTube.urls'  # path to the urls.py file in root UnTube
 TEMPLATES = [
     {
         'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [BASE_DIR / 'backend' / 'templates'],  # type: ignore
+        'DIRS': [BASE_DIR / 'backend' / 'templates'],  # type: ignore  # noqa: F821
         # 'DIRS': [BASE_DIR / 'backend', BASE_DIR / 'backend' / 'templates'],
         # 'DIRS': [os.path.join(BASE_DIR, "templates")],
         'APP_DIRS': True,
@@ -71,8 +69,7 @@ TEMPLATES = [
 ]
 
 AUTHENTICATION_BACKENDS = [
-    'django.contrib.auth.backends.ModelBackend',
-    'allauth.account.auth_backends.AuthenticationBackend'
+    'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend'
 ]
 
 LOGIN_URL = '/'
@@ -84,7 +81,7 @@ WSGI_APPLICATION = 'backend.UnTube.wsgi.application'  # path to the wsgi.py file
 DATABASES = {
     'default': {
         'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': BASE_DIR / 'db.sqlite3',  # type: ignore
+        'NAME': BASE_DIR / 'db.sqlite3',  # type: ignore # noqa: F821
     }
 }
 
@@ -120,13 +117,13 @@ USE_TZ = True
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/3.2/howto/static-files/
 STATIC_URL = '/static/'
-STATIC_ROOT = BASE_DIR / 'static'  # type: ignore
+STATIC_ROOT = BASE_DIR / 'static'  # type: ignore # noqa: F821
 # 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
+    os.path.join(BASE_DIR, 'backend', 'main', 'static'),  # type: ignore # noqa: F821
 )
 
 # Default primary key field type
@@ -134,4 +131,4 @@ STATICFILES_DIRS = (
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 
 MEDIA_URL = '/media/'
-MEDIA_ROOT = BASE_DIR / 'media'  # type: ignore
+MEDIA_ROOT = BASE_DIR / 'media'  # type: ignore # noqa: F821

+ 1 - 1
backend/UnTube/settings/docker.py

@@ -9,4 +9,4 @@ if IN_DOCKER or os.path.isfile('/.dockerenv'):  # type: ignore # noqa: F821
     DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
     STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
 
-    print("Using Docker settings...")
+    print('Using Docker settings...')

+ 2 - 3
backend/UnTube/settings/envvars.py

@@ -1,14 +1,13 @@
 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. 
+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
+deep_update(globals(), get_settings_from_environment(ENVVAR_SETTINGS_PREFIX))  # type: ignore  # noqa: F821

+ 13 - 13
backend/UnTube/settings/pythonanywhere.py

@@ -1,18 +1,18 @@
-if IN_PYTHONANYWHERE:  # type: ignore
+if IN_PYTHONANYWHERE:  # type: ignore # noqa: F821
     # 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
+    GOOGLE_OAUTH_URI = os.environ['GOOGLE_OAUTH_URI']  # type: ignore # noqa: F821 #  "bakaabu.pythonanywhere.com"
+    SECRET_KEY = os.environ['SECRET_KEY']  # type: ignore # noqa: F821
+    YOUTUBE_V3_API_KEY = os.environ['YOUTUBE_V3_API_KEY']  # type: ignore # noqa: F821
 
     # 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
+    ] and not IN_DOCKER  # type: ignore # noqa: F821 # PA does not support dockerized apps
     # Add whitenoise middleware after the security middleware
-    MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')  # type: ignore # noqa: F821
+    MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')  # type: ignore # noqa: F821 # noqa: F821
 
     # configure the domain name using the environment variable found on pythonanywhere
     ALLOWED_HOSTS = ['bakaabu.pythonanywhere.com', '127.0.0.1', 'untube.it']
@@ -23,26 +23,26 @@ if IN_PYTHONANYWHERE:  # type: ignore
     SECURE_SSL_REDIRECT = True
 
     STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
-    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')  # type: ignore
+    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')  # type: ignore # noqa: F821
 
     # DBHOST is only the server name
-    hostname = os.environ['DBHOST']  # type: ignore
+    hostname = os.environ['DBHOST']  # type: ignore # noqa: F821
 
     # 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
+            'NAME': f'{os.environ["DBUSER"]}${os.environ["DBNAME"]}',  # type: ignore # noqa: F821
+            'USER': f'{os.environ["DBUSER"]}',  # type: ignore # noqa: F821
+            'PASSWORD': f'{os.environ["DBPASS"]}',  # type: ignore # noqa: F821
             'HOST': hostname,
             'OPTIONS': {
                 'init_command': "SET sql_mode='STRICT_TRANS_TABLES', innodb_strict_mode=1",
                 'charset': 'utf8mb4',
-                "autocommit": True,
+                'autocommit': True,
             }
         }
     }
 
-    print("Using Pythonanywhere settings...")
+    print('Using Pythonanywhere settings...')

+ 2 - 2
backend/UnTube/settings/templates/settings.dev.py

@@ -1,8 +1,8 @@
 DEBUG = True
-SECRET_KEY = "django-insecure-ycs22y+20sq67y(6dm6ynqw=dlhg!)%vuqpd@$p6rf3!#1h$u="
+SECRET_KEY = 'django-insecure-ycs22y+20sq67y(6dm6ynqw=dlhg!)%vuqpd@$p6rf3!#1h$u='
 ENABLE_PRINT_STATEMENTS = False
 
-GOOGLE_OAUTH_URI = "127.0.0.1:8000"  # this is the URI you will use when creating your OAuth Creds
+GOOGLE_OAUTH_URI = '127.0.0.1:8000'  # this is the URI you will use when creating your OAuth Creds
 SITE_ID = 1  # increment/decrement site ID as necessary
 
 # please fill these in with your own Google OAuth credentials for the app to run properly!

+ 6 - 6
backend/UnTube/urls.py

@@ -14,14 +14,14 @@ Including another URLconf
     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
 """
 from django.contrib import admin
-from django.urls import path, include
+from django.urls import include, path
 
 urlpatterns = [
     path('admin/', admin.site.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")),
+    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')),
 ]

+ 1 - 1
backend/charts/admin.py

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

+ 1 - 1
backend/charts/models.py

@@ -1,3 +1,3 @@
-from django.db import models
+# from django.db import models
 
 # Create your models here.

+ 1 - 1
backend/charts/tests.py

@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
 
 # Create your tests here.

+ 16 - 9
backend/charts/urls.py

@@ -1,13 +1,20 @@
 from django.urls import path
+
 from backend.charts import views
 
 urlpatterns = [
-    path('channel-videos-distribution/<slug:playlist_id>', views.channel_videos_distribution, name='channel_videos_distribution'),
-    path('overall-playlists-distribution/', views.overall_playlists_distribution, name='overall_playlists_distribution'),
-    path('overall-channels-distribution/', views.overall_channels_distribution,
-         name='overall_channels_distribution'),
-
-    path('watching-playlists-percent-distribution/', views.watching_playlists_percent_distribution,
-         name='watching_playlists_percent_distribution'),
-
-]
+    path(
+        'channel-videos-distribution/<slug:playlist_id>',
+        views.channel_videos_distribution,
+        name='channel_videos_distribution'
+    ),
+    path(
+        'overall-playlists-distribution/', views.overall_playlists_distribution, name='overall_playlists_distribution'
+    ),
+    path('overall-channels-distribution/', views.overall_channels_distribution, name='overall_channels_distribution'),
+    path(
+        'watching-playlists-percent-distribution/',
+        views.watching_playlists_percent_distribution,
+        name='watching_playlists_percent_distribution'
+    ),
+]

+ 14 - 21
backend/charts/views.py

@@ -9,8 +9,9 @@ def channel_videos_distribution(request, playlist_id):
 
     playlist_items = request.user.playlists.get(playlist_id=bleach.clean(playlist_id)).playlist_items.all()
 
-    queryset = playlist_items.filter(Q(video__is_unavailable_on_yt=False) & Q(video__was_deleted_on_yt=False)).values(
-        'video__channel_name').annotate(channel_videos_count=Count('video_position'))
+    queryset = playlist_items.filter(Q(video__is_unavailable_on_yt=False) &
+                                     Q(video__was_deleted_on_yt=False))\
+        .values('video__channel_name').annotate(channel_videos_count=Count('video_position'))
 
     for entry in queryset:
         labels.append(entry['video__channel_name'])
@@ -28,8 +29,8 @@ def overall_channels_distribution(request):
 
     videos = request.user.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False))
 
-    queryset = videos.values(
-        'channel_name').annotate(channel_videos_count=Count('video_id')).order_by('-channel_videos_count')[:100]
+    queryset = videos.values('channel_name').annotate(channel_videos_count=Count('video_id')
+                                                      ).order_by('-channel_videos_count')[:100]
 
     for entry in queryset:
         labels.append(entry['channel_name'])
@@ -45,26 +46,18 @@ def overall_playlists_distribution(request):
     labels = []
     data = []
 
-    user_playlists = request.user.playlists.filter(is_in_db=True).exclude(playlist_id="LL")
+    user_playlists = request.user.playlists.filter(is_in_db=True).exclude(playlist_id='LL')
     total_num_playlists = user_playlists.count()
 
-
-    statistics = {
-        "public": 0,
-        "private": 0,
-        "favorites": 0,
-        "watching": 0,
-        "imported": 0,
-        "youtube mixes": 0
-    }
+    statistics = {'public': 0, 'private': 0, 'favorites': 0, 'watching': 0, 'imported': 0, 'youtube mixes': 0}
 
     if total_num_playlists != 0:
-        statistics["public"] = user_playlists.filter(is_private_on_yt=False).count()
-        statistics["private"] = user_playlists.filter(is_private_on_yt=True).count()
-        statistics["favorites"] = user_playlists.filter(is_favorite=True).count()
-        statistics["watching"] = user_playlists.filter(marked_as="watching").count()
-        statistics["imported"] = user_playlists.filter(is_user_owned=False).count()
-        statistics["youtube mixes"] = user_playlists.filter(is_yt_mix=True).count()
+        statistics['public'] = user_playlists.filter(is_private_on_yt=False).count()
+        statistics['private'] = user_playlists.filter(is_private_on_yt=True).count()
+        statistics['favorites'] = user_playlists.filter(is_favorite=True).count()
+        statistics['watching'] = user_playlists.filter(marked_as='watching').count()
+        statistics['imported'] = user_playlists.filter(is_user_owned=False).count()
+        statistics['youtube mixes'] = user_playlists.filter(is_yt_mix=True).count()
 
     for key, value in statistics.items():
         labels.append(key)
@@ -80,7 +73,7 @@ def watching_playlists_percent_distribution(request):
     labels = []
     data = []
 
-    watching_playlists = request.user.playlists.filter(Q(is_in_db=True) & Q(marked_as="watching"))
+    watching_playlists = request.user.playlists.filter(Q(is_in_db=True) & Q(marked_as='watching'))
 
     if watching_playlists.exists():
         for playlist in watching_playlists:

+ 1 - 1
backend/main/admin.py

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

+ 46 - 6
backend/main/migrations/0001_initial.py

@@ -83,7 +83,15 @@ class Migration(migrations.Migration):
                 ('updated_at', models.DateTimeField(auto_now=True)),
                 ('video_details_modified', models.BooleanField(default=False)),
                 ('video_details_modified_at', models.DateTimeField(auto_now_add=True)),
-                ('untube_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='videos', to=settings.AUTH_USER_MODEL)),
+                (
+                    'untube_user',
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='videos',
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
             ],
         ),
         migrations.CreateModel(
@@ -96,7 +104,15 @@ class Migration(migrations.Migration):
                 ('last_views_reset', models.DateTimeField(default=datetime.datetime.now)),
                 ('created_at', models.DateTimeField(auto_now_add=True)),
                 ('updated_at', models.DateTimeField(auto_now=True)),
-                ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='playlist_tags', to=settings.AUTH_USER_MODEL)),
+                (
+                    'created_by',
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='playlist_tags',
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
             ],
         ),
         migrations.CreateModel(
@@ -113,7 +129,15 @@ class Migration(migrations.Migration):
                 ('num_of_accesses', models.IntegerField(default=0)),
                 ('created_at', models.DateTimeField(auto_now_add=True)),
                 ('updated_at', models.DateTimeField(auto_now=True)),
-                ('playlist', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='playlist_items', to='main.playlist')),
+                (
+                    'playlist',
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='playlist_items',
+                        to='main.playlist'
+                    )
+                ),
                 ('video', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.video')),
             ],
         ),
@@ -125,7 +149,12 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='playlist',
             name='untube_user',
-            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='playlists', to=settings.AUTH_USER_MODEL),
+            field=models.ForeignKey(
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='playlists',
+                to=settings.AUTH_USER_MODEL
+            ),
         ),
         migrations.AddField(
             model_name='playlist',
@@ -137,8 +166,19 @@ class Migration(migrations.Migration):
             fields=[
                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('kind', models.CharField(max_length=100)),
-                ('playlist', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.playlist')),
-                ('untube_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pins', to=settings.AUTH_USER_MODEL)),
+                (
+                    'playlist',
+                    models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.playlist')
+                ),
+                (
+                    'untube_user',
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='pins',
+                        to=settings.AUTH_USER_MODEL
+                    )
+                ),
                 ('video', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='main.video')),
             ],
         ),

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 241 - 236
backend/main/models.py


+ 1 - 1
backend/main/tests.py

@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
 
 # Create your tests here.

+ 90 - 51
backend/main/urls.py

@@ -1,61 +1,100 @@
 from django.urls import path
+
 from . import views
 
 urlpatterns = [
 
-    ### STUFF RELATED TO WHOLE SITE
-    path("home/", views.home, name='home'),
-    path("favorites", views.favorites, name="favorites"),
-    path("planned-to-watch", views.planned_to_watch, name="planned_to_watch"),
-    path("library/<slug:library_type>", views.library, name='library'),
-
-    ### STUFF RELATED TO INDIVIDUAL VIDEOS
-    path("video/<slug:video_id>", views.view_video, name='video'),
-    path("video/<slug:video_id>/mark/favorite", views.mark_video_favortie, name='mark_video_favorite'),
-    path("video/<slug:video_id>/mark/planned-to-watch", views.mark_video_planned_to_watch, name='mark_video_planned_to_watch'),
-    path("video/<slug:video_id>/notes", views.video_notes, name='video_notes'),
-    path("video/<slug:video_id>/get-video-completion-times", views.video_completion_times, name="video_completion_times"),
-    path("video/<slug:video_id>/add-user-label", views.add_video_user_label, name='add_video_user_label'),
-
-    ### STUFF RELATED TO VIDEO(S) INSIDE PLAYLISTS
+    # STUFF RELATED TO WHOLE SITE
+    path('home/', views.home, name='home'),
+    path('favorites', views.favorites, name='favorites'),
+    path('planned-to-watch', views.planned_to_watch, name='planned_to_watch'),
+    path('library/<slug:library_type>', views.library, name='library'),
 
-    ### STUFF RELATED TO ONE PLAYLIST
-    path("playlist/<slug:playlist_id>", views.view_playlist, name='playlist'),
-    path("playlist/<slug:playlist_id>/add-user-label", views.add_playlist_user_label, name='add_playlist_user_label'),
+    # STUFF RELATED TO INDIVIDUAL VIDEOS
+    path('video/<slug:video_id>', views.view_video, name='video'),
+    path('video/<slug:video_id>/mark/favorite', views.mark_video_favortie, name='mark_video_favorite'),
+    path(
+        'video/<slug:video_id>/mark/planned-to-watch',
+        views.mark_video_planned_to_watch,
+        name='mark_video_planned_to_watch'
+    ),
+    path('video/<slug:video_id>/notes', views.video_notes, name='video_notes'),
+    path(
+        'video/<slug:video_id>/get-video-completion-times',
+        views.video_completion_times,
+        name='video_completion_times'
+    ),
+    path('video/<slug:video_id>/add-user-label', views.add_video_user_label, name='add_video_user_label'),
 
-    path('playlist/<slug:playlist_id>/<slug:video_id>/mark/watched', views.mark_video_watched,
-         name='mark_video_watched'),
-    path("playlist/<slug:playlist_id>/settings", views.view_playlist_settings, name="view_playlist_settings"),
-    path("playlist/<slug:playlist_id>/order-by/<slug:order_by>", views.order_playlist_by,
-         name='order_playlist_by'),
-    path("playlist/<slug:playlist_id>/mark-as/<slug:mark_as>", views.mark_playlist_as,
-         name='mark_playlist_as'),
-    path("playlist/<slug:playlist_id>/update/<slug:command>", views.update_playlist, name="update_playlist"),
-    path("playlist/<slug:playlist_id>/update-settings", views.update_playlist_settings, name="update_playlist_settings"),
-    path("playlist/<slug:playlist_id>/<slug:order_by>/load-more-videos/<int:page>", views.load_more_videos, name="load_more_videos"),
-    path("playlist/<slug:playlist_id>/create-tag", views.create_playlist_tag, name="create_playlist_tag"),
-    path("playlist/<slug:playlist_id>/add-tag", views.add_playlist_tag, name="add_playlist_tag"),
-    path("playlist/<slug:playlist_id>/remove-tag/<str:tag_name>", views.remove_playlist_tag, name="remove_playlist_tag"),
-    path("playlist/<slug:playlist_id>/get-tags", views.get_playlist_tags, name="get_playlist_tags"),
-    path("playlist/<slug:playlist_id>/get-unused-tags", views.get_unused_playlist_tags, name="get_unused_playlist_tags"),
-    path("playlist/<slug:playlist_id>/get-watch-message", views.get_watch_message, name="get_watch_message"),
-    path("playlist/<slug:playlist_id>/delete-videos/<slug:command>", views.playlist_delete_videos, name='delete_videos'),
-    path("playlist/<slug:playlist_id>/delete-specific-videos/<slug:command>", views.delete_specific_videos, name='delete_specific_videos'),
-    path("playlist/<slug:playlist_id>/delete-playlist", views.delete_playlist, name="delete_playlist"),
-    path("playlist/<slug:playlist_id>/reset-watched", views.reset_watched, name="reset_watched"),
-    path("playlist/<slug:playlist_id>/move-copy-videos/<str:action>", views.playlist_move_copy_videos, name="playlist_move_copy_videos"),
-    path("playlist/<slug:playlist_id>/open-random-video", views.playlist_open_random_video, name="playlist_open_random_video"),
-    path("playlist/<slug:playlist_id>/get-playlist-completion-times", views.playlist_completion_times,
-         name="playlist_completion_times"),
-    path("playlist/<slug:playlist_id>/add-new-videos", views.playlist_add_new_videos,
-         name="playlist_add_new_videos"),
-    path("playlist/<slug:playlist_id>/create-new-playlist", views.playlist_create_new_playlist,
-         name="playlist_create_new_playlist"),
+    # STUFF RELATED TO VIDEO(S) INSIDE PLAYLISTS
 
-    ### STUFF RELATED TO PLAYLISTS IN BULK
-    path("playlists/<slug:playlist_type>/order-by/<slug:order_by>", views.order_playlists_by, name='order_playlists_by'),
-    path("playlists/tag/<str:tag>", views.tagged_playlists, name='tagged_playlists'),
-    path("playlists/tag/<str:tag>/edit", views.edit_tag, name='edit_tag'),
-    path("playlists/tag/<str:tag>/delete", views.delete_tag, name='delete_tag'),
+    # STUFF RELATED TO ONE PLAYLIST
+    path('playlist/<slug:playlist_id>', views.view_playlist, name='playlist'),
+    path('playlist/<slug:playlist_id>/add-user-label', views.add_playlist_user_label, name='add_playlist_user_label'),
+    path(
+        'playlist/<slug:playlist_id>/<slug:video_id>/mark/watched',
+        views.mark_video_watched,
+        name='mark_video_watched'
+    ),
+    path('playlist/<slug:playlist_id>/settings', views.view_playlist_settings, name='view_playlist_settings'),
+    path('playlist/<slug:playlist_id>/order-by/<slug:order_by>', views.order_playlist_by, name='order_playlist_by'),
+    path('playlist/<slug:playlist_id>/mark-as/<slug:mark_as>', views.mark_playlist_as, name='mark_playlist_as'),
+    path('playlist/<slug:playlist_id>/update/<slug:command>', views.update_playlist, name='update_playlist'),
+    path(
+        'playlist/<slug:playlist_id>/update-settings', views.update_playlist_settings, name='update_playlist_settings'
+    ),
+    path(
+        'playlist/<slug:playlist_id>/<slug:order_by>/load-more-videos/<int:page>',
+        views.load_more_videos,
+        name='load_more_videos'
+    ),
+    path('playlist/<slug:playlist_id>/create-tag', views.create_playlist_tag, name='create_playlist_tag'),
+    path('playlist/<slug:playlist_id>/add-tag', views.add_playlist_tag, name='add_playlist_tag'),
+    path(
+        'playlist/<slug:playlist_id>/remove-tag/<str:tag_name>', views.remove_playlist_tag, name='remove_playlist_tag'
+    ),
+    path('playlist/<slug:playlist_id>/get-tags', views.get_playlist_tags, name='get_playlist_tags'),
+    path(
+        'playlist/<slug:playlist_id>/get-unused-tags', views.get_unused_playlist_tags, name='get_unused_playlist_tags'
+    ),
+    path('playlist/<slug:playlist_id>/get-watch-message', views.get_watch_message, name='get_watch_message'),
+    path(
+        'playlist/<slug:playlist_id>/delete-videos/<slug:command>', views.playlist_delete_videos, name='delete_videos'
+    ),
+    path(
+        'playlist/<slug:playlist_id>/delete-specific-videos/<slug:command>',
+        views.delete_specific_videos,
+        name='delete_specific_videos'
+    ),
+    path('playlist/<slug:playlist_id>/delete-playlist', views.delete_playlist, name='delete_playlist'),
+    path('playlist/<slug:playlist_id>/reset-watched', views.reset_watched, name='reset_watched'),
+    path(
+        'playlist/<slug:playlist_id>/move-copy-videos/<str:action>',
+        views.playlist_move_copy_videos,
+        name='playlist_move_copy_videos'
+    ),
+    path(
+        'playlist/<slug:playlist_id>/open-random-video',
+        views.playlist_open_random_video,
+        name='playlist_open_random_video'
+    ),
+    path(
+        'playlist/<slug:playlist_id>/get-playlist-completion-times',
+        views.playlist_completion_times,
+        name='playlist_completion_times'
+    ),
+    path('playlist/<slug:playlist_id>/add-new-videos', views.playlist_add_new_videos, name='playlist_add_new_videos'),
+    path(
+        'playlist/<slug:playlist_id>/create-new-playlist',
+        views.playlist_create_new_playlist,
+        name='playlist_create_new_playlist'
+    ),
 
+    # STUFF RELATED TO PLAYLISTS IN BULK
+    path(
+        'playlists/<slug:playlist_type>/order-by/<slug:order_by>', views.order_playlists_by, name='order_playlists_by'
+    ),
+    path('playlists/tag/<str:tag>', views.tagged_playlists, name='tagged_playlists'),
+    path('playlists/tag/<str:tag>/edit', views.edit_tag, name='edit_tag'),
+    path('playlists/tag/<str:tag>/delete', views.delete_tag, name='delete_tag'),
 ]

+ 20 - 24
backend/main/util.py

@@ -1,7 +1,7 @@
 import datetime
-import humanize
 import re
 
+import humanize
 # given amount of seconds makes it into this 54 MINUTES AND 53 SECONDS (from humanize) then
 # perform a bunch of replace operations to make it look like 54mins. 53secs.
 import pytz
@@ -10,26 +10,26 @@ import pytz
 def getHumanizedTimeString(seconds):
     return humanize.precisedelta(
         datetime.timedelta(seconds=seconds)).upper(). \
-        replace(" month".upper(), "m.").replace(" months".upper(), "m.").replace(" days".upper(), "d.").replace(
-        " day".upper(), "d.").replace(" hours".upper(), "hrs.").replace(" hour".upper(), "hr.").replace(
-        " minutes".upper(), "mins.").replace(" minute".upper(), "min.").replace(
-        "and".upper(), "").replace(" seconds".upper(), "secs.").replace(" second".upper(), "sec.").replace(",", "")
+        replace(' month'.upper(), 'm.').replace(' months'.upper(), 'm.').replace(' days'.upper(), 'd.').replace(
+        ' day'.upper(), 'd.').replace(' hours'.upper(), 'hrs.').replace(' hour'.upper(), 'hr.').replace(
+        ' minutes'.upper(), 'mins.').replace(' minute'.upper(), 'min.').replace(
+        'and'.upper(), '').replace(' seconds'.upper(), 'secs.').replace(' second'.upper(), 'sec.').replace(',', '')
 
 
-# input => ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", ..., "100"]  <- array of 100 video ids
-# output => ["1,2,3,4,...,50", "51,52,53,...,100"]  <- array of 2 video id strings, each with upto 50 comma sperated video ids
+# input => ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ..., '100']  <- array of 100 video ids
+# output => ['1,2,3,4,...,50', '51,52,53,...,100']  <- array of 2 video id strings, each with upto 50 comma sperated video ids
 def getVideoIdsStrings(video_ids):
     output = []
 
     i = 0
     while i < len(video_ids):
-        output.append(",".join(video_ids[i:i + 50]))
+        output.append(','.join(video_ids[i:i + 50]))
         i += 50
 
     return output
 
 
-# input: array of youtube video duration strings => ["24M23S", "2H2M2S", ...]
+# input: array of youtube video duration strings => ['24M23S', '2H2M2S', ...]
 # output:integer => seconds
 def calculateDuration(vid_durations):
     hours_pattern = re.compile(r'(\d+)H')
@@ -38,19 +38,15 @@ def calculateDuration(vid_durations):
 
     total_seconds = 0
     for duration in vid_durations:
-        hours = hours_pattern.search(duration)  # returns matches in the form "24H"
-        mins = minutes_pattern.search(duration)  # "24M"
-        secs = seconds_pattern.search(duration)  # "24S"
+        hours = hours_pattern.search(duration)  # returns matches in the form '24H'
+        mins = minutes_pattern.search(duration)  # '24M'
+        secs = seconds_pattern.search(duration)  # '24S'
 
         hours = int(hours.group(1)) if hours else 0  # returns 24
         mins = int(mins.group(1)) if mins else 0
         secs = int(secs.group(1)) if secs else 0
 
-        video_seconds = datetime.timedelta(
-            hours=hours,
-            minutes=mins,
-            seconds=secs
-        ).total_seconds()
+        video_seconds = datetime.timedelta(hours=hours, minutes=mins, seconds=secs).total_seconds()
 
         total_seconds += video_seconds
 
@@ -58,16 +54,16 @@ def calculateDuration(vid_durations):
 
 
 def getThumbnailURL(thumbnails):
-    priority = ("maxres", "standard", "high", "medium", "default")
+    priority = ('maxres', 'standard', 'high', 'medium', 'default')
 
     for quality in priority:
         if quality in thumbnails:
-            return thumbnails[quality]["url"]
+            return thumbnails[quality]['url']
 
     return ''
 
 
-# generates a message in the form of "1 / 19 watched! 31mins. 15secs. left to go!"
+# generates a message in the form of '1 / 19 watched! 31mins. 15secs. left to go!'
 def generateWatchingMessage(playlist):
     """
     This is the message that will be seen when a playlist is set to watching.
@@ -81,14 +77,14 @@ def getVideoId(video_link):
     """
     takes in a valid video link and returns a video id
     """
-    if "?" not in video_link:
+    if '?' not in video_link:
         return video_link
 
-    temp = video_link.split("?")[-1].split("&")
+    temp = video_link.split('?')[-1].split('&')
 
     for el in temp:
-        if "v=" in el:
-            return el.split("v=")[-1]
+        if 'v=' in el:
+            return el.split('v=')[-1]
 
 
 def increment_tag_views(playlist_tags):

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 359 - 295
backend/main/views.py


+ 2 - 2
backend/manage.py

@@ -13,8 +13,8 @@ def main():
     except ImportError as exc:
         raise ImportError(
             "Couldn't import Django. Are you sure it's installed and "
-            "available on your PYTHONPATH environment variable? Did you "
-            "forget to activate a virtual environment?"
+            'available on your PYTHONPATH environment variable? Did you '
+            'forget to activate a virtual environment?'
         ) from exc
     execute_from_command_line(sys.argv)
 

+ 1 - 1
backend/manage_playlists/admin.py

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

+ 1 - 1
backend/manage_playlists/models.py

@@ -1,3 +1,3 @@
-from django.db import models
+# from django.db import models
 
 # Create your models here.

+ 1 - 1
backend/manage_playlists/tests.py

@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
 
 # Create your tests here.

+ 10 - 7
backend/manage_playlists/urls.py

@@ -1,12 +1,15 @@
 from django.urls import path
+
 from . import views
 
 urlpatterns = [
-    ### STUFF RELATED TO MANAGING A PLAYLIST
-    path("", views.manage_playlists, name='manage_playlists'),
-    path("save/<slug:what>", views.manage_save, name='manage_save'),  # to help auto save the input texts found in the below pages
-    path("view/<slug:page>", views.manage_view_page, name='manage_view_page'),  # views the import pl, create pl, create untube pl pages
-    path("import", views.manage_import_playlists, name="manage_import_playlists"),
-    path("create", views.manage_create_playlist, name="manage_create_playlist"),
-    path("nuke", views.manage_nuke_playlists, name="manage_nuke_playlists"),
+    # STUFF RELATED TO MANAGING A PLAYLIST
+    path('', views.manage_playlists, name='manage_playlists'),
+    path('save/<slug:what>', views.manage_save,
+         name='manage_save'),  # to help auto save the input texts found in the below pages
+    path('view/<slug:page>', views.manage_view_page,
+         name='manage_view_page'),  # views the import pl, create pl, create untube pl pages
+    path('import', views.manage_import_playlists, name='manage_import_playlists'),
+    path('create', views.manage_create_playlist, name='manage_create_playlist'),
+    path('nuke', views.manage_nuke_playlists, name='manage_nuke_playlists'),
 ]

+ 30 - 25
backend/manage_playlists/views.py

@@ -9,16 +9,18 @@ from backend.main.models import Playlist
 
 @login_required
 def manage_playlists(request):
-    return render(request, "manage_playlists.html")
+    return render(request, 'manage_playlists.html')
 
 
 @login_required
 def manage_view_page(request, page):
-    if page == "import":
-        return render(request, "manage_playlists_import.html",
-                      {"manage_playlists_import_textarea": request.user.profile.manage_playlists_import_textarea})
-    elif page == "create":
-        return render(request, "manage_playlists_create.html")
+    if page == 'import':
+        return render(
+            request, 'manage_playlists_import.html',
+            {'manage_playlists_import_textarea': request.user.profile.manage_playlists_import_textarea}
+        )
+    elif page == 'create':
+        return render(request, 'manage_playlists_create.html')
     else:
         return HttpResponse('Working on this!')
 
@@ -26,17 +28,19 @@ def manage_view_page(request, page):
 @login_required
 @require_POST
 def manage_save(request, what):
-    if what == "manage_playlists_import_textarea":
-        request.user.profile.manage_playlists_import_textarea = bleach.clean(request.POST["import-playlist-textarea"])
+    if what == 'manage_playlists_import_textarea':
+        request.user.profile.manage_playlists_import_textarea = bleach.clean(request.POST['import-playlist-textarea'])
         request.user.save()
 
-    return HttpResponse("")
+    return HttpResponse('')
 
 
 @login_required
 @require_POST
 def manage_import_playlists(request):
-    playlist_links = [bleach.clean(link) for link in request.POST["import-playlist-textarea"].replace(",", "").split("\n")]
+    playlist_links = [
+        bleach.clean(link) for link in request.POST['import-playlist-textarea'].replace(',', '').split('\n')
+    ]
 
     num_playlists_already_in_db = 0
     num_playlists_initialized_in_db = 0
@@ -47,15 +51,15 @@ def manage_import_playlists(request):
 
     done = []
     for playlist_link in playlist_links:
-        if playlist_link.strip() != "" and playlist_link.strip() not in done:
+        if playlist_link.strip() != '' and playlist_link.strip() not in done:
             pl_id = Playlist.objects.getPlaylistId(playlist_link.strip())
             if pl_id is None:
                 num_playlists_not_found += 1
                 continue
 
-            status = Playlist.objects.initializePlaylist(request.user, pl_id)["status"]
+            status = Playlist.objects.initializePlaylist(request.user, pl_id)['status']
             if status == -1 or status == -2:
-                print("\nNo such playlist found:", pl_id)
+                print('\nNo such playlist found:', pl_id)
                 num_playlists_not_found += 1
                 not_found_playlists.append(playlist_link)
             elif status == -3:  # playlist already in db
@@ -70,29 +74,30 @@ def manage_import_playlists(request):
                 num_playlists_initialized_in_db += 1
             done.append(playlist_link.strip())
 
-    request.user.profile.manage_playlists_import_textarea = ""
+    request.user.profile.manage_playlists_import_textarea = ''
     request.user.save()
 
-    return HttpResponse(loader.get_template("intercooler/manage_playlists_import_results.html")
-        .render(
-        {"new_playlists": new_playlists,
-         "old_playlists": old_playlists,
-         "not_found_playlists": not_found_playlists,
-         "num_playlists_already_in_db": num_playlists_already_in_db,
-         "num_playlists_initialized_in_db": num_playlists_initialized_in_db,
-         "num_playlists_not_found": num_playlists_not_found
-         }))
+    return HttpResponse(
+        loader.get_template('intercooler/manage_playlists_import_results.html').render({
+            'new_playlists': new_playlists,
+            'old_playlists': old_playlists,
+            'not_found_playlists': not_found_playlists,
+            'num_playlists_already_in_db': num_playlists_already_in_db,
+            'num_playlists_initialized_in_db': num_playlists_initialized_in_db,
+            'num_playlists_not_found': num_playlists_not_found
+        })
+    )
 
 
 @login_required
 @require_POST
 def manage_create_playlist(request):
     print(request.POST)
-    return HttpResponse("")
+    return HttpResponse('')
 
 
 @login_required
 @require_POST
 def manage_nuke_playlists(request):
     print(request.POST)
-    return HttpResponse("")
+    return HttpResponse('')

+ 1 - 1
backend/search/admin.py

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

+ 1 - 1
backend/search/models.py

@@ -1,3 +1,3 @@
-from django.db import models
+# from django.db import models
 
 # Create your models here.

+ 1 - 1
backend/search/tests.py

@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
 
 # Create your tests here.

+ 5 - 4
backend/search/urls.py

@@ -1,9 +1,10 @@
 from django.urls import path
+
 from . import views
 
 urlpatterns = [
-    path("", views.search, name="search"),
-    path("untube/", views.search_UnTube, name="search_UnTube"),
-    path("library/<slug:library_type>", views.search_library, name="search_library"),
-    path("tagged-playlists/<str:tag>", views.search_tagged_playlists, name="search_tagged_playlists"),
+    path('', views.search, name='search'),
+    path('untube/', views.search_UnTube, name='search_UnTube'),
+    path('library/<slug:library_type>', views.search_library, name='search_library'),
+    path('tagged-playlists/<str:tag>', views.search_tagged_playlists, name='search_tagged_playlists'),
 ]

+ 115 - 97
backend/search/views.py

@@ -2,48 +2,52 @@ import bleach
 from django.contrib.auth.decorators import login_required
 from django.db.models import Q
 from django.http import HttpResponse
-from django.shortcuts import render, redirect
+from django.shortcuts import redirect, render
 from django.template import loader
 from django.views.decorators.http import require_POST
+
 from backend.general.utils.misc import print_
 
 
 @login_required
 def search(request):
-    if request.method == "GET":
+    if request.method == 'GET':
         print_(request.GET)
         if 'mode' in request.GET:
             mode = bleach.clean(request.GET['mode'])
         else:
-            mode = "playlists"
+            mode = 'playlists'
 
         if 'type' in request.GET:
-            item_type = bleach.clean(request.GET["type"])
+            item_type = bleach.clean(request.GET['type'])
         else:
-            item_type = "all"
+            item_type = 'all'
 
         if 'query' in request.GET:
-            query = bleach.clean(request.GET["query"])
+            query = bleach.clean(request.GET['query'])
         else:
             query = ''
 
         if 'tag' in request.GET:
-            pl_tag = bleach.clean(request.GET["tag"])
+            pl_tag = bleach.clean(request.GET['tag'])
         else:
-            pl_tag = ""
+            pl_tag = ''
 
         if 'channel' in request.GET:
-            vid_channel_name = bleach.clean(request.GET["channel"])
+            vid_channel_name = bleach.clean(request.GET['channel'])
         else:
-            vid_channel_name = ""
-
-        return render(request, 'search_untube_page.html',
-                      {"playlists": request.user.playlists.all(),
-                       "mode": mode,
-                       "item_type": item_type,
-                       "query": query,
-                       "pl_tag": pl_tag,
-                       "vid_channel_name": vid_channel_name})
+            vid_channel_name = ''
+
+        return render(
+            request, 'search_untube_page.html', {
+                'playlists': request.user.playlists.all(),
+                'mode': mode,
+                'item_type': item_type,
+                'query': query,
+                'pl_tag': pl_tag,
+                'vid_channel_name': vid_channel_name
+            }
+        )
     else:
         return redirect('home')
 
@@ -51,23 +55,23 @@ def search(request):
 @login_required
 @require_POST
 def search_UnTube(request):
-    search_query = bleach.clean(request.POST["search"])
+    search_query = bleach.clean(request.POST['search'])
 
     if request.POST['search-settings'] == 'playlists':
-        playlist_type = bleach.clean(request.POST["playlistsType"])
+        playlist_type = bleach.clean(request.POST['playlistsType'])
 
         all_playlists = request.user.playlists.filter(is_in_db=True)
-        if playlist_type == "Favorite":
+        if playlist_type == 'Favorite':
             all_playlists = all_playlists.filter(is_favorite=True)
-        elif playlist_type == "Watching":
-            all_playlists = all_playlists.filter(marked_as="watching")
-        elif playlist_type == "Plan to Watch":
-            all_playlists = all_playlists.filter(marked_as="plan-to-watch")
-        elif playlist_type == "Owned":
+        elif playlist_type == 'Watching':
+            all_playlists = all_playlists.filter(marked_as='watching')
+        elif playlist_type == 'Plan to Watch':
+            all_playlists = all_playlists.filter(marked_as='plan-to-watch')
+        elif playlist_type == 'Owned':
             all_playlists = all_playlists.filter(is_user_owned=True)
-        elif playlist_type == "Imported":
+        elif playlist_type == 'Imported':
             all_playlists = all_playlists.filter(is_user_owned=False)
-        elif playlist_type == "Mix":
+        elif playlist_type == 'Mix':
             all_playlists = all_playlists.filter(is_yt_mix=True)
 
         if 'playlist-tags' in request.POST:
@@ -75,69 +79,68 @@ def search_UnTube(request):
             for tag in tags:
                 all_playlists = all_playlists.filter(tags__name=tag)
 
-        playlists = all_playlists.filter(Q(name__istartswith=search_query) | Q(
-            user_label__istartswith=search_query))
+        playlists = all_playlists.filter(Q(name__istartswith=search_query) | Q(user_label__istartswith=search_query))
 
         if not playlists.exists():
-            playlists = all_playlists.filter(Q(name__icontains=search_query) | Q(
-                user_label__icontains=search_query))
+            playlists = all_playlists.filter(Q(name__icontains=search_query) | Q(user_label__icontains=search_query))
 
-        if search_query.strip() == "":
+        if search_query.strip() == '':
             playlists = all_playlists
 
         order_by = bleach.clean(request.POST['sortPlaylistsBy'])
         if order_by == 'recently-accessed':
-            playlists = playlists.order_by("-updated_at")
+            playlists = playlists.order_by('-updated_at')
         elif order_by == 'playlist-duration-in-seconds':
-            playlists = playlists.order_by("-playlist_duration_in_seconds")
+            playlists = playlists.order_by('-playlist_duration_in_seconds')
         elif order_by == 'video-count':
-            playlists = playlists.order_by("-video_count")
-
-        return HttpResponse(loader.get_template("intercooler/search_untube_results.html")
-                            .render({"playlists": playlists,
-                                     "view_mode": "playlists",
-                                     "search_query": search_query,
-                                     "playlist_type": playlist_type}))
+            playlists = playlists.order_by('-video_count')
+
+        return HttpResponse(
+            loader.get_template('intercooler/search_untube_results.html').render({
+                'playlists': playlists,
+                'view_mode': 'playlists',
+                'search_query': search_query,
+                'playlist_type': playlist_type
+            })
+        )
     else:
-        videos_type = bleach.clean(request.POST["videosType"])
+        videos_type = bleach.clean(request.POST['videosType'])
 
         all_videos = request.user.videos.filter(is_unavailable_on_yt=False)
-        if videos_type == "Liked":
+        if videos_type == 'Liked':
             all_videos = all_videos.filter(liked=True)
-        elif videos_type == "Favorite":
+        elif videos_type == 'Favorite':
             all_videos = all_videos.filter(is_favorite=True)
-        elif videos_type == "Watched":
+        elif videos_type == 'Watched':
             all_videos = all_videos.filter(is_marked_as_watched=True)
-        elif videos_type == "Planned to Watch":
+        elif videos_type == 'Planned to Watch':
             all_videos = all_videos.filter(is_planned_to_watch=True)
-        elif videos_type == "Unavailable":
+        elif videos_type == 'Unavailable':
             all_videos = all_videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=True))
 
         if 'channel-names' in request.POST:
             channels = [bleach.clean(name) for name in request.POST.getlist('channel-names')]
             all_videos = all_videos.filter(channel_name__in=channels)
 
-        videos = all_videos.filter(
-            Q(name__istartswith=search_query) | Q(user_label__istartswith=search_query))
+        videos = all_videos.filter(Q(name__istartswith=search_query) | Q(user_label__istartswith=search_query))
 
         if not videos.exists():
-            videos = all_videos.filter(Q(name__icontains=search_query) | Q(
-                user_label__icontains=search_query))
+            videos = all_videos.filter(Q(name__icontains=search_query) | Q(user_label__icontains=search_query))
 
-        if search_query.strip() == "":
+        if search_query.strip() == '':
             videos = all_videos
 
         order_by = bleach.clean(request.POST['sortVideosBy'])
         if order_by == 'recently-accessed':
-            videos = videos.order_by("-updated_at")
+            videos = videos.order_by('-updated_at')
         elif order_by == 'video-duration-in-seconds':
-            videos = videos.order_by("-duration_in_seconds")
+            videos = videos.order_by('-duration_in_seconds')
         elif order_by == 'most-liked':
-            videos = videos.order_by("-like_count")
+            videos = videos.order_by('-like_count')
         elif order_by == 'most-views':
-            videos = videos.order_by("-view_count")
+            videos = videos.order_by('-view_count')
         elif order_by == 'date-uploaded':
-            videos = videos.order_by("-published_at")
+            videos = videos.order_by('-published_at')
 
         if 'has-cc' in request.POST:
             videos = videos.filter(has_cc=True)
@@ -146,11 +149,14 @@ def search_UnTube(request):
             playlist_ids = [bleach.clean(pl_id) for pl_id in request.POST.getlist('playlist-ids')]
             videos = videos.filter(playlists__playlist_id__in=playlist_ids)
 
-        return HttpResponse(loader.get_template("intercooler/search_untube_results.html")
-                            .render({"videos": videos,
-                                     "view_mode": "videos",
-                                     "videos_type": videos_type,
-                                     "search_query": search_query}))
+        return HttpResponse(
+            loader.get_template('intercooler/search_untube_results.html').render({
+                'videos': videos,
+                'view_mode': 'videos',
+                'videos_type': videos_type,
+                'search_query': search_query
+            })
+        )
 
 
 @login_required
@@ -158,69 +164,81 @@ def search_UnTube(request):
 def search_library(request, library_type):
     # print_(request.POST)  # prints <QueryDict: {'search': ['aa']}>
 
-    search_query = bleach.clean(request.POST["search"])
+    search_query = bleach.clean(request.POST['search'])
     watching = False
 
     playlists = None
-    if library_type == "all":
+    if library_type == 'all':
         try:
-            playlists = request.user.playlists.all().filter(Q(is_in_db=True)).filter(Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-        except:
+            playlists = request.user.playlists.all().filter(
+                Q(is_in_db=True)
+            ).filter(Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
+        except Exception:
             playlists = request.user.playlists.all().filter(is_in_db=True)
-    elif library_type == "user-owned":  # YT playlists owned by user
+    elif library_type == 'user-owned':  # YT playlists owned by user
         try:
             playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True)).filter(
-                Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-        except:
+                Q(name__startswith=search_query) | Q(user_label__startswith=search_query)
+            )
+        except Exception:
             playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True))
-    elif library_type == "imported":  # YT playlists (public) owned by others
+    elif library_type == 'imported':  # YT playlists (public) owned by others
         try:
             playlists = request.user.playlists.filter(Q(is_user_owned=False) & Q(is_in_db=True)).filter(
-                Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-        except:
+                Q(name__startswith=search_query) | Q(user_label__startswith=search_query)
+            )
+        except Exception:
             playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True))
-    elif library_type == "favorites":  # YT playlists (public) owned by others
+    elif library_type == 'favorites':  # YT playlists (public) owned by others
         try:
             playlists = request.user.playlists.filter(Q(is_favorite=True) & Q(is_in_db=True)).filter(
-                Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-        except:
+                Q(name__startswith=search_query) | Q(user_label__startswith=search_query)
+            )
+        except Exception:
             playlists = request.user.playlists.filter(Q(is_favorite=True) & Q(is_in_db=True))
-    elif library_type in ["watching", "plan-to-watch"]:
+    elif library_type in ['watching', 'plan-to-watch']:
         try:
             playlists = request.user.playlists.filter(Q(marked_as=library_type) & Q(is_in_db=True)).filter(
-                Q(name__startswith=search_query) | Q(user_label__startswith=search_query) )
-        except:
+                Q(name__startswith=search_query) | Q(user_label__startswith=search_query)
+            )
+        except Exception:
             playlists = request.user.playlists.all().filter(Q(marked_as=library_type) & Q(is_in_db=True))
-        if library_type == "watching":
+        if library_type == 'watching':
             watching = True
-    elif library_type == "yt-mix":  # YT playlists owned by user
+    elif library_type == 'yt-mix':  # YT playlists owned by user
         try:
             playlists = request.user.playlists.filter(Q(is_yt_mix=True) & Q(is_in_db=True)).filter(
-                Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-        except:
+                Q(name__startswith=search_query) | Q(user_label__startswith=search_query)
+            )
+        except Exception:
             playlists = request.user.playlists.filter(Q(is_yt_mix=True) & Q(is_in_db=True))
     elif library_type == 'unavailable-videos':
         try:
-            videos = request.user.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=True)).filter(Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-        except:
+            videos = request.user.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=True)).filter(
+                Q(name__startswith=search_query) | Q(user_label__startswith=search_query)
+            )
+        except Exception:
             videos = request.user.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=True))
-        return HttpResponse(loader.get_template("intercooler/video_cards.html").render({"videos": videos}))
+        return HttpResponse(loader.get_template('intercooler/video_cards.html').render({'videos': videos}))
 
-    return HttpResponse(loader.get_template("intercooler/playlists.html")
-                        .render({"playlists": playlists.order_by("-updated_at"),
-                                 "show_controls": True,
-                                 "watching": watching}))
+    return HttpResponse(
+        loader.get_template('intercooler/playlists.html').render({
+            'playlists': playlists.order_by('-updated_at'),
+            'show_controls': True,
+            'watching': watching
+        })
+    )
 
 
 @login_required
 @require_POST
 def search_tagged_playlists(request, tag):
-    search_query = bleach.clean(request.POST["search"])
+    search_query = bleach.clean(request.POST['search'])
     try:
-        playlists = request.user.playlists.all().filter(Q(is_in_db=True) & Q(tags__name=tag)).filter(
-            Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
-    except:
-        playlists = request.user.playlists.all().filter(Q(is_in_db=True) & Q(tags__name=tag)).order_by("-updated_at")
+        playlists = request.user.playlists.all(
+        ).filter(Q(is_in_db=True) &
+                 Q(tags__name=tag)).filter(Q(name__startswith=search_query) | Q(user_label__startswith=search_query))
+    except Exception:
+        playlists = request.user.playlists.all().filter(Q(is_in_db=True) & Q(tags__name=tag)).order_by('-updated_at')
 
-    return HttpResponse(loader.get_template("intercooler/playlists.html")
-                        .render({"playlists": playlists}))
+    return HttpResponse(loader.get_template('intercooler/playlists.html').render({'playlists': playlists}))

+ 2 - 1
backend/users/admin.py

@@ -6,6 +6,7 @@ from backend.users.models import Profile
 
 # Register your models here.
 class ProfileResource(resources.ModelResource):
+
     class Meta:
         model = Profile
 
@@ -16,4 +17,4 @@ class ProfileAdmin(ImportExportModelAdmin):
     list_filter = ('created_at',)
 
 
-admin.site.register(Profile, ProfileAdmin)
+admin.site.register(Profile, ProfileAdmin)

+ 5 - 2
backend/users/migrations/0001_initial.py

@@ -1,8 +1,8 @@
 # Generated by Django 3.2.6 on 2021-12-04 01:50
 
+import django.db.models.deletion
 from django.conf import settings
 from django.db import migrations, models
-import django.db.models.deletion
 
 
 class Migration(migrations.Migration):
@@ -47,7 +47,10 @@ class Migration(migrations.Migration):
                 ('create_playlist_type', models.CharField(default='', max_length=50)),
                 ('create_playlist_add_vids_from_collection', models.CharField(default='', max_length=50)),
                 ('create_playlist_add_vids_from_links', models.CharField(default='', max_length=50)),
-                ('untube_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+                (
+                    'untube_user',
+                    models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)
+                ),
             ],
         ),
     ]

+ 22 - 21
backend/users/models.py

@@ -1,12 +1,12 @@
-from django.db import models
+from allauth.socialaccount.models import SocialToken
+from django.conf import settings
 from django.contrib.auth.models import User
+from django.db import models
 from django.db.models import Count, Q
 from django.db.models.signals import post_save
 from django.dispatch import receiver
-from allauth.socialaccount.models import SocialToken
-from google.oauth2.credentials import Credentials
 from google.auth.transport.requests import Request
-from django.conf import settings
+from google.oauth2.credentials import Credentials
 
 
 class Untube(models.Model):
@@ -22,10 +22,10 @@ class Profile(models.Model):
 
     # settings
     robohash_set = models.IntegerField(default=3)  # determines profile picture from https://robohash.org/
-    user_summary = models.CharField(max_length=300, default="I think my arm is on backward.")
-    user_location = models.CharField(max_length=100, default="Hell, Earth")
+    user_summary = models.CharField(max_length=300, default='I think my arm is on backward.')
+    user_location = models.CharField(max_length=100, default='Hell, Earth')
 
-    ### GLOBAL preferences ###
+    # GLOBAL preferences
     # site preferences
     open_search_new_tab = models.BooleanField(default=True)  # open search page in new tab by default
     enable_gradient_bg = models.BooleanField(default=False)
@@ -39,33 +39,34 @@ class Profile(models.Model):
     show_import_page = models.BooleanField(default=True)  # shows the user tips for a week
     yt_channel_id = models.TextField(default='')
     import_in_progress = models.BooleanField(
-        default=False)  # if True, will not let the user access main site until they import their YT playlists
+        default=False
+    )  # if True, will not let the user access main site until they import their YT playlists
     imported_yt_playlists = models.BooleanField(default=False)  # True if user imported all their YT playlists
 
     # google api token details
-    access_token = models.TextField(default="")
-    refresh_token = models.TextField(default="")
+    access_token = models.TextField(default='')
+    refresh_token = models.TextField(default='')
     expires_at = models.DateTimeField(blank=True, null=True)
 
     # import playlist page
-    manage_playlists_import_textarea = models.TextField(default="")
+    manage_playlists_import_textarea = models.TextField(default='')
 
     # create playlist page
-    create_playlist_name = models.CharField(max_length=50, default="")
-    create_playlist_desc = models.CharField(max_length=50, default="")
-    create_playlist_type = models.CharField(max_length=50, default="")
-    create_playlist_add_vids_from_collection = models.CharField(max_length=50, default="")
-    create_playlist_add_vids_from_links = models.CharField(max_length=50, default="")
+    create_playlist_name = models.CharField(max_length=50, default='')
+    create_playlist_desc = models.CharField(max_length=50, default='')
+    create_playlist_type = models.CharField(max_length=50, default='')
+    create_playlist_add_vids_from_collection = models.CharField(max_length=50, default='')
+    create_playlist_add_vids_from_links = models.CharField(max_length=50, default='')
 
     def __str__(self):
-        return f"{self.untube_user.username} ({self.untube_user.email})"
+        return f'{self.untube_user.username} ({self.untube_user.email})'
 
     def get_credentials(self):
         """
         Returns Google OAuth credentials object by using user's OAuth token
         """
         # if the profile model does not hold the tokens, retrieve them from user's SocialToken entry and save them into profile
-        if self.access_token.strip() == "" or self.refresh_token.strip() == "":
+        if self.access_token.strip() == '' or self.refresh_token.strip() == '':
             user_social_token = SocialToken.objects.get(account__user=self.untube_user)
             self.access_token = user_social_token.token
             self.refresh_token = user_social_token.token_secret
@@ -76,7 +77,7 @@ class Profile(models.Model):
         credentials = Credentials(
             token=self.access_token,
             refresh_token=self.refresh_token,
-            token_uri="https://oauth2.googleapis.com/token",
+            token_uri='https://oauth2.googleapis.com/token',
             client_id=settings.GOOGLE_OAUTH_CLIENT_ID,  # app.client_id,
             client_secret=settings.GOOGLE_OAUTH_CLIENT_SECRET,  # app.secret,
             scopes=['https://www.googleapis.com/auth/youtube']
@@ -94,8 +95,8 @@ class Profile(models.Model):
         channels_list = []
         videos = self.untube_user.videos.filter(Q(is_unavailable_on_yt=False) & Q(was_deleted_on_yt=False))
 
-        queryset = videos.values(
-            'channel_name').annotate(channel_videos_count=Count('video_id')).order_by('-channel_videos_count')
+        queryset = videos.values('channel_name').annotate(channel_videos_count=Count('video_id')
+                                                          ).order_by('-channel_videos_count')
 
         for entry in queryset:
             channels_list.append(entry['channel_name'])

+ 1 - 1
backend/users/tests.py

@@ -1,3 +1,3 @@
-from django.test import TestCase
+# from django.test import TestCase
 
 # Create your tests here.

+ 16 - 18
backend/users/urls.py

@@ -1,23 +1,21 @@
 from django.urls import path
+
 from . import views
 
 urlpatterns = [
-    path("", views.index, name='index'),
-    path("like-untube/", views.like_untube, name="like_untube"),
-    path("unlike-untube/", views.unlike_untube, name="unlike_untube"),
-
-    path("profile/", views.profile, name='profile'),
-    path("about/", views.about, name='about'),
-    path("logout/", views.log_out, name='log_out'),
-    path("update/settings", views.update_settings, name='update_settings'),
-    path("delete/account", views.delete_account, name='delete_account'),
-    path('settings/', views.user_settings, name="settings"),
-
-    path("import/liked-videos-playlist", views.get_user_liked_videos_playlist, name="get_user_liked_videos_playlist"),
-    path("import/init", views.import_user_yt_playlists, name='import_user_yt_playlists'),
-    path("import/start", views.start_import, name='start'),
-    path("import/continue", views.continue_import, name='continue'),
-    path("import/cancel", views.cancel_import, name='cancel'),
-
-    path("updates/user-playlists/<slug:action>", views.user_playlists_updates, name='user_playlists_updates'),
+    path('', views.index, name='index'),
+    path('like-untube/', views.like_untube, name='like_untube'),
+    path('unlike-untube/', views.unlike_untube, name='unlike_untube'),
+    path('profile/', views.profile, name='profile'),
+    path('about/', views.about, name='about'),
+    path('logout/', views.log_out, name='log_out'),
+    path('update/settings', views.update_settings, name='update_settings'),
+    path('delete/account', views.delete_account, name='delete_account'),
+    path('settings/', views.user_settings, name='settings'),
+    path('import/liked-videos-playlist', views.get_user_liked_videos_playlist, name='get_user_liked_videos_playlist'),
+    path('import/init', views.import_user_yt_playlists, name='import_user_yt_playlists'),
+    path('import/start', views.start_import, name='start'),
+    path('import/continue', views.continue_import, name='continue'),
+    path('import/cancel', views.cancel_import, name='cancel'),
+    path('updates/user-playlists/<slug:action>', views.user_playlists_updates, name='user_playlists_updates'),
 ]

+ 178 - 150
backend/users/views.py

@@ -1,19 +1,21 @@
 import bleach
-from django.db.models import Q
-from django.shortcuts import render, redirect
+from allauth.socialaccount.models import SocialApp
+from django.conf import settings
+from django.contrib import messages
 from django.contrib.auth import logout
-from django.views.decorators.http import require_POST
 from django.contrib.auth.decorators import login_required
-from allauth.socialaccount.models import SocialApp
-from django.http import HttpResponse
 from django.contrib.auth.models import User
-from django.contrib import messages
-from backend.main.models import Playlist
-from .models import Untube
-from django.template import loader
-from django.conf import settings
 from django.contrib.sites.models import Site
+from django.db.models import Q
+from django.http import HttpResponse
+from django.shortcuts import redirect, render
+from django.template import loader
+from django.views.decorators.http import require_POST
+
+from backend.main.models import Playlist
+
 from ..general.utils.misc import print_
+from .models import Untube
 
 
 # Create your views here.
@@ -23,19 +25,21 @@ def index(request):
         untube.save()
 
     if settings.GOOGLE_OAUTH_CLIENT_ID is NotImplemented or settings.GOOGLE_OAUTH_CLIENT_SECRET is NotImplemented:
-        messages.error(request, "Please fill in your Google OAuth credentials in the local/settings.dev.py file")
+        messages.error(request, 'Please fill in your Google OAuth credentials in the local/settings.dev.py file')
     else:
-        # messages.success(request, "Thanks for filling in the YouTube API key")
+        # messages.success(request, 'Thanks for filling in the YouTube API key')
 
         if not Site.objects.filter(domain=settings.GOOGLE_OAUTH_URI).exists():
             Site.objects.create(domain=settings.GOOGLE_OAUTH_URI, name=settings.GOOGLE_OAUTH_URI)
 
         if not SocialApp.objects.filter(provider='google').exists():  # create google OAuth app
-            print("Creating Google social app...")
-            app = SocialApp.objects.create(provider="google",
-                                           name="UnTube OAuth",
-                                           client_id=settings.GOOGLE_OAUTH_CLIENT_ID,
-                                           secret=settings.GOOGLE_OAUTH_CLIENT_SECRET)
+            print('Creating Google social app...')
+            app = SocialApp.objects.create(
+                provider='google',
+                name='UnTube OAuth',
+                client_id=settings.GOOGLE_OAUTH_CLIENT_ID,
+                secret=settings.GOOGLE_OAUTH_CLIENT_SECRET
+            )
             site_uri = Site.objects.get(domain=settings.GOOGLE_OAUTH_URI)
             app.sites.add(site_uri)
 
@@ -43,12 +47,16 @@ def index(request):
         request.session.create()
         request.session['liked_untube'] = False
 
-    return render(request, 'index.html', {"likes": Untube.objects.all().first().page_likes,
-                                          "users_joined": User.objects.all().count()})
+    return render(
+        request, 'index.html', {
+            'likes': Untube.objects.all().first().page_likes,
+            'users_joined': User.objects.all().count()
+        }
+    )
 
     # if request.user.is_anonymous:
-    #     return render(request, 'index.html', {"likes": Untube.objects.all().first().page_likes,
-    #                                           "users_joined": User.objects.all().count()})
+    #     return render(request, 'index.html', {'likes': Untube.objects.all().first().page_likes,
+    #                                           'users_joined': User.objects.all().count()})
     # else:
     #     return redirect('home')
 
@@ -60,35 +68,32 @@ def about(request):
 @login_required
 def profile(request):
     user_playlists = request.user.playlists.all()
-    watching = user_playlists.filter(marked_as="watching")
+    watching = user_playlists.filter(marked_as='watching')
 
     total_num_playlists = user_playlists.count()
 
-    statistics = {
-        "public_x": 0,
-        "private_x": 0,
-        "favorites_x": 0,
-        "watching_x": 0,
-        "imported_x": 0
-    }
+    statistics = {'public_x': 0, 'private_x': 0, 'favorites_x': 0, 'watching_x': 0, 'imported_x': 0}
 
     if total_num_playlists != 0:
         # x means  percentage
-        statistics["public_x"] = round(user_playlists.filter(is_private_on_yt=False).count() / total_num_playlists,
-                                       1) * 100
-        statistics["private_x"] = round(user_playlists.filter(is_private_on_yt=True).count() / total_num_playlists,
-                                        1) * 100
-        statistics["favorites_x"] = round(user_playlists.filter(is_favorite=True).count() / total_num_playlists,
-                                          1) * 100
-        statistics["watching_x"] = round(user_playlists.filter(marked_as="watching").count() / total_num_playlists,
-                                         1) * 100
-        statistics["imported_x"] = round(user_playlists.filter(is_user_owned=False).count() / total_num_playlists,
-                                         1) * 100
-
-    return render(request, 'profile.html', {
-        "total_num_playlists": total_num_playlists,
-        "statistics": statistics,
-        "watching": watching})
+        statistics['public_x'
+                   ] = round(user_playlists.filter(is_private_on_yt=False).count() / total_num_playlists, 1) * 100
+        statistics['private_x'
+                   ] = round(user_playlists.filter(is_private_on_yt=True).count() / total_num_playlists, 1) * 100
+        statistics['favorites_x'
+                   ] = round(user_playlists.filter(is_favorite=True).count() / total_num_playlists, 1) * 100
+        statistics['watching_x'
+                   ] = round(user_playlists.filter(marked_as='watching').count() / total_num_playlists, 1) * 100
+        statistics['imported_x'
+                   ] = round(user_playlists.filter(is_user_owned=False).count() / total_num_playlists, 1) * 100
+
+    return render(
+        request, 'profile.html', {
+            'total_num_playlists': total_num_playlists,
+            'statistics': statistics,
+            'watching': watching
+        }
+    )
 
 
 @login_required
@@ -101,17 +106,17 @@ def update_settings(request):
     print(request.POST)
     user = request.user
     username_input = bleach.clean(request.POST['username'].strip())
-    message_content = "Saved!"
-    # message_type = "success"
+    message_content = 'Saved!'
+    # message_type = 'success'
     if username_input != user.username:
         if User.objects.filter(username__exact=username_input).count() != 0:
-            # message_type = "danger"
-            message_content = f"Username {username_input} already taken"
+            # message_type = 'danger'
+            message_content = f'Username {username_input} already taken'
             messages.error(request, message_content)
         else:
             user.username = username_input
             # user.save()
-            message_content = f"Username updated to {username_input}!"
+            message_content = f'Username updated to {username_input}!'
             messages.success(request, message_content)
 
     if 'open search in new tab' in request.POST and user.profile.open_search_new_tab is False:
@@ -136,7 +141,7 @@ def update_settings(request):
 
     user.save()
 
-    if message_content == "Saved!":
+    if message_content == 'Saved!':
         messages.success(request, message_content)
 
     return redirect('settings')
@@ -150,7 +155,7 @@ def delete_account(request):
     request.user.profile.delete()
     request.user.delete()
     request.session.flush()
-    messages.success(request, "Account data deleted successfully.")
+    messages.success(request, 'Account data deleted successfully.')
 
     return redirect('index')
 
@@ -160,11 +165,11 @@ def log_out(request):
     request.session.flush()  # delete all stored session keys
     logout(request)  # log out authenticated user
 
-    if "troll" in request.GET:
-        print("TROLLED")
-        messages.success(request, "Hee Hee")
+    if 'troll' in request.GET:
+        print('TROLLED')
+        messages.success(request, 'Hee Hee')
     else:
-        messages.success(request, "Successfully logged out. Hope to see you back again!")
+        messages.success(request, 'Successfully logged out. Hope to see you back again!')
 
     return redirect('/')
 
@@ -196,50 +201,55 @@ def start_import(request):
     user_profile = request.user.profile
 
     result = Playlist.objects.initializePlaylist(request.user)
-    if result["status"] == -1:
-        print("User has no YT channel")
-
-        return HttpResponse(loader.get_template('intercooler/progress_bar.html').render(
-            {
-                "channel_found": False,
-                "error_message": result["error_message"]
-            }
-        ))
-    elif result["status"] == -2:
+    if result['status'] == -1:
+        print('User has no YT channel')
+
+        return HttpResponse(
+            loader.get_template('intercooler/progress_bar.html').render({
+                'channel_found': False,
+                'error_message': result['error_message']
+            })
+        )
+    elif result['status'] == -2:
         user_profile.import_in_progress = False
         user_profile.imported_yt_playlists = True
         user_profile.show_import_page = True
         user_profile.save()
 
-        print("User has no playlists on YT")
+        print('User has no playlists on YT')
 
-        # if request.user.profile.yt_channel_id == "":
+        # if request.user.profile.yt_channel_id == '':
         #     Playlist.objects.getUserYTChannelID(request.user)
 
-        Playlist.objects.initializePlaylist(request.user, "LL")
-
-        return HttpResponse(loader.get_template('intercooler/progress_bar.html').render(
-            {"total_playlists": 0,
-             "playlists_imported": 0,
-             "done": True,
-             "progress": 100,
-             "channel_found": True}))
+        Playlist.objects.initializePlaylist(request.user, 'LL')
+
+        return HttpResponse(
+            loader.get_template('intercooler/progress_bar.html').render({
+                'total_playlists': 0,
+                'playlists_imported': 0,
+                'done': True,
+                'progress': 100,
+                'channel_found': True
+            })
+        )
     else:
-        # if request.user.profile.yt_channel_id == "":
+        # if request.user.profile.yt_channel_id == '':
         #     Playlist.objects.getUserYTChannelID(request.user)
 
-        Playlist.objects.initializePlaylist(request.user, "LL")
+        Playlist.objects.initializePlaylist(request.user, 'LL')
 
         user_profile.import_in_progress = True
         user_profile.save()
 
-        return HttpResponse(loader.get_template('intercooler/progress_bar.html').render(
-            {"total_playlists": result["num_of_playlists"],
-             "playlist_name": result["first_playlist_name"],
-             "playlists_imported": 0,
-             "progress": 0,
-             "channel_found": True}
-        ))
+        return HttpResponse(
+            loader.get_template('intercooler/progress_bar.html').render({
+                'total_playlists': result['num_of_playlists'],
+                'playlist_name': result['first_playlist_name'],
+                'playlists_imported': 0,
+                'progress': 0,
+                'channel_found': True
+            })
+        )
 
 
 @login_required
@@ -247,28 +257,31 @@ def continue_import(request):
     if request.user.profile.import_in_progress is False:
         return redirect('home')
 
-    num_of_playlists = request.user.playlists.filter(Q(is_user_owned=True)).exclude(playlist_id="LL").count()
-    print_("NUM OF PLAYLISTS", num_of_playlists)
+    num_of_playlists = request.user.playlists.filter(Q(is_user_owned=True)).exclude(playlist_id='LL').count()
+    print_('NUM OF PLAYLISTS', num_of_playlists)
     try:
-        remaining_playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=False)).exclude(
-            playlist_id="LL")
-        print_(remaining_playlists.count(), "REMAINING PLAYLISTS")
+        remaining_playlists = request.user.playlists.filter(Q(is_user_owned=True) &
+                                                            Q(is_in_db=False)).exclude(playlist_id='LL')
+        print_(remaining_playlists.count(), 'REMAINING PLAYLISTS')
         playlists_imported = num_of_playlists - remaining_playlists.count() + 1
-        playlist = remaining_playlists.order_by("created_at")[0]
+        playlist = remaining_playlists.order_by('created_at')[0]
         playlist_name = playlist.name
         playlist_id = playlist.playlist_id
         Playlist.objects.getAllVideosForPlaylist(request.user, playlist_id)
-    except:
-        print_("NO REMAINING PLAYLISTS")
+    except Exception:
+        print_('NO REMAINING PLAYLISTS')
         playlist_id = -1
 
     if playlist_id != -1:
-        return HttpResponse(loader.get_template('intercooler/progress_bar.html').render(
-            {"total_playlists": num_of_playlists,
-             "playlists_imported": playlists_imported,
-             "playlist_name": playlist_name,
-             "progress": round((playlists_imported / num_of_playlists) * 100, 1),
-             "channel_found": True}))
+        return HttpResponse(
+            loader.get_template('intercooler/progress_bar.html').render({
+                'total_playlists': num_of_playlists,
+                'playlists_imported': playlists_imported,
+                'playlist_name': playlist_name,
+                'progress': round((playlists_imported / num_of_playlists) * 100, 1),
+                'channel_found': True
+            })
+        )
     else:
         # request.user.profile.just_joined = False
         request.user.profile.import_in_progress = False
@@ -276,15 +289,18 @@ def continue_import(request):
         request.user.profile.show_import_page = True  # set back to true again so as to show users the welcome screen on 'home'
         request.user.save()
 
-        user_pl_count = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True)).exclude(
-            playlist_id="LL").count()
+        user_pl_count = request.user.playlists.filter(Q(is_user_owned=True) &
+                                                      Q(is_in_db=True)).exclude(playlist_id='LL').count()
 
-        return HttpResponse(loader.get_template('intercooler/progress_bar.html').render(
-            {"total_playlists": user_pl_count,
-             "playlists_imported": user_pl_count,
-             "done": True,
-             "progress": 100,
-             "channel_found": True}))
+        return HttpResponse(
+            loader.get_template('intercooler/progress_bar.html').render({
+                'total_playlists': user_pl_count,
+                'playlists_imported': user_pl_count,
+                'done': True,
+                'progress': 100,
+                'channel_found': True
+            })
+        )
 
 
 @login_required
@@ -295,13 +311,13 @@ def user_playlists_updates(request, action):
     If any new playlist id, imports it to UnTube
     """
     if action == 'check-for-updates':
-        user_playlists_on_UnTube = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=True)).exclude(
-            playlist_id="LL")
+        user_playlists_on_UnTube = request.user.playlists.filter(Q(is_user_owned=True) &
+                                                                 Q(is_in_db=True)).exclude(playlist_id='LL')
 
         result = Playlist.objects.initializePlaylist(request.user)
 
         print_(result)
-        youtube_playlist_ids = result["playlist_ids"]
+        youtube_playlist_ids = result['playlist_ids']
         untube_playlist_ids = []
         for playlist in user_playlists_on_UnTube:
             untube_playlist_ids.append(playlist.playlist_id)
@@ -312,60 +328,68 @@ def user_playlists_updates(request, action):
             if pl_id not in youtube_playlist_ids:  # ie this playlist was deleted on youtube
                 deleted_playlist_ids.append(pl_id)
                 pl = request.user.playlists.get(playlist_id__exact=pl_id)
-                deleted_playlist_names.append(f"{pl.name} (had {pl.video_count} videos)")
+                deleted_playlist_names.append(f'{pl.name} (had {pl.video_count} videos)')
                 pl.delete()
 
-        if result["num_of_playlists"] == user_playlists_on_UnTube.count() and len(deleted_playlist_ids) == 0:
-            print_("No new updates")
+        if result['num_of_playlists'] == user_playlists_on_UnTube.count() and len(deleted_playlist_ids) == 0:
+            print_('No new updates')
             playlists = []
         else:
-            playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=False)).exclude(
-                playlist_id="LL")
+            playlists = request.user.playlists.filter(Q(is_user_owned=True) &
+                                                      Q(is_in_db=False)).exclude(playlist_id='LL')
             print_(
-                f"New updates found! {playlists.count()} newly added and {len(deleted_playlist_ids)} playlists deleted!")
+                f'New updates found! {playlists.count()} newly added and {len(deleted_playlist_ids)} playlists deleted!'
+            )
             print_(deleted_playlist_names)
 
-        return HttpResponse(loader.get_template('intercooler/user_playlist_updates.html').render(
-            {"playlists": playlists,
-             "deleted_playlist_names": deleted_playlist_names}))
+        return HttpResponse(
+            loader.get_template('intercooler/user_playlist_updates.html').render({
+                'playlists': playlists,
+                'deleted_playlist_names': deleted_playlist_names
+            })
+        )
     elif action == 'init-update':
-        unimported_playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=False)).exclude(
-            playlist_id="LL").count()
-
-        return HttpResponse(f"""
-        <div hx-get="/updates/user-playlists/start-update" hx-trigger="load" hx-target="#user-pl-updates">
-            <div class="alert alert-dismissible fade show" role="alert" style="background-color: cadetblue">
-                <div class="d-flex justify-content-center mt-4 mb-3 ms-2" id="loading-sign" >
-                    <img src="/static/svg-loaders/spinning-circles.svg" width="40" height="40">
-                    <h5 class="mt-2 ms-2 text-black">Importing {unimported_playlists} new playlists into UnTube, please wait!</h5>
+        unimported_playlists = request.user.playlists.filter(Q(is_user_owned=True) &
+                                                             Q(is_in_db=False)).exclude(playlist_id='LL').count()
+
+        return HttpResponse(
+            f"""
+        <div hx-get='/updates/user-playlists/start-update' hx-trigger='load' hx-target='#user-pl-updates'>
+            <div class='alert alert-dismissible fade show' role='alert' style='background-color: cadetblue'>
+                <div class='d-flex justify-content-center mt-4 mb-3 ms-2' id='loading-sign' >
+                    <img src='/static/svg-loaders/spinning-circles.svg' width='40' height='40'>
+                    <h5 class='mt-2 ms-2 text-black'>Importing {unimported_playlists} new playlists into UnTube, please wait!</h5>
                 </div>
             </div>
         </div>
-        """)
+        """
+        )
     elif action == 'start-update':
-        unimported_playlists = request.user.playlists.filter(Q(is_user_owned=True) & Q(is_in_db=False)).exclude(
-            playlist_id="LL")
+        unimported_playlists = request.user.playlists.filter(Q(is_user_owned=True) &
+                                                             Q(is_in_db=False)).exclude(playlist_id='LL')
 
         for playlist in unimported_playlists:
             Playlist.objects.getAllVideosForPlaylist(request.user, playlist.playlist_id)
 
-        return HttpResponse("""
-        <div class="alert alert-success alert-dismissible fade show d-flex justify-content-center" role="alert">
-          <h4 class="">Successfully imported new playlists into UnTube!</h4>
-            <meta http-equiv="refresh" content="0;url=/home/#recent-playlists" />
-            <meta http-equiv="refresh" content="2;url=/home/" />
+        return HttpResponse(
+            """
+        <div class='alert alert-success alert-dismissible fade show d-flex justify-content-center' role='alert'>
+          <h4 class=''>Successfully imported new playlists into UnTube!</h4>
+            <meta http-equiv='refresh' content='0;url=/home/#recent-playlists' />
+            <meta http-equiv='refresh' content='2;url=/home/' />
 
-            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-la bel="Close"></button>
+            <button type='button' class='btn-close' data-bs-dismiss='alert' aria-la bel='Close'></button>
         </div>
-        """)
+        """
+        )
 
 
 @login_required
 def get_user_liked_videos_playlist(request):
-    if not request.user.playlists.filter(Q(playlist_id="LL") & Q(is_in_db=True)).exists():
-        Playlist.objects.initializePlaylist(request.user, "LL")
-        Playlist.objects.getAllVideosForPlaylist(request.user, "LL")
-        messages.success(request, "Successfully imported your Liked Videos playlist!")
+    if not request.user.playlists.filter(Q(playlist_id='LL') & Q(is_in_db=True)).exists():
+        Playlist.objects.initializePlaylist(request.user, 'LL')
+        Playlist.objects.getAllVideosForPlaylist(request.user, 'LL')
+        messages.success(request, 'Successfully imported your Liked Videos playlist!')
 
     return HttpResponse("""
         <script>
@@ -374,7 +398,7 @@ def get_user_liked_videos_playlist(request):
     """)
 
 
-### FOR INDEX.HTML
+# FOR INDEX.HTML
 @require_POST
 def like_untube(request):
     untube = Untube.objects.all().first()
@@ -384,11 +408,13 @@ def like_untube(request):
     request.session['liked_untube'] = True
     request.session.save()
 
-    return HttpResponse(f"""
-            <a hx-post="/unlike-untube/" hx-swap="outerHTML" style="text-decoration: none; color: black">
-            <i class="fas fa-heart" style="color: #d02e2e"></i> {untube.page_likes} likes (p.s glad you liked it!)
+    return HttpResponse(
+        f"""
+            <a hx-post='/unlike-untube/' hx-swap='outerHTML' style='text-decoration: none; color: black'>
+            <i class='fas fa-heart' style='color: #d02e2e'></i> {untube.page_likes} likes (p.s glad you liked it!)
           </a>
-    """)
+    """
+    )
 
 
 @require_POST
@@ -400,8 +426,10 @@ def unlike_untube(request):
     request.session['liked_untube'] = False
     request.session.save()
 
-    return HttpResponse(f"""
-                <a hx-post="/like-untube/" hx-swap="outerHTML" style="text-decoration: none; color: black">
-                <i class="fas fa-heart"></i> {untube.page_likes} likes (p.s :/)
+    return HttpResponse(
+        f"""
+                <a hx-post='/like-untube/' hx-swap='outerHTML' style='text-decoration: none; color: black'>
+                <i class='fas fa-heart'></i> {untube.page_likes} likes (p.s :/)
               </a>
-        """)
+        """
+    )

+ 144 - 1
poetry.lock

@@ -138,6 +138,18 @@ files = [
 [package.dependencies]
 pycparser = "*"
 
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+files = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+
 [[package]]
 name = "charset-normalizer"
 version = "3.1.0"
@@ -292,6 +304,18 @@ files = [
 [package.extras]
 dev = ["attribution (==1.6.2)", "black (==23.3.0)", "flit (==3.8.0)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"]
 
+[[package]]
+name = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
+    {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
+]
+
 [[package]]
 name = "django"
 version = "4.2.1"
@@ -418,6 +442,22 @@ files = [
     {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"},
 ]
 
+[[package]]
+name = "filelock"
+version = "3.12.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
+    {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+
 [[package]]
 name = "google-api-core"
 version = "2.11.0"
@@ -551,6 +591,21 @@ files = [
 [package.extras]
 tests = ["freezegun", "pytest", "pytest-cov"]
 
+[[package]]
+name = "identify"
+version = "2.5.24"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
+    {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "idna"
 version = "3.4"
@@ -574,6 +629,21 @@ files = [
     {file = "MarkupPy-1.14.tar.gz", hash = "sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f"},
 ]
 
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
 [[package]]
 name = "oauthlib"
 version = "3.2.2"
@@ -620,6 +690,41 @@ files = [
 [package.dependencies]
 et-xmlfile = "*"
 
+[[package]]
+name = "platformdirs"
+version = "3.5.1"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"},
+    {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pre-commit"
+version = "3.3.2"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pre_commit-3.3.2-py2.py3-none-any.whl", hash = "sha256:8056bc52181efadf4aac792b1f4f255dfd2fb5a350ded7335d251a68561e8cb6"},
+    {file = "pre_commit-3.3.2.tar.gz", hash = "sha256:66e37bec2d882de1f17f88075047ef8962581f83c234ac08da21a0c58953d1f0"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
 [[package]]
 name = "protobuf"
 version = "4.23.1"
@@ -855,6 +960,23 @@ files = [
 [package.dependencies]
 pyasn1 = ">=0.1.3"
 
+[[package]]
+name = "setuptools"
+version = "67.8.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"},
+    {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -967,6 +1089,27 @@ brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
 secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
 socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
+[[package]]
+name = "virtualenv"
+version = "20.23.0"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"},
+    {file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.11,<4"
+platformdirs = ">=3.2,<4"
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"]
+
 [[package]]
 name = "webencodings"
 version = "0.5.1"
@@ -1026,4 +1169,4 @@ files = [
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "1e34707d7fb5f4472a437f38e30f21f5f80278958a99c0aaeea3e2b3916af472"
+content-hash = "d9d46ef9477a9fbbd4d8033b1aebd0d029a35cb98f9ac3b784b5087ad8ab7521"

+ 3 - 0
pyproject.toml

@@ -24,6 +24,9 @@ pyyaml = "^6.0"
 django-import-export = "^3.2.0"
 
 
+[tool.poetry.group.dev.dependencies]
+pre-commit = "^3.3.2"
+
 [build-system]
 requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác