From a2199d2fc03b14fd58d2bc78ca7fe409a1187921 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 18:34:27 -0400 Subject: [PATCH 001/211] Moved pip requirements to pip-requirements directory. Moved games, requirements, tags, and users apps outside of project_manager. Changed packages, plugins, and sub_plugins to all be a part of the project_manager app directly. Moved the User model to users. Some minor admin updates. Added a management command for creating the Game objects. --- .gitignore | 2 +- SPPM/settings/base.py | 19 +- SPPM/wsgi.py | 4 +- games/__init__.py | 1 + {project_manager/games => games}/admin.py | 8 +- .../games => games}/api/__init__.py | 0 .../games => games}/api/serializers.py | 8 +- {project_manager/games => games}/api/urls.py | 8 +- {project_manager/games => games}/api/views.py | 10 +- {project_manager/games => games}/apps.py | 8 +- {project_manager/games => games}/constants.py | 4 +- .../migrations/0001_initial.py | 2 +- .../games => games}/migrations/__init__.py | 0 {project_manager/games => games}/models.py | 8 +- manage.py | 4 +- media/games/{blade.png => berimbau.png} | Bin {requirements => pip-requirements}/base.txt | 1 - {requirements => pip-requirements}/local.txt | 0 {requirements => pip-requirements}/remote.txt | 0 project_manager/__init__.py | 2 - project_manager/admin.py | 38 +- project_manager/api/pagination.py | 4 +- project_manager/api/urls.py | 10 +- project_manager/api/views.py | 6 +- project_manager/apps.py | 6 +- project_manager/common/admin/__init__.py | 6 +- project_manager/common/admin/inlines.py | 23 +- project_manager/common/api/filtersets.py | 6 +- .../common/api/serializers/__init__.py | 22 +- .../common/api/serializers/mixins.py | 6 +- project_manager/common/api/views/__init__.py | 6 +- project_manager/common/api/views/mixins.py | 6 +- project_manager/common/constants.py | 6 +- project_manager/common/context_processors.py | 6 +- project_manager/common/helpers.py | 10 +- project_manager/common/mixins.py | 6 +- project_manager/common/models.py | 27 +- project_manager/common/validators.py | 6 +- project_manager/common/views.py | 8 +- project_manager/constants.py | 16 - project_manager/games/__init__.py | 3 - .../migrations => management}/__init__.py | 0 .../commands}/__init__.py | 0 .../commands/create_game_instances.py | 45 ++ project_manager/migrations/0001_initial.py | 336 ++++++++++++++- project_manager/migrations/0002_initial.py | 114 ++++++ project_manager/migrations/0003_initial.py | 386 ++++++++++++++++++ project_manager/models.py | 77 ---- project_manager/packages/__init__.py | 2 - project_manager/packages/admin/__init__.py | 8 +- project_manager/packages/admin/inlines.py | 6 +- project_manager/packages/api/filtersets.py | 6 +- .../packages/api/serializers/__init__.py | 8 +- .../packages/api/serializers/common.py | 6 +- .../packages/api/serializers/mixins.py | 4 +- project_manager/packages/api/urls.py | 6 +- project_manager/packages/api/views.py | 6 +- project_manager/packages/apps.py | 25 -- project_manager/packages/constants.py | 6 +- project_manager/packages/helpers.py | 8 +- .../packages/migrations/0001_initial.py | 208 ---------- project_manager/packages/models/__init__.py | 26 +- project_manager/packages/models/abstract.py | 10 +- project_manager/packages/views.py | 6 +- project_manager/plugins/__init__.py | 2 - project_manager/plugins/admin/__init__.py | 8 +- project_manager/plugins/admin/inlines.py | 6 +- project_manager/plugins/api/filtersets.py | 6 +- .../plugins/api/serializers/__init__.py | 8 +- .../plugins/api/serializers/mixins.py | 4 +- project_manager/plugins/api/urls.py | 6 +- project_manager/plugins/api/views.py | 6 +- project_manager/plugins/apps.py | 25 -- project_manager/plugins/constants.py | 6 +- project_manager/plugins/helpers.py | 8 +- .../plugins/migrations/0001_initial.py | 229 ----------- project_manager/plugins/models/__init__.py | 30 +- project_manager/plugins/models/abstract.py | 10 +- project_manager/plugins/validators.py | 6 +- project_manager/plugins/views.py | 6 +- project_manager/requirements/__init__.py | 3 - project_manager/sub_plugins/__init__.py | 2 - project_manager/sub_plugins/admin/__init__.py | 10 +- project_manager/sub_plugins/admin/forms.py | 6 +- project_manager/sub_plugins/admin/inlines.py | 6 +- project_manager/sub_plugins/admin/widgets.py | 6 +- project_manager/sub_plugins/api/filtersets.py | 6 +- .../sub_plugins/api/serializers/__init__.py | 8 +- .../sub_plugins/api/serializers/mixins.py | 4 +- project_manager/sub_plugins/api/urls.py | 6 +- project_manager/sub_plugins/api/views.py | 6 +- project_manager/sub_plugins/apps.py | 25 -- project_manager/sub_plugins/constants.py | 6 +- project_manager/sub_plugins/helpers.py | 8 +- .../sub_plugins/migrations/0001_initial.py | 221 ---------- .../sub_plugins/models/__init__.py | 28 +- .../sub_plugins/models/abstract.py | 10 +- project_manager/sub_plugins/views.py | 6 +- project_manager/tags/__init__.py | 3 - project_manager/urls.py | 4 +- project_manager/users/__init__.py | 3 - .../users/migrations/0001_initial.py | 28 -- project_manager/users/migrations/__init__.py | 0 project_manager/views.py | 8 +- requirements/__init__.py | 1 + .../api/__init__.py | 0 .../api/serializers/__init__.py | 0 .../api/serializers/common.py | 6 +- .../requirements => requirements}/apps.py | 8 +- .../constants.py | 4 +- .../migrations/0001_initial.py | 2 +- .../migrations/__init__.py | 0 .../requirements => requirements}/models.py | 8 +- tags/__init__.py | 1 + {project_manager/tags => tags}/admin.py | 8 +- .../tags => tags}/api/__init__.py | 0 .../tags => tags}/api/filtersets.py | 8 +- .../tags => tags}/api/serializers.py | 10 +- {project_manager/tags => tags}/api/urls.py | 8 +- {project_manager/tags => tags}/api/views.py | 12 +- {project_manager/tags => tags}/apps.py | 8 +- {project_manager/tags => tags}/constants.py | 4 +- .../tags => tags}/migrations/0001_initial.py | 5 +- tags/migrations/0002_tag_creator.py | 22 + .../migrations/__init__.py | 0 {project_manager/tags => tags}/models.py | 10 +- {project_manager/tags => tags}/validators.py | 6 +- users/__init__.py | 1 + {project_manager/users => users}/admin.py | 33 +- .../users => users}/api/__init__.py | 0 .../users => users}/api/filtersets.py | 8 +- .../api/serializers/__init__.py | 8 +- .../users => users}/api/serializers/common.py | 8 +- {project_manager/users => users}/api/urls.py | 8 +- {project_manager/users => users}/api/views.py | 12 +- {project_manager/users => users}/apps.py | 8 +- {project_manager/users => users}/constants.py | 10 +- users/migrations/0001_initial.py | 49 +++ .../tags => users}/migrations/__init__.py | 0 {project_manager/users => users}/models.py | 47 ++- 140 files changed, 1411 insertions(+), 1306 deletions(-) create mode 100644 games/__init__.py rename {project_manager/games => games}/admin.py (91%) rename {project_manager/games => games}/api/__init__.py (100%) rename {project_manager/games => games}/api/serializers.py (90%) rename {project_manager/games => games}/api/urls.py (87%) rename {project_manager/games => games}/api/views.py (88%) rename {project_manager/games => games}/apps.py (87%) rename {project_manager/games => games}/constants.py (93%) rename {project_manager/games => games}/migrations/0001_initial.py (92%) rename {project_manager/games => games}/migrations/__init__.py (100%) rename {project_manager/games => games}/models.py (95%) rename media/games/{blade.png => berimbau.png} (100%) rename {requirements => pip-requirements}/base.txt (81%) rename {requirements => pip-requirements}/local.txt (100%) rename {requirements => pip-requirements}/remote.txt (100%) delete mode 100644 project_manager/constants.py delete mode 100644 project_manager/games/__init__.py rename project_manager/{packages/migrations => management}/__init__.py (100%) rename project_manager/{plugins/migrations => management/commands}/__init__.py (100%) create mode 100644 project_manager/management/commands/create_game_instances.py create mode 100644 project_manager/migrations/0002_initial.py create mode 100644 project_manager/migrations/0003_initial.py delete mode 100644 project_manager/models.py delete mode 100644 project_manager/packages/apps.py delete mode 100644 project_manager/packages/migrations/0001_initial.py delete mode 100644 project_manager/plugins/apps.py delete mode 100644 project_manager/plugins/migrations/0001_initial.py delete mode 100644 project_manager/requirements/__init__.py delete mode 100644 project_manager/sub_plugins/apps.py delete mode 100644 project_manager/sub_plugins/migrations/0001_initial.py delete mode 100644 project_manager/tags/__init__.py delete mode 100644 project_manager/users/__init__.py delete mode 100644 project_manager/users/migrations/0001_initial.py delete mode 100644 project_manager/users/migrations/__init__.py create mode 100644 requirements/__init__.py rename {project_manager/requirements => requirements}/api/__init__.py (100%) rename {project_manager/requirements => requirements}/api/serializers/__init__.py (100%) rename {project_manager/requirements => requirements}/api/serializers/common.py (97%) rename {project_manager/requirements => requirements}/apps.py (87%) rename {project_manager/requirements => requirements}/constants.py (94%) rename {project_manager/requirements => requirements}/migrations/0001_initial.py (97%) rename {project_manager/requirements => requirements}/migrations/__init__.py (100%) rename {project_manager/requirements => requirements}/models.py (96%) create mode 100644 tags/__init__.py rename {project_manager/tags => tags}/admin.py (93%) rename {project_manager/tags => tags}/api/__init__.py (100%) rename {project_manager/tags => tags}/api/filtersets.py (89%) rename {project_manager/tags => tags}/api/serializers.py (86%) rename {project_manager/tags => tags}/api/urls.py (87%) rename {project_manager/tags => tags}/api/views.py (87%) rename {project_manager/tags => tags}/apps.py (87%) rename {project_manager/tags => tags}/constants.py (92%) rename {project_manager/tags => tags}/migrations/0001_initial.py (66%) create mode 100644 tags/migrations/0002_tag_creator.py rename {project_manager/sub_plugins => tags}/migrations/__init__.py (100%) rename {project_manager/tags => tags}/models.py (90%) rename {project_manager/tags => tags}/validators.py (92%) create mode 100644 users/__init__.py rename {project_manager/users => users}/admin.py (71%) rename {project_manager/users => users}/api/__init__.py (100%) rename {project_manager/users => users}/api/filtersets.py (95%) rename {project_manager/users => users}/api/serializers/__init__.py (96%) rename {project_manager/users => users}/api/serializers/common.py (91%) rename {project_manager/users => users}/api/urls.py (88%) rename {project_manager/users => users}/api/views.py (91%) rename {project_manager/users => users}/apps.py (87%) rename {project_manager/users => users}/constants.py (80%) create mode 100644 users/migrations/0001_initial.py rename {project_manager/tags => users}/migrations/__init__.py (100%) rename {project_manager/users => users}/models.py (63%) diff --git a/.gitignore b/.gitignore index ff7f7936..8a060d22 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ # Local text files *.txt -!requirements/*.txt +!pip-requirements/*.txt # Compiled Python *.pyc diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index f6d88e17..d0a6a1dd 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -11,14 +11,14 @@ """ # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Python from path import Path # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = Path(__file__).parent.parent.parent @@ -43,19 +43,15 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', - # 'phpbb', 'embed_video', 'precise_bbcode', 'crispy_forms', 'django_filters', 'project_manager', - 'project_manager.games', - 'project_manager.packages', - 'project_manager.plugins', - 'project_manager.requirements', - 'project_manager.sub_plugins', - 'project_manager.tags', - 'project_manager.users', + 'games', + 'requirements', + 'tags', + 'users', ] MIDDLEWARE = [ @@ -87,7 +83,6 @@ ] AUTHENTICATION_BACKENDS = ( - # 'phpbb.backends.PhpbbBackend', 'django.contrib.auth.backends.ModelBackend', ) @@ -128,7 +123,7 @@ }, ] -AUTH_USER_MODEL = 'project_manager.User' +AUTH_USER_MODEL = 'users.User' # Rest Framework REST_FRAMEWORK = { diff --git a/SPPM/wsgi.py b/SPPM/wsgi.py index 36c99bf4..a2859f71 100644 --- a/SPPM/wsgi.py +++ b/SPPM/wsgi.py @@ -8,7 +8,7 @@ """ # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Python import os @@ -18,7 +18,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= os.environ.setdefault("DJANGO_SETTINGS_MODULE", "SPPM.settings") diff --git a/games/__init__.py b/games/__init__.py new file mode 100644 index 00000000..f807581a --- /dev/null +++ b/games/__init__.py @@ -0,0 +1 @@ +"""Game app.""" diff --git a/project_manager/games/admin.py b/games/admin.py similarity index 91% rename from project_manager/games/admin.py rename to games/admin.py index aeb67d80..029669e9 100644 --- a/project_manager/games/admin.py +++ b/games/admin.py @@ -1,17 +1,17 @@ """Game admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin # App -from project_manager.games.models import Game +from games.models import Game # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'GameAdmin', @@ -19,7 +19,7 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= @admin.register(Game) class GameAdmin(admin.ModelAdmin): diff --git a/project_manager/games/api/__init__.py b/games/api/__init__.py similarity index 100% rename from project_manager/games/api/__init__.py rename to games/api/__init__.py diff --git a/project_manager/games/api/serializers.py b/games/api/serializers.py similarity index 90% rename from project_manager/games/api/serializers.py rename to games/api/serializers.py index 6da23fea..4749fd5c 100644 --- a/project_manager/games/api/serializers.py +++ b/games/api/serializers.py @@ -1,17 +1,17 @@ """Game serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.serializers import ModelSerializer # App -from project_manager.games.models import Game +from games.models import Game # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'GameSerializer', @@ -19,7 +19,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class GameSerializer(ModelSerializer): """Serializer for supported games for projects.""" diff --git a/project_manager/games/api/urls.py b/games/api/urls.py similarity index 87% rename from project_manager/games/api/urls.py rename to games/api/urls.py index 2479441b..2541c799 100644 --- a/project_manager/games/api/urls.py +++ b/games/api/urls.py @@ -1,17 +1,17 @@ """Game API URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework import routers # App -from project_manager.games.api.views import GameViewSet +from games.api.views import GameViewSet # ============================================================================= -# >> ROUTERS +# ROUTERS # ============================================================================= router = routers.SimpleRouter() router.register( @@ -22,7 +22,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'games' diff --git a/project_manager/games/api/views.py b/games/api/views.py similarity index 88% rename from project_manager/games/api/views.py rename to games/api/views.py index 65af1cb6..149f9e8d 100644 --- a/project_manager/games/api/views.py +++ b/games/api/views.py @@ -1,7 +1,7 @@ """Game API views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.filters import OrderingFilter @@ -9,12 +9,12 @@ from rest_framework.viewsets import GenericViewSet # App -from project_manager.games.api.serializers import GameSerializer -from project_manager.games.models import Game +from games.api.serializers import GameSerializer +from games.models import Game # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'GameViewSet', @@ -22,7 +22,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class GameViewSet(ListModelMixin, GenericViewSet): """ViewSet for listing Supported Games. diff --git a/project_manager/games/apps.py b/games/apps.py similarity index 87% rename from project_manager/games/apps.py rename to games/apps.py index 7d8896d6..92f9a1d7 100644 --- a/project_manager/games/apps.py +++ b/games/apps.py @@ -1,14 +1,14 @@ """Game app config.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.apps import AppConfig # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'GameConfig', @@ -16,10 +16,10 @@ # ============================================================================= -# >> APPLICATION CONFIG +# APPLICATION CONFIG # ============================================================================= class GameConfig(AppConfig): """Game app config.""" - name = 'project_manager.games' + name = 'games' verbose_name = 'Games' diff --git a/project_manager/games/constants.py b/games/constants.py similarity index 93% rename from project_manager/games/constants.py rename to games/constants.py index 91606d9d..6515771a 100644 --- a/project_manager/games/constants.py +++ b/games/constants.py @@ -1,7 +1,7 @@ """Constants for games.""" # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'GAME_BASENAME_MAX_LENGTH', @@ -11,7 +11,7 @@ # ============================================================================= -# >> CONSTANTS +# CONSTANTS # ============================================================================= GAME_BASENAME_MAX_LENGTH = 16 GAME_NAME_MAX_LENGTH = 16 diff --git a/project_manager/games/migrations/0001_initial.py b/games/migrations/0001_initial.py similarity index 92% rename from project_manager/games/migrations/0001_initial.py rename to games/migrations/0001_initial.py index 118d54da..127dc6ce 100644 --- a/project_manager/games/migrations/0001_initial.py +++ b/games/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 +# Generated by Django 3.2.8 on 2021-10-22 20:02 from django.db import migrations, models diff --git a/project_manager/games/migrations/__init__.py b/games/migrations/__init__.py similarity index 100% rename from project_manager/games/migrations/__init__.py rename to games/migrations/__init__.py diff --git a/project_manager/games/models.py b/games/models.py similarity index 95% rename from project_manager/games/models.py rename to games/models.py index d54e5400..e01ce60e 100644 --- a/project_manager/games/models.py +++ b/games/models.py @@ -1,7 +1,7 @@ """Game model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.urls import reverse @@ -9,7 +9,7 @@ from django.utils.text import slugify # App -from project_manager.games.constants import ( +from games.constants import ( GAME_BASENAME_MAX_LENGTH, GAME_NAME_MAX_LENGTH, GAME_SLUG_MAX_LENGTH, @@ -17,7 +17,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'Game', @@ -25,7 +25,7 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class Game(models.Model): """Game model.""" diff --git a/manage.py b/manage.py index 553f82b0..49b83e9b 100644 --- a/manage.py +++ b/manage.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Python import os @@ -8,7 +8,7 @@ # ============================================================================= -# >> EXECUTE +# EXECUTE # ============================================================================= if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "SPPM.settings.remote") diff --git a/media/games/blade.png b/media/games/berimbau.png similarity index 100% rename from media/games/blade.png rename to media/games/berimbau.png diff --git a/requirements/base.txt b/pip-requirements/base.txt similarity index 81% rename from requirements/base.txt rename to pip-requirements/base.txt index 567e5865..6234d3e9 100644 --- a/requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,4 +1,3 @@ -# -e git://github.com/ZombieToof/django-phpbb.git configobj==5.0.6 django==3.2.8 django-braces==1.14.0 diff --git a/requirements/local.txt b/pip-requirements/local.txt similarity index 100% rename from requirements/local.txt rename to pip-requirements/local.txt diff --git a/requirements/remote.txt b/pip-requirements/remote.txt similarity index 100% rename from requirements/remote.txt rename to pip-requirements/remote.txt diff --git a/project_manager/__init__.py b/project_manager/__init__.py index f9c7547e..3af52a0d 100644 --- a/project_manager/__init__.py +++ b/project_manager/__init__.py @@ -1,3 +1 @@ """Base app.""" - -default_app_config = 'project_manager.apps.ProjectManagerConfig' diff --git a/project_manager/admin.py b/project_manager/admin.py index f7f40ff5..ccd7834a 100644 --- a/project_manager/admin.py +++ b/project_manager/admin.py @@ -1,24 +1,19 @@ """Base app admin.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= +# Python +from importlib import import_module + # Django from django.contrib import admin -from django.contrib.auth import get_user_model, models +from django.contrib.auth import models # Third Party Django from precise_bbcode.models import BBCodeTag, SmileyTag -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ( - 'UserAdmin', -) - - # ============================================================================= # UNREGISTER # ============================================================================= @@ -28,23 +23,8 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= -@admin.register(get_user_model()) -class UserAdmin(admin.ModelAdmin): - """User model Admin.""" - - actions = None - fields = ( - 'username', - 'is_superuser', - 'is_staff', - ) - - def has_add_permission(self, request): - """Disallow creating Users in the Admin.""" - return False - - def has_delete_permission(self, request, obj=None): - """Disallow deleting Users in the Admin.""" - return False +import_module('project_manager.packages.admin') +import_module('project_manager.plugins.admin') +import_module('project_manager.sub_plugins.admin') diff --git a/project_manager/api/pagination.py b/project_manager/api/pagination.py index b54cc8e1..7d173861 100644 --- a/project_manager/api/pagination.py +++ b/project_manager/api/pagination.py @@ -1,14 +1,14 @@ """Base app models.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.pagination import PageNumberPagination # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'BasePagination', diff --git a/project_manager/api/urls.py b/project_manager/api/urls.py index 463effed..d92f1b58 100644 --- a/project_manager/api/urls.py +++ b/project_manager/api/urls.py @@ -1,7 +1,7 @@ """API base URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf.urls import include, url @@ -11,7 +11,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'api' @@ -19,7 +19,7 @@ url( regex=r'^games/', view=include( - 'project_manager.games.api.urls', + 'games.api.urls', namespace='games', ), ), @@ -47,14 +47,14 @@ url( regex=r'^tags/', view=include( - 'project_manager.tags.api.urls', + 'tags.api.urls', namespace='tags', ), ), url( regex=r'^users/', view=include( - 'project_manager.users.api.urls', + 'users.api.urls', namespace='users', ), ), diff --git a/project_manager/api/views.py b/project_manager/api/views.py index ce63d0f0..465dd23b 100644 --- a/project_manager/api/views.py +++ b/project_manager/api/views.py @@ -1,7 +1,7 @@ """API base views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.response import Response @@ -10,7 +10,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectManagerAPIView', @@ -18,7 +18,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class ProjectManagerAPIView(APIView): """Project Manager API listing.""" diff --git a/project_manager/apps.py b/project_manager/apps.py index 65653735..4164168b 100644 --- a/project_manager/apps.py +++ b/project_manager/apps.py @@ -1,14 +1,14 @@ """Base app config.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.apps import AppConfig # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectManagerConfig', @@ -16,7 +16,7 @@ # ============================================================================= -# >> APPLICATION CONFIG +# APPLICATION CONFIG # ============================================================================= class ProjectManagerConfig(AppConfig): """Project Manager app config.""" diff --git a/project_manager/common/admin/__init__.py b/project_manager/common/admin/__init__.py index ec7cab8e..48f93985 100644 --- a/project_manager/common/admin/__init__.py +++ b/project_manager/common/admin/__init__.py @@ -1,14 +1,14 @@ """Common admin classes to use for projects.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectAdmin', @@ -16,7 +16,7 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= class ProjectAdmin(admin.ModelAdmin): """Base admin class for projects.""" diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index dc5cb7cc..328e2b99 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -1,7 +1,7 @@ """Inline for project admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectContributorInline', @@ -21,7 +21,7 @@ # ============================================================================= -# >> INLINES +# INLINES # ============================================================================= class ProjectContributorInline(admin.TabularInline): """Base Project Contributor Inline.""" @@ -47,21 +47,15 @@ def get_formset(self, request, obj=None, **kwargs): class ProjectGameInline(admin.TabularInline): """Base Project Game Inline.""" - extra = 0 fields = ( 'game', ) - - def get_formset(self, request, obj=None, **kwargs): - """Disallow adding/modifying Game objects in the inline.""" - formset = super().get_formset(request=request, obj=obj, **kwargs) - widget = formset.form.base_fields['game'].widget - widget.can_add_related = False - widget.can_change_related = False - return formset + readonly_fields = ( + 'game', + ) def has_add_permission(self, request, obj): - """Disallow adding new images in the Admin.""" + """Disallow adding new games in the Admin.""" return False @@ -117,6 +111,9 @@ class ProjectReleaseInline(admin.StackedInline): 'created', ) + def get_queryset(self, request): + return super().get_queryset(request=request).order_by('-created') + def has_add_permission(self, request, obj): """Disallow adding new images in the Admin.""" return False diff --git a/project_manager/common/api/filtersets.py b/project_manager/common/api/filtersets.py index c27a11cc..3f054e0a 100644 --- a/project_manager/common/api/filtersets.py +++ b/project_manager/common/api/filtersets.py @@ -1,7 +1,7 @@ """Project API filters.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Q @@ -12,7 +12,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectFilterSet', @@ -20,7 +20,7 @@ # ============================================================================= -# >> FILTERS +# FILTERS # ============================================================================= class ProjectFilterSet(FilterSet): """Filters for Projects.""" diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index dd699aec..3fc8885a 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -1,7 +1,7 @@ """Common serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Python from contextlib import suppress @@ -30,20 +30,20 @@ RELEASE_NOTES_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.constants import USER_USERNAME_MAX_LENGTH -from project_manager.games.api.serializers import GameSerializer -from project_manager.games.constants import GAME_SLUG_MAX_LENGTH -from project_manager.games.models import Game -from project_manager.tags.constants import TAG_NAME_MAX_LENGTH -from project_manager.tags.models import Tag -from project_manager.users.api.serializers.common import ( +from games.api.serializers import GameSerializer +from games.constants import GAME_SLUG_MAX_LENGTH +from games.models import Game +from tags.constants import TAG_NAME_MAX_LENGTH +from tags.models import Tag +from users.api.serializers.common import ( ForumUserContributorSerializer, ) -from project_manager.users.models import ForumUser +from users.constants import USER_USERNAME_MAX_LENGTH +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectContributorSerializer', @@ -57,7 +57,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class ProjectSerializer(ModelSerializer, ProjectLocaleMixin): """Base Project Serializer.""" diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 2ef23052..0a92f631 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -1,7 +1,7 @@ """Mixins for common serializers.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.utils import formats @@ -12,7 +12,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectLocaleMixin', @@ -22,7 +22,7 @@ # ============================================================================= -# >> MIXINS +# MIXINS # ============================================================================= class ProjectLocaleMixin: """Mixin for getting the locale for timestamps.""" diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index af52d882..2757ad39 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -1,7 +1,7 @@ """Common views for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db import IntegrityError @@ -27,7 +27,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectAPIView', @@ -41,7 +41,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class ProjectAPIView(APIView): """Base Project API routes.""" diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index 6d45c2cf..d2928767 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -1,7 +1,7 @@ """Mixins for common functionalities between APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from django_filters.rest_framework import DjangoFilterBackend @@ -14,7 +14,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectRelatedInfoMixin', @@ -23,7 +23,7 @@ # ============================================================================= -# >> MIXINS +# MIXINS # ============================================================================= class ProjectRelatedInfoMixin(ModelViewSet): """Mixin used to retrieve information for a specific project.""" diff --git a/project_manager/common/constants.py b/project_manager/common/constants.py index cfe71a3f..d7ed293f 100644 --- a/project_manager/common/constants.py +++ b/project_manager/common/constants.py @@ -1,14 +1,14 @@ """Base constants.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf import settings # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ALLOWED_FILE_TYPES', @@ -38,7 +38,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # Max length constants PROJECT_BASENAME_MAX_LENGTH = 32 diff --git a/project_manager/common/context_processors.py b/project_manager/common/context_processors.py index e60e2fc2..a53b20ba 100644 --- a/project_manager/common/context_processors.py +++ b/project_manager/common/context_processors.py @@ -1,14 +1,14 @@ """Context processors to be added to templates.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf import settings # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'add_common_context_processors', @@ -16,7 +16,7 @@ # ============================================================================= -# >> FUNCTIONS +# FUNCTIONS # ============================================================================= def add_common_context_processors(request): """Expose some settings and other information to all contexts.""" diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 1e7fead2..f347dae7 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -1,7 +1,7 @@ """Common helper functions.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Python import json @@ -16,7 +16,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ProjectZipFile', @@ -29,7 +29,7 @@ # ============================================================================= -# >> CLASSES +# CLASSES # ============================================================================= class ProjectZipFile: """Base ZipFile parsing class.""" @@ -264,7 +264,7 @@ def _validate_requirement( # TODO: validate pypi requirements? # TODO: validate vcs requirements? # pylint: disable=import-outside-toplevel - from project_manager.requirements.models import ( + from requirements.models import ( DownloadRequirement, PyPiRequirement, VersionControlRequirement, @@ -295,7 +295,7 @@ def _validate_requirement( # ============================================================================= -# >> FUNCTIONS +# FUNCTIONS # ============================================================================= def find_image_number(directory, slug): """Return the next available image number.""" diff --git a/project_manager/common/mixins.py b/project_manager/common/mixins.py index 735f3ff5..33c73d75 100644 --- a/project_manager/common/mixins.py +++ b/project_manager/common/mixins.py @@ -1,7 +1,7 @@ """Common mixins for use in multiple apps.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf import settings @@ -11,7 +11,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'DownloadMixin', @@ -19,7 +19,7 @@ # ============================================================================= -# >> MIX-INS +# MIX-INS # ============================================================================= class DownloadMixin(View): """Mixin for handling downloads and download counts.""" diff --git a/project_manager/common/models.py b/project_manager/common/models.py index ddb8a66f..38daf61f 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -1,9 +1,10 @@ """Common models used for inheritance.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Python +import uuid from operator import attrgetter # Django @@ -36,13 +37,13 @@ handle_release_zip_file_upload, ) from project_manager.common.validators import version_validator -from project_manager.models import AbstractUUIDPrimaryKeyModel # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( + 'AbstractUUIDPrimaryKeyModel', 'ProjectBase', 'ProjectContributor', 'ProjectGame', @@ -57,8 +58,24 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= +class AbstractUUIDPrimaryKeyModel(models.Model): + """Abstract model that creates an non-editable UUID primary key.""" + + id = models.UUIDField( + verbose_name='ID', + primary_key=True, + default=uuid.uuid4, + editable=False, + ) + + class Meta: + """Define metaclass attributes.""" + + abstract = True + + class ProjectBase(models.Model): """Base model for projects.""" @@ -416,7 +433,7 @@ class ProjectReleasePackageRequirement(AbstractUUIDPrimaryKeyModel): """Base Package requirement model.""" package_requirement = models.ForeignKey( - to='packages.Package', + to='project_manager.Package', on_delete=models.CASCADE, ) version = models.CharField( diff --git a/project_manager/common/validators.py b/project_manager/common/validators.py index 69b9f52d..2f758ac9 100644 --- a/project_manager/common/validators.py +++ b/project_manager/common/validators.py @@ -1,7 +1,7 @@ """Common validators.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.validators import RegexValidator @@ -11,7 +11,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'basename_validator', @@ -20,7 +20,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # basename values should: # Start with a lower-case character. diff --git a/project_manager/common/views.py b/project_manager/common/views.py index 113af73d..b2129b98 100644 --- a/project_manager/common/views.py +++ b/project_manager/common/views.py @@ -1,7 +1,7 @@ """Common views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.views.generic import ListView @@ -11,7 +11,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'OrderableListView', @@ -21,7 +21,7 @@ # ============================================================================= -# >> HELPERS +# HELPERS # ============================================================================= class _PageObject: def __init__(self, display, url): @@ -33,7 +33,7 @@ def __str__(self): # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class OrderableListView(OrderableListMixin, ListView): """View to be inherited for ordering.""" diff --git a/project_manager/constants.py b/project_manager/constants.py deleted file mode 100644 index ee6ceed8..00000000 --- a/project_manager/constants.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Base constants.""" - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ( - 'USER_EMAIL_MAX_LENGTH', - 'USER_USERNAME_MAX_LENGTH', -) - - -# ============================================================================= -# >> CONSTANTS -# ============================================================================= -USER_USERNAME_MAX_LENGTH = 30 -USER_EMAIL_MAX_LENGTH = 256 diff --git a/project_manager/games/__init__.py b/project_manager/games/__init__.py deleted file mode 100644 index 59076a7c..00000000 --- a/project_manager/games/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Game app.""" - -default_app_config = 'project_manager.games.apps.GameConfig' diff --git a/project_manager/packages/migrations/__init__.py b/project_manager/management/__init__.py similarity index 100% rename from project_manager/packages/migrations/__init__.py rename to project_manager/management/__init__.py diff --git a/project_manager/plugins/migrations/__init__.py b/project_manager/management/commands/__init__.py similarity index 100% rename from project_manager/plugins/migrations/__init__.py rename to project_manager/management/commands/__init__.py diff --git a/project_manager/management/commands/create_game_instances.py b/project_manager/management/commands/create_game_instances.py new file mode 100644 index 00000000..3693cb9b --- /dev/null +++ b/project_manager/management/commands/create_game_instances.py @@ -0,0 +1,45 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.core.management.base import BaseCommand + +# App +from games.models import Game + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +games = { + 'berimbau': 'Blade Symphony', + 'bms': 'Black Mesa', + 'csgo': 'Counter-Strike: Global Offensive', + 'cstrike': 'Counter-Strike: Source', + 'dod': 'Day of Defeat: Source', + 'hl2mp': 'Half-Life 2: DeathMatch', + 'left4dead2': 'Left for Dead 2', + 'tf': 'Team Fortress 2', +} + + +# ============================================================================= +# COMMANDS +# ============================================================================= +class Command(BaseCommand): + """Populate the Game objects.""" + + def handle(self, *args, **options): + current_games = Game.objects.values_list('basename', flat=True) + obj_list = [] + for game in set(games).difference(current_games): + obj_list.append( + Game( + name=games[game], + basename=game, + icon=f'games/{game}.png', + ) + ) + + if obj_list: + Game.objects.bulk_create(objs=obj_list) diff --git a/project_manager/migrations/0001_initial.py b/project_manager/migrations/0001_initial.py index fa290faa..ecb4e37e 100644 --- a/project_manager/migrations/0001_initial.py +++ b/project_manager/migrations/0001_initial.py @@ -1,7 +1,14 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:57 +# Generated by Django 3.2.8 on 2021-10-22 20:02 -import django.contrib.auth.models +import django.core.validators from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import embed_video.fields +import model_utils.fields +import precise_bbcode.fields +import project_manager.common.helpers +import uuid class Migration(migrations.Migration): @@ -9,28 +16,327 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name='User', + name='Package', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(max_length=30, unique=True)), - ('email', models.EmailField(blank=True, max_length=256)), - ('is_staff', models.BooleanField(default=False)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ('name', models.CharField(help_text="The name of the project. Do not include the version, as that is added dynamically to the project's page.", max_length=64)), + ('_configuration_rendered', models.TextField(blank=True, editable=False, null=True)), + ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), + ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), + ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), + ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), + ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), + ('topic', models.IntegerField(blank=True, null=True, unique=True)), + ('created', models.DateTimeField(verbose_name='created')), + ('updated', models.DateTimeField(verbose_name='updated')), + ('basename', models.CharField(blank=True, max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z_]*[0-9a-z]')])), + ('slug', models.SlugField(blank=True, max_length=32, primary_key=True, serialize=False, unique=True)), ], options={ 'abstract': False, }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ), + migrations.CreateModel( + name='PackageContributor', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='PackageGame', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='PackageImage', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ], + options={ + 'verbose_name': 'Image', + 'verbose_name_plural': 'Images', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PackageRelease', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), + ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), + ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('download_count', models.PositiveIntegerField(default=0)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ], + options={ + 'verbose_name': 'Release', + 'verbose_name_plural': 'Releases', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PackageReleaseDownloadRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PackageReleasePackageRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PackageReleasePyPiRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PackageReleaseVersionControlRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PackageTag', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Plugin', + fields=[ + ('name', models.CharField(help_text="The name of the project. Do not include the version, as that is added dynamically to the project's page.", max_length=64)), + ('_configuration_rendered', models.TextField(blank=True, editable=False, null=True)), + ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), + ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), + ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), + ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), + ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), + ('topic', models.IntegerField(blank=True, null=True, unique=True)), + ('created', models.DateTimeField(verbose_name='created')), + ('updated', models.DateTimeField(verbose_name='updated')), + ('basename', models.CharField(blank=True, max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z_]*[0-9a-z]')])), + ('slug', models.SlugField(blank=True, max_length=32, primary_key=True, serialize=False, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PluginContributor', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='PluginGame', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='PluginImage', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ], + options={ + 'verbose_name': 'Image', + 'verbose_name_plural': 'Images', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PluginRelease', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), + ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), + ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('download_count', models.PositiveIntegerField(default=0)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ], + options={ + 'verbose_name': 'Release', + 'verbose_name_plural': 'Releases', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PluginReleaseDownloadRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PluginReleasePackageRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PluginReleasePyPiRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PluginReleaseVersionControlRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='PluginTag', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='SubPlugin', + fields=[ + ('name', models.CharField(help_text="The name of the project. Do not include the version, as that is added dynamically to the project's page.", max_length=64)), + ('_configuration_rendered', models.TextField(blank=True, editable=False, null=True)), + ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), + ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), + ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), + ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), + ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), + ('topic', models.IntegerField(blank=True, null=True, unique=True)), + ('created', models.DateTimeField(verbose_name='created')), + ('updated', models.DateTimeField(verbose_name='updated')), + ('id', models.CharField(blank=True, max_length=65, primary_key=True, serialize=False)), + ('basename', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z_]*[0-9a-z]')])), + ('slug', models.SlugField(blank=True, max_length=32)), + ], + options={ + 'verbose_name': 'SubPlugin', + 'verbose_name_plural': 'SubPlugins', + }, + ), + migrations.CreateModel( + name='SubPluginContributor', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='SubPluginGame', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='SubPluginImage', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ], + options={ + 'verbose_name': 'Image', + 'verbose_name_plural': 'Images', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SubPluginPath', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(max_length=256, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z/\\\\_]*[0-9a-z]')])), + ('allow_module', models.BooleanField(default=False)), + ('allow_package_using_basename', models.BooleanField(default=False)), + ('allow_package_using_init', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'SubPlugin Path', + 'verbose_name_plural': 'SubPlugin Paths', + }, + ), + migrations.CreateModel( + name='SubPluginRelease', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), + ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), + ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('download_count', models.PositiveIntegerField(default=0)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ], + options={ + 'verbose_name': 'Release', + 'verbose_name_plural': 'Releases', + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SubPluginReleaseDownloadRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='SubPluginReleasePackageRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='SubPluginReleasePyPiRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='SubPluginReleaseVersionControlRequirement', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('optional', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='SubPluginTag', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), + ('sub_plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin')), ], ), ] diff --git a/project_manager/migrations/0002_initial.py b/project_manager/migrations/0002_initial.py new file mode 100644 index 00000000..dbc3c789 --- /dev/null +++ b/project_manager/migrations/0002_initial.py @@ -0,0 +1,114 @@ +# Generated by Django 3.2.8 on 2021-10-22 20:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('tags', '0001_initial'), + ('project_manager', '0001_initial'), + ('requirements', '0001_initial'), + ('games', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='subplugintag', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.tag'), + ), + migrations.AddField( + model_name='subpluginreleaseversioncontrolrequirement', + name='sub_plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subpluginrelease'), + ), + migrations.AddField( + model_name='subpluginreleaseversioncontrolrequirement', + name='vcs_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.versioncontrolrequirement'), + ), + migrations.AddField( + model_name='subpluginreleasepypirequirement', + name='pypi_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.pypirequirement'), + ), + migrations.AddField( + model_name='subpluginreleasepypirequirement', + name='sub_plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subpluginrelease'), + ), + migrations.AddField( + model_name='subpluginreleasepackagerequirement', + name='package_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.package'), + ), + migrations.AddField( + model_name='subpluginreleasepackagerequirement', + name='sub_plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subpluginrelease'), + ), + migrations.AddField( + model_name='subpluginreleasedownloadrequirement', + name='download_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.downloadrequirement'), + ), + migrations.AddField( + model_name='subpluginreleasedownloadrequirement', + name='sub_plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subpluginrelease'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='download_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleaseDownloadRequirement', to='requirements.DownloadRequirement'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='package_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleasePackageRequirement', to='project_manager.Package'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='pypi_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleasePyPiRequirement', to='requirements.PyPiRequirement'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='project_manager.subplugin'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='vcs_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), + ), + migrations.AddField( + model_name='subpluginpath', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paths', to='project_manager.plugin'), + ), + migrations.AddField( + model_name='subpluginimage', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='project_manager.subplugin'), + ), + migrations.AddField( + model_name='subplugingame', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game'), + ), + migrations.AddField( + model_name='subplugingame', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin'), + ), + migrations.AddField( + model_name='subplugincontributor', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin'), + ), + ] diff --git a/project_manager/migrations/0003_initial.py b/project_manager/migrations/0003_initial.py new file mode 100644 index 00000000..989fbca2 --- /dev/null +++ b/project_manager/migrations/0003_initial.py @@ -0,0 +1,386 @@ +# Generated by Django 3.2.8 on 2021-10-22 20:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('tags', '0001_initial'), + ('users', '0001_initial'), + ('requirements', '0001_initial'), + ('project_manager', '0002_initial'), + ('games', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='subplugincontributor', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.forumuser'), + ), + migrations.AddField( + model_name='subplugin', + name='contributors', + field=models.ManyToManyField(related_name='subplugin_contributions', through='project_manager.SubPluginContributor', to='users.ForumUser'), + ), + migrations.AddField( + model_name='subplugin', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subplugins', to='users.forumuser'), + ), + migrations.AddField( + model_name='subplugin', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_plugins', to='project_manager.plugin'), + ), + migrations.AddField( + model_name='subplugin', + name='supported_games', + field=models.ManyToManyField(related_name='subplugins', through='project_manager.SubPluginGame', to='games.Game'), + ), + migrations.AddField( + model_name='subplugin', + name='tags', + field=models.ManyToManyField(related_name='subplugins', through='project_manager.SubPluginTag', to='tags.Tag'), + ), + migrations.AddField( + model_name='plugintag', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.plugin'), + ), + migrations.AddField( + model_name='plugintag', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.tag'), + ), + migrations.AddField( + model_name='pluginreleaseversioncontrolrequirement', + name='plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.pluginrelease'), + ), + migrations.AddField( + model_name='pluginreleaseversioncontrolrequirement', + name='vcs_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.versioncontrolrequirement'), + ), + migrations.AddField( + model_name='pluginreleasepypirequirement', + name='plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.pluginrelease'), + ), + migrations.AddField( + model_name='pluginreleasepypirequirement', + name='pypi_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.pypirequirement'), + ), + migrations.AddField( + model_name='pluginreleasepackagerequirement', + name='package_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.package'), + ), + migrations.AddField( + model_name='pluginreleasepackagerequirement', + name='plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.pluginrelease'), + ), + migrations.AddField( + model_name='pluginreleasedownloadrequirement', + name='download_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.downloadrequirement'), + ), + migrations.AddField( + model_name='pluginreleasedownloadrequirement', + name='plugin_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.pluginrelease'), + ), + migrations.AddField( + model_name='pluginrelease', + name='download_requirements', + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleaseDownloadRequirement', to='requirements.DownloadRequirement'), + ), + migrations.AddField( + model_name='pluginrelease', + name='package_requirements', + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleasePackageRequirement', to='project_manager.Package'), + ), + migrations.AddField( + model_name='pluginrelease', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='project_manager.plugin'), + ), + migrations.AddField( + model_name='pluginrelease', + name='pypi_requirements', + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleasePyPiRequirement', to='requirements.PyPiRequirement'), + ), + migrations.AddField( + model_name='pluginrelease', + name='vcs_requirements', + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), + ), + migrations.AddField( + model_name='pluginimage', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='project_manager.plugin'), + ), + migrations.AddField( + model_name='plugingame', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game'), + ), + migrations.AddField( + model_name='plugingame', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.plugin'), + ), + migrations.AddField( + model_name='plugincontributor', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.plugin'), + ), + migrations.AddField( + model_name='plugincontributor', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.forumuser'), + ), + migrations.AddField( + model_name='plugin', + name='contributors', + field=models.ManyToManyField(related_name='plugin_contributions', through='project_manager.PluginContributor', to='users.ForumUser'), + ), + migrations.AddField( + model_name='plugin', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='users.forumuser'), + ), + migrations.AddField( + model_name='plugin', + name='supported_games', + field=models.ManyToManyField(related_name='plugins', through='project_manager.PluginGame', to='games.Game'), + ), + migrations.AddField( + model_name='plugin', + name='tags', + field=models.ManyToManyField(related_name='plugins', through='project_manager.PluginTag', to='tags.Tag'), + ), + migrations.AddField( + model_name='packagetag', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.package'), + ), + migrations.AddField( + model_name='packagetag', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.tag'), + ), + migrations.AddField( + model_name='packagereleaseversioncontrolrequirement', + name='package_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.packagerelease'), + ), + migrations.AddField( + model_name='packagereleaseversioncontrolrequirement', + name='vcs_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.versioncontrolrequirement'), + ), + migrations.AddField( + model_name='packagereleasepypirequirement', + name='package_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.packagerelease'), + ), + migrations.AddField( + model_name='packagereleasepypirequirement', + name='pypi_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.pypirequirement'), + ), + migrations.AddField( + model_name='packagereleasepackagerequirement', + name='package_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.packagerelease'), + ), + migrations.AddField( + model_name='packagereleasepackagerequirement', + name='package_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.package'), + ), + migrations.AddField( + model_name='packagereleasedownloadrequirement', + name='download_requirement', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.downloadrequirement'), + ), + migrations.AddField( + model_name='packagereleasedownloadrequirement', + name='package_release', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.packagerelease'), + ), + migrations.AddField( + model_name='packagerelease', + name='download_requirements', + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleaseDownloadRequirement', to='requirements.DownloadRequirement'), + ), + migrations.AddField( + model_name='packagerelease', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='project_manager.package'), + ), + migrations.AddField( + model_name='packagerelease', + name='package_requirements', + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleasePackageRequirement', to='project_manager.Package'), + ), + migrations.AddField( + model_name='packagerelease', + name='pypi_requirements', + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleasePyPiRequirement', to='requirements.PyPiRequirement'), + ), + migrations.AddField( + model_name='packagerelease', + name='vcs_requirements', + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), + ), + migrations.AddField( + model_name='packageimage', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='project_manager.package'), + ), + migrations.AddField( + model_name='packagegame', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game'), + ), + migrations.AddField( + model_name='packagegame', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.package'), + ), + migrations.AddField( + model_name='packagecontributor', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.package'), + ), + migrations.AddField( + model_name='packagecontributor', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.forumuser'), + ), + migrations.AddField( + model_name='package', + name='contributors', + field=models.ManyToManyField(related_name='package_contributions', through='project_manager.PackageContributor', to='users.ForumUser'), + ), + migrations.AddField( + model_name='package', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='users.forumuser'), + ), + migrations.AddField( + model_name='package', + name='supported_games', + field=models.ManyToManyField(related_name='packages', through='project_manager.PackageGame', to='games.Game'), + ), + migrations.AddField( + model_name='package', + name='tags', + field=models.ManyToManyField(related_name='packages', through='project_manager.PackageTag', to='tags.Tag'), + ), + migrations.AlterUniqueTogether( + name='subplugintag', + unique_together={('sub_plugin', 'tag')}, + ), + migrations.AlterUniqueTogether( + name='subpluginreleaseversioncontrolrequirement', + unique_together={('sub_plugin_release', 'vcs_requirement')}, + ), + migrations.AlterUniqueTogether( + name='subpluginreleasepypirequirement', + unique_together={('sub_plugin_release', 'pypi_requirement')}, + ), + migrations.AlterUniqueTogether( + name='subpluginreleasepackagerequirement', + unique_together={('sub_plugin_release', 'package_requirement')}, + ), + migrations.AlterUniqueTogether( + name='subpluginreleasedownloadrequirement', + unique_together={('sub_plugin_release', 'download_requirement')}, + ), + migrations.AlterUniqueTogether( + name='subpluginpath', + unique_together={('path', 'plugin')}, + ), + migrations.AlterUniqueTogether( + name='subplugingame', + unique_together={('sub_plugin', 'game')}, + ), + migrations.AlterUniqueTogether( + name='subplugincontributor', + unique_together={('sub_plugin', 'user')}, + ), + migrations.AlterUniqueTogether( + name='subplugin', + unique_together={('plugin', 'slug'), ('plugin', 'basename'), ('plugin', 'name')}, + ), + migrations.AlterUniqueTogether( + name='plugintag', + unique_together={('plugin', 'tag')}, + ), + migrations.AlterUniqueTogether( + name='pluginreleaseversioncontrolrequirement', + unique_together={('plugin_release', 'vcs_requirement')}, + ), + migrations.AlterUniqueTogether( + name='pluginreleasepypirequirement', + unique_together={('plugin_release', 'pypi_requirement')}, + ), + migrations.AlterUniqueTogether( + name='pluginreleasepackagerequirement', + unique_together={('plugin_release', 'package_requirement')}, + ), + migrations.AlterUniqueTogether( + name='pluginreleasedownloadrequirement', + unique_together={('plugin_release', 'download_requirement')}, + ), + migrations.AlterUniqueTogether( + name='pluginrelease', + unique_together={('plugin', 'version')}, + ), + migrations.AlterUniqueTogether( + name='plugingame', + unique_together={('plugin', 'game')}, + ), + migrations.AlterUniqueTogether( + name='plugincontributor', + unique_together={('plugin', 'user')}, + ), + migrations.AlterUniqueTogether( + name='packagetag', + unique_together={('package', 'tag')}, + ), + migrations.AlterUniqueTogether( + name='packagereleaseversioncontrolrequirement', + unique_together={('package_release', 'vcs_requirement')}, + ), + migrations.AlterUniqueTogether( + name='packagereleasepypirequirement', + unique_together={('package_release', 'pypi_requirement')}, + ), + migrations.AlterUniqueTogether( + name='packagereleasepackagerequirement', + unique_together={('package_release', 'package_requirement')}, + ), + migrations.AlterUniqueTogether( + name='packagereleasedownloadrequirement', + unique_together={('package_release', 'download_requirement')}, + ), + migrations.AlterUniqueTogether( + name='packagegame', + unique_together={('package', 'game')}, + ), + migrations.AlterUniqueTogether( + name='packagecontributor', + unique_together={('package', 'user')}, + ), + ] diff --git a/project_manager/models.py b/project_manager/models.py deleted file mode 100644 index c8f08f8b..00000000 --- a/project_manager/models.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Base app models.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Python -import uuid - -# Django -from django.contrib.auth.models import ( - AbstractBaseUser, - PermissionsMixin, - UserManager, -) -from django.db import models - -# App -from project_manager.constants import ( - USER_EMAIL_MAX_LENGTH, - USER_USERNAME_MAX_LENGTH, -) - - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ( - 'AbstractUUIDPrimaryKeyModel', - 'User', -) - - -# ============================================================================= -# >> MODELS -# ============================================================================= -class User(AbstractBaseUser, PermissionsMixin): - """Base User Model.""" - - username = models.CharField( - max_length=USER_USERNAME_MAX_LENGTH, - unique=True, - ) - email = models.EmailField( - max_length=USER_EMAIL_MAX_LENGTH, - blank=True, - ) - is_staff = models.BooleanField( - default=False, - ) - - objects = UserManager() - - USERNAME_FIELD = 'username' - - def get_short_name(self): - """Return the short name for the user.""" - return self.username - - def get_full_name(self): - """Return the full name for the user.""" - return self.username - - -class AbstractUUIDPrimaryKeyModel(models.Model): - """Abstract model that creates an non-editable UUID primary key.""" - - id = models.UUIDField( - verbose_name='ID', - primary_key=True, - default=uuid.uuid4, - editable=False, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True diff --git a/project_manager/packages/__init__.py b/project_manager/packages/__init__.py index c84e4271..5f5c84b3 100644 --- a/project_manager/packages/__init__.py +++ b/project_manager/packages/__init__.py @@ -1,3 +1 @@ """Package app.""" - -default_app_config = 'project_manager.packages.apps.PackageConfig' diff --git a/project_manager/packages/admin/__init__.py b/project_manager/packages/admin/__init__.py index f434d229..3bffaccf 100644 --- a/project_manager/packages/admin/__init__.py +++ b/project_manager/packages/admin/__init__.py @@ -1,7 +1,7 @@ """Package admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin @@ -19,7 +19,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageAdmin', @@ -27,7 +27,7 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= @admin.register(Package) class PackageAdmin(ProjectAdmin): @@ -35,8 +35,8 @@ class PackageAdmin(ProjectAdmin): inlines = ( PackageContributorInline, - PackageReleaseInline, PackageGameInline, PackageImageInline, PackageTagInline, + PackageReleaseInline, ) diff --git a/project_manager/packages/admin/inlines.py b/project_manager/packages/admin/inlines.py index bfd5c2cb..b584c76a 100644 --- a/project_manager/packages/admin/inlines.py +++ b/project_manager/packages/admin/inlines.py @@ -1,7 +1,7 @@ """Inline for Package admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.admin.inlines import ( @@ -21,7 +21,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageContributorInline', @@ -33,7 +33,7 @@ # ============================================================================= -# >> INLINES +# INLINES # ============================================================================= class PackageContributorInline(ProjectContributorInline): """Package Contributor Admin Inline.""" diff --git a/project_manager/packages/api/filtersets.py b/project_manager/packages/api/filtersets.py index 255e585d..3e0a25ce 100644 --- a/project_manager/packages/api/filtersets.py +++ b/project_manager/packages/api/filtersets.py @@ -1,7 +1,7 @@ """Package API filters.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.api.filtersets import ProjectFilterSet @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageFilterSet', @@ -17,7 +17,7 @@ # ============================================================================= -# >> FILTERS +# FILTERS # ============================================================================= class PackageFilterSet(ProjectFilterSet): """Filters for Packages.""" diff --git a/project_manager/packages/api/serializers/__init__.py b/project_manager/packages/api/serializers/__init__.py index 0ea3eada..bf762ee5 100644 --- a/project_manager/packages/api/serializers/__init__.py +++ b/project_manager/packages/api/serializers/__init__.py @@ -1,7 +1,7 @@ """Package serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.api.serializers import ( @@ -29,7 +29,7 @@ PackageReleaseVersionControlRequirement, PackageTag, ) -from project_manager.requirements.api.serializers.common import ( +from requirements.api.serializers.common import ( ReleaseDownloadRequirementSerializer, ReleasePyPiRequirementSerializer, ReleaseVersionControlRequirementSerializer, @@ -37,7 +37,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageContributorSerializer', @@ -56,7 +56,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class PackageImageSerializer(ProjectImageSerializer): """Serializer for adding, removing, and listing Package images.""" diff --git a/project_manager/packages/api/serializers/common.py b/project_manager/packages/api/serializers/common.py index 6758ed59..91541250 100644 --- a/project_manager/packages/api/serializers/common.py +++ b/project_manager/packages/api/serializers/common.py @@ -1,7 +1,7 @@ """Package serializers for APIs in other apps.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.fields import ReadOnlyField @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ReleasePackageRequirementSerializer', @@ -17,7 +17,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class ReleasePackageRequirementSerializer(ModelSerializer): """Serializer for Package requirements.""" diff --git a/project_manager/packages/api/serializers/mixins.py b/project_manager/packages/api/serializers/mixins.py index 77f1bf22..7f35fdfe 100644 --- a/project_manager/packages/api/serializers/mixins.py +++ b/project_manager/packages/api/serializers/mixins.py @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageReleaseBase', @@ -17,7 +17,7 @@ # ============================================================================= -# >> MIXINS +# MIXINS # ============================================================================= class PackageReleaseBase: """Serializer for listing Package releases.""" diff --git a/project_manager/packages/api/urls.py b/project_manager/packages/api/urls.py index 2b6a78bf..037e4315 100644 --- a/project_manager/packages/api/urls.py +++ b/project_manager/packages/api/urls.py @@ -1,7 +1,7 @@ """Package API URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf.urls import url @@ -22,7 +22,7 @@ # ============================================================================= -# >> ROUTERS +# ROUTERS # ============================================================================= router = routers.SimpleRouter() router.register( @@ -58,7 +58,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'packages' diff --git a/project_manager/packages/api/views.py b/project_manager/packages/api/views.py index 0e3257fc..6e20c241 100644 --- a/project_manager/packages/api/views.py +++ b/project_manager/packages/api/views.py @@ -1,7 +1,7 @@ """Package API views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Prefetch @@ -41,7 +41,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageAPIView', @@ -55,7 +55,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class PackageAPIView(ProjectAPIView): """Package API routes.""" diff --git a/project_manager/packages/apps.py b/project_manager/packages/apps.py deleted file mode 100644 index bd71fbe3..00000000 --- a/project_manager/packages/apps.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Package app config.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Django -from django.apps import AppConfig - - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ( - 'PackageConfig', -) - - -# ============================================================================= -# >> APPLICATION CONFIG -# ============================================================================= -class PackageConfig(AppConfig): - """Package app config.""" - - name = 'project_manager.packages' - verbose_name = 'Packages' diff --git a/project_manager/packages/constants.py b/project_manager/packages/constants.py index 875bbed7..7165831f 100644 --- a/project_manager/packages/constants.py +++ b/project_manager/packages/constants.py @@ -1,7 +1,7 @@ """Constants for use with Packages.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.constants import ( @@ -11,7 +11,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PACKAGE_ALLOWED_FILE_TYPES', @@ -23,7 +23,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # The base path for packages PACKAGE_PATH = 'addons/source-python/packages/custom/' diff --git a/project_manager/packages/helpers.py b/project_manager/packages/helpers.py index 429b2894..3e251c1a 100644 --- a/project_manager/packages/helpers.py +++ b/project_manager/packages/helpers.py @@ -1,7 +1,7 @@ """Helpers for use with Packages.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.exceptions import ValidationError @@ -18,7 +18,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageZipFile', @@ -29,7 +29,7 @@ # ============================================================================= -# >> CLASSES +# CLASSES # ============================================================================= class PackageZipFile(ProjectZipFile): """Package ZipFile parsing class.""" @@ -84,7 +84,7 @@ def get_requirement_path(self): # ============================================================================= -# >> FUNCTIONS +# FUNCTIONS # ============================================================================= def handle_package_zip_upload(instance, filename): """Return the path to store the zip for the current release.""" diff --git a/project_manager/packages/migrations/0001_initial.py b/project_manager/packages/migrations/0001_initial.py deleted file mode 100644 index 9a828fc8..00000000 --- a/project_manager/packages/migrations/0001_initial.py +++ /dev/null @@ -1,208 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 - -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import embed_video.fields -import model_utils.fields -import precise_bbcode.fields -import project_manager.common.helpers -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('games', '0001_initial'), - ('requirements', '0001_initial'), - ('tags', '0001_initial'), - ('users', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Package', - fields=[ - ('name', models.CharField(help_text="The name of the project. Do not include the version, as that is added dynamically to the project's page.", max_length=64)), - ('_configuration_rendered', models.TextField(blank=True, editable=False, null=True)), - ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), - ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), - ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), - ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), - ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), - ('topic', models.IntegerField(blank=True, null=True, unique=True)), - ('created', models.DateTimeField(verbose_name='created')), - ('updated', models.DateTimeField(verbose_name='updated')), - ('basename', models.CharField(blank=True, max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z_]*[0-9a-z]')])), - ('slug', models.SlugField(blank=True, max_length=32, primary_key=True, serialize=False, unique=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PackageRelease', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), - ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), - ('download_count', models.PositiveIntegerField(default=0)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ], - options={ - 'verbose_name': 'Release', - 'verbose_name_plural': 'Releases', - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PackageTag', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.package')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.tag')), - ], - options={ - 'unique_together': {('package', 'tag')}, - }, - ), - migrations.CreateModel( - name='PackageReleaseVersionControlRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('package_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.packagerelease')), - ('vcs_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.versioncontrolrequirement')), - ], - options={ - 'unique_together': {('package_release', 'vcs_requirement')}, - }, - ), - migrations.CreateModel( - name='PackageReleasePyPiRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('package_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.packagerelease')), - ('pypi_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.pypirequirement')), - ], - options={ - 'unique_together': {('package_release', 'pypi_requirement')}, - }, - ), - migrations.CreateModel( - name='PackageReleasePackageRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('package_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.packagerelease')), - ('package_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.package')), - ], - options={ - 'unique_together': {('package_release', 'package_requirement')}, - }, - ), - migrations.CreateModel( - name='PackageReleaseDownloadRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('optional', models.BooleanField(default=False)), - ('download_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.downloadrequirement')), - ('package_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.packagerelease')), - ], - options={ - 'unique_together': {('package_release', 'download_requirement')}, - }, - ), - migrations.AddField( - model_name='packagerelease', - name='download_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='packages.PackageReleaseDownloadRequirement', to='requirements.DownloadRequirement'), - ), - migrations.AddField( - model_name='packagerelease', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='packages.package'), - ), - migrations.AddField( - model_name='packagerelease', - name='package_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='packages.PackageReleasePackageRequirement', to='packages.Package'), - ), - migrations.AddField( - model_name='packagerelease', - name='pypi_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='packages.PackageReleasePyPiRequirement', to='requirements.PyPiRequirement'), - ), - migrations.AddField( - model_name='packagerelease', - name='vcs_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='packages.PackageReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), - ), - migrations.CreateModel( - name='PackageImage', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='packages.package')), - ], - options={ - 'verbose_name': 'Image', - 'verbose_name_plural': 'Images', - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PackageGame', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.package')), - ], - options={ - 'unique_together': {('package', 'game')}, - }, - ), - migrations.CreateModel( - name='PackageContributor', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.package')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.forumuser')), - ], - options={ - 'unique_together': {('package', 'user')}, - }, - ), - migrations.AddField( - model_name='package', - name='contributors', - field=models.ManyToManyField(related_name='package_contributions', through='packages.PackageContributor', to='users.ForumUser'), - ), - migrations.AddField( - model_name='package', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='users.forumuser'), - ), - migrations.AddField( - model_name='package', - name='supported_games', - field=models.ManyToManyField(related_name='packages', through='packages.PackageGame', to='games.Game'), - ), - migrations.AddField( - model_name='package', - name='tags', - field=models.ManyToManyField(related_name='packages', through='packages.PackageTag', to='tags.Tag'), - ), - ] diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models/__init__.py index 20ffb518..65392ef0 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models/__init__.py @@ -1,7 +1,7 @@ """Package model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.urls import reverse @@ -38,7 +38,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'Package', @@ -55,7 +55,7 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class Package(ProjectBase): """Package project type model.""" @@ -69,7 +69,7 @@ class Package(ProjectBase): contributors = models.ManyToManyField( to='users.ForumUser', related_name='package_contributions', - through='packages.PackageContributor', + through='project_manager.PackageContributor', ) slug = models.SlugField( max_length=PROJECT_SLUG_MAX_LENGTH, @@ -80,12 +80,12 @@ class Package(ProjectBase): supported_games = models.ManyToManyField( to='games.Game', related_name='packages', - through='packages.PackageGame', + through='project_manager.PackageGame', ) tags = models.ManyToManyField( to='tags.Tag', related_name='packages', - through='packages.PackageTag', + through='project_manager.PackageTag', ) handle_logo_upload = handle_package_logo_upload @@ -105,29 +105,29 @@ class PackageRelease(ProjectRelease): """Package release type model.""" package = models.ForeignKey( - to='packages.Package', + to='project_manager.Package', related_name='releases', on_delete=models.CASCADE, ) download_requirements = models.ManyToManyField( to='requirements.DownloadRequirement', related_name='required_in_package_releases', - through='packages.PackageReleaseDownloadRequirement', + through='project_manager.PackageReleaseDownloadRequirement', ) package_requirements = models.ManyToManyField( - to='packages.Package', + to='project_manager.Package', related_name='required_in_package_releases', - through='packages.PackageReleasePackageRequirement', + through='project_manager.PackageReleasePackageRequirement', ) pypi_requirements = models.ManyToManyField( to='requirements.PyPiRequirement', related_name='required_in_package_releases', - through='packages.PackageReleasePyPiRequirement', + through='project_manager.PackageReleasePyPiRequirement', ) vcs_requirements = models.ManyToManyField( to='requirements.VersionControlRequirement', related_name='required_in_package_releases', - through='packages.PackageReleaseVersionControlRequirement', + through='project_manager.PackageReleaseVersionControlRequirement', ) handle_zip_file_upload = handle_package_zip_upload @@ -153,7 +153,7 @@ class PackageImage(ProjectImage): """Package image type model.""" package = models.ForeignKey( - to='packages.Package', + to='project_manager.Package', related_name='images', on_delete=models.CASCADE, ) diff --git a/project_manager/packages/models/abstract.py b/project_manager/packages/models/abstract.py index b7951608..86ba5a4f 100644 --- a/project_manager/packages/models/abstract.py +++ b/project_manager/packages/models/abstract.py @@ -1,14 +1,14 @@ """Base models for Packages.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db import models # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageReleaseThroughBase', @@ -17,13 +17,13 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class PackageThroughBase(models.Model): """Base through model class for Packages.""" package = models.ForeignKey( - to='packages.Package', + to='project_manager.Package', on_delete=models.CASCADE, ) @@ -42,7 +42,7 @@ class PackageReleaseThroughBase(models.Model): """Base through model class for Packages.""" package_release = models.ForeignKey( - to='packages.PackageRelease', + to='project_manager.PackageRelease', on_delete=models.CASCADE, ) diff --git a/project_manager/packages/views.py b/project_manager/packages/views.py index a83d9a97..c9b9d2fc 100644 --- a/project_manager/packages/views.py +++ b/project_manager/packages/views.py @@ -1,7 +1,7 @@ """Package views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.mixins import DownloadMixin @@ -10,7 +10,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PackageReleaseDownloadView', @@ -18,7 +18,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class PackageReleaseDownloadView(DownloadMixin): """Package download view for releases.""" diff --git a/project_manager/plugins/__init__.py b/project_manager/plugins/__init__.py index 445bd037..4a29575f 100644 --- a/project_manager/plugins/__init__.py +++ b/project_manager/plugins/__init__.py @@ -1,3 +1 @@ """Plugin app.""" - -default_app_config = 'project_manager.plugins.apps.PluginConfig' diff --git a/project_manager/plugins/admin/__init__.py b/project_manager/plugins/admin/__init__.py index 6a8e50c0..9c902a48 100644 --- a/project_manager/plugins/admin/__init__.py +++ b/project_manager/plugins/admin/__init__.py @@ -1,7 +1,7 @@ """Plugin admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin @@ -20,7 +20,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginAdmin', @@ -28,7 +28,7 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= @admin.register(Plugin) class PluginAdmin(ProjectAdmin): @@ -36,9 +36,9 @@ class PluginAdmin(ProjectAdmin): inlines = ( PluginContributorInline, - PluginReleaseInline, PluginGameInline, PluginImageInline, PluginTagInline, SubPluginPathInline, + PluginReleaseInline, ) diff --git a/project_manager/plugins/admin/inlines.py b/project_manager/plugins/admin/inlines.py index 403e9610..e23cdd36 100644 --- a/project_manager/plugins/admin/inlines.py +++ b/project_manager/plugins/admin/inlines.py @@ -1,7 +1,7 @@ """Inline for Plugin admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin @@ -25,7 +25,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginContributorInline', @@ -38,7 +38,7 @@ # ============================================================================= -# >> INLINES +# INLINES # ============================================================================= class PluginContributorInline(ProjectContributorInline): """Plugin Contributor Admin Inline.""" diff --git a/project_manager/plugins/api/filtersets.py b/project_manager/plugins/api/filtersets.py index 9b54e808..ca403f01 100644 --- a/project_manager/plugins/api/filtersets.py +++ b/project_manager/plugins/api/filtersets.py @@ -1,7 +1,7 @@ """Plugin API filters.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.api.filtersets import ProjectFilterSet @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginFilterSet', @@ -17,7 +17,7 @@ # ============================================================================= -# >> FILTERS +# FILTERS # ============================================================================= class PluginFilterSet(ProjectFilterSet): """Filters for Plugins.""" diff --git a/project_manager/plugins/api/serializers/__init__.py b/project_manager/plugins/api/serializers/__init__.py index 86d30755..285014e9 100644 --- a/project_manager/plugins/api/serializers/__init__.py +++ b/project_manager/plugins/api/serializers/__init__.py @@ -1,7 +1,7 @@ """Plugin serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.exceptions import ValidationError @@ -34,7 +34,7 @@ PluginTag, SubPluginPath, ) -from project_manager.requirements.api.serializers.common import ( +from requirements.api.serializers.common import ( ReleaseDownloadRequirementSerializer, ReleasePyPiRequirementSerializer, ReleaseVersionControlRequirementSerializer, @@ -42,7 +42,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginContributorSerializer', @@ -62,7 +62,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class PluginImageSerializer(ProjectImageSerializer): """Serializer for adding, removing, and listing Plugin images.""" diff --git a/project_manager/plugins/api/serializers/mixins.py b/project_manager/plugins/api/serializers/mixins.py index e25fd21f..ccae4e84 100644 --- a/project_manager/plugins/api/serializers/mixins.py +++ b/project_manager/plugins/api/serializers/mixins.py @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginReleaseBase', @@ -17,7 +17,7 @@ # ============================================================================= -# >> MIXINS +# MIXINS # ============================================================================= class PluginReleaseBase: """Serializer for listing Plugin releases.""" diff --git a/project_manager/plugins/api/urls.py b/project_manager/plugins/api/urls.py index fbada576..e55664c1 100644 --- a/project_manager/plugins/api/urls.py +++ b/project_manager/plugins/api/urls.py @@ -1,7 +1,7 @@ """Plugin API URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf.urls import url @@ -23,7 +23,7 @@ # ============================================================================= -# >> ROUTERS +# ROUTERS # ============================================================================= router = routers.SimpleRouter() router.register( @@ -64,7 +64,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'plugins' diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index efc6ed4b..3ac7c711 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -1,7 +1,7 @@ """Plugin API views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Prefetch @@ -47,7 +47,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginAPIView', @@ -62,7 +62,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class PluginAPIView(ProjectAPIView): """Plugin API routes.""" diff --git a/project_manager/plugins/apps.py b/project_manager/plugins/apps.py deleted file mode 100644 index fa64917b..00000000 --- a/project_manager/plugins/apps.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Plugin app config.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Django -from django.apps import AppConfig - - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ( - 'PluginConfig', -) - - -# ============================================================================= -# >> APPLICATION CONFIG -# ============================================================================= -class PluginConfig(AppConfig): - """Plugin app config.""" - - name = 'project_manager.plugins' - verbose_name = 'Plugins' diff --git a/project_manager/plugins/constants.py b/project_manager/plugins/constants.py index 955617f0..5d1b3aea 100644 --- a/project_manager/plugins/constants.py +++ b/project_manager/plugins/constants.py @@ -1,7 +1,7 @@ """Constants for use with Plugins.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.constants import ( @@ -14,7 +14,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PATH_MAX_LENGTH', @@ -28,7 +28,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # The base path for plugins PLUGIN_PATH = 'addons/source-python/plugins/' diff --git a/project_manager/plugins/helpers.py b/project_manager/plugins/helpers.py index aabb05e6..6b0d2313 100644 --- a/project_manager/plugins/helpers.py +++ b/project_manager/plugins/helpers.py @@ -1,7 +1,7 @@ """Helpers for use with Plugins.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.exceptions import ValidationError @@ -18,7 +18,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginZipFile', @@ -29,7 +29,7 @@ # ============================================================================= -# >> CLASSES +# CLASSES # ============================================================================= class PluginZipFile(ProjectZipFile): """Plugin ZipFile parsing class.""" @@ -70,7 +70,7 @@ def get_requirement_path(self): # ============================================================================= -# >> FUNCTIONS +# FUNCTIONS # ============================================================================= def handle_plugin_zip_upload(instance, filename): """Return the path to store the zip for the current release.""" diff --git a/project_manager/plugins/migrations/0001_initial.py b/project_manager/plugins/migrations/0001_initial.py deleted file mode 100644 index bfe52091..00000000 --- a/project_manager/plugins/migrations/0001_initial.py +++ /dev/null @@ -1,229 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 - -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import embed_video.fields -import model_utils.fields -import precise_bbcode.fields -import project_manager.common.helpers -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('packages', '0001_initial'), - ('games', '0001_initial'), - ('requirements', '0001_initial'), - ('users', '0001_initial'), - ('tags', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Plugin', - fields=[ - ('name', models.CharField(help_text="The name of the project. Do not include the version, as that is added dynamically to the project's page.", max_length=64)), - ('_configuration_rendered', models.TextField(blank=True, editable=False, null=True)), - ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), - ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), - ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), - ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), - ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), - ('topic', models.IntegerField(blank=True, null=True, unique=True)), - ('created', models.DateTimeField(verbose_name='created')), - ('updated', models.DateTimeField(verbose_name='updated')), - ('basename', models.CharField(blank=True, max_length=32, unique=True, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z_]*[0-9a-z]')])), - ('slug', models.SlugField(blank=True, max_length=32, primary_key=True, serialize=False, unique=True)), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PluginRelease', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), - ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), - ('download_count', models.PositiveIntegerField(default=0)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ], - options={ - 'verbose_name': 'Release', - 'verbose_name_plural': 'Releases', - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PluginTag', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.plugin')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.tag')), - ], - options={ - 'unique_together': {('plugin', 'tag')}, - }, - ), - migrations.CreateModel( - name='PluginReleaseVersionControlRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.pluginrelease')), - ('vcs_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.versioncontrolrequirement')), - ], - options={ - 'unique_together': {('plugin_release', 'vcs_requirement')}, - }, - ), - migrations.CreateModel( - name='PluginReleasePyPiRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.pluginrelease')), - ('pypi_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.pypirequirement')), - ], - options={ - 'unique_together': {('plugin_release', 'pypi_requirement')}, - }, - ), - migrations.CreateModel( - name='PluginReleasePackageRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('package_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.package')), - ('plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.pluginrelease')), - ], - options={ - 'unique_together': {('plugin_release', 'package_requirement')}, - }, - ), - migrations.CreateModel( - name='PluginReleaseDownloadRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('optional', models.BooleanField(default=False)), - ('download_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.downloadrequirement')), - ('plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.pluginrelease')), - ], - options={ - 'unique_together': {('plugin_release', 'download_requirement')}, - }, - ), - migrations.AddField( - model_name='pluginrelease', - name='download_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='plugins.PluginReleaseDownloadRequirement', to='requirements.DownloadRequirement'), - ), - migrations.AddField( - model_name='pluginrelease', - name='package_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='plugins.PluginReleasePackageRequirement', to='packages.Package'), - ), - migrations.AddField( - model_name='pluginrelease', - name='plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='plugins.plugin'), - ), - migrations.AddField( - model_name='pluginrelease', - name='pypi_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='plugins.PluginReleasePyPiRequirement', to='requirements.PyPiRequirement'), - ), - migrations.AddField( - model_name='pluginrelease', - name='vcs_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='plugins.PluginReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), - ), - migrations.CreateModel( - name='PluginImage', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='plugins.plugin')), - ], - options={ - 'verbose_name': 'Image', - 'verbose_name_plural': 'Images', - 'abstract': False, - }, - ), - migrations.CreateModel( - name='PluginGame', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game')), - ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.plugin')), - ], - options={ - 'unique_together': {('plugin', 'game')}, - }, - ), - migrations.CreateModel( - name='PluginContributor', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plugins.plugin')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.forumuser')), - ], - options={ - 'unique_together': {('plugin', 'user')}, - }, - ), - migrations.AddField( - model_name='plugin', - name='contributors', - field=models.ManyToManyField(related_name='plugin_contributions', through='plugins.PluginContributor', to='users.ForumUser'), - ), - migrations.AddField( - model_name='plugin', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='users.forumuser'), - ), - migrations.AddField( - model_name='plugin', - name='supported_games', - field=models.ManyToManyField(related_name='plugins', through='plugins.PluginGame', to='games.Game'), - ), - migrations.AddField( - model_name='plugin', - name='tags', - field=models.ManyToManyField(related_name='plugins', through='plugins.PluginTag', to='tags.Tag'), - ), - migrations.CreateModel( - name='SubPluginPath', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('path', models.CharField(max_length=256, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z/\\\\_]*[0-9a-z]')])), - ('allow_module', models.BooleanField(default=False)), - ('allow_package_using_basename', models.BooleanField(default=False)), - ('allow_package_using_init', models.BooleanField(default=False)), - ('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paths', to='plugins.plugin')), - ], - options={ - 'verbose_name': 'SubPlugin Path', - 'verbose_name_plural': 'SubPlugin Paths', - 'unique_together': {('path', 'plugin')}, - }, - ), - migrations.AlterUniqueTogether( - name='pluginrelease', - unique_together={('plugin', 'version')}, - ), - ] diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index c3738b30..060d12db 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -1,7 +1,7 @@ """Plugin model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.exceptions import ValidationError @@ -26,7 +26,7 @@ ProjectTag, ) from project_manager.common.validators import basename_validator -from project_manager.models import AbstractUUIDPrimaryKeyModel +from project_manager.common.models import AbstractUUIDPrimaryKeyModel from project_manager.plugins.constants import PLUGIN_LOGO_URL, PATH_MAX_LENGTH from project_manager.plugins.helpers import ( handle_plugin_image_upload, @@ -41,7 +41,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'Plugin', @@ -59,7 +59,7 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class Plugin(ProjectBase): """Plugin project type model.""" @@ -73,7 +73,7 @@ class Plugin(ProjectBase): contributors = models.ManyToManyField( to='users.ForumUser', related_name='plugin_contributions', - through='plugins.PluginContributor', + through='project_manager.PluginContributor', ) slug = models.SlugField( max_length=PROJECT_SLUG_MAX_LENGTH, @@ -84,12 +84,12 @@ class Plugin(ProjectBase): supported_games = models.ManyToManyField( to='games.Game', related_name='plugins', - through='plugins.PluginGame', + through='project_manager.PluginGame', ) tags = models.ManyToManyField( to='tags.Tag', related_name='plugins', - through='plugins.PluginTag', + through='project_manager.PluginTag', ) handle_logo_upload = handle_plugin_logo_upload @@ -109,29 +109,29 @@ class PluginRelease(ProjectRelease): """Plugin release type model.""" plugin = models.ForeignKey( - to='plugins.Plugin', + to='project_manager.Plugin', related_name='releases', on_delete=models.CASCADE, ) download_requirements = models.ManyToManyField( to='requirements.DownloadRequirement', related_name='required_in_plugin_releases', - through='plugins.PluginReleaseDownloadRequirement', + through='project_manager.PluginReleaseDownloadRequirement', ) package_requirements = models.ManyToManyField( - to='packages.Package', + to='project_manager.Package', related_name='required_in_plugin_releases', - through='plugins.PluginReleasePackageRequirement', + through='project_manager.PluginReleasePackageRequirement', ) pypi_requirements = models.ManyToManyField( to='requirements.PyPiRequirement', related_name='required_in_plugin_releases', - through='plugins.PluginReleasePyPiRequirement', + through='project_manager.PluginReleasePyPiRequirement', ) vcs_requirements = models.ManyToManyField( to='requirements.VersionControlRequirement', related_name='required_in_plugin_releases', - through='plugins.PluginReleaseVersionControlRequirement', + through='project_manager.PluginReleaseVersionControlRequirement', ) handle_zip_file_upload = handle_plugin_zip_upload @@ -162,7 +162,7 @@ class PluginImage(ProjectImage): """Plugin image type model.""" plugin = models.ForeignKey( - to='plugins.Plugin', + to='project_manager.Plugin', related_name='images', on_delete=models.CASCADE, ) @@ -201,7 +201,7 @@ class SubPluginPath(AbstractUUIDPrimaryKeyModel): """Model to store SubPlugin paths for a Plugin.""" plugin = models.ForeignKey( - to='plugins.Plugin', + to='project_manager.Plugin', related_name='paths', on_delete=models.CASCADE, ) diff --git a/project_manager/plugins/models/abstract.py b/project_manager/plugins/models/abstract.py index 21a00930..1b956256 100644 --- a/project_manager/plugins/models/abstract.py +++ b/project_manager/plugins/models/abstract.py @@ -1,14 +1,14 @@ """Base models for Plugins.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db import models # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginReleaseThroughBase', @@ -17,13 +17,13 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class PluginThroughBase(models.Model): """Base through model class for Plugins.""" plugin = models.ForeignKey( - to='plugins.Plugin', + to='project_manager.Plugin', on_delete=models.CASCADE, ) @@ -42,7 +42,7 @@ class PluginReleaseThroughBase(models.Model): """Base through model class for Packages.""" plugin_release = models.ForeignKey( - to='plugins.PluginRelease', + to='project_manager.PluginRelease', on_delete=models.CASCADE, ) diff --git a/project_manager/plugins/validators.py b/project_manager/plugins/validators.py index 3e794e26..6c937a9f 100644 --- a/project_manager/plugins/validators.py +++ b/project_manager/plugins/validators.py @@ -1,14 +1,14 @@ """SubPluginPath validators.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.validators import RegexValidator # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'sub_plugin_path_validator', @@ -16,7 +16,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # Sub-plugin paths should: # Start with a lower-case character. diff --git a/project_manager/plugins/views.py b/project_manager/plugins/views.py index a879d7c8..063e3a80 100644 --- a/project_manager/plugins/views.py +++ b/project_manager/plugins/views.py @@ -1,7 +1,7 @@ """Plugin views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.mixins import DownloadMixin @@ -10,7 +10,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginReleaseDownloadView', @@ -18,7 +18,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class PluginReleaseDownloadView(DownloadMixin): """Plugin download view for releases.""" diff --git a/project_manager/requirements/__init__.py b/project_manager/requirements/__init__.py deleted file mode 100644 index 17a00bb2..00000000 --- a/project_manager/requirements/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Requirement app.""" - -default_app_config = 'project_manager.requirements.apps.RequirementConfig' diff --git a/project_manager/sub_plugins/__init__.py b/project_manager/sub_plugins/__init__.py index eb45f232..98b33409 100644 --- a/project_manager/sub_plugins/__init__.py +++ b/project_manager/sub_plugins/__init__.py @@ -1,3 +1 @@ """SubPlugin app.""" - -default_app_config = 'project_manager.sub_plugins.apps.SubPluginConfig' diff --git a/project_manager/sub_plugins/admin/__init__.py b/project_manager/sub_plugins/admin/__init__.py index 389f7538..79ea3bd9 100644 --- a/project_manager/sub_plugins/admin/__init__.py +++ b/project_manager/sub_plugins/admin/__init__.py @@ -1,7 +1,7 @@ """SubPlugin admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Python import copy @@ -22,7 +22,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginAdmin', @@ -30,7 +30,7 @@ # ============================================================================= -# >> GLOBALS +# GLOBAL VARIABLES # ============================================================================= _project_fieldsets = copy.deepcopy(ProjectAdmin.fieldsets) _fields = _project_fieldsets[0][1]['fields'] @@ -38,7 +38,7 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= @admin.register(SubPlugin) class SubPluginAdmin(ProjectAdmin): @@ -47,10 +47,10 @@ class SubPluginAdmin(ProjectAdmin): fieldsets = _project_fieldsets inlines = ( SubPluginContributorInline, - SubPluginReleaseInline, SubPluginGameInline, SubPluginImageInline, SubPluginTagInline, + SubPluginReleaseInline, ) list_display = ProjectAdmin.list_display + ( 'plugin', diff --git a/project_manager/sub_plugins/admin/forms.py b/project_manager/sub_plugins/admin/forms.py index d7d22a1c..0e3e1be1 100644 --- a/project_manager/sub_plugins/admin/forms.py +++ b/project_manager/sub_plugins/admin/forms.py @@ -1,7 +1,7 @@ """Forms to use for SubPlugin admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django import forms @@ -13,7 +13,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginAdminForm', @@ -21,7 +21,7 @@ # ============================================================================= -# >> ADMIN FORMS +# ADMIN FORMS # ============================================================================= class SubPluginAdminForm(forms.ModelForm): """Form to use for selecting the Plugin for a SubPlugin.""" diff --git a/project_manager/sub_plugins/admin/inlines.py b/project_manager/sub_plugins/admin/inlines.py index edaadec2..69291350 100644 --- a/project_manager/sub_plugins/admin/inlines.py +++ b/project_manager/sub_plugins/admin/inlines.py @@ -1,7 +1,7 @@ """Inline for SubPlugin admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.admin.inlines import ( @@ -21,7 +21,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginContributorInline', @@ -33,7 +33,7 @@ # ============================================================================= -# >> INLINES +# INLINES # ============================================================================= class SubPluginContributorInline(ProjectContributorInline): """SubPlugin Contributor Admin Inline.""" diff --git a/project_manager/sub_plugins/admin/widgets.py b/project_manager/sub_plugins/admin/widgets.py index 1fe39eef..d7b70a40 100644 --- a/project_manager/sub_plugins/admin/widgets.py +++ b/project_manager/sub_plugins/admin/widgets.py @@ -1,14 +1,14 @@ """Widgets to use for SubPlugin admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib.admin import widgets # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'PluginRawIdWidget', @@ -16,7 +16,7 @@ # ============================================================================= -# >> WIDGETS +# WIDGETS # ============================================================================= class PluginRawIdWidget(widgets.ForeignKeyRawIdWidget): """Widget to use for selecting the Plugin for a SubPlugin.""" diff --git a/project_manager/sub_plugins/api/filtersets.py b/project_manager/sub_plugins/api/filtersets.py index 272815eb..a4069ad4 100644 --- a/project_manager/sub_plugins/api/filtersets.py +++ b/project_manager/sub_plugins/api/filtersets.py @@ -1,7 +1,7 @@ """SubPlugin API filters.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.api.filtersets import ProjectFilterSet @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginFilterSet', @@ -17,7 +17,7 @@ # ============================================================================= -# >> FILTERS +# FILTERS # ============================================================================= class SubPluginFilterSet(ProjectFilterSet): """Filters for SubPlugins.""" diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index e8c6df2c..e6aa10fb 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -1,7 +1,7 @@ """SubPlugin serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.exceptions import ValidationError @@ -20,7 +20,7 @@ ReleasePackageRequirementSerializer, ) from project_manager.plugins.models import Plugin -from project_manager.requirements.api.serializers.common import ( +from requirements.api.serializers.common import ( ReleaseDownloadRequirementSerializer, ReleasePyPiRequirementSerializer, ReleaseVersionControlRequirementSerializer, @@ -43,7 +43,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginContributorSerializer', @@ -62,7 +62,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class SubPluginImageSerializer(ProjectImageSerializer): """Serializer for adding, removing, and listing SubPlugin images.""" diff --git a/project_manager/sub_plugins/api/serializers/mixins.py b/project_manager/sub_plugins/api/serializers/mixins.py index a5647ffb..a402a472 100644 --- a/project_manager/sub_plugins/api/serializers/mixins.py +++ b/project_manager/sub_plugins/api/serializers/mixins.py @@ -13,7 +13,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginReleaseBase', @@ -21,7 +21,7 @@ # ============================================================================= -# >> MIXINS +# MIXINS # ============================================================================= class SubPluginReleaseBase: """Serializer for listing Plugin releases.""" diff --git a/project_manager/sub_plugins/api/urls.py b/project_manager/sub_plugins/api/urls.py index ef9ed7c3..3ba70e26 100644 --- a/project_manager/sub_plugins/api/urls.py +++ b/project_manager/sub_plugins/api/urls.py @@ -1,7 +1,7 @@ """SubPlugin API URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf.urls import url @@ -22,7 +22,7 @@ # ============================================================================= -# >> ROUTERS +# ROUTERS # ============================================================================= router = routers.SimpleRouter() router.register( @@ -60,7 +60,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'sub-plugins' diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index ec0b1420..45af5f65 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -1,7 +1,7 @@ """SubPlugin API views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Prefetch @@ -45,7 +45,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginAPIView', @@ -59,7 +59,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class SubPluginAPIView(ProjectAPIView): """SubPlugin API routes.""" diff --git a/project_manager/sub_plugins/apps.py b/project_manager/sub_plugins/apps.py deleted file mode 100644 index 2ca77377..00000000 --- a/project_manager/sub_plugins/apps.py +++ /dev/null @@ -1,25 +0,0 @@ -"""SubPlugin app config.""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Django -from django.apps import AppConfig - - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ( - 'SubPluginConfig', -) - - -# ============================================================================= -# >> APPLICATION CONFIG -# ============================================================================= -class SubPluginConfig(AppConfig): - """SubPlugin app config.""" - - name = 'project_manager.sub_plugins' - verbose_name = 'SubPlugins' diff --git a/project_manager/sub_plugins/constants.py b/project_manager/sub_plugins/constants.py index 41f46844..a613e9c6 100644 --- a/project_manager/sub_plugins/constants.py +++ b/project_manager/sub_plugins/constants.py @@ -1,7 +1,7 @@ """Constants for use with SubPlugins.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.constants import ( @@ -12,7 +12,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SUB_PLUGIN_ALLOWED_FILE_TYPES', @@ -23,7 +23,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # The allowed file types by directory for sub-plugins SUB_PLUGIN_ALLOWED_FILE_TYPES = dict(ALLOWED_FILE_TYPES) diff --git a/project_manager/sub_plugins/helpers.py b/project_manager/sub_plugins/helpers.py index bd877f7a..5bfe899e 100644 --- a/project_manager/sub_plugins/helpers.py +++ b/project_manager/sub_plugins/helpers.py @@ -1,7 +1,7 @@ """Helpers for use with SubPlugins.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.exceptions import ValidationError @@ -18,7 +18,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginZipFile', @@ -29,7 +29,7 @@ # ============================================================================= -# >> CLASSES +# CLASSES # ============================================================================= class SubPluginZipFile(ProjectZipFile): """SubPlugin ZipFile parsing class.""" @@ -188,7 +188,7 @@ def get_requirement_path(self): # ============================================================================= -# >> FUNCTIONS +# FUNCTIONS # ============================================================================= def handle_sub_plugin_zip_upload(instance, filename): """Return the path to store the zip for the current release.""" diff --git a/project_manager/sub_plugins/migrations/0001_initial.py b/project_manager/sub_plugins/migrations/0001_initial.py deleted file mode 100644 index e8e868e6..00000000 --- a/project_manager/sub_plugins/migrations/0001_initial.py +++ /dev/null @@ -1,221 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 - -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import embed_video.fields -import model_utils.fields -import precise_bbcode.fields -import project_manager.common.helpers -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('plugins', '0001_initial'), - ('packages', '0001_initial'), - ('games', '0001_initial'), - ('requirements', '0001_initial'), - ('users', '0001_initial'), - ('tags', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='SubPlugin', - fields=[ - ('name', models.CharField(help_text="The name of the project. Do not include the version, as that is added dynamically to the project's page.", max_length=64)), - ('_configuration_rendered', models.TextField(blank=True, editable=False, null=True)), - ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), - ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), - ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), - ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), - ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), - ('topic', models.IntegerField(blank=True, null=True, unique=True)), - ('created', models.DateTimeField(verbose_name='created')), - ('updated', models.DateTimeField(verbose_name='updated')), - ('id', models.CharField(blank=True, max_length=65, primary_key=True, serialize=False)), - ('basename', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[a-z][0-9a-z_]*[0-9a-z]')])), - ('slug', models.SlugField(blank=True, max_length=32)), - ], - options={ - 'verbose_name': 'SubPlugin', - 'verbose_name_plural': 'SubPlugins', - }, - ), - migrations.CreateModel( - name='SubPluginRelease', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), - ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), - ('download_count', models.PositiveIntegerField(default=0)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ], - options={ - 'verbose_name': 'Release', - 'verbose_name_plural': 'Releases', - 'abstract': False, - }, - ), - migrations.CreateModel( - name='SubPluginTag', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('sub_plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subplugin')), - ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tags.tag')), - ], - options={ - 'unique_together': {('sub_plugin', 'tag')}, - }, - ), - migrations.CreateModel( - name='SubPluginReleaseVersionControlRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('sub_plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subpluginrelease')), - ('vcs_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.versioncontrolrequirement')), - ], - options={ - 'unique_together': {('sub_plugin_release', 'vcs_requirement')}, - }, - ), - migrations.CreateModel( - name='SubPluginReleasePyPiRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('pypi_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.pypirequirement')), - ('sub_plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subpluginrelease')), - ], - options={ - 'unique_together': {('sub_plugin_release', 'pypi_requirement')}, - }, - ), - migrations.CreateModel( - name='SubPluginReleasePackageRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), - ('optional', models.BooleanField(default=False)), - ('package_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='packages.package')), - ('sub_plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subpluginrelease')), - ], - options={ - 'unique_together': {('sub_plugin_release', 'package_requirement')}, - }, - ), - migrations.CreateModel( - name='SubPluginReleaseDownloadRequirement', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('optional', models.BooleanField(default=False)), - ('download_requirement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='requirements.downloadrequirement')), - ('sub_plugin_release', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subpluginrelease')), - ], - options={ - 'unique_together': {('sub_plugin_release', 'download_requirement')}, - }, - ), - migrations.AddField( - model_name='subpluginrelease', - name='download_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='sub_plugins.SubPluginReleaseDownloadRequirement', to='requirements.DownloadRequirement'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='package_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='sub_plugins.SubPluginReleasePackageRequirement', to='packages.Package'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='pypi_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='sub_plugins.SubPluginReleasePyPiRequirement', to='requirements.PyPiRequirement'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='sub_plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='sub_plugins.subplugin'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='vcs_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='sub_plugins.SubPluginReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), - ), - migrations.CreateModel( - name='SubPluginImage', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('sub_plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='sub_plugins.subplugin')), - ], - options={ - 'verbose_name': 'Image', - 'verbose_name_plural': 'Images', - 'abstract': False, - }, - ), - migrations.CreateModel( - name='SubPluginGame', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game')), - ('sub_plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subplugin')), - ], - options={ - 'unique_together': {('sub_plugin', 'game')}, - }, - ), - migrations.CreateModel( - name='SubPluginContributor', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('sub_plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sub_plugins.subplugin')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.forumuser')), - ], - options={ - 'unique_together': {('sub_plugin', 'user')}, - }, - ), - migrations.AddField( - model_name='subplugin', - name='contributors', - field=models.ManyToManyField(related_name='subplugin_contributions', through='sub_plugins.SubPluginContributor', to='users.ForumUser'), - ), - migrations.AddField( - model_name='subplugin', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subplugins', to='users.forumuser'), - ), - migrations.AddField( - model_name='subplugin', - name='plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_plugins', to='plugins.plugin'), - ), - migrations.AddField( - model_name='subplugin', - name='supported_games', - field=models.ManyToManyField(related_name='subplugins', through='sub_plugins.SubPluginGame', to='games.Game'), - ), - migrations.AddField( - model_name='subplugin', - name='tags', - field=models.ManyToManyField(related_name='subplugins', through='sub_plugins.SubPluginTag', to='tags.Tag'), - ), - migrations.AlterUniqueTogether( - name='subplugin', - unique_together={('plugin', 'basename'), ('plugin', 'name'), ('plugin', 'slug')}, - ), - ] diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models/__init__.py index 77c3c159..1f05bd99 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models/__init__.py @@ -1,7 +1,7 @@ """SubPlugin model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.urls import reverse @@ -38,7 +38,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPlugin', @@ -55,7 +55,7 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class SubPlugin(ProjectBase): """SubPlugin project type model.""" @@ -73,26 +73,26 @@ class SubPlugin(ProjectBase): contributors = models.ManyToManyField( to='users.ForumUser', related_name='subplugin_contributions', - through='sub_plugins.SubPluginContributor', + through='project_manager.SubPluginContributor', ) slug = models.SlugField( max_length=PROJECT_SLUG_MAX_LENGTH, blank=True, ) plugin = models.ForeignKey( - to='plugins.Plugin', + to='project_manager.Plugin', related_name='sub_plugins', on_delete=models.CASCADE, ) supported_games = models.ManyToManyField( to='games.Game', related_name='subplugins', - through='sub_plugins.SubPluginGame', + through='project_manager.SubPluginGame', ) tags = models.ManyToManyField( to='tags.Tag', related_name='subplugins', - through='sub_plugins.SubPluginTag', + through='project_manager.SubPluginTag', ) handle_logo_upload = handle_sub_plugin_logo_upload @@ -141,29 +141,29 @@ class SubPluginRelease(ProjectRelease): """SubPlugin release type model.""" sub_plugin = models.ForeignKey( - to='sub_plugins.SubPlugin', + to='project_manager.SubPlugin', related_name='releases', on_delete=models.CASCADE, ) download_requirements = models.ManyToManyField( to='requirements.DownloadRequirement', related_name='required_in_sub_plugin_releases', - through='sub_plugins.SubPluginReleaseDownloadRequirement', + through='project_manager.SubPluginReleaseDownloadRequirement', ) package_requirements = models.ManyToManyField( - to='packages.Package', + to='project_manager.Package', related_name='required_in_sub_plugin_releases', - through='sub_plugins.SubPluginReleasePackageRequirement', + through='project_manager.SubPluginReleasePackageRequirement', ) pypi_requirements = models.ManyToManyField( to='requirements.PyPiRequirement', related_name='required_in_sub_plugin_releases', - through='sub_plugins.SubPluginReleasePyPiRequirement', + through='project_manager.SubPluginReleasePyPiRequirement', ) vcs_requirements = models.ManyToManyField( to='requirements.VersionControlRequirement', related_name='required_in_sub_plugin_releases', - through='sub_plugins.SubPluginReleaseVersionControlRequirement', + through='project_manager.SubPluginReleaseVersionControlRequirement', ) handle_zip_file_upload = handle_sub_plugin_zip_upload @@ -190,7 +190,7 @@ class SubPluginImage(ProjectImage): """SubPlugin image type model.""" sub_plugin = models.ForeignKey( - to='sub_plugins.SubPlugin', + to='project_manager.SubPlugin', related_name='images', on_delete=models.CASCADE, ) diff --git a/project_manager/sub_plugins/models/abstract.py b/project_manager/sub_plugins/models/abstract.py index cdb2d835..96be72eb 100644 --- a/project_manager/sub_plugins/models/abstract.py +++ b/project_manager/sub_plugins/models/abstract.py @@ -1,14 +1,14 @@ """Base models for SubPlugins.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db import models # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginReleaseThroughBase', @@ -17,13 +17,13 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class SubPluginThroughBase(models.Model): """Base through model class for SubPlugins.""" sub_plugin = models.ForeignKey( - to='sub_plugins.SubPlugin', + to='project_manager.SubPlugin', on_delete=models.CASCADE, ) @@ -42,7 +42,7 @@ class SubPluginReleaseThroughBase(models.Model): """Base through model class for Packages.""" sub_plugin_release = models.ForeignKey( - to='sub_plugins.SubPluginRelease', + to='project_manager.SubPluginRelease', on_delete=models.CASCADE, ) diff --git a/project_manager/sub_plugins/views.py b/project_manager/sub_plugins/views.py index 9de590a3..dbdfd6ab 100644 --- a/project_manager/sub_plugins/views.py +++ b/project_manager/sub_plugins/views.py @@ -1,7 +1,7 @@ """SubPlugin views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # App from project_manager.common.mixins import DownloadMixin @@ -11,7 +11,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'SubPluginReleaseDownloadView', @@ -19,7 +19,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class SubPluginReleaseDownloadView(DownloadMixin): """SubPlugin download view for releases.""" diff --git a/project_manager/tags/__init__.py b/project_manager/tags/__init__.py deleted file mode 100644 index 512fb285..00000000 --- a/project_manager/tags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Tag app.""" - -default_app_config = 'project_manager.tags.apps.TagConfig' diff --git a/project_manager/urls.py b/project_manager/urls.py index 065fc610..5b1b60ff 100644 --- a/project_manager/urls.py +++ b/project_manager/urls.py @@ -1,7 +1,7 @@ """Base App URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf import settings @@ -18,7 +18,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= urlpatterns = [ url( diff --git a/project_manager/users/__init__.py b/project_manager/users/__init__.py deleted file mode 100644 index bee2e175..00000000 --- a/project_manager/users/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""User app.""" - -default_app_config = 'project_manager.users.apps.UserConfig' diff --git a/project_manager/users/migrations/0001_initial.py b/project_manager/users/migrations/0001_initial.py deleted file mode 100644 index f71fc3e9..00000000 --- a/project_manager/users/migrations/0001_initial.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ForumUser', - fields=[ - ('forum_id', models.IntegerField(primary_key=True, serialize=False, unique=True)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='forum_user', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Forum User', - 'verbose_name_plural': 'Forum Users', - }, - ), - ] diff --git a/project_manager/users/migrations/__init__.py b/project_manager/users/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/project_manager/views.py b/project_manager/views.py index 13d4a096..d76c40c8 100644 --- a/project_manager/views.py +++ b/project_manager/views.py @@ -1,7 +1,7 @@ """Base views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Q @@ -11,11 +11,11 @@ from project_manager.packages.models import Package, PackageRelease from project_manager.plugins.models import Plugin, PluginRelease from project_manager.sub_plugins.models import SubPlugin, SubPluginRelease -from project_manager.users.models import ForumUser +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'StatisticsView', @@ -23,7 +23,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class StatisticsView(TemplateView): """View for total Project statistics.""" diff --git a/requirements/__init__.py b/requirements/__init__.py new file mode 100644 index 00000000..b87b6d19 --- /dev/null +++ b/requirements/__init__.py @@ -0,0 +1 @@ +"""Requirement app.""" diff --git a/project_manager/requirements/api/__init__.py b/requirements/api/__init__.py similarity index 100% rename from project_manager/requirements/api/__init__.py rename to requirements/api/__init__.py diff --git a/project_manager/requirements/api/serializers/__init__.py b/requirements/api/serializers/__init__.py similarity index 100% rename from project_manager/requirements/api/serializers/__init__.py rename to requirements/api/serializers/__init__.py diff --git a/project_manager/requirements/api/serializers/common.py b/requirements/api/serializers/common.py similarity index 97% rename from project_manager/requirements/api/serializers/common.py rename to requirements/api/serializers/common.py index b46d7853..94fb6953 100644 --- a/project_manager/requirements/api/serializers/common.py +++ b/requirements/api/serializers/common.py @@ -1,7 +1,7 @@ """Requirement serializers for APIs in other apps.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.fields import ReadOnlyField @@ -9,7 +9,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ReleaseDownloadRequirementSerializer', @@ -19,7 +19,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class ReleaseDownloadRequirementSerializer(ModelSerializer): """Serializer for listing required downloads for projects.""" diff --git a/project_manager/requirements/apps.py b/requirements/apps.py similarity index 87% rename from project_manager/requirements/apps.py rename to requirements/apps.py index 9bd6399f..0a200e25 100644 --- a/project_manager/requirements/apps.py +++ b/requirements/apps.py @@ -1,14 +1,14 @@ """Requirement app config.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.apps import AppConfig # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'RequirementConfig', @@ -16,10 +16,10 @@ # ============================================================================= -# >> APPLICATION CONFIG +# APPLICATION CONFIG # ============================================================================= class RequirementConfig(AppConfig): """Requirement app config.""" - name = 'project_manager.requirements' + name = 'requirements' verbose_name = 'Requirements' diff --git a/project_manager/requirements/constants.py b/requirements/constants.py similarity index 94% rename from project_manager/requirements/constants.py rename to requirements/constants.py index 4fe7da05..cffb26a1 100644 --- a/project_manager/requirements/constants.py +++ b/requirements/constants.py @@ -1,7 +1,7 @@ """Contents for requirements.""" # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'REQUIREMENT_NAME_MAX_LENGTH', @@ -11,7 +11,7 @@ # ============================================================================= -# >> CONSTANTS +# CONSTANTS # ============================================================================= REQUIREMENT_NAME_MAX_LENGTH = 64 REQUIREMENT_SLUG_MAX_LENGTH = 64 diff --git a/project_manager/requirements/migrations/0001_initial.py b/requirements/migrations/0001_initial.py similarity index 97% rename from project_manager/requirements/migrations/0001_initial.py rename to requirements/migrations/0001_initial.py index 1d5ed24c..01c46286 100644 --- a/project_manager/requirements/migrations/0001_initial.py +++ b/requirements/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 +# Generated by Django 3.2.8 on 2021-10-22 20:02 from django.db import migrations, models diff --git a/project_manager/requirements/migrations/__init__.py b/requirements/migrations/__init__.py similarity index 100% rename from project_manager/requirements/migrations/__init__.py rename to requirements/migrations/__init__.py diff --git a/project_manager/requirements/models.py b/requirements/models.py similarity index 96% rename from project_manager/requirements/models.py rename to requirements/models.py index f5ebcf8a..3f860ec7 100644 --- a/project_manager/requirements/models.py +++ b/requirements/models.py @@ -1,7 +1,7 @@ """Requirement model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf import settings @@ -10,7 +10,7 @@ from django.utils.text import slugify # App -from project_manager.requirements.constants import ( +from requirements.constants import ( REQUIREMENT_NAME_MAX_LENGTH, REQUIREMENT_SLUG_MAX_LENGTH, REQUIREMENT_URL_MAX_LENGTH, @@ -18,7 +18,7 @@ # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'DownloadRequirement', @@ -28,7 +28,7 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class DownloadRequirement(models.Model): """Download requirement model.""" diff --git a/tags/__init__.py b/tags/__init__.py new file mode 100644 index 00000000..8d202aca --- /dev/null +++ b/tags/__init__.py @@ -0,0 +1 @@ +"""Tag app.""" diff --git a/project_manager/tags/admin.py b/tags/admin.py similarity index 93% rename from project_manager/tags/admin.py rename to tags/admin.py index 1ea95a18..416cf74e 100644 --- a/project_manager/tags/admin.py +++ b/tags/admin.py @@ -1,17 +1,17 @@ """Tag admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.contrib import admin # App -from project_manager.tags.models import Tag +from tags.models import Tag # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'TagAdmin', @@ -19,7 +19,7 @@ # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= @admin.register(Tag) class TagAdmin(admin.ModelAdmin): diff --git a/project_manager/tags/api/__init__.py b/tags/api/__init__.py similarity index 100% rename from project_manager/tags/api/__init__.py rename to tags/api/__init__.py diff --git a/project_manager/tags/api/filtersets.py b/tags/api/filtersets.py similarity index 89% rename from project_manager/tags/api/filtersets.py rename to tags/api/filtersets.py index 69d27922..0e6eaa42 100644 --- a/project_manager/tags/api/filtersets.py +++ b/tags/api/filtersets.py @@ -1,17 +1,17 @@ """Tag API filters.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from django_filters.filterset import FilterSet # App -from project_manager.tags.models import Tag +from tags.models import Tag # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'TagFilterSet', @@ -19,7 +19,7 @@ # ============================================================================= -# >> FILTERS +# FILTERS # ============================================================================= class TagFilterSet(FilterSet): """Filters for Tags.""" diff --git a/project_manager/tags/api/serializers.py b/tags/api/serializers.py similarity index 86% rename from project_manager/tags/api/serializers.py rename to tags/api/serializers.py index c9da6024..4f25d082 100644 --- a/project_manager/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -1,20 +1,20 @@ """Tag serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.serializers import ModelSerializer # App -from project_manager.tags.models import Tag -from project_manager.users.api.serializers.common import ( +from tags.models import Tag +from users.api.serializers.common import ( ForumUserContributorSerializer, ) # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'TagSerializer', @@ -22,7 +22,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class TagSerializer(ModelSerializer): """Serializer for project Tags.""" diff --git a/project_manager/tags/api/urls.py b/tags/api/urls.py similarity index 87% rename from project_manager/tags/api/urls.py rename to tags/api/urls.py index 8c4b9063..660312c2 100644 --- a/project_manager/tags/api/urls.py +++ b/tags/api/urls.py @@ -1,17 +1,17 @@ """Tag API URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework import routers # App -from project_manager.tags.api.views import TagViewSet +from tags.api.views import TagViewSet # ============================================================================= -# >> ROUTERS +# ROUTERS # ============================================================================= router = routers.SimpleRouter() router.register( @@ -22,7 +22,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'games' diff --git a/project_manager/tags/api/views.py b/tags/api/views.py similarity index 87% rename from project_manager/tags/api/views.py rename to tags/api/views.py index cc62b152..10a5dd94 100644 --- a/project_manager/tags/api/views.py +++ b/tags/api/views.py @@ -1,7 +1,7 @@ """Tag API views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from django_filters.rest_framework import DjangoFilterBackend @@ -10,13 +10,13 @@ from rest_framework.viewsets import GenericViewSet # App -from project_manager.tags.api.filtersets import TagFilterSet -from project_manager.tags.api.serializers import TagSerializer -from project_manager.tags.models import Tag +from tags.api.filtersets import TagFilterSet +from tags.api.serializers import TagSerializer +from tags.models import Tag # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'TagViewSet', @@ -24,7 +24,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class TagViewSet(ListModelMixin, GenericViewSet): """ViewSet for listing Supported Games. diff --git a/project_manager/tags/apps.py b/tags/apps.py similarity index 87% rename from project_manager/tags/apps.py rename to tags/apps.py index 824595f6..7235b4ee 100644 --- a/project_manager/tags/apps.py +++ b/tags/apps.py @@ -1,14 +1,14 @@ """Tag app config.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.apps import AppConfig # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'TagConfig', @@ -16,10 +16,10 @@ # ============================================================================= -# >> APPLICATION CONFIG +# APPLICATION CONFIG # ============================================================================= class TagConfig(AppConfig): """Tag app config.""" - name = 'project_manager.tags' + name = 'tags' verbose_name = 'Tags' diff --git a/project_manager/tags/constants.py b/tags/constants.py similarity index 92% rename from project_manager/tags/constants.py rename to tags/constants.py index 86d3c7ed..259830c5 100644 --- a/project_manager/tags/constants.py +++ b/tags/constants.py @@ -1,7 +1,7 @@ """Constants for tags.""" # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'TAG_NAME_MAX_LENGTH', @@ -9,6 +9,6 @@ # ============================================================================= -# >> CONSTANTS +# CONSTANTS # ============================================================================= TAG_NAME_MAX_LENGTH = 16 diff --git a/project_manager/tags/migrations/0001_initial.py b/tags/migrations/0001_initial.py similarity index 66% rename from project_manager/tags/migrations/0001_initial.py rename to tags/migrations/0001_initial.py index f37d5950..36a3720f 100644 --- a/project_manager/tags/migrations/0001_initial.py +++ b/tags/migrations/0001_initial.py @@ -1,8 +1,7 @@ -# Generated by Django 3.2.8 on 2021-10-22 13:14 +# Generated by Django 3.2.8 on 2021-10-22 20:02 import django.core.validators from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -10,7 +9,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('users', '0001_initial'), ] operations = [ @@ -19,7 +17,6 @@ class Migration(migrations.Migration): fields=[ ('name', models.CharField(max_length=16, primary_key=True, serialize=False, unique=True, validators=[django.core.validators.RegexValidator('^[a-z]*')])), ('black_listed', models.BooleanField(default=False)), - ('creator', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_tags', to='users.forumuser')), ], ), ] diff --git a/tags/migrations/0002_tag_creator.py b/tags/migrations/0002_tag_creator.py new file mode 100644 index 00000000..ff693841 --- /dev/null +++ b/tags/migrations/0002_tag_creator.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.8 on 2021-10-22 20:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('tags', '0001_initial'), + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='creator', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_tags', to='users.forumuser'), + ), + ] diff --git a/project_manager/sub_plugins/migrations/__init__.py b/tags/migrations/__init__.py similarity index 100% rename from project_manager/sub_plugins/migrations/__init__.py rename to tags/migrations/__init__.py diff --git a/project_manager/tags/models.py b/tags/models.py similarity index 90% rename from project_manager/tags/models.py rename to tags/models.py index a1142ec8..2327c34f 100644 --- a/project_manager/tags/models.py +++ b/tags/models.py @@ -1,18 +1,18 @@ """Tag model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db import models # App -from project_manager.tags.constants import TAG_NAME_MAX_LENGTH -from project_manager.tags.validators import tag_name_validator +from tags.constants import TAG_NAME_MAX_LENGTH +from tags.validators import tag_name_validator # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'Tag', @@ -20,7 +20,7 @@ # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= class Tag(models.Model): """Model used to store tags for projects.""" diff --git a/project_manager/tags/validators.py b/tags/validators.py similarity index 92% rename from project_manager/tags/validators.py rename to tags/validators.py index 08450110..0879f0a2 100644 --- a/project_manager/tags/validators.py +++ b/tags/validators.py @@ -1,14 +1,14 @@ """Tag validators.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.core.validators import RegexValidator # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'tag_name_validator', @@ -16,7 +16,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= # Tags should: # Contain only lower-case characters. diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 00000000..b1076175 --- /dev/null +++ b/users/__init__.py @@ -0,0 +1 @@ +"""User app.""" diff --git a/project_manager/users/admin.py b/users/admin.py similarity index 71% rename from project_manager/users/admin.py rename to users/admin.py index 7c3a7998..183875a6 100644 --- a/project_manager/users/admin.py +++ b/users/admin.py @@ -1,26 +1,51 @@ """User admin classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django +from django.contrib.auth import get_user_model from django.contrib import admin # App -from project_manager.users.models import ForumUser +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ForumUserAdmin', + 'UserAdmin', ) # ============================================================================= -# >> ADMINS +# ADMINS # ============================================================================= +@admin.register(get_user_model()) +class UserAdmin(admin.ModelAdmin): + """User model Admin.""" + + actions = None + fields = ( + 'username', + 'is_superuser', + 'is_staff', + ) + readonly_fields = ( + 'username', + ) + + def has_add_permission(self, request): + """Disallow creating Users in the Admin.""" + return False + + def has_delete_permission(self, request, obj=None): + """Disallow deleting Users in the Admin.""" + return False + + @admin.register(ForumUser) class ForumUserAdmin(admin.ModelAdmin): """ForumUser admin.""" diff --git a/project_manager/users/api/__init__.py b/users/api/__init__.py similarity index 100% rename from project_manager/users/api/__init__.py rename to users/api/__init__.py diff --git a/project_manager/users/api/filtersets.py b/users/api/filtersets.py similarity index 95% rename from project_manager/users/api/filtersets.py rename to users/api/filtersets.py index 9e4cb778..2914a190 100644 --- a/project_manager/users/api/filtersets.py +++ b/users/api/filtersets.py @@ -1,7 +1,7 @@ """User API filters.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Count, Q @@ -10,11 +10,11 @@ from django_filters.filters import BooleanFilter from django_filters.filterset import FilterSet -from project_manager.users.models import ForumUser +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ForumUserFilterSet', @@ -22,7 +22,7 @@ # ============================================================================= -# >> FILTERS +# FILTERS # ============================================================================= class ForumUserFilterSet(FilterSet): """Filters for ForumUsers.""" diff --git a/project_manager/users/api/serializers/__init__.py b/users/api/serializers/__init__.py similarity index 96% rename from project_manager/users/api/serializers/__init__.py rename to users/api/serializers/__init__.py index b0b271eb..1516ee65 100644 --- a/project_manager/users/api/serializers/__init__.py +++ b/users/api/serializers/__init__.py @@ -1,7 +1,7 @@ """User serializers for APIs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.fields import SerializerMethodField @@ -11,11 +11,11 @@ from project_manager.packages.models import Package from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin -from project_manager.users.models import ForumUser +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ForumUserSerializer', @@ -27,7 +27,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class ProjectContributionSerializer(ModelSerializer): """Base class for Project contributions.""" diff --git a/project_manager/users/api/serializers/common.py b/users/api/serializers/common.py similarity index 91% rename from project_manager/users/api/serializers/common.py rename to users/api/serializers/common.py index 95b2c5a0..59698668 100644 --- a/project_manager/users/api/serializers/common.py +++ b/users/api/serializers/common.py @@ -1,18 +1,18 @@ """User serializers for APIs in other apps.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # 3rd-Party Django from rest_framework.fields import SerializerMethodField from rest_framework.serializers import ModelSerializer # App -from project_manager.users.models import ForumUser +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ForumUserContributorSerializer', @@ -20,7 +20,7 @@ # ============================================================================= -# >> SERIALIZERS +# SERIALIZERS # ============================================================================= class ForumUserContributorSerializer(ModelSerializer): """Used for owner/contributors for Projects.""" diff --git a/project_manager/users/api/urls.py b/users/api/urls.py similarity index 88% rename from project_manager/users/api/urls.py rename to users/api/urls.py index c11834a7..80f8776f 100644 --- a/project_manager/users/api/urls.py +++ b/users/api/urls.py @@ -1,7 +1,7 @@ """User API URLs.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf.urls import include, url @@ -10,11 +10,11 @@ from rest_framework import routers # App -from project_manager.users.api.views import ForumUserViewSet +from users.api.views import ForumUserViewSet # ============================================================================= -# >> ROUTERS +# ROUTERS # ============================================================================= router = routers.SimpleRouter() router.register( @@ -25,7 +25,7 @@ # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= app_name = 'users' diff --git a/project_manager/users/api/views.py b/users/api/views.py similarity index 91% rename from project_manager/users/api/views.py rename to users/api/views.py index e4f5903a..c3d9dd6b 100644 --- a/project_manager/users/api/views.py +++ b/users/api/views.py @@ -1,7 +1,7 @@ """User API views.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.db.models import Prefetch @@ -15,13 +15,13 @@ from project_manager.packages.models import Package from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin -from project_manager.users.api.filtersets import ForumUserFilterSet -from project_manager.users.api.serializers import ForumUserSerializer -from project_manager.users.models import ForumUser +from users.api.filtersets import ForumUserFilterSet +from users.api.serializers import ForumUserSerializer +from users.models import ForumUser # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ForumUserViewSet', @@ -29,7 +29,7 @@ # ============================================================================= -# >> VIEWS +# VIEWS # ============================================================================= class ForumUserViewSet(ModelViewSet): """ForumUser API view.""" diff --git a/project_manager/users/apps.py b/users/apps.py similarity index 87% rename from project_manager/users/apps.py rename to users/apps.py index 7aa91b86..56cb6e43 100644 --- a/project_manager/users/apps.py +++ b/users/apps.py @@ -1,14 +1,14 @@ """User app config.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.apps import AppConfig # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'UserConfig', @@ -16,10 +16,10 @@ # ============================================================================= -# >> APPLICATION CONFIG +# APPLICATION CONFIG # ============================================================================= class UserConfig(AppConfig): """User app config.""" - name = 'project_manager.users' + name = 'users' verbose_name = 'Users' diff --git a/project_manager/users/constants.py b/users/constants.py similarity index 80% rename from project_manager/users/constants.py rename to users/constants.py index 6059497b..f436c81f 100644 --- a/project_manager/users/constants.py +++ b/users/constants.py @@ -1,23 +1,27 @@ """Constants for use with ForumUsers.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django from django.conf import settings # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'FORUM_MEMBER_URL', + 'USER_EMAIL_MAX_LENGTH', + 'USER_USERNAME_MAX_LENGTH', ) # ============================================================================= -# >> GLOBAL VARIABLES +# GLOBAL VARIABLES # ============================================================================= FORUM_MEMBER_URL = ( settings.FORUM_URL + 'memberlist.php?mode=viewprofile&u={user_id}' ) +USER_USERNAME_MAX_LENGTH = 30 +USER_EMAIL_MAX_LENGTH = 256 diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 00000000..0db509d8 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.8 on 2021-10-22 20:02 + +from django.conf import settings +import django.contrib.auth.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(editable=False, max_length=30, unique=True)), + ('email', models.EmailField(blank=True, max_length=256)), + ('is_staff', models.BooleanField(default=False)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='ForumUser', + fields=[ + ('forum_id', models.IntegerField(primary_key=True, serialize=False, unique=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='forum_user', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Forum User', + 'verbose_name_plural': 'Forum Users', + }, + ), + ] diff --git a/project_manager/tags/migrations/__init__.py b/users/migrations/__init__.py similarity index 100% rename from project_manager/tags/migrations/__init__.py rename to users/migrations/__init__.py diff --git a/project_manager/users/models.py b/users/models.py similarity index 63% rename from project_manager/users/models.py rename to users/models.py index f37af6de..65749a61 100644 --- a/project_manager/users/models.py +++ b/users/models.py @@ -1,28 +1,67 @@ """User model classes.""" # ============================================================================= -# >> IMPORTS +# IMPORTS # ============================================================================= # Django +from django.contrib.auth.models import ( + AbstractBaseUser, + PermissionsMixin, + UserManager, +) from django.conf import settings from django.urls import reverse from django.db import models # App -from project_manager.users.constants import FORUM_MEMBER_URL +from users.constants import ( + FORUM_MEMBER_URL, + USER_EMAIL_MAX_LENGTH, + USER_USERNAME_MAX_LENGTH, +) # ============================================================================= -# >> ALL DECLARATION +# ALL DECLARATION # ============================================================================= __all__ = ( 'ForumUser', + 'User', ) # ============================================================================= -# >> MODELS +# MODELS # ============================================================================= +class User(AbstractBaseUser, PermissionsMixin): + """Base User Model.""" + + username = models.CharField( + editable=False, + max_length=USER_USERNAME_MAX_LENGTH, + unique=True, + ) + email = models.EmailField( + max_length=USER_EMAIL_MAX_LENGTH, + blank=True, + ) + is_staff = models.BooleanField( + default=False, + ) + + objects = UserManager() + + USERNAME_FIELD = 'username' + + def get_short_name(self): + """Return the short name for the user.""" + return self.username + + def get_full_name(self): + """Return the full name for the user.""" + return self.username + + class ForumUser(models.Model): """Model for User based information.""" From f7f69b80774c93b5ffeaf4cb500b0ed7f3a1bd46 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 18:43:27 -0400 Subject: [PATCH 002/211] Fixed an error in Django Admin when adding a contributor to a project by switching the inheritance order. --- SPPM/settings/base.py | 2 +- games/api/serializers.py | 2 +- games/api/urls.py | 2 +- games/api/views.py | 2 +- project_manager/api/pagination.py | 2 +- project_manager/api/views.py | 2 +- project_manager/common/api/filtersets.py | 2 +- project_manager/common/api/serializers/__init__.py | 2 +- project_manager/common/api/serializers/mixins.py | 2 +- project_manager/common/api/views/__init__.py | 2 +- project_manager/common/api/views/mixins.py | 2 +- project_manager/common/models.py | 2 +- project_manager/common/views.py | 2 +- project_manager/packages/api/serializers/common.py | 2 +- project_manager/packages/api/urls.py | 2 +- project_manager/packages/models/__init__.py | 2 +- project_manager/plugins/api/serializers/__init__.py | 2 +- project_manager/plugins/api/urls.py | 2 +- project_manager/plugins/api/views.py | 2 +- project_manager/plugins/models/__init__.py | 2 +- project_manager/sub_plugins/api/serializers/__init__.py | 2 +- project_manager/sub_plugins/api/serializers/mixins.py | 2 +- project_manager/sub_plugins/api/urls.py | 2 +- project_manager/sub_plugins/api/views.py | 2 +- project_manager/sub_plugins/models/__init__.py | 2 +- requirements/api/serializers/common.py | 2 +- tags/api/filtersets.py | 2 +- tags/api/serializers.py | 2 +- tags/api/urls.py | 2 +- tags/api/views.py | 2 +- users/api/filtersets.py | 2 +- users/api/serializers/__init__.py | 2 +- users/api/serializers/common.py | 2 +- users/api/urls.py | 2 +- users/api/views.py | 2 +- 35 files changed, 35 insertions(+), 35 deletions(-) diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index d0a6a1dd..6ba635e9 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -13,7 +13,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Python +# Third Party Python from path import Path diff --git a/games/api/serializers.py b/games/api/serializers.py index 4749fd5c..c78da28f 100644 --- a/games/api/serializers.py +++ b/games/api/serializers.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.serializers import ModelSerializer # App diff --git a/games/api/urls.py b/games/api/urls.py index 2541c799..7123d0ab 100644 --- a/games/api/urls.py +++ b/games/api/urls.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework import routers # App diff --git a/games/api/views.py b/games/api/views.py index 149f9e8d..5820bc39 100644 --- a/games/api/views.py +++ b/games/api/views.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.filters import OrderingFilter from rest_framework.mixins import ListModelMixin from rest_framework.viewsets import GenericViewSet diff --git a/project_manager/api/pagination.py b/project_manager/api/pagination.py index 7d173861..23e47df6 100644 --- a/project_manager/api/pagination.py +++ b/project_manager/api/pagination.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.pagination import PageNumberPagination diff --git a/project_manager/api/views.py b/project_manager/api/views.py index 465dd23b..941990fb 100644 --- a/project_manager/api/views.py +++ b/project_manager/api/views.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView diff --git a/project_manager/common/api/filtersets.py b/project_manager/common/api/filtersets.py index 3f054e0a..7f62b5af 100644 --- a/project_manager/common/api/filtersets.py +++ b/project_manager/common/api/filtersets.py @@ -6,7 +6,7 @@ # Django from django.db.models import Q -# 3rd-Party Django +# Third Party Django from django_filters.filters import CharFilter from django_filters.filterset import FilterSet diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index 3fc8885a..8cb48a55 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -9,7 +9,7 @@ # Django from django.utils.timezone import now -# 3rd-Party Django +# Third Party Django from rest_framework.exceptions import ValidationError from rest_framework.fields import ( CharField, diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 0a92f631..20256045 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -6,7 +6,7 @@ # Django from django.utils import formats -# 3rd-Party Django +# Third Party Django from rest_framework.exceptions import ValidationError from rest_framework.serializers import ModelSerializer diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 2757ad39..08716be1 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -6,7 +6,7 @@ # Django from django.db import IntegrityError -# 3rd-Party Django +# Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.authentication import SessionAuthentication from rest_framework.exceptions import PermissionDenied diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index d2928767..68235fbf 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.authentication import SessionAuthentication from rest_framework.exceptions import PermissionDenied diff --git a/project_manager/common/models.py b/project_manager/common/models.py index 38daf61f..adc3682d 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -13,7 +13,7 @@ from django.db import models from django.utils.text import slugify -# 3rd-Party Django +# Third Party Django from embed_video.fields import EmbedVideoField from model_utils.fields import AutoCreatedField from PIL import Image diff --git a/project_manager/common/views.py b/project_manager/common/views.py index b2129b98..68696bf2 100644 --- a/project_manager/common/views.py +++ b/project_manager/common/views.py @@ -6,7 +6,7 @@ # Django from django.views.generic import ListView -# 3rd-Party Django +# Third Party Django from braces.views import OrderableListMixin diff --git a/project_manager/packages/api/serializers/common.py b/project_manager/packages/api/serializers/common.py index 91541250..f2991c6c 100644 --- a/project_manager/packages/api/serializers/common.py +++ b/project_manager/packages/api/serializers/common.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.fields import ReadOnlyField from rest_framework.serializers import ModelSerializer diff --git a/project_manager/packages/api/urls.py b/project_manager/packages/api/urls.py index 037e4315..7d0a1153 100644 --- a/project_manager/packages/api/urls.py +++ b/project_manager/packages/api/urls.py @@ -6,7 +6,7 @@ # Django from django.conf.urls import url -# 3rd-Party Django +# Third Party Django from rest_framework import routers # App diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models/__init__.py index 65392ef0..bc37cd1d 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models/__init__.py @@ -161,7 +161,7 @@ class PackageImage(ProjectImage): handle_image_upload = handle_package_image_upload -class PackageContributor(ProjectContributor, PackageThroughBase): +class PackageContributor(PackageThroughBase, ProjectContributor): """Package contributors through model.""" class Meta: diff --git a/project_manager/plugins/api/serializers/__init__.py b/project_manager/plugins/api/serializers/__init__.py index 285014e9..23d91881 100644 --- a/project_manager/plugins/api/serializers/__init__.py +++ b/project_manager/plugins/api/serializers/__init__.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.exceptions import ValidationError # App diff --git a/project_manager/plugins/api/urls.py b/project_manager/plugins/api/urls.py index e55664c1..f94dc53a 100644 --- a/project_manager/plugins/api/urls.py +++ b/project_manager/plugins/api/urls.py @@ -6,7 +6,7 @@ # Django from django.conf.urls import url -# 3rd-Party Django +# Third Party Django from rest_framework import routers # App diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index 3ac7c711..3e8ea007 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -6,7 +6,7 @@ # Django from django.db.models import Prefetch -# 3rd-Party Django +# Third Party Django from rest_framework.reverse import reverse # App diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index 060d12db..ee52ccf6 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -170,7 +170,7 @@ class PluginImage(ProjectImage): handle_image_upload = handle_plugin_image_upload -class PluginContributor(ProjectContributor, PluginThroughBase): +class PluginContributor(PluginThroughBase, ProjectContributor): """Plugin contributors through model.""" class Meta: diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index e6aa10fb..649bfb16 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.exceptions import ValidationError # App diff --git a/project_manager/sub_plugins/api/serializers/mixins.py b/project_manager/sub_plugins/api/serializers/mixins.py index a402a472..db0aeed4 100644 --- a/project_manager/sub_plugins/api/serializers/mixins.py +++ b/project_manager/sub_plugins/api/serializers/mixins.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.exceptions import ValidationError # App diff --git a/project_manager/sub_plugins/api/urls.py b/project_manager/sub_plugins/api/urls.py index 3ba70e26..5403c293 100644 --- a/project_manager/sub_plugins/api/urls.py +++ b/project_manager/sub_plugins/api/urls.py @@ -6,7 +6,7 @@ # Django from django.conf.urls import url -# 3rd-Party Django +# Third Party Django from rest_framework import routers # App diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index 45af5f65..ecf74e24 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -6,7 +6,7 @@ # Django from django.db.models import Prefetch -# 3rd-Party Django +# Third Party Django from rest_framework.parsers import ParseError # App diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models/__init__.py index 1f05bd99..380c8dc0 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models/__init__.py @@ -198,7 +198,7 @@ class SubPluginImage(ProjectImage): handle_image_upload = handle_sub_plugin_image_upload -class SubPluginContributor(ProjectContributor, SubPluginThroughBase): +class SubPluginContributor(SubPluginThroughBase, ProjectContributor): """SubPlugin contributors through model.""" class Meta: diff --git a/requirements/api/serializers/common.py b/requirements/api/serializers/common.py index 94fb6953..fc4f4f11 100644 --- a/requirements/api/serializers/common.py +++ b/requirements/api/serializers/common.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.fields import ReadOnlyField from rest_framework.serializers import ModelSerializer diff --git a/tags/api/filtersets.py b/tags/api/filtersets.py index 0e6eaa42..05ac42b8 100644 --- a/tags/api/filtersets.py +++ b/tags/api/filtersets.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from django_filters.filterset import FilterSet # App diff --git a/tags/api/serializers.py b/tags/api/serializers.py index 4f25d082..ef66b2ca 100644 --- a/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.serializers import ModelSerializer # App diff --git a/tags/api/urls.py b/tags/api/urls.py index 660312c2..a5104f9b 100644 --- a/tags/api/urls.py +++ b/tags/api/urls.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework import routers # App diff --git a/tags/api/views.py b/tags/api/views.py index 10a5dd94..0f2a0e1d 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from rest_framework.mixins import ListModelMixin diff --git a/users/api/filtersets.py b/users/api/filtersets.py index 2914a190..ea280282 100644 --- a/users/api/filtersets.py +++ b/users/api/filtersets.py @@ -6,7 +6,7 @@ # Django from django.db.models import Count, Q -# 3rd-Party Django +# Third Party Django from django_filters.filters import BooleanFilter from django_filters.filterset import FilterSet diff --git a/users/api/serializers/__init__.py b/users/api/serializers/__init__.py index 1516ee65..b026b331 100644 --- a/users/api/serializers/__init__.py +++ b/users/api/serializers/__init__.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.fields import SerializerMethodField from rest_framework.serializers import ModelSerializer diff --git a/users/api/serializers/common.py b/users/api/serializers/common.py index 59698668..3a2902f9 100644 --- a/users/api/serializers/common.py +++ b/users/api/serializers/common.py @@ -3,7 +3,7 @@ # ============================================================================= # IMPORTS # ============================================================================= -# 3rd-Party Django +# Third Party Django from rest_framework.fields import SerializerMethodField from rest_framework.serializers import ModelSerializer diff --git a/users/api/urls.py b/users/api/urls.py index 80f8776f..16e793c1 100644 --- a/users/api/urls.py +++ b/users/api/urls.py @@ -6,7 +6,7 @@ # Django from django.conf.urls import include, url -# 3rd-Party Django +# Third Party Django from rest_framework import routers # App diff --git a/users/api/views.py b/users/api/views.py index c3d9dd6b..9a6456b8 100644 --- a/users/api/views.py +++ b/users/api/views.py @@ -6,7 +6,7 @@ # Django from django.db.models import Prefetch -# 3rd-Party Django +# Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from rest_framework.viewsets import ModelViewSet From ef79402b1705b9380690f2f180449655aaf72481 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 18:45:00 -0400 Subject: [PATCH 003/211] Updated readme file. Added a couple management commands for creating Users. Removed an unused requirement. --- pip-requirements/base.txt | 1 - pip-requirements/local.txt | 1 + .../commands/associate_super_user.py | 60 +++++++++++++ .../commands/create_random_users.py | 84 +++++++++++++++++++ readme.md | 41 +++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 project_manager/management/commands/associate_super_user.py create mode 100644 project_manager/management/commands/create_random_users.py diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index 6234d3e9..b4f77d1a 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,4 +1,3 @@ -configobj==5.0.6 django==3.2.8 django-braces==1.14.0 django-crispy-forms==1.13.0 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index fd69fbe8..bff02f26 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -4,3 +4,4 @@ pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 pylint==2.11.1 +random-username==1.0.2 diff --git a/project_manager/management/commands/associate_super_user.py b/project_manager/management/commands/associate_super_user.py new file mode 100644 index 00000000..65f193a7 --- /dev/null +++ b/project_manager/management/commands/associate_super_user.py @@ -0,0 +1,60 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError + +# App +from users.models import ForumUser + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +User = get_user_model() + + +# ============================================================================= +# COMMANDS +# ============================================================================= +class Command(BaseCommand): + """Populate the Game objects.""" + + def add_arguments(self, parser): + parser.add_argument( + 'username', + type=str, + help='The username of the SuperUser.', + ) + parser.add_argument( + 'forum_id', + type=int, + help='The forum id number to associate.', + ) + + def handle(self, *args, **options): + username = options['username'] + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise CommandError( + f'User with the username "{username}" was not found.' + ) from User.DoesNotExist + + forum_id = options['forum_id'] + if ForumUser.objects.filter( + forum_id=forum_id, + ).exists(): + raise CommandError( + f'A user is already associated with the forum id "{forum_id}".' + ) + + ForumUser.objects.create( + user=user, + forum_id=forum_id, + ) + print( + f'User "{username}" successfully associated with forum id ' + f'"{forum_id}".' + ) diff --git a/project_manager/management/commands/create_random_users.py b/project_manager/management/commands/create_random_users.py new file mode 100644 index 00000000..5ceb6391 --- /dev/null +++ b/project_manager/management/commands/create_random_users.py @@ -0,0 +1,84 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from os import urandom + +# Django +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError + +# Third Party Python +from random_username.generate import generate_username + +# App +from users.models import ForumUser + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +User = get_user_model() + + +# ============================================================================= +# COMMANDS +# ============================================================================= +class Command(BaseCommand): + """Populate the Game objects.""" + + def add_arguments(self, parser): + parser.add_argument( + 'count', + type=int, + help='The number of users to create.', + ) + + def handle(self, *args, **options): + count = options['count'] + current_usernames = User.objects.values_list( + 'username', + flat=True, + ) + + username_list = [] + valid = False + while not valid: + username_list = generate_username(count) + valid = self.validate_unique_list( + username_list=username_list, + current_usernames=current_usernames, + count=count, + ) + + current_forum_ids = list( + ForumUser.objects.values_list( + 'forum_id', + flat=True, + ) + ) + max_id = count + len(current_forum_ids) + id_list = list(set(range(1, max_id + 1)).difference(current_forum_ids)) + obj_list = [] + for n, username in enumerate(username_list): + user = User.objects.create_user( + username=username, + password=urandom(8), + ) + obj_list.append( + ForumUser( + user=user, + forum_id=id_list[n], + ) + ) + + if obj_list: + ForumUser.objects.bulk_create( + objs=obj_list, + ) + + print(f'Successfully created "{count}" users.') + + @staticmethod + def validate_unique_list(username_list, current_usernames, count): + return len(set(username_list).difference(current_usernames)) == count diff --git a/readme.md b/readme.md index e69de29b..763096de 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,41 @@ +# Source.Python Project Manager + +This Django app will be used to host Source.Python plugins, sub-plugins, and custom packages. + +## Want to help develop this application? + +If you wish to contribute to this application, follow the instructions below on how to set it up locally. + +### Local setup +1. Clone the repository +2. Create a virtual environment + 1. Some IDEs, like Pycharm, come with tools to automatically create the virtual environment. + 2. If you have to set it up yourself, there are plenty of online guides to help you. + 3. We may Docker-ize the app in the future, which will make this a little simpler, but will require you to install/run Docker. +3. Log into your virtual environment to complete the rest of these steps. +4. Run `pip install -r pip-requirements/local.txt` to install all the Python/Django requirements. +5. Run the [migrate](https://docs.djangoproject.com/en/dev/ref/django-admin/#migrate) management command to create the tables/columns in your database. +6. Run the `create_game_instances` management command to create the Game objects. +7. Run the [createsuperuser](https://docs.djangoproject.com/en/dev/ref/django-admin/#createsuperuser) management command to create your main user. +8. Run the `associate_super_user` management command to associate the Super User you just created with a ForumUser object. + 1. Arguments for the command are: + 1. **username** - The username of the Super User + 2. **forum_id** - The user id from the Source.Python forums. +9. If you want additional users to test with, run the `create_random_users` management command. + 1. Arguments for the command are: + 1. **count** - The number of random Users to create. +10. Run the server using the [runserver](https://docs.djangoproject.com/en/dev/ref/django-admin/#runserver) management command. + 1. Some IDEs, like Pycharm, have tools to run the server instead of manually running the command in a console window. + +## APIs +### Walkable REST APIs +The REST APIs that the frontend will eventually be built off of can be found at `/api`. They are walkable, meaning the APIs are laid out before you on each page, so just click a link to navigate to another API. Some will require you to add a project name to the URL path. + +### Admin +Since you have created a Super User, you should be able to log into `/admin` using your username/password. This will allow you to test the Django Admin functionality if you are working on it. + +### Statistics +There is also a `/statistics` page to display certain statistics for your local environment from a project, user, and download perspective. + +### User Frontend +Eventually we will be adding `/plugins` and `/packages`, as well as `/plugins//sub-plugins` for a frontend User experience. These all still need built, so if you have Javascript experience and are willing to help out, it would be much appreciated. The first obstacle will be to determine which Javascript framework to use. This really depends on what people know, but Vue or React would be preferred. From aa8d1c64fe64a299bf7f64bcb56be3210b5f85b0 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 19:48:02 -0400 Subject: [PATCH 004/211] Updated readme with information on the APIs. Fixed a doc-string for API documentation of filters. --- readme.md | 71 ++++++++++++++++++++++++++++++++++++++++++++++- tags/api/views.py | 4 +-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 763096de..f1c3961d 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,76 @@ If you wish to contribute to this application, follow the instructions below on ## APIs ### Walkable REST APIs -The REST APIs that the frontend will eventually be built off of can be found at `/api`. They are walkable, meaning the APIs are laid out before you on each page, so just click a link to navigate to another API. Some will require you to add a project name to the URL path. +The REST APIs that the frontend will eventually be built off of can be found at `/api`. They are walkable, meaning the APIs are laid out before you on each page, so just click a link to navigate to another API. Some will require you to add a Project name to the URL path. + +Each REST API should also show a list of filters and ordering fields, along with examples. + +GET calls do not require the user to be logged in. +POST calls require the user to be logged in (logging into the Django Admin will suffice for local development). +PATCH and DELETE calls require the user to be logged in, as well as be either the owner or a contributor for the Project (ie package/plugin/sub-plugin contributor). +DELETE cannot be called on Projects themselves, just on the associated models. + +#### Games +`/api/games` +* displays the existing games along with their slug and icon +* allows for GET + +#### Packages +`/api/packages/projects` +* displays all Packages +* allows for GET, POST, and PATCH +* POST not only requires base information for the <package>, but also information for the first release (ie notes, version, and zip file). +* PATCH requires the package to be added to the URL path (ie `/api/packages/packages/`) + +`/api/packages/contributors/` +* displays the contributors for the given <package>. +* allows for GET, POST, and DELETE +* POST and DELETE can only be executed by the owner of the Project +* DELETE requires the id to be added to the URL path (ie `/api/packages/contributors//`) + +`/api/packages/games/` +* displays all associated games for the given <package>. +* allows for GET, POST, and DELETE +* DELETE requires the id to be added to the URL path (ie `/api/packages/games//`) + +`/api/packages/images/` +* displays all images for the given <package>. +* allows for GET, POST, and DELETE +* DELETE requires the id to be added to the URL path (ie `/api/packages/images//`) + +`/api/packages/releases/` +* displays all releases for the given <package>. +* allows for GET and POST +* you cannot currently PATCH or DELETE a release, though the Django Admin does allow for it if a User happens to make a mistake. + +`/api/packages/tags/` +* displays all images for the given <package>. +* allows for GET, POST, and DELETE +* DELETE requires the id to be added to the URL path (ie `/api/packages/tags//`) + +#### Plugins +* All the same APIs for [Packages](#packages) exist for Plugins (using `plugins` and `` in place of `packages` and ``) with the following addition. + +`/api/plugins/paths/` +* displays the Sub-Plugin paths allowed for the given <plugin>. For instance, [GunGame](https://github.com/GunGame-Dev-Team/GunGame-SP) allows for custom Sub-Plugins but requires them to be located in the `../plugins/custom` directory and include a file as `/.py`. +* For example: `../plugins/custom/gg_assists/gg_assists.py` +* allows for GET, POST, PATCH, and DELETE + +#### Sub-Plugins +* All the same APIs for [Packages](#packages) exist for Sub-Plugins, though they require the `` which they are associated as well as the ``. +* For example: `/api/sub-plugins/contributors//` + +#### Tags +`/api/tags` +* displays all created tags +* tags are created via the `Project Tag` APIs listed above for `Packages`, `Plugins`, and `Sub-Plugins`. +* allows for GET +* tags can be black-listed by an Admin/Super User in the Django Admin. due to the black-listing, tags should not be deleted. + +#### Users + `/api/users` +* displays all created users +* allows for GET ### Admin Since you have created a Super User, you should be able to log into `/admin` using your username/password. This will allow you to test the Django Admin functionality if you are working on it. diff --git a/tags/api/views.py b/tags/api/views.py index 0f2a0e1d..cf8c49f7 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -34,9 +34,9 @@ class TagViewSet(ListModelMixin, GenericViewSet): * Filters on blacklisted or not blacklisted. ####Example: - `?game=true` + `?black_listed=true` - `?game=false` + `?black_listed=false` ###Available Ordering: From 733cc8b364e36247d8d5b68bc710e5981c145167 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 20:08:59 -0400 Subject: [PATCH 005/211] Updated all urlpatterns to use path instead of url due to deprecation. --- project_manager/api/urls.py | 30 ++++++++--------- project_manager/packages/api/urls.py | 6 ++-- project_manager/plugins/api/urls.py | 6 ++-- project_manager/sub_plugins/api/urls.py | 6 ++-- project_manager/urls.py | 43 ++++++++++++++----------- users/api/urls.py | 6 ++-- 6 files changed, 51 insertions(+), 46 deletions(-) diff --git a/project_manager/api/urls.py b/project_manager/api/urls.py index d92f1b58..b7d6d4ac 100644 --- a/project_manager/api/urls.py +++ b/project_manager/api/urls.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # Django -from django.conf.urls import include, url +from django.urls import include, path # App from project_manager.api.views import ProjectManagerAPIView @@ -16,50 +16,50 @@ app_name = 'api' urlpatterns = [ - url( - regex=r'^games/', + path( + route='games/', view=include( 'games.api.urls', namespace='games', ), ), - url( - regex=r'^packages/', + path( + route='packages/', view=include( 'project_manager.packages.api.urls', namespace='packages', ), ), - url( - regex=r'^plugins/', + path( + route='plugins/', view=include( 'project_manager.plugins.api.urls', namespace='plugins', ), ), - url( - regex=r'^sub-plugins/', + path( + route='sub-plugins/', view=include( 'project_manager.sub_plugins.api.urls', namespace='sub-plugins', ), ), - url( - regex=r'^tags/', + path( + route='tags/', view=include( 'tags.api.urls', namespace='tags', ), ), - url( - regex=r'^users/', + path( + route='users/', view=include( 'users.api.urls', namespace='users', ), ), - url( - regex=r'^$', + path( + route='', view=ProjectManagerAPIView.as_view(), name='api-root', ), diff --git a/project_manager/packages/api/urls.py b/project_manager/packages/api/urls.py index 7d0a1153..75f0563b 100644 --- a/project_manager/packages/api/urls.py +++ b/project_manager/packages/api/urls.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # Django -from django.conf.urls import url +from django.urls import path # Third Party Django from rest_framework import routers @@ -63,8 +63,8 @@ app_name = 'packages' urlpatterns = [ - url( - regex=r'^$', + path( + route='', view=PackageAPIView.as_view(), name='endpoints', ) diff --git a/project_manager/plugins/api/urls.py b/project_manager/plugins/api/urls.py index f94dc53a..b8b6dc5b 100644 --- a/project_manager/plugins/api/urls.py +++ b/project_manager/plugins/api/urls.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # Django -from django.conf.urls import url +from django.urls import path # Third Party Django from rest_framework import routers @@ -69,8 +69,8 @@ app_name = 'plugins' urlpatterns = [ - url( - regex=r'^$', + path( + route='', view=PluginAPIView.as_view(), name='endpoints', ) diff --git a/project_manager/sub_plugins/api/urls.py b/project_manager/sub_plugins/api/urls.py index 5403c293..11f6f46f 100644 --- a/project_manager/sub_plugins/api/urls.py +++ b/project_manager/sub_plugins/api/urls.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # Django -from django.conf.urls import url +from django.urls import path # Third Party Django from rest_framework import routers @@ -65,8 +65,8 @@ app_name = 'sub-plugins' urlpatterns = [ - url( - regex=r'^$', + path( + route='', view=SubPluginAPIView.as_view(), name='endpoints', ) diff --git a/project_manager/urls.py b/project_manager/urls.py index 5b1b60ff..081fcd07 100644 --- a/project_manager/urls.py +++ b/project_manager/urls.py @@ -5,7 +5,7 @@ # ============================================================================= # Django from django.conf import settings -from django.conf.urls import include, url +from django.urls import include, path from django.conf.urls.static import static from django.contrib import admin from django.views.generic.base import RedirectView @@ -21,55 +21,57 @@ # GLOBAL VARIABLES # ============================================================================= urlpatterns = [ - url( + path( # / - regex=r'^$', + route='', view=RedirectView.as_view( url='plugins', permanent=False, ), name='index', ), - url( + path( # /statistics/ - regex=r'^statistics/', + route='statistics/', view=StatisticsView.as_view(), name='statistics', ), - url( + path( # /admin/ - regex=r'^admin/', + route='admin/', view=admin.site.urls, ), - url( - regex=r'^api/', + path( + route='api/', view=include( 'project_manager.api.urls', namespace='api', ), name='api', ), - # url( - # regex=r'^plugins/', + # path( + # route='plugins/', # view=, # name='plugins', # ), - url( + path( # /media/releases/packages// - regex=r'^media/releases/packages/(?P[\w-]+)/(?P.+)', + route='media/releases/packages//', view=PackageReleaseDownloadView.as_view(), name='package-download', ), - url( + path( # /media/releases/plugins// - regex=r'^media/releases/plugins/(?P[\w-]+)/(?P.+)', + route='media/releases/plugins//', view=PluginReleaseDownloadView.as_view(), name='plugin-download', ), - url( + path( # /media/releases/sub-plugins/// - regex=r'^media/releases/sub-plugins/(?P[\w-]+)/' - r'(?P[\w-]+)/(?P.+)', + route=( + 'media/releases/sub-plugins///' + '' + ), view=SubPluginReleaseDownloadView.as_view(), name='sub-plugin-download', ), @@ -82,5 +84,8 @@ if settings.DEBUG: import debug_toolbar urlpatterns += [ - url(https://github.com/r'%5E__debug__/',%20include(debug_toolbar.urls)), + path( + route='__debug__/', + view=include(debug_toolbar.urls), + ), ] diff --git a/users/api/urls.py b/users/api/urls.py index 16e793c1..1cf6ae08 100644 --- a/users/api/urls.py +++ b/users/api/urls.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # Django -from django.conf.urls import include, url +from django.urls import include, path # Third Party Django from rest_framework import routers @@ -30,8 +30,8 @@ app_name = 'users' urlpatterns = [ - url( - regex=r'^', + path( + route='', view=include(router.urls), ) ] From 0cd6334d333e35811ffc2357faa6bc7c1b59e237 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 20:16:49 -0400 Subject: [PATCH 006/211] Added a simple login page for testing non-Super Users. --- SPPM/settings/base.py | 1 + SPPM/settings/local.py | 3 +++ local-templates/registration/login.html | 7 +++++++ project_manager/urls.py | 8 ++++++++ 4 files changed, 19 insertions(+) create mode 100644 local-templates/registration/login.html diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index 6ba635e9..ca687a19 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -31,6 +31,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False +LOCAL = False ALLOWED_HOSTS = [] diff --git a/SPPM/settings/local.py b/SPPM/settings/local.py index 8862e835..b57faf88 100644 --- a/SPPM/settings/local.py +++ b/SPPM/settings/local.py @@ -1,6 +1,7 @@ from .base import * DEBUG = True +LOCAL = True INTERNAL_IPS = ('127.0.0.1',) @@ -15,3 +16,5 @@ TEMPLATES[0]['OPTIONS']['context_processors'] += [ 'django.template.context_processors.debug', ] +TEMPLATES[0]['DIRS'].append(BASE_DIR / 'local-templates') +LOGIN_REDIRECT_URL = '/' diff --git a/local-templates/registration/login.html b/local-templates/registration/login.html new file mode 100644 index 00000000..e69607d7 --- /dev/null +++ b/local-templates/registration/login.html @@ -0,0 +1,7 @@ + +

Log In

+
+ {% csrf_token %} + {{ form.as_p }} + +
diff --git a/project_manager/urls.py b/project_manager/urls.py index 081fcd07..3fa84070 100644 --- a/project_manager/urls.py +++ b/project_manager/urls.py @@ -89,3 +89,11 @@ view=include(debug_toolbar.urls), ), ] + +if settings.LOCAL: + urlpatterns += [ + path( + route='accounts/', + view=include('django.contrib.auth.urls'), + ) + ] From 162996fa5f9728657f99a6d86ef11d9589e08c61 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 20:32:21 -0400 Subject: [PATCH 007/211] Added a management command to create a non-Super User. Moved all User based commands to the users app. Moved the Game based command to the games app. Updated the readme. --- .../management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/create_game_instances.py | 0 readme.md | 23 ++++-- users/management/__init__.py | 0 users/management/commands/__init__.py | 0 .../commands/associate_super_user.py | 0 .../commands/create_random_users.py | 2 +- users/management/commands/create_test_user.py | 73 +++++++++++++++++++ 9 files changed, 92 insertions(+), 6 deletions(-) rename {project_manager => games}/management/__init__.py (100%) rename {project_manager => games}/management/commands/__init__.py (100%) rename {project_manager => games}/management/commands/create_game_instances.py (100%) create mode 100644 users/management/__init__.py create mode 100644 users/management/commands/__init__.py rename {project_manager => users}/management/commands/associate_super_user.py (100%) rename {project_manager => users}/management/commands/create_random_users.py (97%) create mode 100644 users/management/commands/create_test_user.py diff --git a/project_manager/management/__init__.py b/games/management/__init__.py similarity index 100% rename from project_manager/management/__init__.py rename to games/management/__init__.py diff --git a/project_manager/management/commands/__init__.py b/games/management/commands/__init__.py similarity index 100% rename from project_manager/management/commands/__init__.py rename to games/management/commands/__init__.py diff --git a/project_manager/management/commands/create_game_instances.py b/games/management/commands/create_game_instances.py similarity index 100% rename from project_manager/management/commands/create_game_instances.py rename to games/management/commands/create_game_instances.py diff --git a/readme.md b/readme.md index f1c3961d..79487d89 100644 --- a/readme.md +++ b/readme.md @@ -19,14 +19,27 @@ If you wish to contribute to this application, follow the instructions below on 7. Run the [createsuperuser](https://docs.djangoproject.com/en/dev/ref/django-admin/#createsuperuser) management command to create your main user. 8. Run the `associate_super_user` management command to associate the Super User you just created with a ForumUser object. 1. Arguments for the command are: - 1. **username** - The username of the Super User + 1. **username** - The username of the Super User. 2. **forum_id** - The user id from the Source.Python forums. -9. If you want additional users to test with, run the `create_random_users` management command. +9. If you want to create a test (non-Super User) User, run the `create_test_user` management command. 1. Arguments for the command are: - 1. **count** - The number of random Users to create. -10. Run the server using the [runserver](https://docs.djangoproject.com/en/dev/ref/django-admin/#runserver) management command. + 1. **username** - The username of the User. + 2. **password** - The password to use for the User. + 3. **forum_id** - The user id from the Source.Python forums. +10. If you want additional users to test with, run the `create_random_users` management command. + 1. Arguments for the command are: + 1. **count** - The number of random Users to create. +11. Run the server using the [runserver](https://docs.djangoproject.com/en/dev/ref/django-admin/#runserver) management command. 1. Some IDEs, like Pycharm, have tools to run the server instead of manually running the command in a console window. +## Authentication (logging in) +You can log in one of two ways. + +1. The Django Admin can be used to log in your Super User you created above. +2. A simple login page is available (local only) via the `/accounts/login` page. + +Either way will allow you to login and view/utilize all the APIs except for the Django Admin. Certain APIs require you to be logged in, whether as a regular user or a Super User. + ## APIs ### Walkable REST APIs The REST APIs that the frontend will eventually be built off of can be found at `/api`. They are walkable, meaning the APIs are laid out before you on each page, so just click a link to navigate to another API. Some will require you to add a Project name to the URL path. @@ -34,7 +47,7 @@ The REST APIs that the frontend will eventually be built off of can be found at Each REST API should also show a list of filters and ordering fields, along with examples. GET calls do not require the user to be logged in. -POST calls require the user to be logged in (logging into the Django Admin will suffice for local development). +POST calls require the user to be logged in. PATCH and DELETE calls require the user to be logged in, as well as be either the owner or a contributor for the Project (ie package/plugin/sub-plugin contributor). DELETE cannot be called on Projects themselves, just on the associated models. diff --git a/users/management/__init__.py b/users/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/users/management/commands/__init__.py b/users/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/management/commands/associate_super_user.py b/users/management/commands/associate_super_user.py similarity index 100% rename from project_manager/management/commands/associate_super_user.py rename to users/management/commands/associate_super_user.py diff --git a/project_manager/management/commands/create_random_users.py b/users/management/commands/create_random_users.py similarity index 97% rename from project_manager/management/commands/create_random_users.py rename to users/management/commands/create_random_users.py index 5ceb6391..59ad9c03 100644 --- a/project_manager/management/commands/create_random_users.py +++ b/users/management/commands/create_random_users.py @@ -6,7 +6,7 @@ # Django from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand # Third Party Python from random_username.generate import generate_username diff --git a/users/management/commands/create_test_user.py b/users/management/commands/create_test_user.py new file mode 100644 index 00000000..d157844b --- /dev/null +++ b/users/management/commands/create_test_user.py @@ -0,0 +1,73 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError + +# App +from users.models import ForumUser + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +User = get_user_model() + + +# ============================================================================= +# COMMANDS +# ============================================================================= +class Command(BaseCommand): + """Populate the Game objects.""" + + def add_arguments(self, parser): + parser.add_argument( + 'username', + type=str, + help='The username of the User.', + ) + parser.add_argument( + 'password', + type=str, + help='The password for the User.', + ) + parser.add_argument( + 'forum_id', + type=int, + help='The forum id number to associate.', + ) + + def handle(self, *args, **options): + username = options['username'] + if User.objects.filter(username=username).exists(): + raise CommandError( + f'User with the username "{username}" was not found.' + ) from User.DoesNotExist + + forum_id = options['forum_id'] + if ForumUser.objects.filter( + forum_id=forum_id, + ).exists(): + raise CommandError( + f'A user is already associated with the forum id "{forum_id}".' + ) + + try: + user = User.objects.create_user( + username=username, + password=options['password'], + ) + except Exception as e: + raise CommandError( + f'Unable to create User due to: {e}' + ) + + ForumUser.objects.create( + user=user, + forum_id=forum_id, + ) + print( + f'User "{username}" successfully associated with forum id ' + f'"{forum_id}".' + ) From eed5205676b59dadfb05aec292e9e766410de2a3 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 23:35:08 -0400 Subject: [PATCH 008/211] Added prospector for linting. Updated code to fix issues found by prospector. --- .coveragerc | 14 ++ .gitignore | 6 - .pylintrc | 6 +- games/management/__init__.py | 1 + games/management/commands/__init__.py | 1 + .../commands/create_game_instances.py | 3 + pip-requirements/local.txt | 4 + project_manager/common/admin/inlines.py | 1 + .../common/api/serializers/mixins.py | 157 ++++++++---------- project_manager/common/helpers.py | 48 ++++-- project_manager/common/mixins.py | 2 +- .../sub_plugins/api/serializers/__init__.py | 10 +- project_manager/sub_plugins/views.py | 2 +- prospector.yaml | 51 ++++++ pylint.sh | 12 -- pytest.ini | 7 + users/admin.py | 4 +- users/management/__init__.py | 1 + users/management/commands/__init__.py | 1 + .../commands/associate_super_user.py | 6 +- .../commands/create_random_users.py | 11 +- users/management/commands/create_test_user.py | 12 +- 22 files changed, 224 insertions(+), 136 deletions(-) create mode 100644 .coveragerc create mode 100644 prospector.yaml delete mode 100755 pylint.sh create mode 100644 pytest.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..0a2817c5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,14 @@ +[run] +branch = True +omit = + SPPM/settings/* + SPPM/wsgi.py + +[report] +omit = + */migrations/* + manage.py + +precision = 2 +skip_covered = True +skip_empty = True diff --git a/.gitignore b/.gitignore index 8a060d22..bc71fcbe 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,6 @@ # Database files *.sqlite3 -# Local batch files -*.bat - -# Local shell files -*.sh - # Local text files *.txt !pip-requirements/*.txt diff --git a/.pylintrc b/.pylintrc index 71a8fad7..0724f0be 100644 --- a/.pylintrc +++ b/.pylintrc @@ -11,12 +11,8 @@ ignore= # http://pylint-messages.wikidot.com/all-codes disable= abstract-method, -# attribute-defined-outside-init, - bad-continuation, - duplicate-code, -# fixme, + fixme, no-member, -# protected-access, too-few-public-methods, too-many-ancestors, diff --git a/games/management/__init__.py b/games/management/__init__.py index e69de29b..dcb1ac89 100644 --- a/games/management/__init__.py +++ b/games/management/__init__.py @@ -0,0 +1 @@ +"""Game based management.""" diff --git a/games/management/commands/__init__.py b/games/management/commands/__init__.py index e69de29b..1a5e4005 100644 --- a/games/management/commands/__init__.py +++ b/games/management/commands/__init__.py @@ -0,0 +1 @@ +"""Game based management commands.""" diff --git a/games/management/commands/create_game_instances.py b/games/management/commands/create_game_instances.py index 3693cb9b..839ccc1f 100644 --- a/games/management/commands/create_game_instances.py +++ b/games/management/commands/create_game_instances.py @@ -1,3 +1,5 @@ +"""Command to create Game objects.""" + # ============================================================================= # IMPORTS # ============================================================================= @@ -30,6 +32,7 @@ class Command(BaseCommand): """Populate the Game objects.""" def handle(self, *args, **options): + """Create any missing Game objects.""" current_games = Game.objects.values_list('basename', flat=True) obj_list = [] for game in set(games).difference(current_games): diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index bff02f26..0d369e0e 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,7 +1,11 @@ -r base.txt django-debug-toolbar==3.2.2 +flake8==3.8.4 +prospector==1.3.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 pylint==2.11.1 +pytest-cov==2.12.1 +pytest-django==4.4.0 random-username==1.0.2 diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index 328e2b99..3c0963e9 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -112,6 +112,7 @@ class ProjectReleaseInline(admin.StackedInline): ) def get_queryset(self, request): + """Order the queryset from newest to oldest.""" return super().get_queryset(request=request).order_by('-created') def has_add_permission(self, request, obj): diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 20256045..832d26b1 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -21,6 +21,17 @@ ) +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +GROUP_QUERYSET_NAMES = { + 'custom': 'package', + 'pypi': 'pypi', + 'vcs': 'versioncontrol', + 'download': 'download', +} + + # ============================================================================= # MIXINS # ============================================================================= @@ -101,26 +112,53 @@ def validate(self, attrs): # Validate the version is new for the project parent_project = self.parent_project kwargs = self.get_project_kwargs(parent_project) - try: - project = self.project_class.objects.get(**kwargs) - project_basename = project.basename - except self.project_class.DoesNotExist: - project_basename = None - project = None - else: - kwargs = { - self.project_type.replace('-', '_'): project, - 'version': version, - } - if self.Meta.model.objects.filter(**kwargs).exists(): - raise ValidationError({ - 'version': 'Given version matches existing version.', - }) + project = self.get_project( + kwargs=kwargs, + ) + self.validate_version( + project=project, + version=version, + ) + project_basename = getattr(project, 'basename', None) args = (zip_file,) if parent_project is not None: args += (parent_project,) - zip_validator = self.zip_parser(*args) + + with self.zip_parser(*args) as zip_validator: + self.validate_zip_file( + zip_validator=zip_validator, + project_basename=project_basename, + ) + + # This needs added for project creation + attrs['basename'] = zip_validator.basename + + if project is not None: + attrs[self.project_type.replace('-', '_')] = project + + return attrs + + def get_project(self, kwargs): + """Return the Project for the given kwargs.""" + try: + return self.project_class.objects.get(**kwargs) + except self.project_class.DoesNotExist: + return None + + def validate_version(self, project, version): + """Validate that the version does not already exist.""" + kwargs = { + self.project_type.replace('-', '_'): project, + 'version': version, + } + if self.Meta.model.objects.filter(**kwargs).exists(): + raise ValidationError({ + 'version': 'Given version matches existing version.', + }) + + def validate_zip_file(self, zip_validator, project_basename): + """Validate the files inside the zip file.""" zip_validator.find_base_info() zip_validator.validate_file_paths() zip_validator.validate_basename() @@ -136,13 +174,6 @@ def validate(self, attrs): ) }) - # This needs added for project creation - attrs['basename'] = zip_validator.basename - - if project is not None: - attrs[self.project_type.replace('-', '_')] = project - return attrs - def create(self, validated_data): """Update the project's updated datetime when release is created.""" # Remove the basename before creating the release @@ -167,70 +198,26 @@ def _create_requirements(self, release): # TODO: look into bulk_create project_type = release.__class__.__name__.lower() for group_type, group in self.requirements.items(): - if group_type == 'custom': - for item in group: - self._create_package_requirement( - release=release, - project_type=project_type, - requirement=item, - ) - elif group_type == 'pypi': - for item in group: - self._create_pypi_requirement( - release=release, - project_type=project_type, - requirement=item, - ) - elif group_type == 'vcs': - for item in group: - self._create_vcs_requirement( - release=release, - project_type=project_type, - requirement=item, - ) - else: - for item in group: - self._create_download_requirement( - release=release, - project_type=project_type, - requirement=item, - ) - - @staticmethod - def _create_package_requirement(release, project_type, requirement): - """Create the Package requirement for the release.""" - requirement_set = getattr( - release, - f'{project_type}packagerequirement_set' - ) - requirement_set.create(**requirement) - - @staticmethod - def _create_pypi_requirement(release, project_type, requirement): - """Create the PyPi requirement for the release.""" - requirement_set = getattr( - release, - f'{project_type}pypirequirement_set' - ) - requirement_set.create(**requirement) - - @staticmethod - def _create_vcs_requirement(release, project_type, requirement): - """Create the Version Control requirement for the release.""" - requirement_set = getattr( - release, - f'{project_type}versioncontrolrequirement_set' - ) - requirement_set.create(**requirement) + self._create_group_requirements( + release=release, + project_type=project_type, + group_type=group_type, + group=group, + ) @staticmethod - def _create_download_requirement(release, project_type, requirement): - """Create the Download requirement for the release.""" - requirement_set = getattr( - release, - f'{project_type}downloadrequirement_set' - ) - requirement_set.create(**requirement) + def _create_group_requirements(release, project_type, group_type, group): + queryset_group_name = GROUP_QUERYSET_NAMES.get(group_type) + if not queryset_group_name: + # TODO: should we care if they have invalid groupings? + pass + + for requirement in group: + requirement_set = getattr( + release, + f'{project_type}{queryset_group_name}requirement_set' + ) + requirement_set.create(**requirement) class ProjectThroughMixin(ModelSerializer): diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index f347dae7..fafc49a5 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -8,6 +8,7 @@ from zipfile import ZipFile, BadZipfile # Django +from django.apps import apps from django.conf import settings from django.core.exceptions import ValidationError @@ -28,6 +29,23 @@ ) +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +DownloadRequirement = apps.get_model( + app_label='requirements', + model_name='DownloadRequirement', +) +PyPiRequirement = apps.get_model( + app_label='requirements', + model_name='PyPiRequirement', +) +VersionControlRequirement = apps.get_model( + app_label='requirements', + model_name='VersionControlRequirement', +) + + # ============================================================================= # CLASSES # ============================================================================= @@ -35,10 +53,21 @@ class ProjectZipFile: """Base ZipFile parsing class.""" file_types = None + zip_file = None + + def __enter__(self): + """Open the zip file on entry.""" + self.zip_file = ZipFile(self.zip_file_path) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Close the zip file on exiting.""" + self.zip_file.close() + return not exc_type def __init__(self, zip_file): """Store the base attributes for the zip file.""" - self.zip_file = ZipFile(zip_file) + self.zip_file_path = zip_file self.file_list = self.get_file_list() self.basename = None self.requirements = { @@ -222,8 +251,6 @@ def get_requirement_path(self): def _validate_custom_requirement(self, item): """Verify that the given requirement exists.""" - # pylint: disable=import-outside-toplevel - from project_manager.packages.models import Package basename = item.get('basename') if basename is None: self.requirements_errors.append( @@ -231,9 +258,14 @@ def _validate_custom_requirement(self, item): 'listing in requirements json file.' ) return + + package_model = apps.get_model( + app_label='project_manager', + model_name='Package', + ) try: - package = Package.objects.get(slug=basename) - except Package.DoesNotExist: + package = package_model.objects.get(slug=basename) + except package_model.DoesNotExist: self.requirements_errors.append( f'Custom Package "{basename}" from requirements ' f'json file not found.' @@ -263,12 +295,6 @@ def _validate_requirement( """Verify that the given requirement is valid.""" # TODO: validate pypi requirements? # TODO: validate vcs requirements? - # pylint: disable=import-outside-toplevel - from requirements.models import ( - DownloadRequirement, - PyPiRequirement, - VersionControlRequirement, - ) model = { 'download': DownloadRequirement, 'pypi': PyPiRequirement, diff --git a/project_manager/common/mixins.py b/project_manager/common/mixins.py index 33c73d75..2cb0a562 100644 --- a/project_manager/common/mixins.py +++ b/project_manager/common/mixins.py @@ -68,7 +68,7 @@ def full_path(self): return self._full_path def get_base_path(self): - """Returns the base path for the download.""" + """Return the base path for the download.""" return settings.MEDIA_ROOT / self.base_url / self.kwargs['slug'] def dispatch(self, request, *args, **kwargs): diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index 649bfb16..524e4319 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -20,11 +20,6 @@ ReleasePackageRequirementSerializer, ) from project_manager.plugins.models import Plugin -from requirements.api.serializers.common import ( - ReleaseDownloadRequirementSerializer, - ReleasePyPiRequirementSerializer, - ReleaseVersionControlRequirementSerializer, -) from project_manager.sub_plugins.api.serializers.mixins import ( SubPluginReleaseBase, ) @@ -40,6 +35,11 @@ SubPluginReleaseVersionControlRequirement, SubPluginTag, ) +from requirements.api.serializers.common import ( + ReleaseDownloadRequirementSerializer, + ReleasePyPiRequirementSerializer, + ReleaseVersionControlRequirementSerializer, +) # ============================================================================= diff --git a/project_manager/sub_plugins/views.py b/project_manager/sub_plugins/views.py index dbdfd6ab..4cfc3da6 100644 --- a/project_manager/sub_plugins/views.py +++ b/project_manager/sub_plugins/views.py @@ -38,7 +38,7 @@ def get_instance(self, kwargs): }) def get_base_path(self): - """Returns the base path for the download.""" + """Return the base path for the download.""" base_path = super().get_base_path() slug = self.kwargs.get('sub_plugin_slug') return base_path / slug diff --git a/prospector.yaml b/prospector.yaml new file mode 100644 index 00000000..87df5a4f --- /dev/null +++ b/prospector.yaml @@ -0,0 +1,51 @@ +strictness: veryhigh +test-warnings: true +doc-warnings: true +autodetect: true +member-warnings: true + +uses: + - django + +inherits: + - strictness_veryhigh + +ignore-paths: + - SPPM + - htmlcov + - manage.py + - migrations + - populate + - .git + - .idea + +#ignore-patterns: +# - (^|/)skip(this)?(/|$) +# - ^seed_db(.*) + +pep8: + run: true +# disable: +# - D104 +# - D200 +# - D400 +# - E722 +# - N806 + options: + max-line-length: 100 + +pep257: + run: true + disable: + - D203 + - D213 + +mccabe: + run: false + +pylint: + run: true + disable: + - fixme + options: + max-line-length: 100 diff --git a/pylint.sh b/pylint.sh deleted file mode 100755 index 8511a9f9..00000000 --- a/pylint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -printf "Python Code Style:\n" -python -m pycodestyle --count --benchmark --exclude=migrations project_manager - -printf "\n\nPython Doc Style:\n" -python -m pydocstyle project_manager --match-dir='^(?!migrations).*' - -printf "\n\nPyFlakes:\n" -python -m pyflakes project_manager - -printf "\n\nPyLint:\n" -python -m pylint --rcfile .pylintrc --reports=y project_manager diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..1c004fc0 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +django_debug_mode = true +addopts = + --cov=. + --cov-report=html +DJANGO_SETTINGS_MODULE=SPPM.settings.local +python_files=test_*.py diff --git a/users/admin.py b/users/admin.py index 183875a6..25ef4ded 100644 --- a/users/admin.py +++ b/users/admin.py @@ -69,8 +69,8 @@ def get_queryset(self, request): 'user', ) - # pylint: disable=no-self-use - def get_username(self, obj): + @staticmethod + def get_username(obj): """Return the user's username.""" return obj.user.username get_username.short_description = 'Username' diff --git a/users/management/__init__.py b/users/management/__init__.py index e69de29b..cc352774 100644 --- a/users/management/__init__.py +++ b/users/management/__init__.py @@ -0,0 +1 @@ +"""User based management.""" diff --git a/users/management/commands/__init__.py b/users/management/commands/__init__.py index e69de29b..64313848 100644 --- a/users/management/commands/__init__.py +++ b/users/management/commands/__init__.py @@ -0,0 +1 @@ +"""User based management commands.""" diff --git a/users/management/commands/associate_super_user.py b/users/management/commands/associate_super_user.py index 65f193a7..b3184157 100644 --- a/users/management/commands/associate_super_user.py +++ b/users/management/commands/associate_super_user.py @@ -1,3 +1,5 @@ +"""Command used to associate a User with a ForumUser object.""" + # ============================================================================= # IMPORTS # ============================================================================= @@ -19,9 +21,10 @@ # COMMANDS # ============================================================================= class Command(BaseCommand): - """Populate the Game objects.""" + """Create a ForumUser for a Super User.""" def add_arguments(self, parser): + """Add the required arguments for the command.""" parser.add_argument( 'username', type=str, @@ -34,6 +37,7 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): + """Verify the arguments and associate the User.""" username = options['username'] try: user = User.objects.get(username=username) diff --git a/users/management/commands/create_random_users.py b/users/management/commands/create_random_users.py index 59ad9c03..1ef7f74d 100644 --- a/users/management/commands/create_random_users.py +++ b/users/management/commands/create_random_users.py @@ -1,3 +1,5 @@ +"""Command used to create random users.""" + # ============================================================================= # IMPORTS # ============================================================================= @@ -25,9 +27,10 @@ # COMMANDS # ============================================================================= class Command(BaseCommand): - """Populate the Game objects.""" + """Create some random Users.""" def add_arguments(self, parser): + """Add the required arguments for the command.""" parser.add_argument( 'count', type=int, @@ -35,6 +38,7 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): + """Verify the arguments and create the Users.""" count = options['count'] current_usernames = User.objects.values_list( 'username', @@ -60,7 +64,7 @@ def handle(self, *args, **options): max_id = count + len(current_forum_ids) id_list = list(set(range(1, max_id + 1)).difference(current_forum_ids)) obj_list = [] - for n, username in enumerate(username_list): + for index, username in enumerate(username_list): user = User.objects.create_user( username=username, password=urandom(8), @@ -68,7 +72,7 @@ def handle(self, *args, **options): obj_list.append( ForumUser( user=user, - forum_id=id_list[n], + forum_id=id_list[index], ) ) @@ -81,4 +85,5 @@ def handle(self, *args, **options): @staticmethod def validate_unique_list(username_list, current_usernames, count): + """Validate the given list is unique and has the correct count.""" return len(set(username_list).difference(current_usernames)) == count diff --git a/users/management/commands/create_test_user.py b/users/management/commands/create_test_user.py index d157844b..f764beef 100644 --- a/users/management/commands/create_test_user.py +++ b/users/management/commands/create_test_user.py @@ -1,3 +1,5 @@ +"""Command used to create a non-Super User.""" + # ============================================================================= # IMPORTS # ============================================================================= @@ -19,9 +21,10 @@ # COMMANDS # ============================================================================= class Command(BaseCommand): - """Populate the Game objects.""" + """Create a test User.""" def add_arguments(self, parser): + """Add the required arguments for the command.""" parser.add_argument( 'username', type=str, @@ -39,6 +42,7 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): + """Verify the arguments and create the User.""" username = options['username'] if User.objects.filter(username=username).exists(): raise CommandError( @@ -58,10 +62,10 @@ def handle(self, *args, **options): username=username, password=options['password'], ) - except Exception as e: + except Exception as error: raise CommandError( - f'Unable to create User due to: {e}' - ) + f'Unable to create User due to: {error}' + ) from Exception ForumUser.objects.create( user=user, From 69820a0f6e8a7a3e9df3bda082a2b0c9f3f6b0a1 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 22 Oct 2021 23:38:46 -0400 Subject: [PATCH 009/211] Updated local pip requirements. --- pip-requirements/local.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index 0d369e0e..894f01ca 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,11 +1,11 @@ -r base.txt django-debug-toolbar==3.2.2 -flake8==3.8.4 -prospector==1.3.1 +flake8==3.9.2 +prospector==1.5.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 pylint==2.11.1 -pytest-cov==2.12.1 +pytest-cov==3.0.0 pytest-django==4.4.0 random-username==1.0.2 From 2df6f684854151f7cc546bc17ae230bc167c18f5 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 01:00:31 -0400 Subject: [PATCH 010/211] Added factoryboy requirement to help with testing. Added tests for games app. --- .coveragerc | 1 + .gitignore | 4 + .../commands/create_game_instances.py | 9 ++- games/tests/__init__.py | 0 games/tests/test_admin.py | 51 +++++++++++++ games/tests/test_commands.py | 27 +++++++ games/tests/test_models.py | 68 +++++++++++++++++ games/tests/test_views.py | 73 +++++++++++++++++++ pip-requirements/local.txt | 1 + test_utils/__init__.py | 0 test_utils/factories/__init__.py | 0 test_utils/factories/games.py | 8 ++ 12 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 games/tests/__init__.py create mode 100644 games/tests/test_admin.py create mode 100644 games/tests/test_commands.py create mode 100644 games/tests/test_models.py create mode 100644 games/tests/test_views.py create mode 100644 test_utils/__init__.py create mode 100644 test_utils/factories/__init__.py create mode 100644 test_utils/factories/games.py diff --git a/.coveragerc b/.coveragerc index 0a2817c5..7152e8fb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,7 @@ omit = omit = */migrations/* manage.py + populate/* precision = 2 skip_covered = True diff --git a/.gitignore b/.gitignore index bc71fcbe..85d35fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ # Favicon favicon.png + +htmlcov +.coverage +.pytest_cache diff --git a/games/management/commands/create_game_instances.py b/games/management/commands/create_game_instances.py index 839ccc1f..b5f94c8a 100644 --- a/games/management/commands/create_game_instances.py +++ b/games/management/commands/create_game_instances.py @@ -13,7 +13,7 @@ # ============================================================================= # GLOBAL VARIABLES # ============================================================================= -games = { +GAMES = { 'berimbau': 'Blade Symphony', 'bms': 'Black Mesa', 'csgo': 'Counter-Strike: Global Offensive', @@ -35,14 +35,15 @@ def handle(self, *args, **options): """Create any missing Game objects.""" current_games = Game.objects.values_list('basename', flat=True) obj_list = [] - for game in set(games).difference(current_games): + for game in set(GAMES).difference(current_games): obj_list.append( Game( - name=games[game], basename=game, icon=f'games/{game}.png', + name=GAMES[game], + slug=game, ) ) - if obj_list: + if obj_list: # pragma: no branch Game.objects.bulk_create(objs=obj_list) diff --git a/games/tests/__init__.py b/games/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/tests/test_admin.py b/games/tests/test_admin.py new file mode 100644 index 00000000..5f190756 --- /dev/null +++ b/games/tests/test_admin.py @@ -0,0 +1,51 @@ +from django.contrib import admin +from django.test import TestCase + +from games.admin import GameAdmin + + +class GameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(GameAdmin, admin.ModelAdmin), + ) + + def test_exclude(self): + self.assertTupleEqual( + tuple1=GameAdmin.exclude, + tuple2=('slug',), + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=GameAdmin.list_display, + tuple2=( + 'basename', + 'name', + 'icon', + ), + ) + + def test_list_editable(self): + self.assertTupleEqual( + tuple1=GameAdmin.list_editable, + tuple2=( + 'name', + 'icon', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=GameAdmin.readonly_fields, + tuple2=('basename',), + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=GameAdmin.search_fields, + tuple2=( + 'name', + 'basename', + ), + ) diff --git a/games/tests/test_commands.py b/games/tests/test_commands.py new file mode 100644 index 00000000..757b2003 --- /dev/null +++ b/games/tests/test_commands.py @@ -0,0 +1,27 @@ +from random import choice + +from django.core.management import call_command +from django.test import TestCase + +from games.management.commands.create_game_instances import GAMES +from games.models import Game +from test_utils.factories.games import GameFactory + + +class CommandsTestCase(TestCase): + def test_create_game_instances(self): + game = choice(list(GAMES)) + GameFactory( + name=GAMES[game], + basename=game, + icon=f'games/{game}.png', + ) + self.assertEqual( + first=Game.objects.count(), + second=1, + ) + call_command('create_game_instances') + self.assertEqual( + first=Game.objects.count(), + second=len(GAMES), + ) diff --git a/games/tests/test_models.py b/games/tests/test_models.py new file mode 100644 index 00000000..358e29c7 --- /dev/null +++ b/games/tests/test_models.py @@ -0,0 +1,68 @@ +from django.db import models +from django.test import TestCase + +from games.constants import ( + GAME_BASENAME_MAX_LENGTH, + GAME_NAME_MAX_LENGTH, + GAME_SLUG_MAX_LENGTH, +) +from games.models import Game + + +class GameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(Game, models.Model), + ) + + def test_name_field(self): + field = Game._meta.get_field('name') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=GAME_NAME_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_basename_field(self): + field = Game._meta.get_field('basename') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=GAME_BASENAME_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_slug_field(self): + field = Game._meta.get_field('slug') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=GAME_SLUG_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.primary_key) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_icon_field(self): + field = Game._meta.get_field('icon') + self.assertIsInstance( + obj=field, + cls=models.ImageField, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) diff --git a/games/tests/test_views.py b/games/tests/test_views.py new file mode 100644 index 00000000..3a6dfcc9 --- /dev/null +++ b/games/tests/test_views.py @@ -0,0 +1,73 @@ +from django.core.management import call_command + +from rest_framework import status +from rest_framework.filters import OrderingFilter +from rest_framework.test import APITestCase + +from games.api.serializers import GameSerializer +from games.api.views import GameViewSet +from games.management.commands.create_game_instances import GAMES + + +class GameViewSetAPITestCase(APITestCase): + + def test_filter_backends(self): + self.assertTupleEqual( + tuple1=GameViewSet.filter_backends, + tuple2=(OrderingFilter,) + ) + + def test_serializer_class(self): + self.assertEqual( + first=GameViewSet.serializer_class, + second=GameSerializer, + ) + + def test_ordering(self): + self.assertTupleEqual( + tuple1=GameViewSet.ordering, + tuple2=('name',), + ) + + def test_ordering_fields(self): + self.assertTupleEqual( + tuple1=GameViewSet.ordering_fields, + tuple2=('basename', 'name',), + ) + + def test_can_list(self): + response = self.client.get(path='/api/games/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + + call_command('create_game_instances') + response = self.client.get(path='/api/games/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=len(GAMES), + ) + + def test_cannot_post(self): + game = list(GAMES)[0] + response = self.client.post( + path='/api/games/', + data={ + 'basename': game, + 'icon': f'games/{game}.png', + 'name': GAMES[game], + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_405_METHOD_NOT_ALLOWED, + ) diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index 894f01ca..17e3e0bf 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,5 +1,6 @@ -r base.txt django-debug-toolbar==3.2.2 +factory-boy==3.2.0 flake8==3.9.2 prospector==1.5.1 pycodestyle==2.8.0 diff --git a/test_utils/__init__.py b/test_utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test_utils/factories/__init__.py b/test_utils/factories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test_utils/factories/games.py b/test_utils/factories/games.py new file mode 100644 index 00000000..ed643012 --- /dev/null +++ b/test_utils/factories/games.py @@ -0,0 +1,8 @@ +import factory + +from games.models import Game + + +class GameFactory(factory.django.DjangoModelFactory): + class Meta: + model = Game From 8620823897c3470e029a86a6eef2f2c582e30335 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 01:05:59 -0400 Subject: [PATCH 011/211] Updated readme for testing. --- readme.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/readme.md b/readme.md index 79487d89..594dd293 100644 --- a/readme.md +++ b/readme.md @@ -121,3 +121,13 @@ There is also a `/statistics` page to display certain statistics for your local ### User Frontend Eventually we will be adding `/plugins` and `/packages`, as well as `/plugins//sub-plugins` for a frontend User experience. These all still need built, so if you have Javascript experience and are willing to help out, it would be much appreciated. The first obstacle will be to determine which Javascript framework to use. This really depends on what people know, but Vue or React would be preferred. + +## Testing + +### Unit Testing +To run the Django test suite, run `pytest`. The output will show you any tests that are failing. It will also show you a list of warnings, which will help with deprecated functionalities that may need updated in the future. + +`pytest` also creates a coverage report that can be found at `htmlcov/index.html`. This report shows where there are gaps in the coverage. + +### Linting +To run the linters, run `prospector`. The output will tell you where there are coding standards violations that need fixed. From 5e0373d6d68d135b393d2507ce4ce07779028600 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 09:24:54 -0400 Subject: [PATCH 012/211] Renamed base project model. --- project_manager/common/models.py | 4 ++-- project_manager/packages/models/__init__.py | 4 ++-- project_manager/plugins/models/__init__.py | 4 ++-- project_manager/sub_plugins/models/__init__.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/project_manager/common/models.py b/project_manager/common/models.py index adc3682d..e511f783 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -44,7 +44,7 @@ # ============================================================================= __all__ = ( 'AbstractUUIDPrimaryKeyModel', - 'ProjectBase', + 'Project', 'ProjectContributor', 'ProjectGame', 'ProjectImage', @@ -76,7 +76,7 @@ class Meta: abstract = True -class ProjectBase(models.Model): +class Project(models.Model): """Base model for projects.""" name = models.CharField( diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models/__init__.py index bc37cd1d..f418de5d 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models/__init__.py @@ -13,7 +13,7 @@ PROJECT_SLUG_MAX_LENGTH, ) from project_manager.common.models import ( - ProjectBase, + Project, ProjectContributor, ProjectGame, ProjectImage, @@ -57,7 +57,7 @@ # ============================================================================= # MODELS # ============================================================================= -class Package(ProjectBase): +class Package(Project): """Package project type model.""" basename = models.CharField( diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index ee52ccf6..9f35b6b2 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -14,7 +14,7 @@ PROJECT_SLUG_MAX_LENGTH, ) from project_manager.common.models import ( - ProjectBase, + Project, ProjectContributor, ProjectGame, ProjectImage, @@ -61,7 +61,7 @@ # ============================================================================= # MODELS # ============================================================================= -class Plugin(ProjectBase): +class Plugin(Project): """Plugin project type model.""" basename = models.CharField( diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models/__init__.py index 380c8dc0..e9f25454 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models/__init__.py @@ -13,7 +13,7 @@ PROJECT_SLUG_MAX_LENGTH, ) from project_manager.common.models import ( - ProjectBase, + Project, ProjectContributor, ProjectGame, ProjectImage, @@ -57,7 +57,7 @@ # ============================================================================= # MODELS # ============================================================================= -class SubPlugin(ProjectBase): +class SubPlugin(Project): """SubPlugin project type model.""" id = models.CharField( From 286b81eaeba742ae2d26c6b17455406b3186e19f Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 09:26:02 -0400 Subject: [PATCH 013/211] Added django-extensions to local settings. --- SPPM/settings/local.py | 1 + pip-requirements/local.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/SPPM/settings/local.py b/SPPM/settings/local.py index b57faf88..6fde2edb 100644 --- a/SPPM/settings/local.py +++ b/SPPM/settings/local.py @@ -7,6 +7,7 @@ INSTALLED_APPS += [ 'debug_toolbar', + 'django_extensions', ] MIDDLEWARE += [ diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index 17e3e0bf..1ae7f676 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,5 +1,6 @@ -r base.txt django-debug-toolbar==3.2.2 +django-extensions==3.1.3 factory-boy==3.2.0 flake8==3.9.2 prospector==1.5.1 From 9f4d75d01ec4788bf84dd0d3d2d037ee8a49ba29 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 09:26:53 -0400 Subject: [PATCH 014/211] Added test for Game and removed an unused method. --- games/models.py | 9 --------- games/tests/test_models.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/games/models.py b/games/models.py index e01ce60e..e6a6120e 100644 --- a/games/models.py +++ b/games/models.py @@ -62,12 +62,3 @@ def save( using=using, update_fields=update_fields, ) - - def get_absolute_url(https://github.com/self): - """Return the URL for the Game.""" - return reverse( - viewname='games:detail', - kwargs={ - 'slug': self.slug, - } - ) diff --git a/games/tests/test_models.py b/games/tests/test_models.py index 358e29c7..94919a62 100644 --- a/games/tests/test_models.py +++ b/games/tests/test_models.py @@ -6,7 +6,9 @@ GAME_NAME_MAX_LENGTH, GAME_SLUG_MAX_LENGTH, ) +from games.management.commands.create_game_instances import GAMES from games.models import Game +from test_utils.factories.games import GameFactory class GameTestCase(TestCase): @@ -66,3 +68,15 @@ def test_icon_field(self): ) self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + + def test__str__(self): + game = list(GAMES)[0] + obj = GameFactory( + name=GAMES[game], + basename=game, + icon=f'games/{game}.png', + ) + self.assertEqual( + first=str(obj), + second=obj.name, + ) From 5b70ad78b4be0c07f6f41f32e342c42aef3fd0ec Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 10:10:30 -0400 Subject: [PATCH 015/211] Disabled running prospector for the tests directories. --- prospector.yaml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/prospector.yaml b/prospector.yaml index 87df5a4f..9c0cb40d 100644 --- a/prospector.yaml +++ b/prospector.yaml @@ -16,21 +16,12 @@ ignore-paths: - manage.py - migrations - populate + - tests - .git - .idea -#ignore-patterns: -# - (^|/)skip(this)?(/|$) -# - ^seed_db(.*) - pep8: run: true -# disable: -# - D104 -# - D200 -# - D400 -# - E722 -# - N806 options: max-line-length: 100 From 8e142fc879d2f5823e528bdd90c037acb5565980 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 10:11:19 -0400 Subject: [PATCH 016/211] Fixed some prospector issues. --- games/models.py | 1 - games/tests/__init__.py | 1 + games/tests/test_admin.py | 13 +++++++++++-- test_utils/__init__.py | 1 + test_utils/factories/__init__.py | 1 + test_utils/factories/games.py | 13 +++++++++++++ 6 files changed, 27 insertions(+), 3 deletions(-) diff --git a/games/models.py b/games/models.py index e6a6120e..cbc4048f 100644 --- a/games/models.py +++ b/games/models.py @@ -4,7 +4,6 @@ # IMPORTS # ============================================================================= # Django -from django.urls import reverse from django.db import models from django.utils.text import slugify diff --git a/games/tests/__init__.py b/games/tests/__init__.py index e69de29b..854d5dfc 100644 --- a/games/tests/__init__.py +++ b/games/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Game functionality.""" diff --git a/games/tests/test_admin.py b/games/tests/test_admin.py index 5f190756..1b9d8e83 100644 --- a/games/tests/test_admin.py +++ b/games/tests/test_admin.py @@ -1,11 +1,20 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django from django.contrib import admin from django.test import TestCase +# App from games.admin import GameAdmin -class GameTestCase(TestCase): - def test_model_inheritance(self): +# ============================================================================= +# TEST CASES +# ============================================================================= +class GameAdminTestCase(TestCase): + + def test_class_inheritance(self): self.assertTrue( expr=issubclass(GameAdmin, admin.ModelAdmin), ) diff --git a/test_utils/__init__.py b/test_utils/__init__.py index e69de29b..60789003 100644 --- a/test_utils/__init__.py +++ b/test_utils/__init__.py @@ -0,0 +1 @@ +"""Utilities for running tests.""" diff --git a/test_utils/factories/__init__.py b/test_utils/factories/__init__.py index e69de29b..07c7dbee 100644 --- a/test_utils/factories/__init__.py +++ b/test_utils/factories/__init__.py @@ -0,0 +1 @@ +"""Model factories for use when running tests.""" diff --git a/test_utils/factories/games.py b/test_utils/factories/games.py index ed643012..d124c6a9 100644 --- a/test_utils/factories/games.py +++ b/test_utils/factories/games.py @@ -1,8 +1,21 @@ +"""Factories for use when testing with Game functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django import factory +# App from games.models import Game +# ============================================================================= +# FACTORIES +# ============================================================================= class GameFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with Game objects.""" + class Meta: + """Define metaclass attributes.""" model = Game From 3771aff33b6480bb4e180b1f570dd44cd4c65d29 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 12:56:01 -0400 Subject: [PATCH 017/211] Added some tests for the users app. --- .pylintrc | 1 + games/models.py | 17 ++ games/tests/test_commands.py | 9 + games/tests/test_models.py | 18 ++ games/tests/test_views.py | 9 + project_manager/packages/models/__init__.py | 1 + project_manager/plugins/models/__init__.py | 2 + .../sub_plugins/models/__init__.py | 1 + requirements/models.py | 10 -- test_utils/factories/games.py | 1 + test_utils/factories/users.py | 47 +++++ users/admin.py | 4 +- .../commands/create_random_users.py | 2 +- users/management/commands/create_test_user.py | 12 +- users/models.py | 2 + users/tests/__init__.py | 0 users/tests/test_admin.py | 112 ++++++++++++ users/tests/test_commands.py | 156 +++++++++++++++++ users/tests/test_models.py | 161 ++++++++++++++++++ users/tests/test_views.py | 0 20 files changed, 545 insertions(+), 20 deletions(-) create mode 100644 test_utils/factories/users.py create mode 100644 users/tests/__init__.py create mode 100644 users/tests/test_admin.py create mode 100644 users/tests/test_commands.py create mode 100644 users/tests/test_models.py create mode 100644 users/tests/test_views.py diff --git a/.pylintrc b/.pylintrc index 0724f0be..2548c73a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -12,6 +12,7 @@ ignore= disable= abstract-method, fixme, + locally-disabled, no-member, too-few-public-methods, too-many-ancestors, diff --git a/games/models.py b/games/models.py index cbc4048f..bbde86a7 100644 --- a/games/models.py +++ b/games/models.py @@ -5,6 +5,7 @@ # ============================================================================= # Django from django.db import models +from django.urls import reverse from django.utils.text import slugify # App @@ -45,6 +46,12 @@ class Game(models.Model): ) icon = models.ImageField() + class Meta: + """Define metaclass attributes.""" + + verbose_name = 'Game' + verbose_name_plural = 'Games' + def __str__(self): """Return the object's name when str cast.""" return str(self.name) @@ -61,3 +68,13 @@ def save( using=using, update_fields=update_fields, ) + + def get_absolute_url(https://github.com/self): + """Return the URL for the Game.""" + # TODO: add tests once this view is created + return reverse( + viewname='games:detail', + kwargs={ + 'slug': self.slug, + } + ) diff --git a/games/tests/test_commands.py b/games/tests/test_commands.py index 757b2003..7da9f758 100644 --- a/games/tests/test_commands.py +++ b/games/tests/test_commands.py @@ -1,13 +1,22 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python from random import choice +# Django from django.core.management import call_command from django.test import TestCase +# App from games.management.commands.create_game_instances import GAMES from games.models import Game from test_utils.factories.games import GameFactory +# ============================================================================= +# TEST CASES +# ============================================================================= class CommandsTestCase(TestCase): def test_create_game_instances(self): game = choice(list(GAMES)) diff --git a/games/tests/test_models.py b/games/tests/test_models.py index 94919a62..9d995b55 100644 --- a/games/tests/test_models.py +++ b/games/tests/test_models.py @@ -1,6 +1,11 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django from django.db import models from django.test import TestCase +# App from games.constants import ( GAME_BASENAME_MAX_LENGTH, GAME_NAME_MAX_LENGTH, @@ -11,6 +16,9 @@ from test_utils.factories.games import GameFactory +# ============================================================================= +# TEST CASES +# ============================================================================= class GameTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( @@ -69,6 +77,16 @@ def test_icon_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_meta_class(self): + self.assertEqual( + first=Game._meta.verbose_name, + second='Game', + ) + self.assertEqual( + first=Game._meta.verbose_name_plural, + second='Games', + ) + def test__str__(self): game = list(GAMES)[0] obj = GameFactory( diff --git a/games/tests/test_views.py b/games/tests/test_views.py index 3a6dfcc9..3429f605 100644 --- a/games/tests/test_views.py +++ b/games/tests/test_views.py @@ -1,14 +1,23 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django from django.core.management import call_command +# Third Party Django from rest_framework import status from rest_framework.filters import OrderingFilter from rest_framework.test import APITestCase +# App from games.api.serializers import GameSerializer from games.api.views import GameViewSet from games.management.commands.create_game_instances import GAMES +# ============================================================================= +# TEST CASES +# ============================================================================= class GameViewSetAPITestCase(APITestCase): def test_filter_backends(self): diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models/__init__.py index f418de5d..7db6519e 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models/__init__.py @@ -93,6 +93,7 @@ class Package(Project): def get_absolute_url(https://github.com/self): """Return the URL for the Package.""" + # TODO: add tests once this view is created return reverse( viewname='packages:detail', kwargs={ diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index 9f35b6b2..82e63168 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -97,6 +97,7 @@ class Plugin(Project): def get_absolute_url(https://github.com/self): """Return the URL for the Plugin.""" + # TODO: add tests once this view is created return reverse( viewname='plugins:detail', kwargs={ @@ -247,6 +248,7 @@ def clean(self): def get_absolute_url(https://github.com/self): """Return the SubPluginPath listing URL for the Plugin.""" + # TODO: add tests once this view is created return reverse( viewname='plugins:path_list', kwargs={ diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models/__init__.py index e9f25454..9282f7b4 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models/__init__.py @@ -115,6 +115,7 @@ def __str__(self): def get_absolute_url(https://github.com/self): """Return the URL for the SubPlugin.""" + # TODO: add tests once this view is created return reverse( viewname='plugins:sub-plugins:detail', kwargs={ diff --git a/requirements/models.py b/requirements/models.py index 3f860ec7..ce23e599 100644 --- a/requirements/models.py +++ b/requirements/models.py @@ -5,7 +5,6 @@ # ============================================================================= # Django from django.conf import settings -from django.urls import reverse from django.db import models from django.utils.text import slugify @@ -79,15 +78,6 @@ def save( update_fields=update_fields, ) - def get_absolute_url(https://github.com/self): - """Return the URL for the PyPiRequirement.""" - return reverse( - viewname='pypi:detail', - kwargs={ - 'slug': self.slug, - } - ) - def get_pypi_url(https://github.com/self): """Return the PyPi URL for the requirement.""" return settings.PYPI_URL + f'/{self.name}' diff --git a/test_utils/factories/games.py b/test_utils/factories/games.py index d124c6a9..df4963dc 100644 --- a/test_utils/factories/games.py +++ b/test_utils/factories/games.py @@ -18,4 +18,5 @@ class GameFactory(factory.django.DjangoModelFactory): class Meta: """Define metaclass attributes.""" + model = Game diff --git a/test_utils/factories/users.py b/test_utils/factories/users.py new file mode 100644 index 00000000..d3e7216c --- /dev/null +++ b/test_utils/factories/users.py @@ -0,0 +1,47 @@ +"""Factories for use when testing with User functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +import factory + +# App +from users.models import ForumUser, User + + +# ============================================================================= +# FACTORIES +# ============================================================================= +class NonAdminUserFactory(factory.django.DjangoModelFactory): + """Factory for a non-admin User to use in tests.""" + + username = factory.Sequence(function=lambda n: f'user_{n}') + is_staff = False + is_superuser = False + + class Meta: + """Define metaclass attributes.""" + + model = User + + +class AdminUserFactory(NonAdminUserFactory): + """Factory for an Admin User to use in tests.""" + + is_staff = True + is_superuser = True + + +class ForumUserFactory(factory.django.DjangoModelFactory): + """Factory for Forum based User to use in tests.""" + + user = factory.SubFactory( + factory='test_utils.factories.users.NonAdminUserFactory', + ) + forum_id = factory.Sequence(function=lambda n: n) + + class Meta: + """Define metaclass attributes.""" + + model = ForumUser diff --git a/users/admin.py b/users/admin.py index 25ef4ded..183875a6 100644 --- a/users/admin.py +++ b/users/admin.py @@ -69,8 +69,8 @@ def get_queryset(self, request): 'user', ) - @staticmethod - def get_username(obj): + # pylint: disable=no-self-use + def get_username(self, obj): """Return the user's username.""" return obj.user.username get_username.short_description = 'Username' diff --git a/users/management/commands/create_random_users.py b/users/management/commands/create_random_users.py index 1ef7f74d..627703ec 100644 --- a/users/management/commands/create_random_users.py +++ b/users/management/commands/create_random_users.py @@ -76,7 +76,7 @@ def handle(self, *args, **options): ) ) - if obj_list: + if obj_list: # pragma: no branch ForumUser.objects.bulk_create( objs=obj_list, ) diff --git a/users/management/commands/create_test_user.py b/users/management/commands/create_test_user.py index f764beef..5401fb60 100644 --- a/users/management/commands/create_test_user.py +++ b/users/management/commands/create_test_user.py @@ -46,13 +46,11 @@ def handle(self, *args, **options): username = options['username'] if User.objects.filter(username=username).exists(): raise CommandError( - f'User with the username "{username}" was not found.' - ) from User.DoesNotExist + f'User with the username "{username}" already exists.' + ) forum_id = options['forum_id'] - if ForumUser.objects.filter( - forum_id=forum_id, - ).exists(): + if ForumUser.objects.filter(forum_id=forum_id).exists(): raise CommandError( f'A user is already associated with the forum id "{forum_id}".' ) @@ -72,6 +70,6 @@ def handle(self, *args, **options): forum_id=forum_id, ) print( - f'User "{username}" successfully associated with forum id ' - f'"{forum_id}".' + f'Successfully created user "{username}" and associated it with ' + f'forum id "{forum_id}".' ) diff --git a/users/models.py b/users/models.py index 65749a61..3247d4f6 100644 --- a/users/models.py +++ b/users/models.py @@ -41,6 +41,7 @@ class User(AbstractBaseUser, PermissionsMixin): max_length=USER_USERNAME_MAX_LENGTH, unique=True, ) + # TODO: should we be storing the email? email = models.EmailField( max_length=USER_EMAIL_MAX_LENGTH, blank=True, @@ -87,6 +88,7 @@ def __str__(self): def get_absolute_url(https://github.com/self): """Return the URL for the user.""" + # TODO: add tests once this view is created return reverse( viewname='users:detail', kwargs={ diff --git a/users/tests/__init__.py b/users/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/users/tests/test_admin.py b/users/tests/test_admin.py new file mode 100644 index 00000000..678965e1 --- /dev/null +++ b/users/tests/test_admin.py @@ -0,0 +1,112 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.test import TestCase + +# App +from test_utils.factories.users import ForumUserFactory +from users.admin import ForumUserAdmin, UserAdmin +from users.models import ForumUser, User + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ForumUserAdminTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ForumUserAdmin, admin.ModelAdmin), + ) + + def test_actions(self): + self.assertIsNone(obj=ForumUserAdmin.actions) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=ForumUserAdmin.list_display, + tuple2=( + 'get_username', + 'forum_id', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ForumUserAdmin.readonly_fields, + tuple2=( + 'user', + 'forum_id', + ), + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=ForumUserAdmin.search_fields, + tuple2=('user__username',), + ) + + def test_get_username(self): + user = ForumUserFactory() + method = ForumUserAdmin(ForumUser, '').get_username + self.assertEqual( + first=method(user), + second=user.user.username, + ) + self.assertEqual( + first=getattr(method, 'short_description'), + second='Username', + ) + self.assertEqual( + first=getattr(method, 'admin_order_field'), + second='user__username', + ) + + def test_has_add_permission(self): + self.assertFalse( + expr=ForumUserAdmin(ForumUser, '').has_add_permission(''), + ) + + def test_has_delete_permission(self): + self.assertFalse( + expr=ForumUserAdmin(ForumUser, '').has_delete_permission(''), + ) + + +class UserAdminTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(UserAdmin, admin.ModelAdmin), + ) + + def test_actions(self): + self.assertIsNone(obj=UserAdmin.actions) + + def test_fields(self): + self.assertTupleEqual( + tuple1=UserAdmin.fields, + tuple2=( + 'username', + 'is_superuser', + 'is_staff', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=UserAdmin.readonly_fields, + tuple2=('username',), + ) + + def test_has_add_permission(self): + self.assertFalse( + expr=UserAdmin(User, '').has_add_permission(''), + ) + + def test_has_delete_permission(self): + self.assertFalse( + expr=UserAdmin(User, '').has_delete_permission(''), + ) diff --git a/users/tests/test_commands.py b/users/tests/test_commands.py new file mode 100644 index 00000000..4f0f62e2 --- /dev/null +++ b/users/tests/test_commands.py @@ -0,0 +1,156 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from random import randint +from unittest import mock + +# Django +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase + +# App +from test_utils.factories.users import AdminUserFactory, ForumUserFactory +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class CommandsTestCase(TestCase): + + def test_associate_super_user(self): + user = AdminUserFactory() + self.assertEqual( + first=ForumUser.objects.count(), + second=0, + ) + forum_id = randint(1, 10) + call_command('associate_super_user', user.username, forum_id) + self.assertEqual( + first=ForumUser.objects.count(), + second=1, + ) + forum_user = ForumUser.objects.get() + self.assertEqual( + first=forum_user.user.id, + second=user.id, + ) + self.assertEqual( + first=forum_user.forum_id, + second=forum_id, + ) + + def test_associate_super_user_invalid_username(self): + user = AdminUserFactory() + self.assertEqual( + first=ForumUser.objects.count(), + second=0, + ) + forum_id = randint(1, 10) + username = user.username + '1' + with self.assertRaises(CommandError) as context: + call_command('associate_super_user', username, forum_id) + + self.assertEqual( + first=str(context.exception), + second=f'User with the username "{username}" was not found.', + ) + + def test_associate_super_user_forum_user_exists(self): + forum_id = randint(1, 10) + user = ForumUserFactory( + forum_id=forum_id, + ) + with self.assertRaises(CommandError) as context: + call_command('associate_super_user', user.user.username, forum_id) + + self.assertEqual( + first=str(context.exception), + second=( + f'A user is already associated with the forum id "{forum_id}".' + ), + ) + + def test_create_random_users(self): + forum_id = randint(1, 10) + ForumUserFactory( + forum_id=forum_id, + ) + call_command('create_random_users', 10) + self.assertEqual( + first=ForumUser.objects.count(), + second=11, + ) + query = ForumUser.objects.values_list('forum_id', flat=True) + self.assertListEqual( + list1=list(query.order_by('forum_id')), + list2=list(range(1, 12)), + ) + + def test_create_test_user(self): + username = 'test-user' + forum_id = randint(1, 10) + call_command('create_test_user', username, 'password', forum_id) + self.assertEqual( + first=ForumUser.objects.count(), + second=1, + ) + user = ForumUser.objects.get() + self.assertEqual( + first=user.forum_id, + second=forum_id, + ) + self.assertEqual( + first=user.user.username, + second=username, + ) + + def test_create_test_user_username_exists(self): + forum_id = randint(1, 10) + user = ForumUserFactory( + forum_id=forum_id + 1, + ) + username = user.user.username + with self.assertRaises(CommandError) as context: + call_command('create_test_user', username, 'password', forum_id) + + self.assertEqual( + first=str(context.exception), + second=f'User with the username "{username}" already exists.', + ) + + def test_create_test_user_forum_id_exists(self): + forum_id = randint(1, 10) + user = ForumUserFactory( + forum_id=forum_id, + ) + username = user.user.username + '1' + with self.assertRaises(CommandError) as context: + call_command('create_test_user', username, 'password', forum_id) + + self.assertEqual( + first=str(context.exception), + second=( + f'A user is already associated with the forum id "{forum_id}".' + ), + ) + + @mock.patch( + target='users.management.commands.create_test_user.User', + ) + def test_create_test_user_error_on_create(self, mock_get_user_model): + manager = mock_get_user_model.objects + manager.filter.return_value.exists.return_value = False + message = 'something went wrong' + manager.create_user.side_effect = ValueError(message) + username = 'test-user' + forum_id = randint(1, 10) + with self.assertRaises(CommandError) as context: + call_command('create_test_user', username, 'password', forum_id) + + self.assertEqual( + first=str(context.exception), + second=f'Unable to create User due to: {message}', + ) diff --git a/users/tests/test_models.py b/users/tests/test_models.py new file mode 100644 index 00000000..6bb0cf23 --- /dev/null +++ b/users/tests/test_models.py @@ -0,0 +1,161 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib.auth import get_user_model +from django.contrib.auth.models import ( + AbstractBaseUser, + PermissionsMixin, + UserManager, +) +from django.db import models +from django.test import TestCase + +# App +from test_utils.factories.users import ForumUserFactory, NonAdminUserFactory +from users.constants import ( + FORUM_MEMBER_URL, + USER_EMAIL_MAX_LENGTH, + USER_USERNAME_MAX_LENGTH, +) +from users.models import ForumUser, User + + +# ============================================================================= +# GLOBALS +# ============================================================================= +UserModel = get_user_model() + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ForumUserTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(ForumUser, models.Model), + ) + + def test_user_field(self): + field = ForumUser._meta.get_field('user') + self.assertIsInstance( + obj=field, + cls=models.OneToOneField, + ) + self.assertEqual( + first=field.remote_field.model, + second=UserModel, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='forum_user', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_forum_id_field(self): + field = ForumUser._meta.get_field('forum_id') + self.assertIsInstance( + obj=field, + cls=models.IntegerField, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.primary_key) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertEqual( + first=ForumUser._meta.verbose_name, + second='Forum User', + ) + self.assertEqual( + first=ForumUser._meta.verbose_name_plural, + second='Forum Users', + ) + + def test__str__(self): + user = ForumUserFactory() + self.assertEqual( + first=str(user), + second=user.user.username, + ) + + def test_get_forum_url(https://github.com/self): + user = ForumUserFactory() + self.assertEqual( + first=user.get_forum_url(), + second=FORUM_MEMBER_URL.format(user_id=user.forum_id) + ) + + +class UserTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(User, AbstractBaseUser), + ) + self.assertTrue( + expr=issubclass(User, PermissionsMixin), + ) + + def test_username_field(self): + field = User._meta.get_field('username') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=USER_USERNAME_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertFalse(expr=field.editable) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_email_field(self): + field = User._meta.get_field('email') + self.assertIsInstance( + obj=field, + cls=models.EmailField, + ) + self.assertEqual( + first=field.max_length, + second=USER_EMAIL_MAX_LENGTH, + ) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_is_staff_field(self): + field = User._meta.get_field('is_staff') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_objects(self): + self.assertIsInstance( + obj=User.objects, + cls=UserManager, + ) + + def test_get_short_name(self): + user = NonAdminUserFactory() + self.assertEqual( + first=user.get_short_name(), + second=user.username, + ) + + def test_get_full_name(self): + user = NonAdminUserFactory() + self.assertEqual( + first=user.get_full_name(), + second=user.username, + ) diff --git a/users/tests/test_views.py b/users/tests/test_views.py new file mode 100644 index 00000000..e69de29b From 961f9f3c2dfba931f7eea5ba9b3ef8d150f6148a Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 13:52:26 -0400 Subject: [PATCH 018/211] Added more tests for users. --- project_manager/urls.py | 4 +- users/tests/test_views.py | 110 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/project_manager/urls.py b/project_manager/urls.py index 3fa84070..d226d36a 100644 --- a/project_manager/urls.py +++ b/project_manager/urls.py @@ -81,7 +81,7 @@ settings.STATIC_URL, document_root=settings.STATIC_ROOT ) -if settings.DEBUG: +if settings.DEBUG: # pragma: no branch import debug_toolbar urlpatterns += [ path( @@ -90,7 +90,7 @@ ), ] -if settings.LOCAL: +if settings.LOCAL: # pragma: no branch urlpatterns += [ path( route='accounts/', diff --git a/users/tests/test_views.py b/users/tests/test_views.py index e69de29b..cebf07ce 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -0,0 +1,110 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import status +from rest_framework.filters import OrderingFilter +from rest_framework.test import APITestCase + +# App +from test_utils.factories.users import ForumUserFactory +from users.api.filtersets import ForumUserFilterSet +from users.api.serializers import ForumUserSerializer +from users.api.views import ForumUserViewSet + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ForumUserViewSetAPITestCase(APITestCase): + + def test_filter_backends(self): + self.assertTupleEqual( + tuple1=ForumUserViewSet.filter_backends, + tuple2=(OrderingFilter, DjangoFilterBackend) + ) + + def test_filter_class(self): + self.assertEqual( + first=ForumUserViewSet.filter_class, + second=ForumUserFilterSet, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=ForumUserViewSet.http_method_names, + tuple2=('get', 'options'), + ) + + def test_serializer_class(self): + self.assertEqual( + first=ForumUserViewSet.serializer_class, + second=ForumUserSerializer, + ) + + def test_ordering(self): + self.assertTupleEqual( + tuple1=ForumUserViewSet.ordering, + tuple2=('user__username',), + ) + + def test_ordering_fields(self): + self.assertTupleEqual( + tuple1=ForumUserViewSet.ordering_fields, + tuple2=('forum_id', 'user__username'), + ) + + def test_get(self): + user = ForumUserFactory() + response = self.client.get(path='/api/users/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual( + first=content['count'], + second=1, + ) + content_user = content['results'][0] + self.assertEqual( + first=content_user['forum_id'], + second=user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=user.user.username, + ) + + def test_get_filter(self): + user = ForumUserFactory() + response = self.client.get(path='/api/users/?has_contributions=true') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + + response = self.client.get(path='/api/users/?has_contributions=false') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual( + first=content['count'], + second=1, + ) + content_user = content['results'][0] + self.assertEqual( + first=content_user['forum_id'], + second=user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=user.user.username, + ) From 96321d7049fce1a8a166da78187751aea0184b7b Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 14:36:25 -0400 Subject: [PATCH 019/211] Added tests for the tags app. --- tags/admin.py | 2 +- tags/models.py | 6 ++ tags/tests/__init__.py | 1 + tags/tests/test_admin.py | 68 +++++++++++++++++++++ tags/tests/test_models.py | 115 +++++++++++++++++++++++++++++++++++ tags/tests/test_views.py | 82 +++++++++++++++++++++++++ test_utils/factories/tags.py | 27 ++++++++ 7 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 tags/tests/__init__.py create mode 100644 tags/tests/test_admin.py create mode 100644 tags/tests/test_models.py create mode 100644 tags/tests/test_views.py create mode 100644 test_utils/factories/tags.py diff --git a/tags/admin.py b/tags/admin.py index 416cf74e..e7edaaae 100644 --- a/tags/admin.py +++ b/tags/admin.py @@ -31,10 +31,10 @@ class TagAdmin(admin.ModelAdmin): 'black_listed', 'creator', ) + list_display_links = None list_filter = ( 'black_listed', ) - list_display_links = None list_editable = ( 'black_listed', 'creator', diff --git a/tags/models.py b/tags/models.py index 2327c34f..a3b2f144 100644 --- a/tags/models.py +++ b/tags/models.py @@ -41,6 +41,12 @@ class Tag(models.Model): on_delete=models.CASCADE, ) + class Meta: + """Define metaclass attributes.""" + + verbose_name = 'Tag' + verbose_name_plural = 'Tags' + def __str__(self): """Return the tag's name.""" return str(self.name) diff --git a/tags/tests/__init__.py b/tags/tests/__init__.py new file mode 100644 index 00000000..854d5dfc --- /dev/null +++ b/tags/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Game functionality.""" diff --git a/tags/tests/test_admin.py b/tags/tests/test_admin.py new file mode 100644 index 00000000..dd04162e --- /dev/null +++ b/tags/tests/test_admin.py @@ -0,0 +1,68 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.test import TestCase + +# App +from tags.admin import TagAdmin +from tags.models import Tag + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class TagAdminTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(TagAdmin, admin.ModelAdmin), + ) + + def test_actions(self): + self.assertIsNone(obj=TagAdmin.actions) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=TagAdmin.list_display, + tuple2=( + 'name', + 'black_listed', + 'creator', + ), + ) + + def test_list_display_links(self): + self.assertIsNone(obj=TagAdmin.list_display_links) + + def test_list_filter(self): + self.assertTupleEqual( + tuple1=TagAdmin.list_filter, + tuple2=('black_listed',), + ) + + def test_list_editable(self): + self.assertTupleEqual( + tuple1=TagAdmin.list_editable, + tuple2=( + 'black_listed', + 'creator', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=TagAdmin.readonly_fields, + tuple2=('name',), + ) + + def test_has_add_permission(self): + self.assertFalse( + expr=TagAdmin(Tag, '').has_add_permission(''), + ) + + def test_has_delete_permission(self): + self.assertFalse( + expr=TagAdmin(Tag, '').has_delete_permission(''), + ) diff --git a/tags/tests/test_models.py b/tags/tests/test_models.py new file mode 100644 index 00000000..0e1e091c --- /dev/null +++ b/tags/tests/test_models.py @@ -0,0 +1,115 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from unittest import mock + +# Django +from django.db import models +from django.test import TestCase + +# App +from tags.constants import TAG_NAME_MAX_LENGTH +from tags.models import Tag +from tags.validators import tag_name_validator +from test_utils.factories.tags import TagFactory +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class GameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(Tag, models.Model), + ) + + def test_name_field(self): + field = Tag._meta.get_field('name') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=TAG_NAME_MAX_LENGTH, + ) + self.assertTrue(expr=field.primary_key) + self.assertTrue(expr=field.unique) + self.assertIn( + member=tag_name_validator, + container=field.validators, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_black_listed_field(self): + field = Tag._meta.get_field('black_listed') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_creator_field(self): + field = Tag._meta.get_field('creator') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='created_tags', + ) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertEqual( + first=Tag._meta.verbose_name, + second='Tag', + ) + self.assertEqual( + first=Tag._meta.verbose_name_plural, + second='Tags', + ) + + def test__str__(self): + tag = TagFactory() + self.assertEqual( + first=str(tag), + second=tag.name, + ) + + @mock.patch.object( + target=Tag, + attribute='packagetag_set', + ) + @mock.patch.object( + target=Tag, + attribute='plugintag_set', + ) + @mock.patch.object( + target=Tag, + attribute='subplugintag_set', + ) + def test_save_on_black_listed( + self, sub_plugin_set, plugin_set, package_set + ): + tag = TagFactory() + tag.black_listed = True + tag.save() + package_set.all.return_value.delete.assert_called_once_with() + plugin_set.all.return_value.delete.assert_called_once_with() + sub_plugin_set.all.return_value.delete.assert_called_once_with() diff --git a/tags/tests/test_views.py b/tags/tests/test_views.py new file mode 100644 index 00000000..3429f605 --- /dev/null +++ b/tags/tests/test_views.py @@ -0,0 +1,82 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.core.management import call_command + +# Third Party Django +from rest_framework import status +from rest_framework.filters import OrderingFilter +from rest_framework.test import APITestCase + +# App +from games.api.serializers import GameSerializer +from games.api.views import GameViewSet +from games.management.commands.create_game_instances import GAMES + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class GameViewSetAPITestCase(APITestCase): + + def test_filter_backends(self): + self.assertTupleEqual( + tuple1=GameViewSet.filter_backends, + tuple2=(OrderingFilter,) + ) + + def test_serializer_class(self): + self.assertEqual( + first=GameViewSet.serializer_class, + second=GameSerializer, + ) + + def test_ordering(self): + self.assertTupleEqual( + tuple1=GameViewSet.ordering, + tuple2=('name',), + ) + + def test_ordering_fields(self): + self.assertTupleEqual( + tuple1=GameViewSet.ordering_fields, + tuple2=('basename', 'name',), + ) + + def test_can_list(self): + response = self.client.get(path='/api/games/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + + call_command('create_game_instances') + response = self.client.get(path='/api/games/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=len(GAMES), + ) + + def test_cannot_post(self): + game = list(GAMES)[0] + response = self.client.post( + path='/api/games/', + data={ + 'basename': game, + 'icon': f'games/{game}.png', + 'name': GAMES[game], + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_405_METHOD_NOT_ALLOWED, + ) diff --git a/test_utils/factories/tags.py b/test_utils/factories/tags.py new file mode 100644 index 00000000..f74f9bae --- /dev/null +++ b/test_utils/factories/tags.py @@ -0,0 +1,27 @@ +"""Factories for use when testing with Game functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +import factory + +# App +from tags.models import Tag + + +# ============================================================================= +# FACTORIES +# ============================================================================= +class TagFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with Game objects.""" + + name = factory.Sequence(function=lambda n: f'tag_{n}') + creator = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + + class Meta: + """Define metaclass attributes.""" + + model = Tag From 38821f62282168ee4a6ffc68b4ec4bcc06aae835 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 15:12:17 -0400 Subject: [PATCH 020/211] Updated filter_class to filterset_class due to warnings. Added tests for the requirements app. Fixed tests for tags views. --- project_manager/packages/api/views.py | 2 +- project_manager/plugins/api/views.py | 2 +- project_manager/sub_plugins/api/views.py | 2 +- requirements/models.py | 8 ++ requirements/tests/__init__.py | 1 + requirements/tests/test_models.py | 160 +++++++++++++++++++++++ tags/api/views.py | 2 +- tags/tests/test_views.py | 55 ++++---- test_utils/factories/games.py | 8 ++ test_utils/factories/requirements.py | 60 +++++++++ test_utils/factories/tags.py | 10 +- test_utils/factories/users.py | 10 ++ users/api/views.py | 2 +- users/tests/test_views.py | 4 +- 14 files changed, 294 insertions(+), 32 deletions(-) create mode 100644 requirements/tests/__init__.py create mode 100644 requirements/tests/test_models.py create mode 100644 test_utils/factories/requirements.py diff --git a/project_manager/packages/api/views.py b/project_manager/packages/api/views.py index 6e20c241..c74fbf02 100644 --- a/project_manager/packages/api/views.py +++ b/project_manager/packages/api/views.py @@ -104,7 +104,7 @@ class PackageViewSet(ProjectViewSet): `?ordering=-updated` """ - filter_class = PackageFilterSet + filterset_class = PackageFilterSet queryset = Package.objects.prefetch_related( Prefetch( lookup='releases', diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index 3e8ea007..226ae741 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -120,7 +120,7 @@ class PluginViewSet(ProjectViewSet): `?ordering=-updated` """ - filter_class = PluginFilterSet + filterset_class = PluginFilterSet queryset = Plugin.objects.prefetch_related( Prefetch( lookup='releases', diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index ecf74e24..a6a23f81 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -109,7 +109,7 @@ class SubPluginViewSet(ProjectViewSet): `?ordering=-updated` """ - filter_class = SubPluginFilterSet + filterset_class = SubPluginFilterSet queryset = SubPlugin.objects.prefetch_related( Prefetch( lookup='releases', diff --git a/requirements/models.py b/requirements/models.py index ce23e599..0ffd5116 100644 --- a/requirements/models.py +++ b/requirements/models.py @@ -42,6 +42,10 @@ class Meta: verbose_name = 'Download Requirement' verbose_name_plural = 'Download Requirements' + def __str__(self): + """Return the object's url when str cast.""" + return str(self.url) + class PyPiRequirement(models.Model): """PyPi requirement model.""" @@ -95,3 +99,7 @@ class Meta: verbose_name = 'Version Control Requirement' verbose_name_plural = 'Version Control Requirements' + + def __str__(self): + """Return the object's url when str cast.""" + return str(self.url) diff --git a/requirements/tests/__init__.py b/requirements/tests/__init__.py new file mode 100644 index 00000000..854d5dfc --- /dev/null +++ b/requirements/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Game functionality.""" diff --git a/requirements/tests/test_models.py b/requirements/tests/test_models.py new file mode 100644 index 00000000..b44fbd57 --- /dev/null +++ b/requirements/tests/test_models.py @@ -0,0 +1,160 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.conf import settings +from django.db import models +from django.test import TestCase + +# App +from requirements.constants import ( + REQUIREMENT_NAME_MAX_LENGTH, + REQUIREMENT_SLUG_MAX_LENGTH, + REQUIREMENT_URL_MAX_LENGTH, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.requirements import ( + DownloadRequirementFactory, + PyPiRequirementFactory, + VersionControlRequirementFactory, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class DownloadRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(DownloadRequirement, models.Model), + ) + + def test_url_field(self): + field = DownloadRequirement._meta.get_field('url') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=REQUIREMENT_URL_MAX_LENGTH, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertEqual( + first=DownloadRequirement._meta.verbose_name, + second='Download Requirement', + ) + self.assertEqual( + first=DownloadRequirement._meta.verbose_name_plural, + second='Download Requirements', + ) + + def test__str__(self): + download_requirement = DownloadRequirementFactory() + self.assertEqual( + first=str(download_requirement), + second=download_requirement.url, + ) + + +class PyPiRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PyPiRequirement, models.Model), + ) + + def test_name_field(self): + field = PyPiRequirement._meta.get_field('name') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=REQUIREMENT_NAME_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_slug_field(self): + field = PyPiRequirement._meta.get_field('slug') + self.assertIsInstance( + obj=field, + cls=models.SlugField, + ) + self.assertEqual( + first=field.max_length, + second=REQUIREMENT_SLUG_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertEqual( + first=PyPiRequirement._meta.verbose_name, + second='PyPi Requirement', + ) + self.assertEqual( + first=PyPiRequirement._meta.verbose_name_plural, + second='PyPi Requirements', + ) + + def test__str__(self): + pypi_requirement = PyPiRequirementFactory() + self.assertEqual( + first=str(pypi_requirement), + second=pypi_requirement.name, + ) + + def test_get_pypi_url(https://github.com/self): + pypi_requirement = PyPiRequirementFactory() + self.assertEqual( + first=pypi_requirement.get_pypi_url(), + second=settings.PYPI_URL + f'/{pypi_requirement.name}' + ) + + +class VersionControlRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(VersionControlRequirement, models.Model), + ) + + def test_url_field(self): + field = VersionControlRequirement._meta.get_field('url') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=REQUIREMENT_URL_MAX_LENGTH, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertEqual( + first=VersionControlRequirement._meta.verbose_name, + second='Version Control Requirement', + ) + self.assertEqual( + first=VersionControlRequirement._meta.verbose_name_plural, + second='Version Control Requirements', + ) + + def test__str__(self): + vcs_requirement = VersionControlRequirementFactory() + self.assertEqual( + first=str(vcs_requirement), + second=vcs_requirement.url, + ) diff --git a/tags/api/views.py b/tags/api/views.py index cf8c49f7..64b34149 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -49,7 +49,7 @@ class TagViewSet(ListModelMixin, GenericViewSet): """ filter_backends = (OrderingFilter, DjangoFilterBackend) - filter_class = TagFilterSet + filterset_class = TagFilterSet serializer_class = TagSerializer queryset = Tag.objects.select_related( 'creator__user', diff --git a/tags/tests/test_views.py b/tags/tests/test_views.py index 3429f605..3b448436 100644 --- a/tags/tests/test_views.py +++ b/tags/tests/test_views.py @@ -1,51 +1,56 @@ # ============================================================================= # IMPORTS # ============================================================================= -# Django -from django.core.management import call_command - # Third Party Django +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework.filters import OrderingFilter from rest_framework.test import APITestCase # App -from games.api.serializers import GameSerializer -from games.api.views import GameViewSet -from games.management.commands.create_game_instances import GAMES +from tags.api.filtersets import TagFilterSet +from tags.api.serializers import TagSerializer +from tags.api.views import TagViewSet +from test_utils.factories.tags import TagFactory # ============================================================================= # TEST CASES # ============================================================================= -class GameViewSetAPITestCase(APITestCase): +class TagViewSetAPITestCase(APITestCase): def test_filter_backends(self): self.assertTupleEqual( - tuple1=GameViewSet.filter_backends, - tuple2=(OrderingFilter,) + tuple1=TagViewSet.filter_backends, + tuple2=(OrderingFilter, DjangoFilterBackend) + ) + + def test_filterset_class(self): + self.assertEqual( + first=TagViewSet.filterset_class, + second=TagFilterSet, ) def test_serializer_class(self): self.assertEqual( - first=GameViewSet.serializer_class, - second=GameSerializer, + first=TagViewSet.serializer_class, + second=TagSerializer, ) def test_ordering(self): self.assertTupleEqual( - tuple1=GameViewSet.ordering, + tuple1=TagViewSet.ordering, tuple2=('name',), ) def test_ordering_fields(self): self.assertTupleEqual( - tuple1=GameViewSet.ordering_fields, - tuple2=('basename', 'name',), + tuple1=TagViewSet.ordering_fields, + tuple2=('name',), ) def test_can_list(self): - response = self.client.get(path='/api/games/') + response = self.client.get(path='/api/tags/') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -55,25 +60,27 @@ def test_can_list(self): second=0, ) - call_command('create_game_instances') - response = self.client.get(path='/api/games/') + tag = TagFactory() + response = self.client.get(path='/api/tags/') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) + content = response.json() self.assertEqual( - first=response.json()['count'], - second=len(GAMES), + first=content['count'], + second=1, + ) + self.assertEqual( + first=content['results'][0]['name'], + second=tag.name, ) def test_cannot_post(self): - game = list(GAMES)[0] response = self.client.post( - path='/api/games/', + path='/api/tags/', data={ - 'basename': game, - 'icon': f'games/{game}.png', - 'name': GAMES[game], + 'name': 'test', } ) self.assertEqual( diff --git a/test_utils/factories/games.py b/test_utils/factories/games.py index df4963dc..6bbd3db4 100644 --- a/test_utils/factories/games.py +++ b/test_utils/factories/games.py @@ -10,6 +10,14 @@ from games.models import Game +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'GameFactory', +) + + # ============================================================================= # FACTORIES # ============================================================================= diff --git a/test_utils/factories/requirements.py b/test_utils/factories/requirements.py new file mode 100644 index 00000000..572c2be4 --- /dev/null +++ b/test_utils/factories/requirements.py @@ -0,0 +1,60 @@ +"""Factories for use when testing with Game functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +import factory + +# App +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'DownloadRequirementFactory', + 'PyPiRequirementFactory', + 'VersionControlRequirementFactory', +) + + +# ============================================================================= +# FACTORIES +# ============================================================================= +class DownloadRequirementFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with Download Requirement objects.""" + + url = factory.Sequence(function=lambda n: f'download_{n}') + + class Meta: + """Define metaclass attributes.""" + + model = DownloadRequirement + + +class PyPiRequirementFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PyPi Requirement objects.""" + + name = factory.Sequence(function=lambda n: f'pypi_{n}') + + class Meta: + """Define metaclass attributes.""" + + model = PyPiRequirement + + +class VersionControlRequirementFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with VCS Requirement objects.""" + + url = factory.Sequence(function=lambda n: f'vcs_{n}') + + class Meta: + """Define metaclass attributes.""" + + model = VersionControlRequirement diff --git a/test_utils/factories/tags.py b/test_utils/factories/tags.py index f74f9bae..b929a42e 100644 --- a/test_utils/factories/tags.py +++ b/test_utils/factories/tags.py @@ -10,11 +10,19 @@ from tags.models import Tag +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'TagFactory', +) + + # ============================================================================= # FACTORIES # ============================================================================= class TagFactory(factory.django.DjangoModelFactory): - """Model factory to use when testing with Game objects.""" + """Model factory to use when testing with Tag objects.""" name = factory.Sequence(function=lambda n: f'tag_{n}') creator = factory.SubFactory( diff --git a/test_utils/factories/users.py b/test_utils/factories/users.py index d3e7216c..5b8e0053 100644 --- a/test_utils/factories/users.py +++ b/test_utils/factories/users.py @@ -10,6 +10,16 @@ from users.models import ForumUser, User +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'AdminUserFactory', + 'ForumUserFactory', + 'NonAdminUserFactory', +) + + # ============================================================================= # FACTORIES # ============================================================================= diff --git a/users/api/views.py b/users/api/views.py index 9a6456b8..f5137ad5 100644 --- a/users/api/views.py +++ b/users/api/views.py @@ -35,7 +35,7 @@ class ForumUserViewSet(ModelViewSet): """ForumUser API view.""" filter_backends = (OrderingFilter, DjangoFilterBackend) - filter_class = ForumUserFilterSet + filterset_class = ForumUserFilterSet http_method_names = ('get', 'options') ordering = ('user__username',) ordering_fields = ('forum_id', 'user__username') diff --git a/users/tests/test_views.py b/users/tests/test_views.py index cebf07ce..e150c875 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -25,9 +25,9 @@ def test_filter_backends(self): tuple2=(OrderingFilter, DjangoFilterBackend) ) - def test_filter_class(self): + def test_filterset_class(self): self.assertEqual( - first=ForumUserViewSet.filter_class, + first=ForumUserViewSet.filterset_class, second=ForumUserFilterSet, ) From c3aff36c90bfb6fbb218d3052eda52ebf5a710d4 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Oct 2021 17:42:52 -0400 Subject: [PATCH 021/211] Moved view tests into api sub-directories. --- games/api/tests/__init__.py | 0 games/{ => api}/tests/test_views.py | 0 tags/api/tests/__init__.py | 0 tags/{ => api}/tests/test_views.py | 0 users/api/tests/__init__.py | 0 users/{ => api}/tests/test_views.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 games/api/tests/__init__.py rename games/{ => api}/tests/test_views.py (100%) create mode 100644 tags/api/tests/__init__.py rename tags/{ => api}/tests/test_views.py (100%) create mode 100644 users/api/tests/__init__.py rename users/{ => api}/tests/test_views.py (100%) diff --git a/games/api/tests/__init__.py b/games/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/games/tests/test_views.py b/games/api/tests/test_views.py similarity index 100% rename from games/tests/test_views.py rename to games/api/tests/test_views.py diff --git a/tags/api/tests/__init__.py b/tags/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tags/tests/test_views.py b/tags/api/tests/test_views.py similarity index 100% rename from tags/tests/test_views.py rename to tags/api/tests/test_views.py diff --git a/users/api/tests/__init__.py b/users/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/users/tests/test_views.py b/users/api/tests/test_views.py similarity index 100% rename from users/tests/test_views.py rename to users/api/tests/test_views.py From 34502d7dae14ff136ee2a02275b43520f879646c Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Oct 2021 01:04:20 -0400 Subject: [PATCH 022/211] Added more tests. --- project_manager/api/tests/__init__.py | 0 project_manager/api/tests/test_views.py | 62 ++ project_manager/common/admin/__init__.py | 8 +- project_manager/common/admin/inlines.py | 6 +- project_manager/common/models.py | 4 +- project_manager/common/tests/__init__.py | 0 project_manager/common/tests/test_admin.py | 244 +++++++ project_manager/common/tests/test_models.py | 731 ++++++++++++++++++++ project_manager/common/tests/test_views.py | 0 9 files changed, 1046 insertions(+), 9 deletions(-) create mode 100644 project_manager/api/tests/__init__.py create mode 100644 project_manager/api/tests/test_views.py create mode 100644 project_manager/common/tests/__init__.py create mode 100644 project_manager/common/tests/test_admin.py create mode 100644 project_manager/common/tests/test_models.py create mode 100644 project_manager/common/tests/test_views.py diff --git a/project_manager/api/tests/__init__.py b/project_manager/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/api/tests/test_views.py b/project_manager/api/tests/test_views.py new file mode 100644 index 00000000..7b13d309 --- /dev/null +++ b/project_manager/api/tests/test_views.py @@ -0,0 +1,62 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + +# App + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ProjectManagerAPIViewAPITestCase(APITestCase): + + def test_get(self): + response = self.client.get(path='/api/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'games': reverse( + viewname='api:games:games-list', + request=response.wsgi_request, + ), + 'packages': reverse( + viewname='api:packages:endpoints', + request=response.wsgi_request, + ), + 'plugins': reverse( + viewname='api:plugins:endpoints', + request=response.wsgi_request, + ), + 'sub-plugins': reverse( + viewname='api:sub-plugins:endpoints', + request=response.wsgi_request, + ), + 'tags': reverse( + viewname='api:tags:tags-list', + request=response.wsgi_request, + ), + 'users': reverse( + viewname='api:users:users-list', + request=response.wsgi_request, + ), + }, + ) + + def test_options(self): + response = self.client.options(path='/api/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['name'], + second='Project Manager APIs', + ) diff --git a/project_manager/common/admin/__init__.py b/project_manager/common/admin/__init__.py index 48f93985..33752833 100644 --- a/project_manager/common/admin/__init__.py +++ b/project_manager/common/admin/__init__.py @@ -72,10 +72,10 @@ class ProjectAdmin(admin.ModelAdmin): 'contributors__user__username', ) - def has_delete_permission(self, request, obj=None): - """Disallow deletion of Project in the Admin.""" - return False - def has_add_permission(self, request): """Disallow creation of a Project in the Admin.""" return False + + def has_delete_permission(self, request, obj=None): + """Disallow deletion of Project in the Admin.""" + return False diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index 3c0963e9..b47b81fa 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -54,7 +54,7 @@ class ProjectGameInline(admin.TabularInline): 'game', ) - def has_add_permission(self, request, obj): + def has_add_permission(self, request, obj=None): """Disallow adding new games in the Admin.""" return False @@ -87,7 +87,7 @@ class ProjectImageInline(admin.TabularInline): 'created', ) - def has_add_permission(self, request, obj): + def has_add_permission(self, request, obj=None): """Disallow adding new images in the Admin.""" return False @@ -115,6 +115,6 @@ def get_queryset(self, request): """Order the queryset from newest to oldest.""" return super().get_queryset(request=request).order_by('-created') - def has_add_permission(self, request, obj): + def has_add_permission(self, request, obj=None): """Disallow adding new images in the Admin.""" return False diff --git a/project_manager/common/models.py b/project_manager/common/models.py index e511f783..66c3cf9f 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -4,8 +4,8 @@ # IMPORTS # ============================================================================= # Python -import uuid from operator import attrgetter +from uuid import uuid4 # Django from django.conf import settings @@ -66,7 +66,7 @@ class AbstractUUIDPrimaryKeyModel(models.Model): id = models.UUIDField( verbose_name='ID', primary_key=True, - default=uuid.uuid4, + default=uuid4, editable=False, ) diff --git a/project_manager/common/tests/__init__.py b/project_manager/common/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/common/tests/test_admin.py b/project_manager/common/tests/test_admin.py new file mode 100644 index 00000000..f3e2b10b --- /dev/null +++ b/project_manager/common/tests/test_admin.py @@ -0,0 +1,244 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.test import TestCase + +# App +from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin.inlines import ( + ProjectContributorInline, + ProjectGameInline, + ProjectImageInline, + ProjectReleaseInline, + ProjectTagInline, +) +from project_manager.common.models import Project + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ProjectAdminTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectAdmin, admin.ModelAdmin), + ) + + def test_actions(self): + self.assertIsNone(obj=ProjectAdmin.actions) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.fieldsets, + tuple2=( + ( + 'Project Info', + { + 'classes': ('wide',), + 'fields': ( + 'name', + 'owner', + 'configuration', + 'description', + 'synopsis', + 'logo', + 'topic', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'basename', + 'slug', + 'created', + 'updated', + ), + }, + ) + ), + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.list_display, + tuple2=( + 'name', + 'basename', + 'owner', + ), + ) + + def test_raw_id_fields(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.raw_id_fields, + tuple2=('owner',), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.readonly_fields, + tuple2=( + 'basename', + 'created', + 'slug', + 'updated', + ), + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.search_fields, + tuple2=( + 'name', + 'basename', + 'owner__user__username', + 'contributors__user__username', + ) + ) + + def test_has_add_permission(self): + self.assertFalse( + expr=ProjectAdmin(Project, '').has_add_permission(''), + ) + + def test_has_delete_permission(self): + self.assertFalse( + expr=ProjectAdmin(Project, '').has_delete_permission(''), + ) + + +class ProjectContributorInlineTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectContributorInline, admin.TabularInline), + ) + + def test_extra(self): + self.assertEqual( + first=ProjectContributorInline.extra, + second=0, + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectContributorInline.fields, + tuple2=('user',), + ) + + def test_raw_id_fields(self): + self.assertTupleEqual( + tuple1=ProjectContributorInline.raw_id_fields, + tuple2=('user',), + ) + + +class ProjectGameInlineTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectGameInline, admin.TabularInline), + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectGameInline.fields, + tuple2=('game',), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectGameInline.readonly_fields, + tuple2=('game',), + ) + + +class ProjectImageInlineTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectImageInline, admin.TabularInline), + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectImageInline.fields, + tuple2=( + 'image', + 'created', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectImageInline.readonly_fields, + tuple2=( + 'image', + 'created', + ), + ) + + +class ProjectReleaseInlineTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectReleaseInline, admin.StackedInline), + ) + + def test_extra(self): + self.assertEqual( + first=ProjectReleaseInline.extra, + second=0, + ) + + def test_view_on_site(self): + self.assertFalse(expr=ProjectReleaseInline.view_on_site) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectReleaseInline.fields, + tuple2=( + 'version', + 'notes', + 'zip_file', + 'download_count', + 'created', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectReleaseInline.readonly_fields, + tuple2=( + 'zip_file', + 'download_count', + 'created', + ), + ) + + +class ProjectTagInlineTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectTagInline, admin.TabularInline), + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectTagInline.fields, + tuple2=('tag',), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectTagInline.readonly_fields, + tuple2=('tag',), + ) diff --git a/project_manager/common/tests/test_models.py b/project_manager/common/tests/test_models.py new file mode 100644 index 00000000..33f4550e --- /dev/null +++ b/project_manager/common/tests/test_models.py @@ -0,0 +1,731 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from uuid import uuid4 + +# Django +from django.db import models +from django.test import TestCase + +# Third Party Django +from embed_video.fields import EmbedVideoField +from model_utils.fields import AutoCreatedField +from precise_bbcode.fields import BBCodeTextField + +# App +from project_manager.common.constants import ( + PROJECT_CONFIGURATION_MAX_LENGTH, + PROJECT_DESCRIPTION_MAX_LENGTH, + PROJECT_NAME_MAX_LENGTH, + PROJECT_SYNOPSIS_MAX_LENGTH, + RELEASE_NOTES_MAX_LENGTH, + RELEASE_VERSION_MAX_LENGTH, +) +from project_manager.common.helpers import ( + handle_project_image_upload, + handle_project_logo_upload, + handle_release_zip_file_upload, +) +from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, + Project, + ProjectContributor, + ProjectGame, + ProjectImage, + ProjectRelease, + ProjectReleaseDownloadRequirement, + ProjectReleasePackageRequirement, + ProjectReleasePyPiRequirement, + ProjectReleaseVersionControlRequirement, + ProjectTag, +) +from project_manager.common.validators import version_validator + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class AbstractUUIDPrimaryKeyModelTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(AbstractUUIDPrimaryKeyModel, models.Model) + ) + + def test_id_field(self): + field = AbstractUUIDPrimaryKeyModel._meta.get_field('id') + self.assertIsInstance( + obj=field, + cls=models.UUIDField, + ) + self.assertTrue(expr=field.primary_key) + self.assertFalse(expr=field.editable) + self.assertEqual( + first=field.verbose_name, + second='ID', + ) + self.assertEqual( + first=field.default, + second=uuid4, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=AbstractUUIDPrimaryKeyModel._meta.abstract + ) + + +class ProjectTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue(expr=issubclass(Project, models.Model)) + + def test_name_field(self): + field = Project._meta.get_field('name') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_NAME_MAX_LENGTH, + ) + self.assertEqual( + first=field.help_text, + second=( + "The name of the project. Do not include the version, as that " + "is added dynamically to the project's page." + ), + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_configuration_field(self): + field = Project._meta.get_field('configuration') + self.assertIsInstance( + obj=field, + cls=BBCodeTextField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_CONFIGURATION_MAX_LENGTH, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The configuration of the project. If too long, post on the ' + 'forum and provide the link here. BBCode is allowed. 1024 ' + 'char limit.' + ), + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_description_field(self): + field = Project._meta.get_field('description') + self.assertIsInstance( + obj=field, + cls=BBCodeTextField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_DESCRIPTION_MAX_LENGTH, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The full description of the project. BBCode is allowed. ' + '1024 char limit.' + ), + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_image_field(self): + field = Project._meta.get_field('logo') + self.assertIsInstance( + obj=field, + cls=models.ImageField, + ) + self.assertEqual( + first=field.upload_to, + second=handle_project_logo_upload, + ) + self.assertEqual( + first=field.help_text, + second="The project's logo image.", + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_video_field(self): + field = Project._meta.get_field('video') + self.assertIsInstance( + obj=field, + cls=EmbedVideoField, + ) + self.assertEqual( + first=field.help_text, + second="The project's video.", + ) + self.assertFalse(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_owner_field(self): + field = Project._meta.get_field('owner') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='users.ForumUser', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='%(class)ss', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_synopsis_field(self): + field = Project._meta.get_field('synopsis') + self.assertIsInstance( + obj=field, + cls=BBCodeTextField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_SYNOPSIS_MAX_LENGTH, + ) + self.assertEqual( + first=field.help_text, + second=( + 'A brief description of the project. BBCode is allowed. ' + '128 char limit.' + ), + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_topic_field(self): + field = Project._meta.get_field('topic') + self.assertIsInstance( + obj=field, + cls=models.IntegerField, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_created_field(self): + field = Project._meta.get_field('created') + self.assertIsInstance( + obj=field, + cls=models.DateTimeField, + ) + self.assertEqual( + first=field.verbose_name, + second='created', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_updated_field(self): + field = Project._meta.get_field('updated') + self.assertIsInstance( + obj=field, + cls=models.DateTimeField, + ) + self.assertEqual( + first=field.verbose_name, + second='updated', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=Project._meta.abstract + ) + + +class ProjectContributorTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectContributor, AbstractUUIDPrimaryKeyModel), + ) + + def test_user_field(self): + field = ProjectContributor._meta.get_field('user') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='users.ForumUser', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectContributor._meta.abstract + ) + + +class ProjectGameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectGame, AbstractUUIDPrimaryKeyModel), + ) + + def test_game_field(self): + field = ProjectGame._meta.get_field('game') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='games.Game', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectGame._meta.abstract + ) + + +class ProjectImageTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectImage, AbstractUUIDPrimaryKeyModel), + ) + + def test_image_field(self): + field = ProjectImage._meta.get_field('image') + self.assertIsInstance( + obj=field, + cls=models.ImageField, + ) + self.assertEqual( + first=field.upload_to, + second=handle_project_image_upload, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_created_field(self): + field = ProjectImage._meta.get_field('created') + self.assertIsInstance( + obj=field, + cls=AutoCreatedField, + ) + self.assertEqual( + first=field.verbose_name, + second='created', + ) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectImage._meta.abstract + ) + self.assertEqual( + first=ProjectImage._meta.verbose_name, + second='Image', + ) + self.assertEqual( + first=ProjectImage._meta.verbose_name_plural, + second='Images', + ) + + +class ProjectReleaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectRelease, AbstractUUIDPrimaryKeyModel), + ) + + def test_version_field(self): + field = ProjectRelease._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second='The version for this release of the project.', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_notes_field(self): + field = ProjectRelease._meta.get_field('notes') + self.assertIsInstance( + obj=field, + cls=BBCodeTextField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_NOTES_MAX_LENGTH, + ) + self.assertEqual( + first=field.help_text, + second='The notes for this particular release of the project.', + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_zip_file_field(self): + field = ProjectRelease._meta.get_field('zip_file') + self.assertIsInstance( + obj=field, + cls=models.FileField, + ) + self.assertEqual( + first=field.upload_to, + second=handle_release_zip_file_upload, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_count_field(self): + field = ProjectRelease._meta.get_field('download_count') + self.assertIsInstance( + obj=field, + cls=models.PositiveIntegerField, + ) + self.assertEqual( + first=field.default, + second=0, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_created_field(self): + field = ProjectRelease._meta.get_field('created') + self.assertIsInstance( + obj=field, + cls=AutoCreatedField, + ) + self.assertEqual( + first=field.verbose_name, + second='created', + ) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectRelease._meta.abstract + ) + self.assertEqual( + first=ProjectRelease._meta.verbose_name, + second='Release', + ) + self.assertEqual( + first=ProjectRelease._meta.verbose_name_plural, + second='Releases', + ) + + +class ProjectReleaseDownloadRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + ProjectReleaseDownloadRequirement, + AbstractUUIDPrimaryKeyModel, + ), + ) + + def test_download_requirement_field(self): + field = ProjectReleaseDownloadRequirement._meta.get_field( + 'download_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='requirements.DownloadRequirement', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_optional_field(self): + field = ProjectReleaseDownloadRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectReleaseDownloadRequirement._meta.abstract + ) + + +class ProjectReleasePackageRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + ProjectReleasePackageRequirement, + AbstractUUIDPrimaryKeyModel, + ), + ) + + def test_package_requirement_field(self): + field = ProjectReleasePackageRequirement._meta.get_field( + 'package_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.Package', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = ProjectReleasePackageRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the custom package for this release of the ' + 'project.' + ) + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = ProjectReleasePackageRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectReleasePackageRequirement._meta.abstract + ) + + +class ProjectReleasePyPiRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + ProjectReleasePyPiRequirement, + AbstractUUIDPrimaryKeyModel, + ), + ) + + def test_pypi_requirement_field(self): + field = ProjectReleasePyPiRequirement._meta.get_field( + 'pypi_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='requirements.PyPiRequirement', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = ProjectReleasePyPiRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the PyPi package for this release of the ' + 'project.' + ) + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = ProjectReleasePyPiRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectReleasePyPiRequirement._meta.abstract + ) + + +class ProjectReleaseVersionControlRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + ProjectReleaseVersionControlRequirement, + AbstractUUIDPrimaryKeyModel, + ), + ) + + def test_vcs_requirement_field(self): + field = ProjectReleaseVersionControlRequirement._meta.get_field( + 'vcs_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='requirements.VersionControlRequirement', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = ProjectReleaseVersionControlRequirement._meta.get_field( + 'version', + ) + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the VCS package for this release of the ' + 'project.' + ) + ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = ProjectReleaseVersionControlRequirement._meta.get_field( + 'optional', + ) + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=ProjectReleaseVersionControlRequirement._meta.abstract + ) + + +class ProjectTagTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectTag, AbstractUUIDPrimaryKeyModel), + ) + + def test_tag_field(self): + field = ProjectTag._meta.get_field('tag') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='tags.Tag', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue(expr=ProjectTag._meta.abstract) diff --git a/project_manager/common/tests/test_views.py b/project_manager/common/tests/test_views.py new file mode 100644 index 00000000..e69de29b From 5b1fe75432c231a1d9afa643f759c113f72bff31 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Oct 2021 09:54:26 -0400 Subject: [PATCH 023/211] Added create_secret_key_file command and tests. --- .gitignore | 1 + SPPM/settings/base.py | 6 +- project_manager/management/__init__.py | 1 + .../management/commands/__init__.py | 1 + .../commands/create_secret_key_file.py | 56 +++++++++++++++++++ project_manager/tests/__init__.py | 0 project_manager/tests/test_commands.py | 50 +++++++++++++++++ 7 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 project_manager/management/__init__.py create mode 100644 project_manager/management/commands/__init__.py create mode 100644 project_manager/management/commands/create_secret_key_file.py create mode 100644 project_manager/tests/__init__.py create mode 100644 project_manager/tests/test_commands.py diff --git a/.gitignore b/.gitignore index 85d35fc4..6ef1b11f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ favicon.png htmlcov .coverage .pytest_cache +.secret_key diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index ca687a19..af87b1e1 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -27,7 +27,11 @@ # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'l4%##*%y2ev_1jvv4x2si_9$1j9meyscczf*gafp7^@rdl8v#=' +if (BASE_DIR / '.secret_key').isfile(): + with (BASE_DIR / '.secret_key').open() as _: + SECRET_KEY = _.read() +else: + SECRET_KEY = 'l4%##*%y2ev_1jvv4x2si_9$1j9meyscczf*gafp7^@rdl8v#=' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False diff --git a/project_manager/management/__init__.py b/project_manager/management/__init__.py new file mode 100644 index 00000000..1a7d1c8e --- /dev/null +++ b/project_manager/management/__init__.py @@ -0,0 +1 @@ +"""Project based management.""" diff --git a/project_manager/management/commands/__init__.py b/project_manager/management/commands/__init__.py new file mode 100644 index 00000000..fd770a88 --- /dev/null +++ b/project_manager/management/commands/__init__.py @@ -0,0 +1 @@ +"""Project based management commands.""" diff --git a/project_manager/management/commands/create_secret_key_file.py b/project_manager/management/commands/create_secret_key_file.py new file mode 100644 index 00000000..c6ca0816 --- /dev/null +++ b/project_manager/management/commands/create_secret_key_file.py @@ -0,0 +1,56 @@ +"""Command to create the secret key file for the environment.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import string + +# Django +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.utils.crypto import get_random_string + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +SECRET_FILE = settings.BASE_DIR / '.secret_key' +ALLOWED_CHARS = string.printable + +# Remove quotes +ALLOWED_CHARS = ALLOWED_CHARS.replace("'", '').replace('"', '') + +# Remove slashes +ALLOWED_CHARS = ALLOWED_CHARS.replace('\\', '').replace('/', '') + +# Remove extra characters +ALLOWED_CHARS = ALLOWED_CHARS.replace('`', '').split(' ', maxsplit=1)[0] + + +# ============================================================================= +# COMMANDS +# ============================================================================= +class Command(BaseCommand): + """Create the secret key file.""" + + def add_arguments(self, parser): + """Add the required arguments for the command.""" + parser.add_argument( + 'length', + type=int, + help='The number of characters to have in the secret key.', + ) + + def handle(self, *args, **options): + """Create the file to store the secret key.""" + if SECRET_FILE.isfile(): + raise CommandError('Secret key file already exists.') + + secret_key = get_random_string( + length=options['length'], + allowed_chars=ALLOWED_CHARS, + ) + + with SECRET_FILE.open('w') as open_file: + open_file.write(secret_key) diff --git a/project_manager/tests/__init__.py b/project_manager/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/tests/test_commands.py b/project_manager/tests/test_commands.py new file mode 100644 index 00000000..7a04c860 --- /dev/null +++ b/project_manager/tests/test_commands.py @@ -0,0 +1,50 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from unittest import mock +# Django +from django.core.management import call_command +from django.core.management.base import CommandError +from django.test import TestCase + +# App +from project_manager.management.commands.create_secret_key_file import ALLOWED_CHARS + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class CommandsTestCase(TestCase): + + @mock.patch( + target='project_manager.management.commands.create_secret_key_file.SECRET_FILE' + ) + @mock.patch( + target='project_manager.management.commands.create_secret_key_file.get_random_string' + ) + def test_create_secret_key_file(self, mock_get_random_string, mock_secret_file): + length = 50 + mock_secret_file.isfile.return_value = False + call_command('create_secret_key_file', length) + mock_secret_file.isfile.assert_called_once_with() + mock_get_random_string.assert_called_once_with( + length=length, + allowed_chars=ALLOWED_CHARS, + ) + mock_secret_file.open.assert_called_once_with('w') + open_file = mock_secret_file.open.return_value.__enter__.return_value + open_file.write.assert_called_once_with(mock_get_random_string.return_value) + + @mock.patch( + target='project_manager.management.commands.create_secret_key_file.SECRET_FILE' + ) + def test_create_secret_key_file_key_file_exists(self, mock_secret_file): + mock_secret_file.isfile.return_value = True + with self.assertRaises(CommandError) as context: + call_command('create_secret_key_file', 50) + + self.assertEqual( + first=str(context.exception), + second='Secret key file already exists.' + ) From e1354ae262e8f1132f508a4bc54ee9ad0e10e38d Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Oct 2021 10:38:40 -0400 Subject: [PATCH 024/211] Added logging configuration. Updated user commands to use logging instead of print. Updated user commands to only run in LOCAL environment. --- SPPM/settings/base.py | 28 +++++++ project_manager/tests/test_commands.py | 1 + .../commands/associate_super_user.py | 14 +++- .../commands/create_random_users.py | 14 +++- users/management/commands/create_test_user.py | 14 +++- users/tests/test_commands.py | 78 +++++++++++++++++-- 6 files changed, 138 insertions(+), 11 deletions(-) diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index af87b1e1..e502929c 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -13,6 +13,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +import sys + # Third Party Python from path import Path @@ -141,6 +144,31 @@ ), } +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(message)s', + }, + }, + 'handlers': { + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + 'stream': sys.stdout, + }, + }, + 'loggers': { + '': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + } + } +} + EMBED_VIDEO_BACKENDS = ( 'embed_video.backends.YoutubeBackend', ) diff --git a/project_manager/tests/test_commands.py b/project_manager/tests/test_commands.py index 7a04c860..8a60abd3 100644 --- a/project_manager/tests/test_commands.py +++ b/project_manager/tests/test_commands.py @@ -3,6 +3,7 @@ # ============================================================================= # Python from unittest import mock + # Django from django.core.management import call_command from django.core.management.base import CommandError diff --git a/users/management/commands/associate_super_user.py b/users/management/commands/associate_super_user.py index b3184157..00aeb59c 100644 --- a/users/management/commands/associate_super_user.py +++ b/users/management/commands/associate_super_user.py @@ -3,7 +3,11 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +import logging + # Django +from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError @@ -16,6 +20,8 @@ # ============================================================================= User = get_user_model() +logger = logging.getLogger(__name__) + # ============================================================================= # COMMANDS @@ -38,6 +44,12 @@ def add_arguments(self, parser): def handle(self, *args, **options): """Verify the arguments and associate the User.""" + # Only allow this command in local development + if not settings.LOCAL: + raise CommandError( + 'Command can only be run for local development.' + ) + username = options['username'] try: user = User.objects.get(username=username) @@ -58,7 +70,7 @@ def handle(self, *args, **options): user=user, forum_id=forum_id, ) - print( + logger.info( f'User "{username}" successfully associated with forum id ' f'"{forum_id}".' ) diff --git a/users/management/commands/create_random_users.py b/users/management/commands/create_random_users.py index 627703ec..2e9c2059 100644 --- a/users/management/commands/create_random_users.py +++ b/users/management/commands/create_random_users.py @@ -4,11 +4,13 @@ # IMPORTS # ============================================================================= # Python +import logging from os import urandom # Django +from django.conf import settings from django.contrib.auth import get_user_model -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError # Third Party Python from random_username.generate import generate_username @@ -22,6 +24,8 @@ # ============================================================================= User = get_user_model() +logger = logging.getLogger(__name__) + # ============================================================================= # COMMANDS @@ -39,6 +43,12 @@ def add_arguments(self, parser): def handle(self, *args, **options): """Verify the arguments and create the Users.""" + # Only allow this command in local development + if not settings.LOCAL: + raise CommandError( + 'Command can only be run for local development.' + ) + count = options['count'] current_usernames = User.objects.values_list( 'username', @@ -81,7 +91,7 @@ def handle(self, *args, **options): objs=obj_list, ) - print(f'Successfully created "{count}" users.') + logger.info(f'Successfully created "{count}" users.') @staticmethod def validate_unique_list(username_list, current_usernames, count): diff --git a/users/management/commands/create_test_user.py b/users/management/commands/create_test_user.py index 5401fb60..b084030a 100644 --- a/users/management/commands/create_test_user.py +++ b/users/management/commands/create_test_user.py @@ -3,7 +3,11 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +import logging + # Django +from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError @@ -16,6 +20,8 @@ # ============================================================================= User = get_user_model() +logger = logging.getLogger(__name__) + # ============================================================================= # COMMANDS @@ -43,6 +49,12 @@ def add_arguments(self, parser): def handle(self, *args, **options): """Verify the arguments and create the User.""" + # Only allow this command in local development + if not settings.LOCAL: + raise CommandError( + 'Command can only be run for local development.' + ) + username = options['username'] if User.objects.filter(username=username).exists(): raise CommandError( @@ -69,7 +81,7 @@ def handle(self, *args, **options): user=user, forum_id=forum_id, ) - print( + logger.info( f'Successfully created user "{username}" and associated it with ' f'forum id "{forum_id}".' ) diff --git a/users/tests/test_commands.py b/users/tests/test_commands.py index 4f0f62e2..b5b4e1a2 100644 --- a/users/tests/test_commands.py +++ b/users/tests/test_commands.py @@ -8,7 +8,7 @@ # Django from django.core.management import call_command from django.core.management.base import CommandError -from django.test import TestCase +from django.test import TestCase, override_settings # App from test_utils.factories.users import AdminUserFactory, ForumUserFactory @@ -18,9 +18,13 @@ # ============================================================================= # TEST CASES # ============================================================================= +@override_settings(LOCAL=True) class CommandsTestCase(TestCase): - def test_associate_super_user(self): + @mock.patch( + 'users.management.commands.associate_super_user.logger', + ) + def test_associate_super_user(self, mock_logger): user = AdminUserFactory() self.assertEqual( first=ForumUser.objects.count(), @@ -41,6 +45,25 @@ def test_associate_super_user(self): first=forum_user.forum_id, second=forum_id, ) + mock_logger.info.assert_called_once_with( + f'User "{user.username}" successfully associated with forum id "{forum_id}".' + ) + + @override_settings(LOCAL=False) + def test_associate_super_user_local_only(self): + user = AdminUserFactory() + self.assertEqual( + first=ForumUser.objects.count(), + second=0, + ) + forum_id = randint(1, 10) + with self.assertRaises(CommandError) as context: + call_command('associate_super_user', user.username, forum_id) + + self.assertEqual( + first=str(context.exception), + second='Command can only be run for local development.', + ) def test_associate_super_user_invalid_username(self): user = AdminUserFactory() @@ -73,23 +96,49 @@ def test_associate_super_user_forum_user_exists(self): ), ) - def test_create_random_users(self): + @mock.patch( + 'users.management.commands.create_random_users.logger', + ) + def test_create_random_users(self, mock_logger): + count = randint(1, 10) forum_id = randint(1, 10) ForumUserFactory( forum_id=forum_id, ) - call_command('create_random_users', 10) + call_command('create_random_users', count) self.assertEqual( first=ForumUser.objects.count(), - second=11, + second=count + 1, ) query = ForumUser.objects.values_list('forum_id', flat=True) + id_list = list(range(1, count + 1)) + id_list.append(count + 1 if forum_id in id_list else forum_id) self.assertListEqual( list1=list(query.order_by('forum_id')), - list2=list(range(1, 12)), + list2=id_list, + ) + mock_logger.info.assert_called_once_with( + f'Successfully created "{count}" users.' + ) + + @override_settings(LOCAL=False) + def test_create_random_users_local_only(self): + forum_id = randint(1, 10) + ForumUserFactory( + forum_id=forum_id, + ) + with self.assertRaises(CommandError) as context: + call_command('create_random_users', 10) + + self.assertEqual( + first=str(context.exception), + second='Command can only be run for local development.', ) - def test_create_test_user(self): + @mock.patch( + 'users.management.commands.create_test_user.logger', + ) + def test_create_test_user(self, mock_logger): username = 'test-user' forum_id = randint(1, 10) call_command('create_test_user', username, 'password', forum_id) @@ -106,6 +155,21 @@ def test_create_test_user(self): first=user.user.username, second=username, ) + mock_logger.info.assert_called_once_with( + f'Successfully created user "{username}" and associated it with forum id "{forum_id}".' + ) + + @override_settings(LOCAL=False) + def test_create_test_user_local_only(self): + username = 'test-user' + forum_id = randint(1, 10) + with self.assertRaises(CommandError) as context: + call_command('create_test_user', username, 'password', forum_id) + + self.assertEqual( + first=str(context.exception), + second='Command can only be run for local development.', + ) def test_create_test_user_username_exists(self): forum_id = randint(1, 10) From 8ffb049174b8a40b449edcc163a5ace6b4321c64 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Oct 2021 11:13:08 -0400 Subject: [PATCH 025/211] Added tests for some serializers. --- .pylintrc | 1 + games/api/tests/test_serializers.py | 37 +++++ requirements/api/tests/__init__.py | 0 requirements/api/tests/test_serializers.py | 166 +++++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 games/api/tests/test_serializers.py create mode 100644 requirements/api/tests/__init__.py create mode 100644 requirements/api/tests/test_serializers.py diff --git a/.pylintrc b/.pylintrc index 2548c73a..14ea522e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -16,6 +16,7 @@ disable= no-member, too-few-public-methods, too-many-ancestors, + logging-fstring-interpolation, [BASIC] diff --git a/games/api/tests/test_serializers.py b/games/api/tests/test_serializers.py new file mode 100644 index 00000000..0c39265d --- /dev/null +++ b/games/api/tests/test_serializers.py @@ -0,0 +1,37 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework.serializers import ModelSerializer + +# App +from games.api.serializers import GameSerializer +from games.models import Game + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class GameSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(GameSerializer, ModelSerializer), + ) + + def test_meta_class(self): + self.assertEqual( + first=GameSerializer.Meta.model, + second=Game, + ) + self.assertTupleEqual( + tuple1=GameSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'icon', + ), + ) diff --git a/requirements/api/tests/__init__.py b/requirements/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/requirements/api/tests/test_serializers.py b/requirements/api/tests/test_serializers.py new file mode 100644 index 00000000..dd69f17d --- /dev/null +++ b/requirements/api/tests/test_serializers.py @@ -0,0 +1,166 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework.fields import ReadOnlyField +from rest_framework.serializers import ModelSerializer + +# App +from requirements.api.serializers.common import ( + ReleaseDownloadRequirementSerializer, + ReleasePyPiRequirementSerializer, + ReleaseVersionControlRequirementSerializer, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ReleaseDownloadRequirementSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ReleaseDownloadRequirementSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(ReleaseDownloadRequirementSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=1, + ) + + self.assertIn( + member='url', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['url'], + cls=ReadOnlyField, + ) + self.assertEqual( + first=declared_fields['url'].source, + second='download_requirement.url', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ReleaseDownloadRequirementSerializer.Meta.fields, + tuple2=( + 'url', + 'optional', + ), + ) + + +class ReleasePyPiRequirementSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ReleasePyPiRequirementSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(ReleasePyPiRequirementSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=3, + ) + + self.assertIn( + member='name', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['name'], + cls=ReadOnlyField, + ) + self.assertEqual( + first=declared_fields['name'].source, + second='pypi_requirement.name', + ) + + self.assertIn( + member='slug', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['slug'], + cls=ReadOnlyField, + ) + self.assertEqual( + first=declared_fields['slug'].source, + second='pypi_requirement.slug', + ) + + self.assertIn( + member='version', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['version'], + cls=ReadOnlyField, + ) + self.assertIsNone(obj=declared_fields['version'].source) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ReleasePyPiRequirementSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'version', + 'optional', + ), + ) + + +class ReleaseVersionControlRequirementSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ReleaseVersionControlRequirementSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(ReleaseVersionControlRequirementSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=2, + ) + + self.assertIn( + member='url', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['url'], + cls=ReadOnlyField, + ) + self.assertEqual( + first=declared_fields['url'].source, + second='vcs_requirement.url', + ) + + self.assertIn( + member='version', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['version'], + cls=ReadOnlyField, + ) + self.assertIsNone(obj=declared_fields['version'].source) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ReleaseVersionControlRequirementSerializer.Meta.fields, + tuple2=( + 'url', + 'version', + 'optional', + ), + ) From 6843d60624d359413b21593b5bae685060a8f62f Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Oct 2021 12:05:01 -0400 Subject: [PATCH 026/211] Added more tests. --- tags/api/serializers.py | 4 +- tags/api/tests/test_filtersets.py | 33 +++ tags/api/tests/test_serializers.py | 55 +++++ users/api/tests/test_filtersets.py | 58 +++++ users/api/tests/test_serializers.py | 326 ++++++++++++++++++++++++++++ 5 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 tags/api/tests/test_filtersets.py create mode 100644 tags/api/tests/test_serializers.py create mode 100644 users/api/tests/test_filtersets.py create mode 100644 users/api/tests/test_serializers.py diff --git a/tags/api/serializers.py b/tags/api/serializers.py index ef66b2ca..5d96f933 100644 --- a/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -8,9 +8,7 @@ # App from tags.models import Tag -from users.api.serializers.common import ( - ForumUserContributorSerializer, -) +from users.api.serializers.common import ForumUserContributorSerializer # ============================================================================= diff --git a/tags/api/tests/test_filtersets.py b/tags/api/tests/test_filtersets.py new file mode 100644 index 00000000..3e44d35b --- /dev/null +++ b/tags/api/tests/test_filtersets.py @@ -0,0 +1,33 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from django_filters.filterset import FilterSet + +# App +from tags.api.filtersets import TagFilterSet +from tags.models import Tag + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class TagFilterSetTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(TagFilterSet, FilterSet), + ) + + def test_meta_class(self): + self.assertEqual( + first=TagFilterSet.Meta.model, + second=Tag, + ) + self.assertTupleEqual( + tuple1=TagFilterSet.Meta.fields, + tuple2=('black_listed',), + ) diff --git a/tags/api/tests/test_serializers.py b/tags/api/tests/test_serializers.py new file mode 100644 index 00000000..aaad1fdb --- /dev/null +++ b/tags/api/tests/test_serializers.py @@ -0,0 +1,55 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework.serializers import ModelSerializer + +# App +from tags.api.serializers import TagSerializer +from tags.models import Tag +from users.api.serializers.common import ForumUserContributorSerializer + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class TagSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(TagSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(TagSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=1, + ) + + self.assertIn( + member='creator', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['creator'], + cls=ForumUserContributorSerializer, + ) + self.assertTrue(expr=declared_fields['creator'].read_only) + + def test_meta_class(self): + self.assertEqual( + first=TagSerializer.Meta.model, + second=Tag, + ) + self.assertTupleEqual( + tuple1=TagSerializer.Meta.fields, + tuple2=( + 'name', + 'black_listed', + 'creator', + ), + ) diff --git a/users/api/tests/test_filtersets.py b/users/api/tests/test_filtersets.py new file mode 100644 index 00000000..9e4c99fb --- /dev/null +++ b/users/api/tests/test_filtersets.py @@ -0,0 +1,58 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from django_filters.filters import BooleanFilter +from django_filters.filterset import FilterSet + +# App +from users.api.filtersets import ForumUserFilterSet +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ForumUserFilterSetTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ForumUserFilterSet, FilterSet), + ) + + def test_base_filters(self): + base_filters = ForumUserFilterSet.base_filters + self.assertEqual( + first=len(base_filters), + second=1, + ) + + self.assertIn( + member='has_contributions', + container=base_filters, + ) + self.assertIsInstance( + obj=base_filters['has_contributions'], + cls=BooleanFilter, + ) + self.assertEqual( + first=base_filters['has_contributions'].method, + second='filter_has_contributions', + ) + self.assertEqual( + first=base_filters['has_contributions'].label, + second='Has Contributions', + ) + + def test_meta_class(self): + self.assertEqual( + first=ForumUserFilterSet.Meta.model, + second=ForumUser, + ) + self.assertTupleEqual( + tuple1=ForumUserFilterSet.Meta.fields, + tuple2=('has_contributions',), + ) diff --git a/users/api/tests/test_serializers.py b/users/api/tests/test_serializers.py new file mode 100644 index 00000000..af9a5809 --- /dev/null +++ b/users/api/tests/test_serializers.py @@ -0,0 +1,326 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework.fields import SerializerMethodField +from rest_framework.serializers import ListSerializer, ModelSerializer + +# App +from project_manager.packages.models import Package +from project_manager.plugins.models import Plugin +from project_manager.sub_plugins.models import SubPlugin +from users.api.serializers import ( + ForumUserSerializer, + PackageContributionSerializer, + PluginContributionSerializer, + ProjectContributionSerializer, + SubPluginContributionSerializer, +) +from users.api.serializers.common import ForumUserContributorSerializer +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ForumUserSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ForumUserSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(ForumUserSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=7, + ) + + self.assertIn( + member='username', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['username'], + cls=SerializerMethodField, + ) + + self.assertIn( + member='packages', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['packages'], + cls=ListSerializer, + ) + self.assertTrue(expr=declared_fields['packages'].many) + self.assertTrue(expr=declared_fields['packages'].read_only) + self.assertIsInstance( + obj=declared_fields['packages'].child, + cls=PackageContributionSerializer, + ) + self.assertTrue(expr=declared_fields['packages'].child.read_only) + + self.assertIn( + member='package_contributions', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['package_contributions'], + cls=ListSerializer, + ) + self.assertTrue(expr=declared_fields['package_contributions'].many) + self.assertTrue(expr=declared_fields['package_contributions'].read_only) + self.assertIsInstance( + obj=declared_fields['package_contributions'].child, + cls=PackageContributionSerializer, + ) + self.assertTrue(expr=declared_fields['package_contributions'].child.read_only) + + self.assertIn( + member='plugins', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['plugins'], + cls=ListSerializer, + ) + self.assertTrue(expr=declared_fields['plugins'].many) + self.assertTrue(expr=declared_fields['plugins'].read_only) + self.assertIsInstance( + obj=declared_fields['plugins'].child, + cls=PluginContributionSerializer, + ) + self.assertTrue(expr=declared_fields['plugins'].child.read_only) + + self.assertIn( + member='plugin_contributions', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['plugin_contributions'], + cls=ListSerializer, + ) + self.assertTrue(expr=declared_fields['plugin_contributions'].many) + self.assertTrue(expr=declared_fields['plugin_contributions'].read_only) + self.assertIsInstance( + obj=declared_fields['plugin_contributions'].child, + cls=PluginContributionSerializer, + ) + self.assertTrue(expr=declared_fields['plugin_contributions'].child.read_only) + + self.assertIn( + member='subplugins', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['subplugins'], + cls=ListSerializer, + ) + self.assertTrue(expr=declared_fields['subplugins'].many) + self.assertTrue(expr=declared_fields['subplugins'].read_only) + self.assertIsInstance( + obj=declared_fields['subplugins'].child, + cls=SubPluginContributionSerializer, + ) + self.assertTrue(expr=declared_fields['subplugins'].child.read_only) + + self.assertIn( + member='subplugin_contributions', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['subplugin_contributions'], + cls=ListSerializer, + ) + self.assertTrue(expr=declared_fields['subplugin_contributions'].many) + self.assertTrue(expr=declared_fields['subplugin_contributions'].read_only) + self.assertIsInstance( + obj=declared_fields['subplugin_contributions'].child, + cls=SubPluginContributionSerializer, + ) + self.assertTrue(expr=declared_fields['subplugin_contributions'].child.read_only) + + def test_meta_class(self): + self.assertEqual( + first=ForumUserSerializer.Meta.model, + second=ForumUser, + ) + self.assertTupleEqual( + tuple1=ForumUserSerializer.Meta.fields, + tuple2=( + 'forum_id', + 'username', + 'packages', + 'package_contributions', + 'plugins', + 'plugin_contributions', + 'subplugins', + 'subplugin_contributions', + ), + ) + + +class PackageContributionSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageContributionSerializer, ProjectContributionSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(PackageContributionSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=0, + ) + + def test_meta_class(self): + self.assertEqual( + first=PackageContributionSerializer.Meta.model, + second=Package, + ) + self.assertTrue( + expr=issubclass( + PackageContributionSerializer.Meta, + ProjectContributionSerializer.Meta, + ), + ) + self.assertTupleEqual( + tuple1=PackageContributionSerializer.Meta.fields, + tuple2=ProjectContributionSerializer.Meta.fields, + ) + + +class PluginContributionSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginContributionSerializer, ProjectContributionSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(PluginContributionSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=0, + ) + + def test_meta_class(self): + self.assertEqual( + first=PluginContributionSerializer.Meta.model, + second=Plugin, + ) + self.assertTrue( + expr=issubclass( + PluginContributionSerializer.Meta, + ProjectContributionSerializer.Meta, + ), + ) + self.assertTupleEqual( + tuple1=PluginContributionSerializer.Meta.fields, + tuple2=ProjectContributionSerializer.Meta.fields, + ) + + +class ProjectContributionSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectContributionSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectContributionSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=0, + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectContributionSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + ), + ) + + +class SubPluginContributionSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginContributionSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(SubPluginContributionSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=1, + ) + + self.assertIn( + member='plugin', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['plugin'], + cls=PluginContributionSerializer, + ) + + def test_meta_class(self): + self.assertEqual( + first=SubPluginContributionSerializer.Meta.model, + second=SubPlugin, + ) + self.assertTupleEqual( + tuple1=SubPluginContributionSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'plugin', + ), + ) + + +class ForumUserContributorSerializerTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ForumUserContributorSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(ForumUserContributorSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=1, + ) + + self.assertIn( + member='username', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['username'], + cls=SerializerMethodField, + ) + + def test_meta_class(self): + self.assertEqual( + first=ForumUserContributorSerializer.Meta.model, + second=ForumUser, + ) + self.assertTupleEqual( + tuple1=ForumUserContributorSerializer.Meta.fields, + tuple2=( + 'forum_id', + 'username', + ), + ) From f4da2f212f298e9e59c4b85857a1c0c6bd87bded Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:15:42 -0400 Subject: [PATCH 027/211] Removed unused view mixins. Created missing migrations. Added a test to verify there are no new migrations. --- .../test_views.py => SPPM/tests/__init__.py | 0 SPPM/tests/test_new_migrations.py | 27 +++ games/migrations/0002_alter_game_options.py | 17 ++ project_manager/common/views.py | 160 ------------------ .../migrations/0004_auto_20211103_1055.py | 29 ++++ tags/migrations/0003_alter_tag_options.py | 17 ++ 6 files changed, 90 insertions(+), 160 deletions(-) rename project_manager/common/tests/test_views.py => SPPM/tests/__init__.py (100%) create mode 100644 SPPM/tests/test_new_migrations.py create mode 100644 games/migrations/0002_alter_game_options.py delete mode 100644 project_manager/common/views.py create mode 100644 project_manager/migrations/0004_auto_20211103_1055.py create mode 100644 tags/migrations/0003_alter_tag_options.py diff --git a/project_manager/common/tests/test_views.py b/SPPM/tests/__init__.py similarity index 100% rename from project_manager/common/tests/test_views.py rename to SPPM/tests/__init__.py diff --git a/SPPM/tests/test_new_migrations.py b/SPPM/tests/test_new_migrations.py new file mode 100644 index 00000000..e33ba3a5 --- /dev/null +++ b/SPPM/tests/test_new_migrations.py @@ -0,0 +1,27 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import sys +from io import StringIO + +# Django +from django.core.management import call_command +from django.test import TestCase + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class MigrationTest(TestCase): + + def test_pending_migrations(self): + out, sys.stdout = sys.stdout, StringIO() + call_command('makemigrations', '--dry-run') + sys.stdout.seek(0) + output = sys.stdout.read() + sys.stdout = out + self.assertEqual( + first=output, + second='No changes detected\n', + ) diff --git a/games/migrations/0002_alter_game_options.py b/games/migrations/0002_alter_game_options.py new file mode 100644 index 00000000..d2aef368 --- /dev/null +++ b/games/migrations/0002_alter_game_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.8 on 2021-11-03 11:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='game', + options={'verbose_name': 'Game', 'verbose_name_plural': 'Games'}, + ), + ] diff --git a/project_manager/common/views.py b/project_manager/common/views.py deleted file mode 100644 index 68696bf2..00000000 --- a/project_manager/common/views.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Common views.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.views.generic import ListView - -# Third Party Django -from braces.views import OrderableListMixin - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'OrderableListView', - 'OrderablePaginatedListView', - 'PaginatedListView', -) - - -# ============================================================================= -# HELPERS -# ============================================================================= -class _PageObject: - def __init__(self, display, url): - self.display = display - self.url = url - - def __str__(self): - return str(self.display) - - -# ============================================================================= -# VIEWS -# ============================================================================= -class OrderableListView(OrderableListMixin, ListView): - """View to be inherited for ordering.""" - - def get_context_data(self, **kwargs): - """Update ordering.""" - context = super().get_context_data(**kwargs) - default = self.get_orderable_columns_default() - orderable_columns = sorted(self.get_orderable_columns()) - order_by = context['order_by'] - order_by_url = ( - f'order_by={order_by}' - if order_by in orderable_columns and - order_by != default else None - ) - ordering_url = ( - 'ordering=desc' if context['ordering'] == 'desc' else None - ) - order_list = filter(None, [order_by_url, ordering_url]) - context.update({ - 'orderable_columns': orderable_columns, - 'order_url': '&'.join(order_list) if order_list else None, - }) - return context - - -class PaginatedListView(ListView): - """View to be inherited for pagination.""" - - next_pages = 2 - previous_pages = 2 - - def get_next_pages(self): - """Return the next page URLs.""" - if not isinstance(self.next_pages, int) or self.next_pages <= 0: - raise AttributeError( - f'"{self.next_pages}" is not a valid value for ' - f'{self.__class__.__name__}.next_pages.' - ) - return self.next_pages - - def get_previous_pages(self): - """Return the previous page URLs.""" - if not isinstance(self.next_pages, int) or self.previous_pages <= 0: - raise AttributeError( - f'"{self.next_pages}" is not a valid value for ' - f'{self.__class__.__name__}.previous_pages.' - ) - return self.previous_pages - - def get_context_data(self, *, object_list=None, **kwargs): - """Add pagination to the view's context.""" - context = super().get_context_data(object_list=object_list, **kwargs) - paginator = context['paginator'] - page = context['page_obj'] - total_pages = paginator.num_pages - - previous_pages = self.get_previous_pages() - next_pages = self.get_next_pages() - current_page = page.number - previous_page_list = [x for x in range( - current_page - previous_pages, current_page) if x > 0] - next_page_list = [ - x for x in range( - current_page + 1, - current_page + 1 + next_pages - ) if x <= total_pages - ] - page_url_list = [] - if context['is_paginated']: - if current_page != 1: - page_url_list.append( - _PageObject('prev', f'?page={current_page - 1}') - ) - if 1 not in previous_page_list + [current_page]: - page_url_list.append( - _PageObject('1', '?page=1') - ) - if 2 not in previous_page_list + next_page_list + [current_page]: - page_url_list.append( - _PageObject('...', None) - ) - for item in previous_page_list: - page_url_list.append( - _PageObject(item, f'?page={item}') - ) - page_url_list.append( - _PageObject(current_page, None) - ) - for item in next_page_list: - page_url_list.append( - _PageObject(item, f'?page={item}') - ) - if total_pages - 1 not in ( - previous_page_list + next_page_list + [current_page]): - page_url_list.append( - _PageObject('...', None) - ) - if total_pages not in next_page_list + [current_page]: - page_url_list.append( - _PageObject(total_pages, f'?page={total_pages}') - ) - if current_page != total_pages: - page_url_list.append( - _PageObject('next', f'?page={current_page + 1}') - ) - context.update({ - 'page_url_list': page_url_list, - }) - return context - - -class OrderablePaginatedListView(OrderableListView, PaginatedListView): - """View to be inherited for both ordering and pagination.""" - - def get_context_data(self, **kwargs): - """Update the ordering and pagination.""" - context = super().get_context_data(**kwargs) - order_url = context['order_url'] - if order_url: - for item in context['page_url_list']: - if item.url is not None: - item.url += '&' + order_url - return context diff --git a/project_manager/migrations/0004_auto_20211103_1055.py b/project_manager/migrations/0004_auto_20211103_1055.py new file mode 100644 index 00000000..f69db531 --- /dev/null +++ b/project_manager/migrations/0004_auto_20211103_1055.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.8 on 2021-11-03 14:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('project_manager', '0003_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='package', + options={'verbose_name': 'Package', 'verbose_name_plural': 'Packages'}, + ), + migrations.AlterModelOptions( + name='plugin', + options={'verbose_name': 'Plugin', 'verbose_name_plural': 'Plugins'}, + ), + migrations.AlterUniqueTogether( + name='packagerelease', + unique_together={('package', 'version')}, + ), + migrations.AlterUniqueTogether( + name='subpluginrelease', + unique_together={('sub_plugin', 'version')}, + ), + ] diff --git a/tags/migrations/0003_alter_tag_options.py b/tags/migrations/0003_alter_tag_options.py new file mode 100644 index 00000000..14cb696c --- /dev/null +++ b/tags/migrations/0003_alter_tag_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.8 on 2021-11-03 11:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tags', '0002_tag_creator'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tag', + options={'verbose_name': 'Tag', 'verbose_name_plural': 'Tags'}, + ), + ] From 6b3aba76982777ed4eb18fb73656c585b3023243 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:16:16 -0400 Subject: [PATCH 028/211] Added factories for testing projects. --- test_utils/factories/packages.py | 193 +++++++++++++++++++++++++ test_utils/factories/plugins.py | 209 ++++++++++++++++++++++++++++ test_utils/factories/sub_plugins.py | 196 ++++++++++++++++++++++++++ 3 files changed, 598 insertions(+) create mode 100644 test_utils/factories/packages.py create mode 100644 test_utils/factories/plugins.py create mode 100644 test_utils/factories/sub_plugins.py diff --git a/test_utils/factories/packages.py b/test_utils/factories/packages.py new file mode 100644 index 00000000..2f2a742f --- /dev/null +++ b/test_utils/factories/packages.py @@ -0,0 +1,193 @@ +"""Factories for use when testing with Package functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.utils.timezone import get_current_timezone + +# Third Party Django +import factory + +# App +from project_manager.packages.models import ( + Package, + PackageContributor, + PackageGame, + PackageRelease, + PackageReleaseDownloadRequirement, + PackageReleasePackageRequirement, + PackageReleasePyPiRequirement, + PackageReleaseVersionControlRequirement, + PackageTag, +) + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'PackageContributorFactory', + 'PackageFactory', + 'PackageGameFactory', + 'PackageReleaseFactory', + 'PackageReleaseDownloadRequirementFactory', + 'PackageReleasePackageRequirementFactory', + 'PackageReleasePyPiRequirementFactory', + 'PackageReleaseVersionControlRequirementFactory', + 'PackageTagFactory', +) + + +# ============================================================================= +# FACTORIES +# ============================================================================= +class PackageFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with Package objects.""" + + name = factory.Sequence(function=lambda n: f'Package {n}') + basename = factory.Sequence(function=lambda n: f'package_{n}') + owner = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + created = factory.Faker('date_time', tzinfo=get_current_timezone()) + updated = factory.Faker('date_time', tzinfo=get_current_timezone()) + + class Meta: + """Define metaclass attributes.""" + + model = Package + + +class PackageReleaseFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PackageRelease objects.""" + + package = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + version = factory.Sequence(function=lambda n: f'1.0.{n}') + + class Meta: + """Define metaclass attributes.""" + + model = PackageRelease + + +class PackageContributorFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PackageContributor objects.""" + + package = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + user = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + + class Meta: + """Define metaclass attributes.""" + + model = PackageContributor + + +class PackageGameFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PackageGame objects.""" + + package = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + game = factory.SubFactory( + factory='test_utils.factories.games.GameFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PackageGame + + +class PackageTagFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PackageTag objects.""" + + package = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + tag = factory.SubFactory( + factory='test_utils.factories.tags.TagFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PackageTag + + +class PackageReleaseDownloadRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PackageReleaseDownloadRequirement objects.""" + + package_release = factory.SubFactory( + factory='test_utils.factories.packages.PackageReleaseFactory', + ) + download_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.DownloadRequirement', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PackageReleaseDownloadRequirement + + +class PackageReleasePackageRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PackageReleasePackageRequirement objects.""" + + package_release = factory.SubFactory( + factory='test_utils.factories.packages.PackageReleaseFactory', + ) + package_requirement = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PackageReleasePackageRequirement + + +class PackageReleasePyPiRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PackageReleasePyPiRequirement objects.""" + + package_release = factory.SubFactory( + factory='test_utils.factories.packages.PackageReleaseFactory', + ) + pypi_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.PyPiRequirement', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PackageReleasePyPiRequirement + + +class PackageReleaseVersionControlRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PackageReleaseVersionControlRequirement objects.""" + + package_release = factory.SubFactory( + factory='test_utils.factories.packages.PackageReleaseFactory', + ) + vcs_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.VersionControlRequirementFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PackageReleaseVersionControlRequirement diff --git a/test_utils/factories/plugins.py b/test_utils/factories/plugins.py new file mode 100644 index 00000000..e5ee4ab0 --- /dev/null +++ b/test_utils/factories/plugins.py @@ -0,0 +1,209 @@ +"""Factories for use when testing with Plugin functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.utils.timezone import get_current_timezone + +# Third Party Django +import factory + +# App +from project_manager.plugins.models import ( + Plugin, + PluginContributor, + PluginGame, + PluginRelease, + PluginReleaseDownloadRequirement, + PluginReleasePackageRequirement, + PluginReleasePyPiRequirement, + PluginReleaseVersionControlRequirement, + PluginTag, + SubPluginPath, +) + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'PluginContributorFactory', + 'PluginFactory', + 'PluginGameFactory', + 'PluginReleaseFactory', + 'PluginReleaseDownloadRequirementFactory', + 'PluginReleasePackageRequirementFactory', + 'PluginReleasePyPiRequirementFactory', + 'PluginReleaseVersionControlRequirementFactory', + 'PluginTagFactory', + 'SubPluginPathFactory', +) + + +# ============================================================================= +# FACTORIES +# ============================================================================= +class PluginFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with Plugin objects.""" + + name = factory.Sequence(function=lambda n: f'Plugin {n}') + basename = factory.Sequence(function=lambda n: f'plugin_{n}') + owner = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + created = factory.Faker('date_time', tzinfo=get_current_timezone()) + updated = factory.Faker('date_time', tzinfo=get_current_timezone()) + + class Meta: + """Define metaclass attributes.""" + + model = Plugin + + +class PluginReleaseFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PluginRelease objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + version = factory.Sequence(function=lambda n: f'1.0.{n}') + + class Meta: + """Define metaclass attributes.""" + + model = PluginRelease + + +class PluginContributorFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PluginContributor objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + user = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + + class Meta: + """Define metaclass attributes.""" + + model = PluginContributor + + +class PluginGameFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PluginGame objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + game = factory.SubFactory( + factory='test_utils.factories.games.GameFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PluginGame + + +class PluginTagFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PluginTag objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + tag = factory.SubFactory( + factory='test_utils.factories.tags.TagFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PluginTag + + +class PluginReleaseDownloadRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PluginReleaseDownloadRequirement objects.""" + + plugin_release = factory.SubFactory( + factory='test_utils.factories.plugins.PluginReleaseFactory', + ) + download_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.DownloadRequirement', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PluginReleaseDownloadRequirement + + +class PluginReleasePackageRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PluginReleasePackageRequirement objects.""" + + plugin_release = factory.SubFactory( + factory='test_utils.factories.plugins.PluginReleaseFactory', + ) + package_requirement = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PluginReleasePackageRequirement + + +class PluginReleasePyPiRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PluginReleasePyPiRequirement objects.""" + + plugin_release = factory.SubFactory( + factory='test_utils.factories.plugins.PluginReleaseFactory', + ) + pypi_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.PyPiRequirement', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PluginReleasePyPiRequirement + + +class PluginReleaseVersionControlRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with PluginReleaseVersionControlRequirement objects.""" + + plugin_release = factory.SubFactory( + factory='test_utils.factories.plugins.PluginReleaseFactory', + ) + vcs_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.VersionControlRequirementFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = PluginReleaseVersionControlRequirement + + +class SubPluginPathFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPluginPath objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + path = factory.Sequence(function=lambda n: f'some/path/{n}') + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginPath diff --git a/test_utils/factories/sub_plugins.py b/test_utils/factories/sub_plugins.py new file mode 100644 index 00000000..f7d3f4bc --- /dev/null +++ b/test_utils/factories/sub_plugins.py @@ -0,0 +1,196 @@ +"""Factories for use when testing with SubPlugin functionality.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.utils.timezone import get_current_timezone + +# Third Party Django +import factory + +# App +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginContributor, + SubPluginGame, + SubPluginRelease, + SubPluginReleaseDownloadRequirement, + SubPluginReleasePackageRequirement, + SubPluginReleasePyPiRequirement, + SubPluginReleaseVersionControlRequirement, + SubPluginTag, +) + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'SubPluginContributorFactory', + 'SubPluginGameFactory', + 'SubPluginFactory', + 'SubPluginReleaseFactory', + 'SubPluginReleaseDownloadRequirementFactory', + 'SubPluginReleasePackageRequirementFactory', + 'SubPluginReleasePyPiRequirementFactory', + 'SubPluginReleaseVersionControlRequirementFactory', + 'SubPluginTagFactory', +) + + +# ============================================================================= +# FACTORIES +# ============================================================================= +class SubPluginFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPlugin objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + name = factory.Sequence(function=lambda n: f'SubPlugin {n}') + basename = factory.Sequence(function=lambda n: f'sub_plugin_{n}') + owner = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + created = factory.Faker('date_time', tzinfo=get_current_timezone()) + updated = factory.Faker('date_time', tzinfo=get_current_timezone()) + + class Meta: + """Define metaclass attributes.""" + + model = SubPlugin + + +class SubPluginReleaseFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPluginRelease objects.""" + + sub_plugin = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginFactory', + ) + version = factory.Sequence(function=lambda n: f'1.0.{n}') + + class Meta: + """Define metaclass attributes.""" + + model = SubPluginRelease + + +class SubPluginContributorFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPluginContributor objects.""" + + sub_plugin = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginFactory', + ) + user = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) + + class Meta: + """Define metaclass attributes.""" + + model = SubPluginContributor + + +class SubPluginGameFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPluginGame objects.""" + + sub_plugin = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginFactory', + ) + game = factory.SubFactory( + factory='test_utils.factories.games.GameFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginGame + + +class SubPluginTagFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPluginTag objects.""" + + sub_plugin = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginFactory', + ) + tag = factory.SubFactory( + factory='test_utils.factories.tags.TagFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginTag + + +class SubPluginReleaseDownloadRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with SubPluginReleaseDownloadRequirement objects.""" + + sub_plugin_release = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginReleaseFactory', + ) + download_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.DownloadRequirement', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginReleaseDownloadRequirement + + +class SubPluginReleasePackageRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with SubPluginReleasePackageRequirement objects.""" + + sub_plugin_release = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginReleaseFactory', + ) + package_requirement = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginReleasePackageRequirement + + +class SubPluginReleasePyPiRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with SubPluginReleasePyPiRequirement objects.""" + + sub_plugin_release = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginReleaseFactory', + ) + pypi_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.PyPiRequirement', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginReleasePyPiRequirement + + +class SubPluginReleaseVersionControlRequirementFactory( + factory.django.DjangoModelFactory +): + """Model factory to use when testing with SubPluginReleaseVersionControlRequirement objects.""" + + sub_plugin_release = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginReleaseFactory', + ) + vcs_requirement = factory.SubFactory( + factory='test_utils.factories.requirements.VersionControlRequirementFactory', + ) + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginReleaseVersionControlRequirement From d8b82caf0fa59f8e4d81da3d2b5105da4616c668 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:20:34 -0400 Subject: [PATCH 029/211] Fixed some logic and other minor changes. --- games/models.py | 12 +---- project_manager/api/tests/test_views.py | 2 - .../common/api/serializers/__init__.py | 7 ++- project_manager/common/helpers.py | 19 ++----- project_manager/common/mixins.py | 8 +-- project_manager/common/models.py | 54 +++++++------------ requirements/models.py | 12 +---- tags/models.py | 12 +---- 8 files changed, 36 insertions(+), 90 deletions(-) diff --git a/games/models.py b/games/models.py index bbde86a7..84cff302 100644 --- a/games/models.py +++ b/games/models.py @@ -56,18 +56,10 @@ def __str__(self): """Return the object's name when str cast.""" return str(self.name) - def save( - self, force_insert=False, force_update=False, using=None, - update_fields=None - ): + def save(self, *args, **kwargs): """Store the slug.""" self.slug = slugify(self.basename).replace('_', '-') - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(*args, **kwargs) def get_absolute_url(https://github.com/self): """Return the URL for the Game.""" diff --git a/project_manager/api/tests/test_views.py b/project_manager/api/tests/test_views.py index 7b13d309..1bbe3834 100644 --- a/project_manager/api/tests/test_views.py +++ b/project_manager/api/tests/test_views.py @@ -6,8 +6,6 @@ from rest_framework.reverse import reverse from rest_framework.test import APITestCase -# App - # ============================================================================= # TEST CASES diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index 8cb48a55..dbfb5c8e 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -96,7 +96,7 @@ class Meta: def project_type(self): """Return the project's type.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_type" attribute.' ) @@ -104,7 +104,7 @@ def project_type(self): def release_model(self): """Return the model to use for releases.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"release_model" attribute.' ) @@ -214,8 +214,7 @@ def get_requirements(release): def get_extra_kwargs(self): """Set the 'name' field to read-only when updating.""" extra_kwargs = super().get_extra_kwargs() - action = self.context['view'].action - if action == 'update': + if self.context['view'].action == 'update': name_kwargs = extra_kwargs.get('name', {}) name_kwargs['read_only'] = True extra_kwargs['name'] = name_kwargs diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index fafc49a5..109b1211 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -22,7 +22,6 @@ __all__ = ( 'ProjectZipFile', 'find_image_number', - 'get_groups', 'handle_project_image_upload', 'handle_project_logo_upload', 'handle_release_zip_file_upload', @@ -82,21 +81,21 @@ def __init__(self, zip_file): def project_type(self): """Return the type of project.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' f'"project_type" attribute.' ) def find_base_info(self): """Store all base information for the zip file.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' f'"find_base_info" method.' ) def get_base_paths(self): """Return a list of base paths to check against.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' f'"get_base_paths" method.' ) @@ -245,7 +244,7 @@ def validate_requirements(self): def get_requirement_path(self): """Return the path for the requirements json file.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' f'"get_requirement_path" method.' ) @@ -330,16 +329,6 @@ def find_image_number(directory, slug): return f'{max(map(int, current_files or [0])) + 1}:04' -def get_groups(iterable, count=3): - """Return lists from the given iterable in chunks of 'count'.""" - if not iterable: - return iterable - iterable = list(iterable) - remainder = len(iterable) % count - iterable.extend([''] * (count - remainder)) - return zip(*(iter(iterable),) * count) - - def handle_project_image_upload(instance, filename): """Handle uploading the image by directing to the proper directory.""" return instance.handle_image_upload(filename) diff --git a/project_manager/common/mixins.py b/project_manager/common/mixins.py index 2cb0a562..1c088cfd 100644 --- a/project_manager/common/mixins.py +++ b/project_manager/common/mixins.py @@ -30,7 +30,7 @@ class DownloadMixin(View): def model(self): """Return the release model.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"model" attribute.' ) @@ -38,7 +38,7 @@ def model(self): def base_url(https://github.com/self): """Return the base url for the download.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"base_url" attribute.' ) @@ -46,7 +46,7 @@ def base_url(https://github.com/self): def project_model(self): """Return the project model.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_model" attribute.' ) @@ -55,7 +55,7 @@ def model_kwarg(self): """Return the project's kwarg key.""" if self.model_kwarg is not None: raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"model_kwarg" attribute.' ) diff --git a/project_manager/common/models.py b/project_manager/common/models.py index 66c3cf9f..03e6430d 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -190,32 +190,28 @@ def total_downloads(self): def clean(self): """Clean all attributes and raise any errors that occur.""" - errors = {} - logo_errors = self.clean_logo() - if logo_errors: - errors['logo'] = logo_errors - if errors: - raise ValidationError(errors) + self.clean_logo() return super().clean() def clean_logo(self): """Verify the logo is within the proper dimensions.""" errors = [] if not self.logo: - return errors + return + width, height = Image.open(self.logo).size if width > LOGO_MAX_WIDTH: errors.append(f'Logo width must be no more than {LOGO_MAX_WIDTH}.') + if height > LOGO_MAX_HEIGHT: errors.append( f'Logo height must be no more than {LOGO_MAX_HEIGHT}.' ) - return errors - def save( - self, force_insert=False, force_update=False, using=None, - update_fields=None - ): + if errors: + raise ValidationError(errors) + + def save(self, *args, **kwargs): """Store the slug and remove old logo if necessary.""" self.slug = self.get_slug_value() if all([ @@ -224,16 +220,12 @@ def save( self.logo_path not in str(self.logo) ]): path = settings.MEDIA_ROOT / self.logo_path - if path.isdir(): - logo = [x for x in path.files() if x.stem == self.slug] - if logo: - logo[0].remove() - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + if path.isdir(): # pragma: no branch + logo_files = [x for x in path.files() if x.stem == self.slug] + if logo_files: # pragma: no branch + logo_files[0].remove() + + super().save(*args, **kwargs) def get_forum_url(https://github.com/self): """Return the forum topic URL.""" @@ -281,7 +273,7 @@ class Meta: def project_class(self): """Return the project's class.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_class" attribute.' ) @@ -289,7 +281,7 @@ def project_class(self): def project(self): """Return the project's class.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project" property.' ) @@ -310,18 +302,10 @@ def __str__(self): """Return the project name + release version.""" return f'{self.project} - {self.version}' - def save( - self, force_insert=False, force_update=False, using=None, - update_fields=None - ): + def save(self, *args, **kwargs): """Update the Project's 'updated' value to the releases 'created'.""" pk = self.pk - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(*args, **kwargs) if pk is None: self.project_class.objects.filter( pk=self.project.pk, @@ -377,7 +361,7 @@ def __str__(self): def project(self): """Return the project's class.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' f'"project" property.' ) diff --git a/requirements/models.py b/requirements/models.py index 0ffd5116..53c0473c 100644 --- a/requirements/models.py +++ b/requirements/models.py @@ -69,18 +69,10 @@ def __str__(self): """Return the object's name when str cast.""" return str(self.name) - def save( - self, force_insert=False, force_update=False, using=None, - update_fields=None - ): + def save(self, *args, **kwargs): """Set the slug and save the Requirement.""" self.slug = slugify(self.name) - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(*args, **kwargs) def get_pypi_url(https://github.com/self): """Return the PyPi URL for the requirement.""" diff --git a/tags/models.py b/tags/models.py index a3b2f144..2593bd4d 100644 --- a/tags/models.py +++ b/tags/models.py @@ -51,18 +51,10 @@ def __str__(self): """Return the tag's name.""" return str(self.name) - def save( - self, force_insert=False, force_update=False, using=None, - update_fields=None - ): + def save(self, *args, **kwargs): """Remove all through model instances if black-listed.""" if self.black_listed: self.plugintag_set.all().delete() self.packagetag_set.all().delete() self.subplugintag_set.all().delete() - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(*args, **kwargs) From 7c3d21012cc03e7b18b614615880fe756392b822 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:27:04 -0400 Subject: [PATCH 030/211] Changed some code that was a convenience for sub-plugins. Now sub-plugins is responsible for making its own necessary changes to serializers/views. --- .../common/api/serializers/mixins.py | 14 ++---- project_manager/common/api/views/__init__.py | 36 +++++--------- project_manager/common/api/views/mixins.py | 20 ++++---- .../packages/api/serializers/mixins.py | 2 +- .../plugins/api/serializers/mixins.py | 2 +- .../sub_plugins/api/serializers/mixins.py | 4 +- project_manager/sub_plugins/api/views.py | 48 ++++++++++++++++--- 7 files changed, 72 insertions(+), 54 deletions(-) diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 832d26b1..6f4e1bc9 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -64,14 +64,13 @@ def get_date_display(date, date_format): class ProjectReleaseCreationMixin(ModelSerializer): """Mixin for validation/creation of a project release.""" - parent_project = None requirements = None @property def project_class(self): """Return the project's class.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_class" attribute.' ) @@ -79,7 +78,7 @@ def project_class(self): def project_type(self): """Return the project's type.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_type" attribute.' ) @@ -87,11 +86,11 @@ def project_type(self): def zip_parser(self): """Return the project's zip parsing function.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"zip_parser" attribute.' ) - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Return kwargs for the project.""" return { 'pk': self.context['view'].kwargs.get('pk') @@ -110,8 +109,7 @@ def validate(self, attrs): }) # Validate the version is new for the project - parent_project = self.parent_project - kwargs = self.get_project_kwargs(parent_project) + kwargs = self.get_project_kwargs() project = self.get_project( kwargs=kwargs, ) @@ -122,8 +120,6 @@ def validate(self, attrs): project_basename = getattr(project, 'basename', None) args = (zip_file,) - if parent_project is not None: - args += (parent_project,) with self.zip_parser(*args) as zip_validator: self.validate_zip_file( diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 08716be1..b782cfe2 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -49,7 +49,6 @@ class ProjectAPIView(APIView): http_method_names = ('get', 'options') project_type = None - extra_params = '' def get(self, request): """Return all the API routes for Projects.""" @@ -59,28 +58,28 @@ def get(self, request): viewname=f'api:{self.project_type}s:endpoints', request=request, ) + ( - f'contributors/{self.extra_params}<{self.project_type}>/' + f'contributors/<{self.project_type}>/' ), 'games': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'games/{self.extra_params}<{self.project_type}>/', + ) + f'games/<{self.project_type}>/', 'images': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'images/{self.extra_params}<{self.project_type}>/', + ) + f'images/<{self.project_type}>/', 'projects': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'projects/{self.extra_params}', + ) + f'projects/', 'releases': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'releases/{self.extra_params}<{self.project_type}>/', + ) + f'releases/<{self.project_type}>/', 'tags': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'tags/{self.extra_params}<{self.project_type}>/', + ) + f'tags/<{self.project_type}>/', } ) @@ -102,13 +101,12 @@ class ProjectViewSet(ModelViewSet): stored_contributors = None stored_supported_games = None stored_tags = None - _obj = None @property def creation_serializer_class(self): """Return the serializer class to use ONLY when creating a project.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"creation_serializer_class" attribute.' ) @@ -166,20 +164,13 @@ def update(self, request, *args, **kwargs): self.store_many_to_many_fields(request=request) return super().update(request, *args, **kwargs) - def get_view_name(self): - """Return the project so it's name is in the view.""" - if self._obj is not None: - return self._obj - return super().get_view_name() - class ProjectImageViewSet(ProjectThroughModelMixin): """Base Image View.""" ordering = ('-created',) ordering_fields = ('created',) - - api_type = 'Images' + related_model_type = 'Image' class ProjectReleaseViewSet(ProjectRelatedInfoMixin): @@ -190,8 +181,7 @@ class ProjectReleaseViewSet(ProjectRelatedInfoMixin): ordering_fields = ('created',) lookup_value_regex = RELEASE_VERSION_REGEX lookup_field = 'version' - - api_type = 'Releases' + related_model_type = 'Release' class ProjectGameViewSet(ProjectThroughModelMixin): @@ -199,8 +189,7 @@ class ProjectGameViewSet(ProjectThroughModelMixin): ordering = ('-game',) ordering_fields = ('game',) - - api_type = 'Supported Games' + related_model_type = 'Game' class ProjectTagViewSet(ProjectThroughModelMixin): @@ -208,8 +197,7 @@ class ProjectTagViewSet(ProjectThroughModelMixin): ordering = ('-tag',) ordering_fields = ('tag',) - - api_type = 'Tags' + related_model_type = 'Tag' class ProjectContributorViewSet(ProjectThroughModelMixin): @@ -217,6 +205,6 @@ class ProjectContributorViewSet(ProjectThroughModelMixin): ordering = ('-user',) ordering_fields = ('user',) + related_model_type = 'Contributor' - api_type = 'Contributors' owner_only = True diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index 68235fbf..12d6953f 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -30,8 +30,7 @@ class ProjectRelatedInfoMixin(ModelViewSet): filter_backends = (OrderingFilter, DjangoFilterBackend) - api_type = None - parent_project = None + related_model_type = None _project = None @property @@ -39,7 +38,7 @@ def project(self): """Return the project for the image.""" if self._project is not None: return self._project - kwargs = self.get_project_kwargs(self.parent_project) + kwargs = self.get_project_kwargs() try: self._project = self.project_model.objects.select_related( 'owner__user' @@ -54,7 +53,7 @@ def project(self): def project_model(self): """Return the model to use for the project.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_model" attribute.' ) @@ -62,11 +61,11 @@ def project_model(self): def project_type(self): """Return the project's type.""" raise NotImplementedError( - f'Class {self.__class__.__name__} must implement a ' + f'Class "{self.__class__.__name__}" must implement a ' '"project_type" attribute.' ) - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Return the kwargs to use to filter for the project.""" project_slug = f"{self.project_type.replace('-', '_')}_slug" return { @@ -83,8 +82,9 @@ def get_queryset(self): def get_view_name(self): """Return the name for the view.""" - if hasattr(self, 'kwargs') and self.api_type is not None: - return f'{self.project} - {self.api_type}' + if hasattr(self, 'kwargs') and self.related_model_type is not None: + plural = 's' if self.action == 'list' else '' + return f'{self.project} - {self.related_model_type}{plural}' return super().get_view_name() @@ -97,7 +97,7 @@ class ProjectThroughModelMixin(ProjectRelatedInfoMixin): owner_only = False _owner = None - _contributors = [] + _contributors = None @property def owner(self): @@ -109,7 +109,7 @@ def owner(self): @property def contributors(self): """Return a Queryset for the project's contributors.""" - if isinstance(self._contributors, list): + if self._contributors is None: self._contributors = self.project.contributors.values_list( 'user', flat=True, diff --git a/project_manager/packages/api/serializers/mixins.py b/project_manager/packages/api/serializers/mixins.py index 7f35fdfe..33c7a7b2 100644 --- a/project_manager/packages/api/serializers/mixins.py +++ b/project_manager/packages/api/serializers/mixins.py @@ -30,7 +30,7 @@ def zip_parser(self): """Return the Package zip parsing function.""" return PackageZipFile - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Return kwargs for the project.""" return { 'pk': self.context['view'].kwargs.get('package_slug') diff --git a/project_manager/plugins/api/serializers/mixins.py b/project_manager/plugins/api/serializers/mixins.py index ccae4e84..04b2de51 100644 --- a/project_manager/plugins/api/serializers/mixins.py +++ b/project_manager/plugins/api/serializers/mixins.py @@ -30,7 +30,7 @@ def zip_parser(self): """Return the Plugin zip parsing function.""" return PluginZipFile - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Return kwargs for the project.""" return { 'pk': self.context['view'].kwargs.get('plugin_slug') diff --git a/project_manager/sub_plugins/api/serializers/mixins.py b/project_manager/sub_plugins/api/serializers/mixins.py index db0aeed4..a6ddf348 100644 --- a/project_manager/sub_plugins/api/serializers/mixins.py +++ b/project_manager/sub_plugins/api/serializers/mixins.py @@ -47,10 +47,10 @@ def zip_parser(self): """Return the SubPlugin zip parsing function.""" return SubPluginZipFile - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Return kwargs for the project.""" kwargs = self.context['view'].kwargs return { 'slug': kwargs.get('sub_plugin_slug'), - 'plugin': parent_project, + 'plugin': self.parent_project, } diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index a6a23f81..7b62a799 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -8,6 +8,8 @@ # Third Party Django from rest_framework.parsers import ParseError +from rest_framework.response import Response +from rest_framework.reverse import reverse # App from project_manager.common.api.views import ( @@ -65,7 +67,39 @@ class SubPluginAPIView(ProjectAPIView): """SubPlugin API routes.""" project_type = 'sub-plugin' - extra_params = '/' + + def get(self, request): + """Return all the API routes for Projects.""" + return Response( + data={ + 'contributors': reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) + ( + f'contributors//<{self.project_type}>/' + ), + 'games': reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) + f'games//<{self.project_type}>/', + 'images': reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) + f'images//<{self.project_type}>/', + 'projects': reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) + f'projects//', + 'releases': reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) + f'releases//<{self.project_type}>/', + 'tags': reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) + f'tags//<{self.project_type}>/', + } + ) class SubPluginViewSet(ProjectViewSet): @@ -163,11 +197,11 @@ def parent_project(self): ) from Plugin.DoesNotExist return plugin - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Add the Plugin to the kwargs for filtering for the project.""" - kwargs = super().get_project_kwargs(parent_project=parent_project) + kwargs = super().get_project_kwargs() kwargs.update( - plugin=parent_project, + plugin=self.parent_project, ) return kwargs @@ -230,11 +264,11 @@ def parent_project(self): ) from Plugin.DoesNotExist return plugin - def get_project_kwargs(self, parent_project=None): + def get_project_kwargs(self): """Add the Plugin to the kwargs for filtering for the project.""" - kwargs = super().get_project_kwargs(parent_project=parent_project) + kwargs = super().get_project_kwargs() kwargs.update( - plugin=parent_project, + plugin=self.parent_project, ) return kwargs From 7abe6be3ca7ebbee6ad5de1b705677f34a74fa11 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:28:00 -0400 Subject: [PATCH 031/211] Added some meta classes to some models. --- project_manager/packages/models/__init__.py | 11 +++++++++++ project_manager/plugins/models/__init__.py | 6 ++++++ project_manager/sub_plugins/models/__init__.py | 17 +++++++---------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models/__init__.py index 7db6519e..4d71ab09 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models/__init__.py @@ -91,6 +91,12 @@ class Package(Project): handle_logo_upload = handle_package_logo_upload logo_path = PACKAGE_LOGO_URL + class Meta: + """Define metaclass attributes.""" + + verbose_name = 'Package' + verbose_name_plural = 'Packages' + def get_absolute_url(https://github.com/self): """Return the URL for the Package.""" # TODO: add tests once this view is created @@ -139,6 +145,11 @@ def project(self): """Return the Package.""" return self.package + class Meta(ProjectRelease.Meta): + """Define metaclass attributes.""" + + unique_together = ('package', 'version') + def get_absolute_url(https://github.com/self): """Return the URL for the PackageRelease.""" return reverse( diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index 82e63168..d714371c 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -95,6 +95,12 @@ class Plugin(Project): handle_logo_upload = handle_plugin_logo_upload logo_path = PLUGIN_LOGO_URL + class Meta: + """Define metaclass attributes.""" + + verbose_name = 'Plugin' + verbose_name_plural = 'Plugins' + def get_absolute_url(https://github.com/self): """Return the URL for the Plugin.""" # TODO: add tests once this view is created diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models/__init__.py index 9282f7b4..785e5cf0 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models/__init__.py @@ -124,18 +124,10 @@ def get_absolute_url(https://github.com/self): } ) - def save( - self, force_insert=False, force_update=False, using=None, - update_fields=None - ): + def save(self, *args, **kwargs): """Set the id using the plugin's slug and the project's slug.""" self.id = f'{self.plugin.slug}.{self.get_slug_value()}' - super().save( - force_insert=force_insert, - force_update=force_update, - using=using, - update_fields=update_fields, - ) + super().save(*args, **kwargs) class SubPluginRelease(ProjectRelease): @@ -175,6 +167,11 @@ def project(self): """Return the SubPlugin.""" return self.sub_plugin + class Meta(ProjectRelease.Meta): + """Define metaclass attributes.""" + + unique_together = ('sub_plugin', 'version') + def get_absolute_url(https://github.com/self): """Return the URL for the SubPluginRelease.""" return reverse( From bf725c48a3df7818afd7dd6d3590f60dcb22d582 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:28:53 -0400 Subject: [PATCH 032/211] Added a lot more tests. --- project_manager/common/tests/test_models.py | 91 ++ project_manager/packages/tests/__init__.py | 0 project_manager/packages/tests/test_models.py | 804 +++++++++++++++ project_manager/plugins/tests/__init__.py | 0 project_manager/plugins/tests/test_models.py | 927 ++++++++++++++++++ project_manager/sub_plugins/tests/__init__.py | 0 .../sub_plugins/tests/test_models.py | 835 ++++++++++++++++ 7 files changed, 2657 insertions(+) create mode 100644 project_manager/packages/tests/__init__.py create mode 100644 project_manager/packages/tests/test_models.py create mode 100644 project_manager/plugins/tests/__init__.py create mode 100644 project_manager/plugins/tests/test_models.py create mode 100644 project_manager/sub_plugins/tests/__init__.py create mode 100644 project_manager/sub_plugins/tests/test_models.py diff --git a/project_manager/common/tests/test_models.py b/project_manager/common/tests/test_models.py index 33f4550e..5e9e1eba 100644 --- a/project_manager/common/tests/test_models.py +++ b/project_manager/common/tests/test_models.py @@ -249,6 +249,32 @@ def test_updated_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_handle_logo_upload_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + Project.handle_logo_upload.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"handle_logo_upload" attribute.' + ), + ) + + def test_releases_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + Project.releases.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a "releases"' + f' field via ForeignKey relationship.' + ), + ) + def test_meta_class(self): self.assertTrue( expr=Project._meta.abstract @@ -278,6 +304,19 @@ def test_user_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_project_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectContributor.project.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a "project"' + f' property.' + ), + ) + def test_meta_class(self): self.assertTrue( expr=ProjectContributor._meta.abstract @@ -343,6 +382,19 @@ def test_created_field(self): second='created', ) + def test_handle_image_upload_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectImage.handle_image_upload.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"handle_image_upload" attribute.' + ), + ) + def test_meta_class(self): self.assertTrue( expr=ProjectImage._meta.abstract @@ -438,6 +490,45 @@ def test_created_field(self): second='created', ) + def test_project_class_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectRelease.project_class.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_class" attribute.' + ), + ) + + def test_project_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectRelease.project.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a "project"' + f' property.' + ), + ) + + def test_handle_zip_file_upload_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectRelease.handle_zip_file_upload.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"handle_zip_file_upload" attribute.' + ), + ) + def test_meta_class(self): self.assertTrue( expr=ProjectRelease._meta.abstract diff --git a/project_manager/packages/tests/__init__.py b/project_manager/packages/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py new file mode 100644 index 00000000..af5f2f7e --- /dev/null +++ b/project_manager/packages/tests/test_models.py @@ -0,0 +1,804 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from datetime import timedelta +from random import randint, sample +from unittest import mock + +# Django +from django.core.exceptions import ValidationError +from django.db import models +from django.test import TestCase +from django.urls import reverse +from django.utils.timezone import now + +# App +from games.models import Game +from project_manager.common.constants import ( + FORUM_THREAD_URL, + LOGO_MAX_HEIGHT, + LOGO_MAX_WIDTH, + PROJECT_BASENAME_MAX_LENGTH, + PROJECT_SLUG_MAX_LENGTH, +) +from project_manager.common.models import ( + Project, + ProjectContributor, + ProjectGame, + ProjectImage, + ProjectRelease, + ProjectReleaseDownloadRequirement, + ProjectReleasePackageRequirement, + ProjectReleasePyPiRequirement, + ProjectReleaseVersionControlRequirement, + ProjectTag, +) +from project_manager.common.validators import basename_validator +from project_manager.packages.constants import PACKAGE_LOGO_URL +from project_manager.packages.helpers import ( + handle_package_image_upload, + handle_package_logo_upload, + handle_package_zip_upload, +) +from project_manager.packages.models import ( + Package, + PackageContributor, + PackageGame, + PackageImage, + PackageRelease, + PackageReleaseDownloadRequirement, + PackageReleasePackageRequirement, + PackageReleasePyPiRequirement, + PackageReleaseVersionControlRequirement, + PackageTag, +) +from project_manager.packages.models.abstract import ( + PackageReleaseThroughBase, + PackageThroughBase, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from tags.models import Tag +from test_utils.factories.packages import ( + PackageContributorFactory, + PackageFactory, + PackageGameFactory, + PackageReleaseFactory, + PackageReleaseDownloadRequirementFactory, + PackageReleasePackageRequirementFactory, + PackageReleasePyPiRequirementFactory, + PackageReleaseVersionControlRequirementFactory, + PackageTagFactory, +) +from test_utils.factories.requirements import ( + DownloadRequirementFactory, + PyPiRequirementFactory, + VersionControlRequirementFactory, +) +from test_utils.factories.users import ForumUserFactory +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageReleaseThroughBaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageReleaseThroughBase, models.Model) + ) + + def test_id_field(self): + field = PackageReleaseThroughBase._meta.get_field('package_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.PackageRelease', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=PackageReleaseThroughBase._meta.abstract + ) + + +class PackageThroughBaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageThroughBase, models.Model) + ) + + def test_id_field(self): + field = PackageThroughBase._meta.get_field('package') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.Package', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=PackageThroughBase._meta.abstract + ) + + +class PackageTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(Package, Project) + ) + + def test_basename_field(self): + field = Package._meta.get_field('basename') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_BASENAME_MAX_LENGTH, + ) + self.assertIn( + member=basename_validator, + container=field.validators, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_contributors_field(self): + field = Package._meta.get_field('contributors') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='package_contributions', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageContributor, + ) + + def test_slug_field(self): + field = Package._meta.get_field('slug') + self.assertIsInstance( + obj=field, + cls=models.SlugField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_SLUG_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.primary_key) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_supported_games_field(self): + field = Package._meta.get_field('supported_games') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Game, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='packages', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageGame, + ) + + def test_tags_field(self): + field = Package._meta.get_field('tags') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Tag, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='packages', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageTag, + ) + + def test_primary_attributes(self): + self.assertEqual( + first=Package.handle_logo_upload, + second=handle_package_logo_upload, + ) + self.assertEqual( + first=Package.logo_path, + second=PACKAGE_LOGO_URL, + ) + + def test__str__(self): + package = PackageFactory() + self.assertEqual( + first=str(package), + second=package.name, + ) + + def test_current_version(self): + package = PackageFactory() + created = now() + for n, version in enumerate([ + '1.0.0', + '1.0.1', + '1.1.0', + '1.0.9', + ]): + release = PackageReleaseFactory( + package=package, + version=version, + created=created + timedelta(seconds=n), + ) + self.assertEqual( + first=package.current_version, + second=release.version, + ) + + def test_total_downloads(self): + package = PackageFactory() + total_downloads = 0 + for _ in range(randint(3, 7)): + download_count = randint(1, 20) + total_downloads += download_count + PackageReleaseFactory( + package=package, + download_count=download_count, + ) + + self.assertEqual( + first=package.total_downloads, + second=total_downloads, + ) + + @mock.patch( + target='project_manager.common.models.Image.open', + ) + def test_clean_logo(self, mock_image_open): + Package().clean() + mock_image_open.return_value.size = ( + LOGO_MAX_WIDTH, + LOGO_MAX_HEIGHT, + ) + Package(logo='test.jpg').clean() + + mock_image_open.return_value.size = ( + LOGO_MAX_WIDTH + 1, + LOGO_MAX_HEIGHT + 1, + ) + with self.assertRaises(ValidationError) as context: + Package(logo='test.jpg').clean() + + self.assertEqual( + first=len(context.exception.messages), + second=2, + ) + self.assertIn( + member=f'Logo width must be no more than {LOGO_MAX_WIDTH}.', + container=context.exception.messages, + ) + self.assertIn( + member=f'Logo height must be no more than {LOGO_MAX_HEIGHT}.', + container=context.exception.messages, + ) + + @mock.patch( + target='project_manager.common.models.settings.MEDIA_ROOT', + ) + def test_save(self, mock_media_root): + basename = 'test' + mock_obj = mock.Mock( + stem=basename, + ) + mock_media_root.__truediv__.return_value.files.return_value = [mock_obj] + PackageFactory( + basename=basename, + logo='test.jpg', + ) + mock_obj.remove.assert_called_once_with() + + def test_get_forum_url(https://github.com/self): + package = PackageFactory() + self.assertIsNone(obj=package.get_forum_url()) + + topic = randint(1, 40) + package = PackageFactory( + topic=topic, + ) + self.assertEqual( + first=package.get_forum_url(), + second=FORUM_THREAD_URL.format(topic=topic), + ) + + def test_meta_class(self): + self.assertTrue(issubclass(Package.Meta, Project.Meta)) + self.assertEqual( + first=Package._meta.verbose_name, + second='Package', + ) + self.assertEqual( + first=Package._meta.verbose_name_plural, + second='Packages', + ) + + +class PackageReleaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageRelease, ProjectRelease) + ) + + def test_package_field(self): + field = PackageRelease._meta.get_field('package') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='releases', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_requirements_field(self): + field = PackageRelease._meta.get_field('download_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=DownloadRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_package_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageReleaseDownloadRequirement, + ) + + def test_package_requirements_field(self): + field = PackageRelease._meta.get_field('package_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_package_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageReleasePackageRequirement, + ) + + def test_pypi_requirements_field(self): + field = PackageRelease._meta.get_field('pypi_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=PyPiRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_package_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageReleasePyPiRequirement, + ) + + def test_vcs_requirements_field(self): + field = PackageRelease._meta.get_field('vcs_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=VersionControlRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_package_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PackageReleaseVersionControlRequirement, + ) + + def test_primary_attributes(self): + self.assertEqual( + first=PackageRelease.handle_zip_file_upload, + second=handle_package_zip_upload, + ) + self.assertEqual( + first=PackageRelease.project_class, + second=Package, + ) + + def test_file_name(self): + file_name = 'test.zip' + release = PackageReleaseFactory( + zip_file=f'directory/path/{file_name}', + ) + self.assertEqual( + first=release.file_name, + second=file_name, + ) + + def test__str__(self): + release = PackageReleaseFactory() + self.assertEqual( + first=str(release), + second=f'{release.project} - {release.version}', + ) + + def test_save(self): + original_updated = now() + package = PackageFactory( + created=original_updated, + updated=original_updated, + ) + release_created = original_updated + timedelta(seconds=1) + PackageReleaseFactory( + pk=None, + package=package, + created=release_created, + version='1.0.0', + ) + self.assertEqual( + first=Package.objects.get(pk=package.pk).updated, + second=release_created, + ) + + def test_get_absolute_url(https://github.com/self): + release = PackageReleaseFactory(zip_file='/test/this.py') + self.assertEqual( + first=release.get_absolute_url(), + second=reverse( + viewname='package-download', + kwargs={ + 'slug': release.package.slug, + 'zip_file': release.file_name, + }, + ), + ) + + def test_meta_class(self): + self.assertTrue(issubclass(PackageRelease.Meta, ProjectRelease.Meta)) + self.assertTupleEqual( + tuple1=PackageRelease._meta.unique_together, + tuple2=(('package', 'version'),), + ) + + +class PackageImageTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageImage, ProjectImage) + ) + + def test_package_field(self): + field = PackageImage._meta.get_field('package') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='images', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_primary_attributes(self): + self.assertEqual( + first=PackageImage.handle_image_upload, + second=handle_package_image_upload, + ) + + +class PackageContributorTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageContributor, ProjectContributor) + ) + self.assertTrue( + expr=issubclass(PackageContributor, PackageThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(PackageContributorFactory()), + second='Project Contributor', + ) + + def test_clean(self): + owner = ForumUserFactory() + contributor = ForumUserFactory() + package = PackageFactory(owner=owner) + PackageContributor( + user=contributor, + package=package, + ).clean() + + with self.assertRaises(ValidationError) as context: + PackageContributor( + user=owner, + package=package, + ).clean() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='user', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['user']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['user'][0], + second=( + f'{owner} is the owner and cannot be added as a contributor.' + ), + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageContributor._meta.unique_together, + tuple2=(('package', 'user'),), + ) + + +class PackageGameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageGame, ProjectGame) + ) + self.assertTrue( + expr=issubclass(PackageGame, PackageThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(PackageGameFactory()), + second='Project Game', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageGame._meta.unique_together, + tuple2=(('package', 'game'),), + ) + + +class PackageTagTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PackageTag, ProjectTag) + ) + self.assertTrue( + expr=issubclass(PackageTag, PackageThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(PackageTagFactory()), + second='Project Tag', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageTag._meta.unique_together, + tuple2=(('package', 'tag'),), + ) + + +class PackageReleaseDownloadRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleaseDownloadRequirement, + ProjectReleaseDownloadRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PackageReleaseDownloadRequirement, + PackageReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = DownloadRequirementFactory() + self.assertEqual( + first=str( + PackageReleaseDownloadRequirementFactory( + download_requirement=requirement, + ) + ), + second=requirement.url, + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageReleaseDownloadRequirement._meta.unique_together, + tuple2=(('package_release', 'download_requirement'),), + ) + + +class PackageReleasePackageRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleasePackageRequirement, + ProjectReleasePackageRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PackageReleasePackageRequirement, + PackageReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = PackageFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + PackageReleasePackageRequirementFactory( + package_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.name} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageReleasePackageRequirement._meta.unique_together, + tuple2=(('package_release', 'package_requirement'),), + ) + + +class PackageReleasePypiRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleasePyPiRequirement, + ProjectReleasePyPiRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PackageReleasePyPiRequirement, + PackageReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = PyPiRequirementFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + PackageReleasePyPiRequirementFactory( + pypi_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.name} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageReleasePyPiRequirement._meta.unique_together, + tuple2=(('package_release', 'pypi_requirement'),), + ) + + +class PackageReleaseVersionControlRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleaseVersionControlRequirement, + ProjectReleaseVersionControlRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PackageReleaseVersionControlRequirement, + PackageReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = VersionControlRequirementFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + PackageReleaseVersionControlRequirementFactory( + vcs_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.url} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PackageReleaseVersionControlRequirement._meta.unique_together, + tuple2=(('package_release', 'vcs_requirement'),), + ) diff --git a/project_manager/plugins/tests/__init__.py b/project_manager/plugins/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py new file mode 100644 index 00000000..51b84b52 --- /dev/null +++ b/project_manager/plugins/tests/test_models.py @@ -0,0 +1,927 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from datetime import timedelta +from random import randint, sample +from unittest import mock + +# Django +from django.core.exceptions import ValidationError +from django.db import models +from django.test import TestCase +from django.urls import reverse +from django.utils.timezone import now + +# App +from games.models import Game +from project_manager.common.constants import ( + FORUM_THREAD_URL, + LOGO_MAX_HEIGHT, + LOGO_MAX_WIDTH, + PROJECT_BASENAME_MAX_LENGTH, + PROJECT_SLUG_MAX_LENGTH, +) +from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, + Project, + ProjectContributor, + ProjectGame, + ProjectImage, + ProjectRelease, + ProjectReleaseDownloadRequirement, + ProjectReleasePackageRequirement, + ProjectReleasePyPiRequirement, + ProjectReleaseVersionControlRequirement, + ProjectTag, +) +from project_manager.common.validators import basename_validator +from project_manager.packages.models import Package +from project_manager.plugins.constants import PLUGIN_LOGO_URL, PATH_MAX_LENGTH +from project_manager.plugins.helpers import ( + handle_plugin_image_upload, + handle_plugin_logo_upload, + handle_plugin_zip_upload, +) +from project_manager.plugins.models import ( + Plugin, + PluginContributor, + PluginGame, + PluginImage, + PluginRelease, + PluginReleaseDownloadRequirement, + PluginReleasePackageRequirement, + PluginReleasePyPiRequirement, + PluginReleaseVersionControlRequirement, + PluginTag, + SubPluginPath, +) +from project_manager.plugins.models.abstract import ( + PluginReleaseThroughBase, + PluginThroughBase, +) +from project_manager.plugins.validators import sub_plugin_path_validator +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from tags.models import Tag +from test_utils.factories.packages import PackageFactory +from test_utils.factories.plugins import ( + PluginContributorFactory, + PluginFactory, + PluginGameFactory, + PluginReleaseFactory, + PluginReleaseDownloadRequirementFactory, + PluginReleasePackageRequirementFactory, + PluginReleasePyPiRequirementFactory, + PluginReleaseVersionControlRequirementFactory, + PluginTagFactory, + SubPluginPathFactory, +) +from test_utils.factories.requirements import ( + DownloadRequirementFactory, + PyPiRequirementFactory, + VersionControlRequirementFactory, +) +from test_utils.factories.users import ForumUserFactory +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginReleaseThroughBaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginReleaseThroughBase, models.Model) + ) + + def test_id_field(self): + field = PluginReleaseThroughBase._meta.get_field('plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.PluginRelease', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=PluginReleaseThroughBase._meta.abstract + ) + + +class PluginThroughBaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginThroughBase, models.Model) + ) + + def test_id_field(self): + field = PluginThroughBase._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.Plugin', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=PluginThroughBase._meta.abstract + ) + + +class PluginTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(Plugin, Project) + ) + + def test_basename_field(self): + field = Plugin._meta.get_field('basename') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_BASENAME_MAX_LENGTH, + ) + self.assertIn( + member=basename_validator, + container=field.validators, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_contributors_field(self): + field = Plugin._meta.get_field('contributors') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='plugin_contributions', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginContributor, + ) + + def test_slug_field(self): + field = Plugin._meta.get_field('slug') + self.assertIsInstance( + obj=field, + cls=models.SlugField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_SLUG_MAX_LENGTH, + ) + self.assertTrue(expr=field.unique) + self.assertTrue(expr=field.primary_key) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_supported_games_field(self): + field = Plugin._meta.get_field('supported_games') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Game, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='plugins', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginGame, + ) + + def test_tags_field(self): + field = Plugin._meta.get_field('tags') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Tag, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='plugins', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginTag, + ) + + def test_primary_attributes(self): + self.assertEqual( + first=Plugin.handle_logo_upload, + second=handle_plugin_logo_upload + ) + self.assertEqual( + first=Plugin.logo_path, + second=PLUGIN_LOGO_URL, + ) + + def test__str__(self): + plugin = PluginFactory() + self.assertEqual( + first=str(plugin), + second=plugin.name, + ) + + def test_current_version(self): + plugin = PluginFactory() + created = now() + for n, version in enumerate([ + '1.0.0', + '1.0.1', + '1.1.0', + '1.0.9', + ]): + release = PluginReleaseFactory( + plugin=plugin, + version=version, + created=created + timedelta(seconds=n), + ) + self.assertEqual( + first=plugin.current_version, + second=release.version, + ) + + def test_total_downloads(self): + plugin = PluginFactory() + total_downloads = 0 + for _ in range(randint(3, 7)): + download_count = randint(1, 20) + total_downloads += download_count + PluginReleaseFactory( + plugin=plugin, + download_count=download_count, + ) + + self.assertEqual( + first=plugin.total_downloads, + second=total_downloads, + ) + + @mock.patch( + target='project_manager.common.models.Image.open', + ) + def test_clean_logo(self, mock_image_open): + Plugin().clean() + mock_image_open.return_value.size = ( + LOGO_MAX_WIDTH, + LOGO_MAX_HEIGHT, + ) + Plugin(logo='test.jpg').clean() + + mock_image_open.return_value.size = ( + LOGO_MAX_WIDTH + 1, + LOGO_MAX_HEIGHT + 1, + ) + with self.assertRaises(ValidationError) as context: + Plugin(logo='test.jpg').clean() + + self.assertEqual( + first=len(context.exception.messages), + second=2, + ) + self.assertIn( + member=f'Logo width must be no more than {LOGO_MAX_WIDTH}.', + container=context.exception.messages, + ) + self.assertIn( + member=f'Logo height must be no more than {LOGO_MAX_HEIGHT}.', + container=context.exception.messages, + ) + + @mock.patch( + target='project_manager.common.models.settings.MEDIA_ROOT', + ) + def test_save(self, mock_media_root): + basename = 'test' + mock_obj = mock.Mock( + stem=basename, + ) + mock_media_root.__truediv__.return_value.files.return_value = [mock_obj] + PluginFactory( + basename=basename, + logo='test.jpg', + ) + mock_obj.remove.assert_called_once_with() + + def test_get_forum_url(https://github.com/self): + plugin = PluginFactory() + self.assertIsNone(obj=plugin.get_forum_url()) + + topic = randint(1, 40) + plugin = PluginFactory( + topic=topic, + ) + self.assertEqual( + first=plugin.get_forum_url(), + second=FORUM_THREAD_URL.format(topic=topic), + ) + + def test_meta_class(self): + self.assertTrue(issubclass(Plugin.Meta, Project.Meta)) + self.assertEqual( + first=Plugin._meta.verbose_name, + second='Plugin', + ) + self.assertEqual( + first=Plugin._meta.verbose_name_plural, + second='Plugins', + ) + + +class PluginReleaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginRelease, ProjectRelease) + ) + + def test_plugin_field(self): + field = PluginRelease._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='releases', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_requirements_field(self): + field = PluginRelease._meta.get_field('download_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=DownloadRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginReleaseDownloadRequirement, + ) + + def test_package_requirements_field(self): + field = PluginRelease._meta.get_field('package_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginReleasePackageRequirement, + ) + + def test_pypi_requirements_field(self): + field = PluginRelease._meta.get_field('pypi_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=PyPiRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginReleasePyPiRequirement, + ) + + def test_vcs_requirements_field(self): + field = PluginRelease._meta.get_field('vcs_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=VersionControlRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=PluginReleaseVersionControlRequirement, + ) + + def test_primary_attributes(self): + self.assertEqual( + first=PluginRelease.handle_zip_file_upload, + second=handle_plugin_zip_upload, + ) + self.assertEqual( + first=PluginRelease.project_class, + second=Plugin, + ) + + def test_file_name(self): + file_name = 'test.zip' + release = PluginReleaseFactory( + zip_file=f'directory/path/{file_name}', + ) + self.assertEqual( + first=release.file_name, + second=file_name, + ) + + def test__str__(self): + release = PluginReleaseFactory() + self.assertEqual( + first=str(release), + second=f'{release.project} - {release.version}', + ) + + def test_save(self): + original_updated = now() + plugin = PluginFactory( + created=original_updated, + updated=original_updated, + ) + release_created = original_updated + timedelta(seconds=1) + PluginReleaseFactory( + pk=None, + plugin=plugin, + created=release_created, + version='1.0.0', + ) + self.assertEqual( + first=Plugin.objects.get(pk=plugin.pk).updated, + second=release_created, + ) + + def test_get_absolute_url(https://github.com/self): + release = PluginReleaseFactory(zip_file='/test/this.py') + self.assertEqual( + first=release.get_absolute_url(), + second=reverse( + viewname='plugin-download', + kwargs={ + 'slug': release.plugin.slug, + 'zip_file': release.file_name, + }, + ), + ) + + def test_meta_class(self): + self.assertTrue(issubclass(PluginRelease.Meta, ProjectRelease.Meta)) + self.assertTupleEqual( + tuple1=PluginRelease._meta.unique_together, + tuple2=(('plugin', 'version'),), + ) + + +class PluginImageTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginImage, ProjectImage) + ) + + def test_plugin_field(self): + field = PluginImage._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='images', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_primary_attributes(self): + self.assertEqual( + first=PluginImage.handle_image_upload, + second=handle_plugin_image_upload, + ) + + +class PluginContributorTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginContributor, ProjectContributor) + ) + self.assertTrue( + expr=issubclass(PluginContributor, PluginThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(PluginContributorFactory()), + second='Project Contributor', + ) + + def test_clean(self): + owner = ForumUserFactory() + contributor = ForumUserFactory() + plugin = PluginFactory(owner=owner) + PluginContributor( + user=contributor, + plugin=plugin, + ).clean() + + with self.assertRaises(ValidationError) as context: + PluginContributor( + user=owner, + plugin=plugin, + ).clean() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='user', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['user']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['user'][0], + second=( + f'{owner} is the owner and cannot be added as a contributor.' + ), + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginContributor._meta.unique_together, + tuple2=(('plugin', 'user'),), + ) + + +class PluginGameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginGame, ProjectGame) + ) + self.assertTrue( + expr=issubclass(PluginGame, PluginThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(PluginGameFactory()), + second='Project Game', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginGame._meta.unique_together, + tuple2=(('plugin', 'game'),), + ) + + +class PluginTagTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(PluginTag, ProjectTag) + ) + self.assertTrue( + expr=issubclass(PluginTag, PluginThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(PluginTagFactory()), + second='Project Tag', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginTag._meta.unique_together, + tuple2=(('plugin', 'tag'),), + ) + + +class PluginReleaseDownloadRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleaseDownloadRequirement, + ProjectReleaseDownloadRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PluginReleaseDownloadRequirement, + PluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = DownloadRequirementFactory() + self.assertEqual( + first=str( + PluginReleaseDownloadRequirementFactory( + download_requirement=requirement, + ) + ), + second=requirement.url, + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginReleaseDownloadRequirement._meta.unique_together, + tuple2=(('plugin_release', 'download_requirement'),), + ) + + +class PluginReleasePackageRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleasePackageRequirement, + ProjectReleasePackageRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PluginReleasePackageRequirement, + PluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = PackageFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + PluginReleasePackageRequirementFactory( + package_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.name} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginReleasePackageRequirement._meta.unique_together, + tuple2=(('plugin_release', 'package_requirement'),), + ) + + +class PluginReleasePypiRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleasePyPiRequirement, + ProjectReleasePyPiRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PluginReleasePyPiRequirement, + PluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = PyPiRequirementFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + PluginReleasePyPiRequirementFactory( + pypi_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.name} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginReleasePyPiRequirement._meta.unique_together, + tuple2=(('plugin_release', 'pypi_requirement'),), + ) + + +class PluginReleaseVersionControlRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleaseVersionControlRequirement, + ProjectReleaseVersionControlRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + PluginReleaseVersionControlRequirement, + PluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = VersionControlRequirementFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + PluginReleaseVersionControlRequirementFactory( + vcs_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.url} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=PluginReleaseVersionControlRequirement._meta.unique_together, + tuple2=(('plugin_release', 'vcs_requirement'),), + ) + + +class SubPluginPathTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginPath, AbstractUUIDPrimaryKeyModel)) + + def test_plugin_field(self): + field = SubPluginPath._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='paths', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + + def test_path_field(self): + field = SubPluginPath._meta.get_field('path') + self.assertIsInstance(obj=field, cls=models.CharField) + self.assertEqual( + first=field.max_length, + second=PATH_MAX_LENGTH, + ) + self.assertIn( + member=sub_plugin_path_validator, + container=field.validators, + ) + + def test_allow_module_field(self): + field = SubPluginPath._meta.get_field('allow_module') + self.assertIsInstance(obj=field, cls=models.BooleanField) + self.assertFalse(expr=field.default) + + def test_allow_package_using_basename_field(self): + field = SubPluginPath._meta.get_field('allow_package_using_basename') + self.assertIsInstance(obj=field, cls=models.BooleanField) + self.assertFalse(expr=field.default) + + def test_allow_package_using_init_field(self): + field = SubPluginPath._meta.get_field('allow_package_using_init') + self.assertIsInstance(obj=field, cls=models.BooleanField) + self.assertFalse(expr=field.default) + + def test__str__(self): + path = SubPluginPathFactory() + self.assertEqual( + first=str(path), + second=path.path, + ) + + def test_clean(self): + SubPluginPath( + allow_module=True, + allow_package_using_basename=False, + allow_package_using_init=False, + ).clean() + SubPluginPath( + allow_module=False, + allow_package_using_basename=True, + allow_package_using_init=False, + ).clean() + SubPluginPath( + allow_module=False, + allow_package_using_basename=False, + allow_package_using_init=True, + ).clean() + with self.assertRaises(ValidationError) as context: + SubPluginPath( + allow_module=False, + allow_package_using_basename=False, + allow_package_using_init=False, + ).clean() + + self.assertEqual( + first=len(context.exception.message_dict), + second=3, + ) + for attribute in ( + 'allow_module', + 'allow_package_using_basename', + 'allow_package_using_init', + ): + self.assertIn( + member=attribute, + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict[attribute]), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict[attribute][0], + second='At least one of the "Allow" fields must be True.', + ) + + def test_meta_class(self): + self.assertEqual( + first=SubPluginPath._meta.verbose_name, + second='SubPlugin Path', + ) + self.assertEqual( + first=SubPluginPath._meta.verbose_name_plural, + second='SubPlugin Paths', + ) + self.assertTupleEqual( + tuple1=SubPluginPath._meta.unique_together, + tuple2=(('path', 'plugin'),), + ) diff --git a/project_manager/sub_plugins/tests/__init__.py b/project_manager/sub_plugins/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py new file mode 100644 index 00000000..125dda4c --- /dev/null +++ b/project_manager/sub_plugins/tests/test_models.py @@ -0,0 +1,835 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from datetime import timedelta +from random import randint, sample +from unittest import mock + +# Django +from django.core.exceptions import ValidationError +from django.db import models +from django.test import TestCase +from django.urls import reverse +from django.utils.timezone import now + +# App +from games.models import Game +from project_manager.common.constants import ( + FORUM_THREAD_URL, + LOGO_MAX_HEIGHT, + LOGO_MAX_WIDTH, + PROJECT_BASENAME_MAX_LENGTH, + PROJECT_SLUG_MAX_LENGTH, +) +from project_manager.common.models import ( + Project, + ProjectContributor, + ProjectGame, + ProjectImage, + ProjectRelease, + ProjectReleaseDownloadRequirement, + ProjectReleasePackageRequirement, + ProjectReleasePyPiRequirement, + ProjectReleaseVersionControlRequirement, + ProjectTag, +) +from project_manager.common.validators import basename_validator +from project_manager.packages.models import Package +from project_manager.plugins.models import Plugin +from project_manager.sub_plugins.constants import SUB_PLUGIN_LOGO_URL +from project_manager.sub_plugins.helpers import ( + handle_sub_plugin_image_upload, + handle_sub_plugin_logo_upload, + handle_sub_plugin_zip_upload, +) +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginContributor, + SubPluginGame, + SubPluginImage, + SubPluginRelease, + SubPluginReleaseDownloadRequirement, + SubPluginReleasePackageRequirement, + SubPluginReleasePyPiRequirement, + SubPluginReleaseVersionControlRequirement, + SubPluginTag, +) +from project_manager.sub_plugins.models.abstract import ( + SubPluginReleaseThroughBase, + SubPluginThroughBase, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from tags.models import Tag +from test_utils.factories.packages import PackageFactory +from test_utils.factories.requirements import ( + DownloadRequirementFactory, + PyPiRequirementFactory, + VersionControlRequirementFactory, +) +from test_utils.factories.sub_plugins import ( + SubPluginContributorFactory, + SubPluginFactory, + SubPluginGameFactory, + SubPluginReleaseFactory, + SubPluginReleaseDownloadRequirementFactory, + SubPluginReleasePackageRequirementFactory, + SubPluginReleasePyPiRequirementFactory, + SubPluginReleaseVersionControlRequirementFactory, + SubPluginTagFactory, +) +from test_utils.factories.users import ForumUserFactory +from users.models import ForumUser + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginReleaseThroughBaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginReleaseThroughBase, models.Model) + ) + + def test_id_field(self): + field = SubPluginReleaseThroughBase._meta.get_field('sub_plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.SubPluginRelease', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=SubPluginReleaseThroughBase._meta.abstract + ) + + +class SubPluginThroughBaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginThroughBase, models.Model) + ) + + def test_id_field(self): + field = SubPluginThroughBase._meta.get_field('sub_plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='project_manager.SubPlugin', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_meta_class(self): + self.assertTrue( + expr=SubPluginThroughBase._meta.abstract + ) + + +class SubPluginTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPlugin, Project) + ) + + def test_basename_field(self): + field = SubPlugin._meta.get_field('basename') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_BASENAME_MAX_LENGTH, + ) + self.assertIn( + member=basename_validator, + container=field.validators, + ) + self.assertFalse(expr=field.unique) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_contributors_field(self): + field = SubPlugin._meta.get_field('contributors') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='subplugin_contributions', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginContributor, + ) + + def test_slug_field(self): + field = SubPlugin._meta.get_field('slug') + self.assertIsInstance( + obj=field, + cls=models.SlugField, + ) + self.assertEqual( + first=field.max_length, + second=PROJECT_SLUG_MAX_LENGTH, + ) + self.assertFalse(expr=field.unique) + self.assertFalse(expr=field.primary_key) + self.assertTrue(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_plugin_field(self): + field = SubPlugin._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='sub_plugins', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + + def test_supported_games_field(self): + field = SubPlugin._meta.get_field('supported_games') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Game, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='subplugins', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginGame, + ) + + def test_tags_field(self): + field = SubPlugin._meta.get_field('tags') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Tag, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='subplugins', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginTag, + ) + + def test_primary_attributes(self): + self.assertEqual( + first=SubPlugin.handle_logo_upload, + second=handle_sub_plugin_logo_upload + ) + self.assertEqual( + first=SubPlugin.logo_path, + second=SUB_PLUGIN_LOGO_URL, + ) + + def test__str__(self): + sub_plugin = SubPluginFactory() + self.assertEqual( + first=str(sub_plugin), + second=f'{sub_plugin.plugin.name}: {sub_plugin.name}', + ) + + def test_current_version(self): + sub_plugin = SubPluginFactory() + created = now() + for n, version in enumerate([ + '1.0.0', + '1.0.1', + '1.1.0', + '1.0.9', + ]): + release = SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version=version, + created=created + timedelta(seconds=n), + ) + self.assertEqual( + first=sub_plugin.current_version, + second=release.version, + ) + + def test_total_downloads(self): + sub_plugin = SubPluginFactory() + total_downloads = 0 + for _ in range(randint(3, 7)): + download_count = randint(1, 20) + total_downloads += download_count + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + download_count=download_count, + ) + + self.assertEqual( + first=sub_plugin.total_downloads, + second=total_downloads, + ) + + @mock.patch( + target='project_manager.common.models.Image.open', + ) + def test_clean_logo(self, mock_image_open): + SubPlugin().clean() + mock_image_open.return_value.size = ( + LOGO_MAX_WIDTH, + LOGO_MAX_HEIGHT, + ) + SubPlugin(logo='test.jpg').clean() + + mock_image_open.return_value.size = ( + LOGO_MAX_WIDTH + 1, + LOGO_MAX_HEIGHT + 1, + ) + with self.assertRaises(ValidationError) as context: + SubPlugin(logo='test.jpg').clean() + + self.assertEqual( + first=len(context.exception.messages), + second=2, + ) + self.assertIn( + member=f'Logo width must be no more than {LOGO_MAX_WIDTH}.', + container=context.exception.messages, + ) + self.assertIn( + member=f'Logo height must be no more than {LOGO_MAX_HEIGHT}.', + container=context.exception.messages, + ) + + @mock.patch( + target='project_manager.common.models.settings.MEDIA_ROOT', + ) + def test_save(self, mock_media_root): + basename = 'test' + mock_obj = mock.Mock( + stem=basename, + ) + mock_media_root.__truediv__.return_value.files.return_value = [mock_obj] + SubPluginFactory( + basename=basename, + logo='test.jpg', + ) + mock_obj.remove.assert_called_once_with() + + def test_get_forum_url(https://github.com/self): + sub_plugin = SubPluginFactory() + self.assertIsNone(obj=sub_plugin.get_forum_url()) + + topic = randint(1, 40) + sub_plugin = SubPluginFactory( + topic=topic, + ) + self.assertEqual( + first=sub_plugin.get_forum_url(), + second=FORUM_THREAD_URL.format(topic=topic), + ) + + def test_meta_class(self): + self.assertTrue(issubclass(SubPlugin.Meta, Project.Meta)) + self.assertEqual( + first=SubPlugin._meta.verbose_name, + second='SubPlugin', + ) + self.assertEqual( + first=SubPlugin._meta.verbose_name_plural, + second='SubPlugins', + ) + self.assertTupleEqual( + tuple1=SubPlugin._meta.unique_together, + tuple2=( + ('plugin', 'basename'), + ('plugin', 'name'), + ('plugin', 'slug'), + ) + ) + + +class SubPluginReleaseTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginRelease, ProjectRelease) + ) + + def test_plugin_field(self): + field = SubPluginRelease._meta.get_field('sub_plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPlugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='releases', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_requirements_field(self): + field = SubPluginRelease._meta.get_field('download_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=DownloadRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_sub_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginReleaseDownloadRequirement, + ) + + def test_package_requirements_field(self): + field = SubPluginRelease._meta.get_field('package_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_sub_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginReleasePackageRequirement, + ) + + def test_pypi_requirements_field(self): + field = SubPluginRelease._meta.get_field('pypi_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=PyPiRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_sub_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginReleasePyPiRequirement, + ) + + def test_vcs_requirements_field(self): + field = SubPluginRelease._meta.get_field('vcs_requirements') + self.assertIsInstance( + obj=field, + cls=models.ManyToManyField, + ) + self.assertEqual( + first=field.remote_field.model, + second=VersionControlRequirement, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='required_in_sub_plugin_releases', + ) + self.assertEqual( + first=field.remote_field.through, + second=SubPluginReleaseVersionControlRequirement, + ) + + def test_primary_attributes(self): + self.assertEqual( + first=SubPluginRelease.handle_zip_file_upload, + second=handle_sub_plugin_zip_upload, + ) + self.assertEqual( + first=SubPluginRelease.project_class, + second=SubPlugin, + ) + + def test_file_name(self): + file_name = 'test.zip' + release = SubPluginReleaseFactory( + zip_file=f'directory/path/{file_name}', + ) + self.assertEqual( + first=release.file_name, + second=file_name, + ) + + def test__str__(self): + release = SubPluginReleaseFactory() + self.assertEqual( + first=str(release), + second=f'{release.project} - {release.version}', + ) + + def test_save(self): + original_updated = now() + sub_plugin = SubPluginFactory( + created=original_updated, + updated=original_updated, + ) + release_created = original_updated + timedelta(seconds=1) + SubPluginReleaseFactory( + pk=None, + sub_plugin=sub_plugin, + created=release_created, + version='1.0.0', + ) + self.assertEqual( + first=SubPlugin.objects.get(pk=sub_plugin.pk).updated, + second=release_created, + ) + + def test_get_absolute_url(https://github.com/self): + release = SubPluginReleaseFactory(zip_file='/test/this.py') + self.assertEqual( + first=release.get_absolute_url(), + second=reverse( + viewname='sub-plugin-download', + kwargs={ + 'slug': release.sub_plugin.plugin.slug, + 'sub_plugin_slug': release.sub_plugin.slug, + 'zip_file': release.file_name, + }, + ), + ) + + def test_meta_class(self): + self.assertTrue(issubclass(SubPluginRelease.Meta, ProjectRelease.Meta)) + self.assertTupleEqual( + tuple1=SubPluginRelease._meta.unique_together, + tuple2=(('sub_plugin', 'version'),), + ) + + +class SubPluginImageTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginImage, ProjectImage) + ) + + def test_plugin_field(self): + field = SubPluginImage._meta.get_field('sub_plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPlugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='images', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_primary_attributes(self): + self.assertEqual( + first=SubPluginImage.handle_image_upload, + second=handle_sub_plugin_image_upload, + ) + + +class SubPluginContributorTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginContributor, ProjectContributor) + ) + self.assertTrue( + expr=issubclass(SubPluginContributor, SubPluginThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(SubPluginContributorFactory()), + second='Project Contributor', + ) + + def test_clean(self): + owner = ForumUserFactory() + contributor = ForumUserFactory() + sub_plugin = SubPluginFactory(owner=owner) + SubPluginContributor( + user=contributor, + sub_plugin=sub_plugin, + ).clean() + + with self.assertRaises(ValidationError) as context: + SubPluginContributor( + user=owner, + sub_plugin=sub_plugin, + ).clean() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='user', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['user']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['user'][0], + second=( + f'{owner} is the owner and cannot be added as a contributor.' + ), + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginContributor._meta.unique_together, + tuple2=(('sub_plugin', 'user'),), + ) + + +class SubPluginGameTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginGame, ProjectGame) + ) + self.assertTrue( + expr=issubclass(SubPluginGame, SubPluginThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(SubPluginGameFactory()), + second='Project Game', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginGame._meta.unique_together, + tuple2=(('sub_plugin', 'game'),), + ) + + +class SubPluginTagTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginTag, ProjectTag) + ) + self.assertTrue( + expr=issubclass(SubPluginTag, SubPluginThroughBase) + ) + + def test__str__(self): + self.assertEqual( + first=str(SubPluginTagFactory()), + second='Project Tag', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginTag._meta.unique_together, + tuple2=(('sub_plugin', 'tag'),), + ) + + +class SubPluginReleaseDownloadRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseDownloadRequirement, + ProjectReleaseDownloadRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + SubPluginReleaseDownloadRequirement, + SubPluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = DownloadRequirementFactory() + self.assertEqual( + first=str( + SubPluginReleaseDownloadRequirementFactory( + download_requirement=requirement, + ) + ), + second=requirement.url, + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseDownloadRequirement._meta.unique_together, + tuple2=(('sub_plugin_release', 'download_requirement'),), + ) + + +class SubPluginReleasePackageRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleasePackageRequirement, + ProjectReleasePackageRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + SubPluginReleasePackageRequirement, + SubPluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = PackageFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + SubPluginReleasePackageRequirementFactory( + package_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.name} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginReleasePackageRequirement._meta.unique_together, + tuple2=(('sub_plugin_release', 'package_requirement'),), + ) + + +class SubPluginReleasePypiRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleasePyPiRequirement, + ProjectReleasePyPiRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + SubPluginReleasePyPiRequirement, + SubPluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = PyPiRequirementFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + SubPluginReleasePyPiRequirementFactory( + pypi_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.name} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginReleasePyPiRequirement._meta.unique_together, + tuple2=(('sub_plugin_release', 'pypi_requirement'),), + ) + + +class SubPluginReleaseVersionControlRequirementTestCase(TestCase): + def test_model_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseVersionControlRequirement, + ProjectReleaseVersionControlRequirement, + ) + ) + self.assertTrue( + expr=issubclass( + SubPluginReleaseVersionControlRequirement, + SubPluginReleaseThroughBase, + ) + ) + + def test__str__(self): + requirement = VersionControlRequirementFactory() + version = '.'.join(map(str, sample(range(100), 3))) + self.assertEqual( + first=str( + SubPluginReleaseVersionControlRequirementFactory( + vcs_requirement=requirement, + version=version, + ) + ), + second=f'{requirement.url} - {version}', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseVersionControlRequirement._meta.unique_together, + tuple2=(('sub_plugin_release', 'vcs_requirement'),), + ) From 349546698eb4f326e08e2469d983cf3db93cf649 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:29:38 -0400 Subject: [PATCH 033/211] Added fixes for plugin views. --- project_manager/plugins/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index 226ae741..60cf8028 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -75,7 +75,7 @@ def get(self, request): response.data['paths'] = reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'paths/{self.extra_params}<{self.project_type}>/' + ) + f'paths/<{self.project_type}>/' return response @@ -242,6 +242,6 @@ class SubPluginPathViewSet(ProjectThroughModelMixin): ) serializer_class = SubPluginPathSerializer - api_type = 'Sub-Plugin Paths' project_type = 'plugin' project_model = Plugin + related_model_type = 'Sub-Plugin Path' From 9d959abc69aa5fb3bea28ec13a626b48e6cf42ae Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:30:17 -0400 Subject: [PATCH 034/211] Added test for user admin. --- users/tests/test_admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/users/tests/test_admin.py b/users/tests/test_admin.py index 678965e1..ad5fd764 100644 --- a/users/tests/test_admin.py +++ b/users/tests/test_admin.py @@ -74,6 +74,13 @@ def test_has_delete_permission(self): expr=ForumUserAdmin(ForumUser, '').has_delete_permission(''), ) + def test_get_queryset(self): + query = ForumUserAdmin(ForumUser, '').get_queryset('').query + self.assertIn( + member='user', + container=query.select_related, + ) + class UserAdminTestCase(TestCase): From 383981e7d09843f33061af0b1538a36677573d62 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:30:39 -0400 Subject: [PATCH 035/211] Fixed the statistics page queries. --- project_manager/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project_manager/views.py b/project_manager/views.py index d76c40c8..b75de9be 100644 --- a/project_manager/views.py +++ b/project_manager/views.py @@ -34,19 +34,19 @@ def get_context_data(self, **kwargs): """Return all statistical context data.""" context = super().get_context_data(**kwargs) package_downloads = sum( - PackageRelease.objects.all().values_list( + PackageRelease.objects.values_list( 'download_count', flat=True, ) ) plugin_downloads = sum( - PluginRelease.objects.all().values_list( + PluginRelease.objects.values_list( 'download_count', flat=True, ) ) sub_plugin_downloads = sum( - SubPluginRelease.objects.all().values_list( + SubPluginRelease.objects.values_list( 'download_count', flat=True, ) @@ -58,7 +58,7 @@ def get_context_data(self, **kwargs): Q(subplugin_contributions__isnull=False) | Q(packages__isnull=False) | Q(package_contributions__isnull=False) - ).count() + ).distinct().count() packages = Package.objects.count() plugins = Plugin.objects.count() sub_plugins = SubPlugin.objects.count() From 86b1bd49543fce09ba2cafa7776874871fa185f2 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:31:38 -0400 Subject: [PATCH 036/211] Added tests for the statistics page. --- project_manager/tests/test_views.py | 157 ++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 project_manager/tests/test_views.py diff --git a/project_manager/tests/test_views.py b/project_manager/tests/test_views.py new file mode 100644 index 00000000..88f47364 --- /dev/null +++ b/project_manager/tests/test_views.py @@ -0,0 +1,157 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from random import choice, randint, sample + +# Django +from django.test import TestCase +from django.views.generic import TemplateView + +# Third Party Django +from rest_framework import status + +# App +from project_manager.views import StatisticsView +from test_utils.factories.packages import ( + PackageContributorFactory, + PackageFactory, + PackageReleaseFactory, +) +from test_utils.factories.plugins import ( + PluginContributorFactory, + PluginFactory, + PluginReleaseFactory, +) +from test_utils.factories.sub_plugins import ( + SubPluginContributorFactory, + SubPluginFactory, + SubPluginReleaseFactory, +) +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class StatisticsViewTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(StatisticsView, TemplateView), + ) + + def test_template_name(self): + self.assertEqual( + first=StatisticsView.template_name, + second='statistics.html', + ) + + def test_get_view(self): + contributing_users = set() + total_users = randint(20, 30) + user_list = [ForumUserFactory() for _ in range(total_users)] + total_download_count = 0 + + package_download_count = 0 + package_count = randint(2, 8) + for _ in range(package_count): + contributors = sample(user_list, randint(1, 3)) + contributing_users.update(contributors) + owner = contributors.pop() + package = PackageFactory( + owner=owner, + ) + for contributor in contributors: + PackageContributorFactory( + user=contributor, + package=package, + ) + for _ in range(randint(1, 3)): + download_count = randint(1, 20) + package_download_count += download_count + total_download_count += download_count + PackageReleaseFactory( + package=package, + download_count=download_count, + ) + + sub_plugin_download_count = 0 + sub_plugin_count = 0 + plugin_download_count = 0 + plugin_count = randint(4, 8) + for _ in range(plugin_count): + contributors = sample(user_list, randint(1, 3)) + contributing_users.update(contributors) + owner = contributors.pop() + plugin = PluginFactory( + owner=owner, + ) + for contributor in contributors: + PluginContributorFactory( + user=contributor, + plugin=plugin, + ) + for _ in range(randint(1, 3)): + download_count = randint(1, 20) + plugin_download_count += download_count + total_download_count += download_count + PluginReleaseFactory( + plugin=plugin, + download_count=download_count, + ) + + if choice([True, False]): + count = randint(1, 2) + sub_plugin_count += count + for _ in range(count): + contributors = sample(user_list, randint(1, 3)) + contributing_users.update(contributors) + owner = contributors.pop() + sub_plugin = SubPluginFactory( + plugin=plugin, + owner=owner, + ) + for contributor in contributors: + SubPluginContributorFactory( + user=contributor, + sub_plugin=sub_plugin, + ) + for _ in range(randint(1, 3)): + download_count = randint(1, 20) + sub_plugin_download_count += download_count + total_download_count += download_count + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + download_count=download_count, + ) + + response = self.client.get('/statistics/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + data = dict(response.context_data) + del data['view'] + self.assertDictEqual( + d1=data, + d2={ + 'users': len(contributing_users), + 'package_count': package_count, + 'plugin_count': plugin_count, + 'sub_plugin_count': sub_plugin_count, + 'total_projects': sum([ + package_count, + plugin_count, + sub_plugin_count, + ]), + 'package_downloads': package_download_count, + 'plugin_downloads': plugin_download_count, + 'sub_plugin_downloads': sub_plugin_download_count, + 'total_downloads': sum([ + package_download_count, + plugin_download_count, + sub_plugin_download_count, + ]) + } + ) From 11c0120d9b436231093be22fb495e390390af26a Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:32:57 -0400 Subject: [PATCH 037/211] Added more tests. --- project_manager/common/api/tests/__init__.py | 0 .../common/api/tests/test_filtersets.py | 91 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 project_manager/common/api/tests/__init__.py create mode 100644 project_manager/common/api/tests/test_filtersets.py diff --git a/project_manager/common/api/tests/__init__.py b/project_manager/common/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/common/api/tests/test_filtersets.py b/project_manager/common/api/tests/test_filtersets.py new file mode 100644 index 00000000..6fa5f441 --- /dev/null +++ b/project_manager/common/api/tests/test_filtersets.py @@ -0,0 +1,91 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from django_filters.filters import CharFilter +from django_filters.filterset import FilterSet + +# App +from project_manager.common.api.filtersets import ProjectFilterSet + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ProjectFilterSetTestCase(TestCase): + + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectFilterSet, FilterSet), + ) + + def test_base_filters(self): + base_filters = ProjectFilterSet.base_filters + self.assertEqual( + first=len(base_filters), + second=3, + ) + + self.assertIn( + member='game', + container=base_filters, + ) + self.assertIsInstance( + obj=base_filters['game'], + cls=CharFilter, + ) + self.assertEqual( + first=base_filters['game'].field_name, + second='supported_games__basename', + ) + self.assertEqual( + first=base_filters['game'].label, + second='Game', + ) + + self.assertIn( + member='tag', + container=base_filters, + ) + self.assertIsInstance( + obj=base_filters['tag'], + cls=CharFilter, + ) + self.assertEqual( + first=base_filters['tag'].field_name, + second='tags__name', + ) + self.assertEqual( + first=base_filters['tag'].label, + second='Tag', + ) + + self.assertIn( + member='user', + container=base_filters, + ) + self.assertIsInstance( + obj=base_filters['user'], + cls=CharFilter, + ) + self.assertEqual( + first=base_filters['user'].method, + second='filter_user', + ) + self.assertEqual( + first=base_filters['user'].label, + second='User', + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectFilterSet.Meta.fields, + tuple2=( + 'game', + 'tag', + 'user', + ), + ) From 30b774cc4b163b5664a0db754c005cf8d6096839 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Wed, 3 Nov 2021 19:34:46 -0400 Subject: [PATCH 038/211] Fixed some prospector issues. --- project_manager/common/api/views/__init__.py | 2 +- project_manager/sub_plugins/api/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index b782cfe2..512648ca 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -71,7 +71,7 @@ def get(self, request): 'projects': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'projects/', + ) + 'projects/', 'releases': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index 7b62a799..b3dd34c9 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -89,7 +89,7 @@ def get(self, request): 'projects': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, - ) + f'projects//', + ) + 'projects//', 'releases': reverse( viewname=f'api:{self.project_type}s:endpoints', request=request, From af02c17bc06ef7db9243eaf1195f02e461bafc8b Mon Sep 17 00:00:00 2001 From: satoon101 Date: Thu, 4 Nov 2021 00:11:47 -0400 Subject: [PATCH 039/211] Removed unused code. --- project_manager/sub_plugins/admin/forms.py | 38 -------------------- project_manager/sub_plugins/admin/widgets.py | 28 --------------- 2 files changed, 66 deletions(-) delete mode 100644 project_manager/sub_plugins/admin/forms.py delete mode 100644 project_manager/sub_plugins/admin/widgets.py diff --git a/project_manager/sub_plugins/admin/forms.py b/project_manager/sub_plugins/admin/forms.py deleted file mode 100644 index 0e3e1be1..00000000 --- a/project_manager/sub_plugins/admin/forms.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Forms to use for SubPlugin admin classes.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django import forms -from django.contrib.admin.sites import site - -# App -from project_manager.sub_plugins.admin.widgets import PluginRawIdWidget -from project_manager.sub_plugins.models import SubPlugin - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'SubPluginAdminForm', -) - - -# ============================================================================= -# ADMIN FORMS -# ============================================================================= -class SubPluginAdminForm(forms.ModelForm): - """Form to use for selecting the Plugin for a SubPlugin.""" - - def __init__(self, *args, **kwargs): - """Set the widget.""" - super().__init__(*args, **kwargs) - self.fields['plugin'].queryset = self.fields['plugin'].queryset.filter( - paths__isnull=False, - ) - self.fields['plugin'].widget = PluginRawIdWidget( - rel=getattr(SubPlugin.plugin, 'remote_field'), - admin_site=site, - ) diff --git a/project_manager/sub_plugins/admin/widgets.py b/project_manager/sub_plugins/admin/widgets.py deleted file mode 100644 index d7b70a40..00000000 --- a/project_manager/sub_plugins/admin/widgets.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Widgets to use for SubPlugin admin classes.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.contrib.admin import widgets - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'PluginRawIdWidget', -) - - -# ============================================================================= -# WIDGETS -# ============================================================================= -class PluginRawIdWidget(widgets.ForeignKeyRawIdWidget): - """Widget to use for selecting the Plugin for a SubPlugin.""" - - def url_parameters(self): - """Set the parameter to limit Plugins to only those with paths.""" - res = super().url_parameters() - res['paths__isnull'] = '0' - return res From 467dc49b2704bf888b0d1fd8c5443c3081538a39 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Thu, 4 Nov 2021 00:12:19 -0400 Subject: [PATCH 040/211] Updated requirements. --- pip-requirements/base.txt | 2 +- pip-requirements/local.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index b4f77d1a..854f8d74 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,4 +1,4 @@ -django==3.2.8 +django==3.2.9 django-braces==1.14.0 django-crispy-forms==1.13.0 django-embed-video==1.4.0 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index 1ae7f676..f42061bb 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,7 +1,7 @@ -r base.txt django-debug-toolbar==3.2.2 django-extensions==3.1.3 -factory-boy==3.2.0 +factory-boy==3.2.1 flake8==3.9.2 prospector==1.5.1 pycodestyle==2.8.0 From 6d1aceca792229a1a7b1849a22253364af9e8eca Mon Sep 17 00:00:00 2001 From: satoon101 Date: Thu, 4 Nov 2021 00:12:35 -0400 Subject: [PATCH 041/211] Added default argument value. --- project_manager/plugins/admin/inlines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project_manager/plugins/admin/inlines.py b/project_manager/plugins/admin/inlines.py index e23cdd36..f81d80ed 100644 --- a/project_manager/plugins/admin/inlines.py +++ b/project_manager/plugins/admin/inlines.py @@ -86,6 +86,6 @@ class SubPluginPathInline(admin.StackedInline): ) model = SubPluginPath - def has_add_permission(self, request, obj): + def has_add_permission(self, request, obj=None): """Disallow adding new images in the Admin.""" return False From 80beed36d60920d6786aca243157760658ad92fe Mon Sep 17 00:00:00 2001 From: satoon101 Date: Thu, 4 Nov 2021 00:14:33 -0400 Subject: [PATCH 042/211] Updated contributors sample size to at least 2, so that all projects will have at least 1 contributor in the statistics page test. --- project_manager/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/project_manager/tests/test_views.py b/project_manager/tests/test_views.py index 88f47364..92471a8b 100644 --- a/project_manager/tests/test_views.py +++ b/project_manager/tests/test_views.py @@ -56,7 +56,7 @@ def test_get_view(self): package_download_count = 0 package_count = randint(2, 8) for _ in range(package_count): - contributors = sample(user_list, randint(1, 3)) + contributors = sample(user_list, randint(2, 4)) contributing_users.update(contributors) owner = contributors.pop() package = PackageFactory( @@ -81,7 +81,7 @@ def test_get_view(self): plugin_download_count = 0 plugin_count = randint(4, 8) for _ in range(plugin_count): - contributors = sample(user_list, randint(1, 3)) + contributors = sample(user_list, randint(2, 4)) contributing_users.update(contributors) owner = contributors.pop() plugin = PluginFactory( @@ -105,7 +105,7 @@ def test_get_view(self): count = randint(1, 2) sub_plugin_count += count for _ in range(count): - contributors = sample(user_list, randint(1, 3)) + contributors = sample(user_list, randint(2, 4)) contributing_users.update(contributors) owner = contributors.pop() sub_plugin = SubPluginFactory( From cf1a918cbe688b01b67f084fe2a55e095ef6ef88 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Thu, 4 Nov 2021 00:15:00 -0400 Subject: [PATCH 043/211] Added admin tests. --- project_manager/packages/tests/test_admin.py | 157 +++++++++++++ project_manager/plugins/tests/test_admin.py | 206 ++++++++++++++++++ .../sub_plugins/tests/test_admin.py | 157 +++++++++++++ 3 files changed, 520 insertions(+) create mode 100644 project_manager/packages/tests/test_admin.py create mode 100644 project_manager/plugins/tests/test_admin.py create mode 100644 project_manager/sub_plugins/tests/test_admin.py diff --git a/project_manager/packages/tests/test_admin.py b/project_manager/packages/tests/test_admin.py new file mode 100644 index 00000000..fae2f7e3 --- /dev/null +++ b/project_manager/packages/tests/test_admin.py @@ -0,0 +1,157 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.test import TestCase + +# App +from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin.inlines import ( + ProjectContributorInline, + ProjectGameInline, + ProjectImageInline, + ProjectReleaseInline, + ProjectTagInline, +) +from project_manager.packages.admin import PackageAdmin +from project_manager.packages.admin.inlines import ( + PackageContributorInline, + PackageGameInline, + PackageImageInline, + PackageReleaseInline, + PackageTagInline, +) +from project_manager.packages.models import ( + PackageContributor, + PackageGame, + PackageImage, + PackageRelease, + PackageTag, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageAdmin, ProjectAdmin), + ) + + def test_inlines(self): + self.assertTupleEqual( + tuple1=PackageAdmin.inlines, + tuple2=( + PackageContributorInline, + PackageGameInline, + PackageImageInline, + PackageTagInline, + PackageReleaseInline, + ), + ) + + +class PackageContributorInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageContributorInline, + ProjectContributorInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PackageContributorInline.model, + second=PackageContributor, + ) + + +class PackageGameInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageGameInline, + ProjectGameInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PackageGameInline.model, + second=PackageGame, + ) + + def test_has_add_permission(self): + obj = PackageGameInline(PackageGame, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class PackageImageInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageImageInline, + ProjectImageInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PackageImageInline.model, + second=PackageImage, + ) + + def test_has_add_permission(self): + obj = PackageImageInline(PackageImage, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class PackageTagInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageTagInline, + ProjectTagInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PackageTagInline.model, + second=PackageTag, + ) + + def test_has_add_permission(self): + obj = PackageTagInline(PackageTag, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class PackageReleaseInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleaseInline, + ProjectReleaseInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PackageReleaseInline.model, + second=PackageRelease, + ) + + def test_has_add_permission(self): + obj = PackageReleaseInline(PackageRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) diff --git a/project_manager/plugins/tests/test_admin.py b/project_manager/plugins/tests/test_admin.py new file mode 100644 index 00000000..915dc39c --- /dev/null +++ b/project_manager/plugins/tests/test_admin.py @@ -0,0 +1,206 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.test import TestCase + +# App +from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin.inlines import ( + ProjectContributorInline, + ProjectGameInline, + ProjectImageInline, + ProjectReleaseInline, + ProjectTagInline, +) +from project_manager.plugins.admin import PluginAdmin +from project_manager.plugins.admin.inlines import ( + PluginContributorInline, + PluginGameInline, + PluginImageInline, + PluginReleaseInline, + PluginTagInline, + SubPluginPathInline, +) +from project_manager.plugins.models import ( + PluginContributor, + PluginGame, + PluginImage, + PluginRelease, + PluginTag, + SubPluginPath, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginAdmin, ProjectAdmin), + ) + + def test_inlines(self): + self.assertTupleEqual( + tuple1=PluginAdmin.inlines, + tuple2=( + PluginContributorInline, + PluginGameInline, + PluginImageInline, + PluginTagInline, + SubPluginPathInline, + PluginReleaseInline, + ), + ) + + +class PluginContributorInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginContributorInline, + ProjectContributorInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PluginContributorInline.model, + second=PluginContributor, + ) + + +class PluginGameInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginGameInline, + ProjectGameInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PluginGameInline.model, + second=PluginGame, + ) + + def test_has_add_permission(self): + obj = PluginGameInline(PluginGame, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class PluginImageInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginImageInline, + ProjectImageInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PluginImageInline.model, + second=PluginImage, + ) + + def test_has_add_permission(self): + obj = PluginImageInline(PluginImage, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class PluginTagInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginTagInline, + ProjectTagInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PluginTagInline.model, + second=PluginTag, + ) + + def test_has_add_permission(self): + obj = PluginTagInline(PluginTag, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class PluginReleaseInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleaseInline, + ProjectReleaseInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=PluginReleaseInline.model, + second=PluginRelease, + ) + + def test_has_add_permission(self): + obj = PluginReleaseInline(PluginRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class SubPluginPathInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginPathInline, + admin.StackedInline, + ), + ) + + def test_extra(self): + self.assertEqual( + first=SubPluginPathInline.extra, + second=0, + ) + + def test_view_on_site(self): + self.assertFalse(expr=SubPluginPathInline.view_on_site) + + def test_fields(self): + self.assertTupleEqual( + tuple1=SubPluginPathInline.fields, + tuple2=( + 'path', + 'allow_module', + 'allow_package_using_basename', + 'allow_package_using_init', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=SubPluginPathInline.readonly_fields, + tuple2=('path',), + ) + + def test_model(self): + self.assertEqual( + first=SubPluginPathInline.model, + second=SubPluginPath, + ) + + def test_has_add_permission(self): + obj = SubPluginPathInline(SubPluginPath, admin.AdminSite()) + self.assertFalse(expr=obj.has_add_permission('')) diff --git a/project_manager/sub_plugins/tests/test_admin.py b/project_manager/sub_plugins/tests/test_admin.py new file mode 100644 index 00000000..702b6fd4 --- /dev/null +++ b/project_manager/sub_plugins/tests/test_admin.py @@ -0,0 +1,157 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.test import TestCase + +# App +from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin.inlines import ( + ProjectContributorInline, + ProjectGameInline, + ProjectImageInline, + ProjectReleaseInline, + ProjectTagInline, +) +from project_manager.sub_plugins.admin import SubPluginAdmin +from project_manager.sub_plugins.admin.inlines import ( + SubPluginContributorInline, + SubPluginGameInline, + SubPluginImageInline, + SubPluginReleaseInline, + SubPluginTagInline, +) +from project_manager.sub_plugins.models import ( + SubPluginContributor, + SubPluginGame, + SubPluginImage, + SubPluginRelease, + SubPluginTag, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginAdmin, ProjectAdmin), + ) + + def test_inlines(self): + self.assertTupleEqual( + tuple1=SubPluginAdmin.inlines, + tuple2=( + SubPluginContributorInline, + SubPluginGameInline, + SubPluginImageInline, + SubPluginTagInline, + SubPluginReleaseInline, + ), + ) + + +class SubPluginContributorInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginContributorInline, + ProjectContributorInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=SubPluginContributorInline.model, + second=SubPluginContributor, + ) + + +class SubPluginGameInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginGameInline, + ProjectGameInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=SubPluginGameInline.model, + second=SubPluginGame, + ) + + def test_has_add_permission(self): + obj = SubPluginGameInline(SubPluginGame, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class SubPluginImageInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginImageInline, + ProjectImageInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=SubPluginImageInline.model, + second=SubPluginImage, + ) + + def test_has_add_permission(self): + obj = SubPluginImageInline(SubPluginImage, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class SubPluginTagInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginTagInline, + ProjectTagInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=SubPluginTagInline.model, + second=SubPluginTag, + ) + + def test_has_add_permission(self): + obj = SubPluginTagInline(SubPluginTag, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + +class SubPluginReleaseInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseInline, + ProjectReleaseInline, + ), + ) + + def test_model(self): + self.assertEqual( + first=SubPluginReleaseInline.model, + second=SubPluginRelease, + ) + + def test_has_add_permission(self): + obj = SubPluginReleaseInline(SubPluginRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) From f8ec0bfeb614cf88ce0d8bc1083f5528291d08df Mon Sep 17 00:00:00 2001 From: satoon101 Date: Thu, 4 Nov 2021 17:45:38 -0400 Subject: [PATCH 044/211] Added more tests. --- .../test-package/test-package-v1.0.0.zip | Bin 0 -> 1174 bytes .../test-plugin/test-plugin-v1.0.0.zip | Bin 0 -> 1172 bytes .../test-sub-plugin-v1.0.0.zip | Bin 0 -> 1760 bytes project_manager/common/mixins.py | 9 +- project_manager/common/tests/test_mixins.py | 71 +++++++++ project_manager/packages/tests/test_admin.py | 32 ++++ project_manager/packages/tests/test_views.py | 108 ++++++++++++++ project_manager/plugins/tests/test_views.py | 114 +++++++++++++++ .../sub_plugins/tests/test_views.py | 137 ++++++++++++++++++ project_manager/sub_plugins/views.py | 2 +- 10 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 fixtures/releases/packages/test-package/test-package-v1.0.0.zip create mode 100644 fixtures/releases/plugins/test-plugin/test-plugin-v1.0.0.zip create mode 100644 fixtures/releases/sub-plugins/test-plugin/test-sub-plugin/test-sub-plugin-v1.0.0.zip create mode 100644 project_manager/common/tests/test_mixins.py create mode 100644 project_manager/packages/tests/test_views.py create mode 100644 project_manager/plugins/tests/test_views.py create mode 100644 project_manager/sub_plugins/tests/test_views.py diff --git a/fixtures/releases/packages/test-package/test-package-v1.0.0.zip b/fixtures/releases/packages/test-package/test-package-v1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..cfac93cce5ca951dc77159d2ef893b8df6aac77e GIT binary patch literal 1174 zcmWIWW@Zs#0D&T>lwdFeO0Waz#FUi$ykh+ToQgydii-0~i;`1y3o1)8^7HU$k;A2> zATc>RF+CO8x*9Y$sS?ziTv}X`pNnLD7n%`9#2QhOT3iwjbrak?4unEz z;{n9BWSJKqpP83g5+AQuP^sJP$aO%0hvj`!{R!W_Ul?oKzboV}VVrpB?Eg%b_MN=N zy=(!ZHzb)Yx0fVcTX}4b;^OI|X786@E?U*!bjR)4pCIaYW zXsSd_aPZWF&pw#F$o65TF=TH|A!r{o#UWd3J>#7#C^TU-Hfu4{C9<{GiM19bl_I<0 zqJvuq$PF+W(+$|tGRzGMK>O7h2?aIO>nLtOO4-QHNu48;0&>orG8Hh*a5!Ij&H9Us X0p6@^K)V>&fN&QR1H&<(r3?%J(yuI* literal 0 HcmV?d00001 diff --git a/fixtures/releases/plugins/test-plugin/test-plugin-v1.0.0.zip b/fixtures/releases/plugins/test-plugin/test-plugin-v1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..4c91f628cc4c2198cdb0437ea9dbebae944bbe8c GIT binary patch literal 1172 zcmWIWW@Zs#0D&T>lwdFeO0Waz#FUi$ykh+TxT0w;sER}pii-0~i;`1y3o1)8^7D|i z%tq59i%UyEPHB23ifzl#v}zL4T9R5^5)Uy4?g9>m=|G?Tnl5AD0@UsS#O9io3+My}hLvdkGaBG`W3vV`xgc8;fy)|L;z73VdbH+JkaaM6_C-#xFOhwPnZ%H-n@q?$ zq|~90Z1uK>J{v$*Z}xk<5kzBJjV)QitX2S8dy5pSk(>cc4#*BkogkgQ+YGW5Ml&4FmtM2}BG47AY(Rf9 QumRytCI*J1K)VlwdFeO0Waz#FUi$ykh+TxT0w;sER}pii-0~i;`1y3o1)8^7D|i z%tq59i%UyEPHB23iftd!v}zL4T9R5^5)Uy4$pt^qj4>s}nBvkTsENqo@EgrYS8|O6 zyBJ{y+|?Wm(}D5vYr2eq3(#l}AP%RN6=+uK6;$eWJ8~UR;9+^+RDZ&E?-$0}_U{V0 zOBg3!I{QD9rF|!FaW7kd=nY9`%k3pe*H#{zqqumwsM-7Fmy1^QH{Ef2_IRQfk3A@j zZ~>jjz_1c6Yyzm|@c8)5yv&mLc%X{|ycwD78E_?B6|g%P7zCg+G#PU-D1ZeR86+4c zWU^bAUA)dHuw83n1(1gEkqyR7?8pYI0?mRNjGFY3tvNjD)O?WfFdCaRm>C4wnh0Fh zz_JUnb=RXcmx8Q=(X%gdf_;hXE6hxVY~5r+)*)vxWUF7~9b>5kS`DMIS&f+yk*&T( ziq(iLQ>>5dj2oAVctFm8(Iy_Ie?cLF90Hh`7ugxQj5y*I7U0Msfb0yke1+ny3rAH1 zK+b~Emv^pX2k{Zk!j|D-&Qbt|%mivV3(c9p%!C|%sdHpfK>1>=>)rjJaFd!GwH6V6 zxH34hLp_ Date: Thu, 4 Nov 2021 18:16:14 -0400 Subject: [PATCH 045/211] Fixed issues with a few POST APIs. --- .../common/api/serializers/mixins.py | 24 ++++++++--------- project_manager/common/helpers.py | 26 +++++-------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 6f4e1bc9..1ff29145 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -113,7 +113,7 @@ def validate(self, attrs): project = self.get_project( kwargs=kwargs, ) - self.validate_version( + self.run_version_validation( project=project, version=version, ) @@ -121,17 +121,17 @@ def validate(self, attrs): args = (zip_file,) - with self.zip_parser(*args) as zip_validator: - self.validate_zip_file( - zip_validator=zip_validator, - project_basename=project_basename, - ) + zip_validator = self.zip_parser(*args) + self.run_zip_file_validation( + zip_validator=zip_validator, + project_basename=project_basename, + ) - # This needs added for project creation - attrs['basename'] = zip_validator.basename + # This needs added for project creation + attrs['basename'] = zip_validator.basename - if project is not None: - attrs[self.project_type.replace('-', '_')] = project + if project is not None: + attrs[self.project_type.replace('-', '_')] = project return attrs @@ -142,7 +142,7 @@ def get_project(self, kwargs): except self.project_class.DoesNotExist: return None - def validate_version(self, project, version): + def run_version_validation(self, project, version): """Validate that the version does not already exist.""" kwargs = { self.project_type.replace('-', '_'): project, @@ -153,7 +153,7 @@ def validate_version(self, project, version): 'version': 'Given version matches existing version.', }) - def validate_zip_file(self, zip_validator, project_basename): + def run_zip_file_validation(self, zip_validator, project_basename): """Validate the files inside the zip file.""" zip_validator.find_base_info() zip_validator.validate_file_paths() diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 109b1211..8bb0c0c4 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -52,22 +52,12 @@ class ProjectZipFile: """Base ZipFile parsing class.""" file_types = None - zip_file = None - - def __enter__(self): - """Open the zip file on entry.""" - self.zip_file = ZipFile(self.zip_file_path) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Close the zip file on exiting.""" - self.zip_file.close() - return not exc_type def __init__(self, zip_file): """Store the base attributes for the zip file.""" - self.zip_file_path = zip_file - self.file_list = self.get_file_list() + self.zip_file = zip_file + with ZipFile(self.zip_file) as zip_obj: + self.file_list = self.get_file_list(zip_obj) self.basename = None self.requirements = { 'custom': [], @@ -133,13 +123,11 @@ def _validate_path(self, path): return False - def get_file_list(self): + @staticmethod + def get_file_list(zip_obj): """Return a list of all files in the given zip file.""" try: - return [ - x for x in self.zip_file.namelist() - if not x.endswith('/') - ] + return [x for x in zip_obj.namelist() if not x.endswith('/')] except BadZipfile: raise ValidationError({ 'zip_file': 'Given file is not a valid zip file.' @@ -187,7 +175,7 @@ def validate_requirements(self): """Return the requirements for the release.""" requirement_path = self.get_requirement_path() try: - with self.zip_file.open(requirement_path) as requirement_file: + with ZipFile(self.zip_file).open(requirement_path) as requirement_file: contents = json.load(requirement_file) except KeyError: return From dda957838b1da7eedffac2bef73201584593a4a1 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 07:54:48 -0400 Subject: [PATCH 046/211] Fixed API documentation rendering by adding Markdown requirement. --- pip-requirements/base.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index 854f8d74..e08a13a7 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -6,4 +6,5 @@ django-filter==21.1 django-model-utils==4.2.0 django-precise-bbcode==1.2.13 djangorestframework==3.12.4 +Markdown==3.3.4 path==16.2.0 From 19df1e35e2a65b5e849ab70b18695490875e7038 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 07:56:49 -0400 Subject: [PATCH 047/211] Fixed some class names and other minor changes. --- project_manager/packages/tests/test_models.py | 2 +- project_manager/plugins/constants.py | 1 + project_manager/plugins/tests/test_models.py | 2 +- project_manager/sub_plugins/tests/test_models.py | 2 +- tags/tests/test_models.py | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index af5f2f7e..c7eb11f5 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -734,7 +734,7 @@ def test_meta_class(self): ) -class PackageReleasePypiRequirementTestCase(TestCase): +class PackageReleasePyPiRequirementTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( expr=issubclass( diff --git a/project_manager/plugins/constants.py b/project_manager/plugins/constants.py index 5d1b3aea..8eaf4b35 100644 --- a/project_manager/plugins/constants.py +++ b/project_manager/plugins/constants.py @@ -19,6 +19,7 @@ __all__ = ( 'PATH_MAX_LENGTH', 'PLUGIN_ALLOWED_FILE_TYPES', + 'PLUGIN_DATA_PATH', 'PLUGIN_IMAGE_URL', 'PLUGIN_LOGO_URL', 'PLUGIN_PATH', diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 51b84b52..fa54be11 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -740,7 +740,7 @@ def test_meta_class(self): ) -class PluginReleasePypiRequirementTestCase(TestCase): +class PluginReleasePyPiRequirementTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( expr=issubclass( diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index 125dda4c..8aa22879 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -765,7 +765,7 @@ def test_meta_class(self): ) -class SubPluginReleasePypiRequirementTestCase(TestCase): +class SubPluginReleasePyPiRequirementTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( expr=issubclass( diff --git a/tags/tests/test_models.py b/tags/tests/test_models.py index 0e1e091c..2ac8bbee 100644 --- a/tags/tests/test_models.py +++ b/tags/tests/test_models.py @@ -19,7 +19,7 @@ # ============================================================================= # TEST CASES # ============================================================================= -class GameTestCase(TestCase): +class TagTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( expr=issubclass(Tag, models.Model), From 36108fb1f866fec6a62170a16d2e2034f4f90acd Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 19:16:58 -0400 Subject: [PATCH 048/211] Moved django-extensions to be a part of the base requirements/settings instead of local only. --- SPPM/settings/base.py | 1 + SPPM/settings/local.py | 1 - pip-requirements/base.txt | 1 + pip-requirements/local.txt | 1 - 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index e502929c..0eafbb94 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -54,6 +54,7 @@ 'embed_video', 'precise_bbcode', 'crispy_forms', + 'django_extensions', 'django_filters', 'project_manager', 'games', diff --git a/SPPM/settings/local.py b/SPPM/settings/local.py index 6fde2edb..b57faf88 100644 --- a/SPPM/settings/local.py +++ b/SPPM/settings/local.py @@ -7,7 +7,6 @@ INSTALLED_APPS += [ 'debug_toolbar', - 'django_extensions', ] MIDDLEWARE += [ diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index e08a13a7..336a6f79 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -2,6 +2,7 @@ django==3.2.9 django-braces==1.14.0 django-crispy-forms==1.13.0 django-embed-video==1.4.0 +django-extensions==3.1.3 django-filter==21.1 django-model-utils==4.2.0 django-precise-bbcode==1.2.13 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index f42061bb..ca5dbd61 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,6 +1,5 @@ -r base.txt django-debug-toolbar==3.2.2 -django-extensions==3.1.3 factory-boy==3.2.1 flake8==3.9.2 prospector==1.5.1 From b228c7fb7ba5f43f7ec3b326bb509e30596074ec Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 19:18:53 -0400 Subject: [PATCH 049/211] Removed 'filename' as an argument for the zip upload handler functions, as it was never used. Fixed a bug in the image name logic. --- project_manager/common/helpers.py | 4 ++-- project_manager/packages/helpers.py | 7 ++++--- project_manager/plugins/helpers.py | 8 ++++---- project_manager/sub_plugins/helpers.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 8bb0c0c4..77afc29c 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -314,7 +314,7 @@ def find_image_number(directory, slug): """Return the next available image number.""" path = settings.MEDIA_ROOT / 'images' / directory / slug current_files = [x.stem for x in path.files()] if path.isdir() else [] - return f'{max(map(int, current_files or [0])) + 1}:04' + return f'{max(map(int, current_files or [0])) + 1:04}' def handle_project_image_upload(instance, filename): @@ -329,4 +329,4 @@ def handle_project_logo_upload(instance, filename): def handle_release_zip_file_upload(instance, filename): """Handle uploading the zip file by directing to the proper directory.""" - return instance.handle_zip_file_upload(filename) + return instance.handle_zip_file_upload() diff --git a/project_manager/packages/helpers.py b/project_manager/packages/helpers.py index 3e251c1a..d4d88d19 100644 --- a/project_manager/packages/helpers.py +++ b/project_manager/packages/helpers.py @@ -41,8 +41,6 @@ class PackageZipFile(ProjectZipFile): def find_base_info(self): """Store all base information for the zip file.""" for file_path in self.file_list: - if not file_path.endswith('.py'): - continue if not file_path.startswith(PACKAGE_PATH): continue @@ -50,6 +48,9 @@ def find_base_info(self): if not current: continue + if not file_path.endswith('.py'): + continue + if '/' not in current: current = current.rsplit('.', 1)[0] self.is_module = True @@ -86,7 +87,7 @@ def get_requirement_path(self): # ============================================================================= # FUNCTIONS # ============================================================================= -def handle_package_zip_upload(instance, filename): +def handle_package_zip_upload(instance): """Return the path to store the zip for the current release.""" slug = instance.package.slug return f'{PACKAGE_RELEASE_URL}{slug}/{slug}-v{instance.version}.zip' diff --git a/project_manager/plugins/helpers.py b/project_manager/plugins/helpers.py index 6b0d2313..2d57dfe3 100644 --- a/project_manager/plugins/helpers.py +++ b/project_manager/plugins/helpers.py @@ -40,9 +40,6 @@ class PluginZipFile(ProjectZipFile): def find_base_info(self): """Store all base information for the zip file.""" for file_path in self.file_list: - if not file_path.endswith('.py'): - continue - if not file_path.startswith(PLUGIN_PATH): continue @@ -50,6 +47,9 @@ def find_base_info(self): if not current: continue + if not file_path.endswith('.py'): + continue + current = current.split('/', 1)[0] if self.basename is None: @@ -72,7 +72,7 @@ def get_requirement_path(self): # ============================================================================= # FUNCTIONS # ============================================================================= -def handle_plugin_zip_upload(instance, filename): +def handle_plugin_zip_upload(instance): """Return the path to store the zip for the current release.""" slug = instance.plugin.slug return f'{PLUGIN_RELEASE_URL}{slug}/{slug}-v{instance.version}.zip' diff --git a/project_manager/sub_plugins/helpers.py b/project_manager/sub_plugins/helpers.py index 5bfe899e..155309e8 100644 --- a/project_manager/sub_plugins/helpers.py +++ b/project_manager/sub_plugins/helpers.py @@ -190,7 +190,7 @@ def get_requirement_path(self): # ============================================================================= # FUNCTIONS # ============================================================================= -def handle_sub_plugin_zip_upload(instance, filename): +def handle_sub_plugin_zip_upload(instance): """Return the path to store the zip for the current release.""" slug = instance.sub_plugin.slug return ( From 9089443cc14f92fcfe857b3267ac6f50e29e644d Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 19:19:43 -0400 Subject: [PATCH 050/211] Added factories for project images. --- test_utils/factories/packages.py | 16 ++++++++++++++++ test_utils/factories/plugins.py | 16 ++++++++++++++++ test_utils/factories/sub_plugins.py | 18 +++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/test_utils/factories/packages.py b/test_utils/factories/packages.py index 2f2a742f..f5c6f0dd 100644 --- a/test_utils/factories/packages.py +++ b/test_utils/factories/packages.py @@ -14,6 +14,7 @@ Package, PackageContributor, PackageGame, + PackageImage, PackageRelease, PackageReleaseDownloadRequirement, PackageReleasePackageRequirement, @@ -30,6 +31,7 @@ 'PackageContributorFactory', 'PackageFactory', 'PackageGameFactory', + 'PackageImageFactory', 'PackageReleaseFactory', 'PackageReleaseDownloadRequirementFactory', 'PackageReleasePackageRequirementFactory', @@ -105,6 +107,20 @@ class Meta: model = PackageGame +class PackageImageFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PackageImage objects.""" + + package = factory.SubFactory( + factory='test_utils.factories.packages.PackageFactory', + ) + image = factory.Sequence(function=lambda n: f'image_{n}.jpg') + + class Meta: + """Define the metaclass attributes.""" + + model = PackageImage + + class PackageTagFactory(factory.django.DjangoModelFactory): """Model factory to use when testing with PackageTag objects.""" diff --git a/test_utils/factories/plugins.py b/test_utils/factories/plugins.py index e5ee4ab0..b2011e25 100644 --- a/test_utils/factories/plugins.py +++ b/test_utils/factories/plugins.py @@ -14,6 +14,7 @@ Plugin, PluginContributor, PluginGame, + PluginImage, PluginRelease, PluginReleaseDownloadRequirement, PluginReleasePackageRequirement, @@ -31,6 +32,7 @@ 'PluginContributorFactory', 'PluginFactory', 'PluginGameFactory', + 'PluginImageFactory', 'PluginReleaseFactory', 'PluginReleaseDownloadRequirementFactory', 'PluginReleasePackageRequirementFactory', @@ -107,6 +109,20 @@ class Meta: model = PluginGame +class PluginImageFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with PluginImage objects.""" + + plugin = factory.SubFactory( + factory='test_utils.factories.plugins.PluginFactory', + ) + image = factory.Sequence(function=lambda n: f'image_{n}.jpg') + + class Meta: + """Define the metaclass attributes.""" + + model = PluginImage + + class PluginTagFactory(factory.django.DjangoModelFactory): """Model factory to use when testing with PluginTag objects.""" diff --git a/test_utils/factories/sub_plugins.py b/test_utils/factories/sub_plugins.py index f7d3f4bc..5f21f9b9 100644 --- a/test_utils/factories/sub_plugins.py +++ b/test_utils/factories/sub_plugins.py @@ -14,6 +14,7 @@ SubPlugin, SubPluginContributor, SubPluginGame, + SubPluginImage, SubPluginRelease, SubPluginReleaseDownloadRequirement, SubPluginReleasePackageRequirement, @@ -28,8 +29,9 @@ # ============================================================================= __all__ = ( 'SubPluginContributorFactory', - 'SubPluginGameFactory', 'SubPluginFactory', + 'SubPluginGameFactory', + 'SubPluginImageFactory', 'SubPluginReleaseFactory', 'SubPluginReleaseDownloadRequirementFactory', 'SubPluginReleasePackageRequirementFactory', @@ -108,6 +110,20 @@ class Meta: model = SubPluginGame +class SubPluginImageFactory(factory.django.DjangoModelFactory): + """Model factory to use when testing with SubPluginImage objects.""" + + sub_plugin = factory.SubFactory( + factory='test_utils.factories.sub_plugins.SubPluginFactory', + ) + image = factory.Sequence(function=lambda n: f'image_{n}.jpg') + + class Meta: + """Define the metaclass attributes.""" + + model = SubPluginImage + + class SubPluginTagFactory(factory.django.DjangoModelFactory): """Model factory to use when testing with SubPluginTag objects.""" From ae534f3dce6063f93c6f98c41dae076e57fe13d9 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 19:20:16 -0400 Subject: [PATCH 051/211] Added some tests for project helpers. --- .../packages/tests/test_helpers.py | 225 ++++++++++++++++++ project_manager/plugins/tests/test_helpers.py | 176 ++++++++++++++ .../sub_plugins/tests/test_helpers.py | 85 +++++++ 3 files changed, 486 insertions(+) create mode 100644 project_manager/packages/tests/test_helpers.py create mode 100644 project_manager/plugins/tests/test_helpers.py create mode 100644 project_manager/sub_plugins/tests/test_helpers.py diff --git a/project_manager/packages/tests/test_helpers.py b/project_manager/packages/tests/test_helpers.py new file mode 100644 index 00000000..a4d3d61f --- /dev/null +++ b/project_manager/packages/tests/test_helpers.py @@ -0,0 +1,225 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from random import randint +from unittest import mock + +# Django +from django.core.exceptions import ValidationError +from django.test import TestCase + +# App +from project_manager.common.helpers import ProjectZipFile +from project_manager.packages.constants import ( + PACKAGE_ALLOWED_FILE_TYPES, + PACKAGE_IMAGE_URL, + PACKAGE_LOGO_URL, + PACKAGE_PATH, + PACKAGE_RELEASE_URL, +) +from project_manager.packages.helpers import ( + PackageZipFile, + handle_package_image_upload, + handle_package_logo_upload, + handle_package_zip_upload, +) +from test_utils.factories.packages import ( + PackageFactory, + PackageImageFactory, + PackageReleaseFactory, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageZipFileTestCase(TestCase): + @staticmethod + def _get_module_file_list(package_basename): + return tuple( + reversed([ + PACKAGE_PATH.rsplit('/', i)[0] + '/' + for i in range(1, PACKAGE_PATH.count('/') + 1) + ]) + ) + ( + f'{PACKAGE_PATH}{package_basename}.py', + f'{PACKAGE_PATH}{package_basename}_requirements.json', + ) + + @staticmethod + def _get_package_file_list(package_basename): + return tuple( + reversed([ + PACKAGE_PATH.rsplit('/', i)[0] + '/' + for i in range(1, PACKAGE_PATH.count('/') + 1) + ]) + ) + ( + f'{PACKAGE_PATH}{package_basename}', + f'{PACKAGE_PATH}{package_basename}/__init__.py', + f'{PACKAGE_PATH}{package_basename}/helpers.py', + f'{PACKAGE_PATH}{package_basename}_requirements.json', + ) + + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(PackageZipFile, ProjectZipFile)) + + def test_project_type(self): + self.assertEqual( + first=PackageZipFile.project_type, + second='Package', + ) + + def test_file_types(self): + self.assertDictEqual( + d1=PackageZipFile.file_types, + d2=PACKAGE_ALLOWED_FILE_TYPES, + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ) + def test_find_base_info(self, mock_get_file_list, _): + package_basename = 'test_package_as_module' + mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + self.assertTrue(expr=obj.is_module) + self.assertEqual( + first=obj.basename, + second=package_basename, + ) + + package_basename = 'test_package_as_package' + mock_get_file_list.return_value = self._get_package_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + self.assertFalse(expr=obj.is_module) + self.assertEqual( + first=obj.basename, + second=package_basename, + ) + + mock_get_file_list.return_value += ( + f'{PACKAGE_PATH}second_basename/__init__.py', + ) + with self.assertRaises(ValidationError) as context: + obj = PackageZipFile('') + obj.find_base_info() + + self.assertEqual( + first=context.exception.message, + second='Multiple base directories found for package.', + ) + self.assertEqual( + first=context.exception.code, + second='multiple', + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ) + def test_get_base_paths(self, mock_get_file_list, _): + package_basename = 'test_package_as_module' + mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + self.assertListEqual( + list1=obj.get_base_paths(), + list2=[f'{PACKAGE_PATH}{package_basename}.py'], + ) + + package_basename = 'test_package_as_package' + mock_get_file_list.return_value = self._get_package_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + self.assertListEqual( + list1=obj.get_base_paths(), + list2=[ + f'{PACKAGE_PATH}{package_basename}/{package_basename}.py', + f'{PACKAGE_PATH}{package_basename}/__init__.py', + ], + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ) + def test_get_requirement_path(self, mock_get_file_list, _): + package_basename = 'test_package_as_module' + mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + self.assertEqual( + first=obj.get_requirement_path(), + second=f'{PACKAGE_PATH}{package_basename}_requirements.json', + ) + + package_basename = 'test_package_as_package' + mock_get_file_list.return_value = self._get_package_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + self.assertEqual( + first=obj.get_requirement_path(), + second=f'{PACKAGE_PATH}{package_basename}/requirements.json', + ) + + +class HelperFunctionsTestCase(TestCase): + def test_handle_package_zip_upload(self): + obj = PackageReleaseFactory() + slug = obj.package.slug + self.assertEqual( + first=handle_package_zip_upload(obj), + second=f'{PACKAGE_RELEASE_URL}{slug}/{slug}-v{obj.version}.zip' + ) + + def test_handle_package_logo_upload(self): + obj = PackageFactory() + extension = 'jpg' + filename = f'test_image.{extension}' + self.assertEqual( + first=handle_package_logo_upload( + instance=obj, + filename=filename, + ), + second=f'{PACKAGE_LOGO_URL}{obj.slug}.{extension}', + ) + + def test_handle_package_image_upload(self): + obj = PackageImageFactory() + slug = obj.package.slug + extension = 'jpg' + filename = f'test_image.{extension}' + image_number = f'{randint(1, 10):04}' + with mock.patch( + target='project_manager.packages.helpers.find_image_number', + return_value=image_number, + ): + self.assertEqual( + first=handle_package_image_upload( + instance=obj, + filename=filename, + ), + second=f'{PACKAGE_IMAGE_URL}{slug}/{image_number}.{extension}', + ) diff --git a/project_manager/plugins/tests/test_helpers.py b/project_manager/plugins/tests/test_helpers.py new file mode 100644 index 00000000..ef12a54b --- /dev/null +++ b/project_manager/plugins/tests/test_helpers.py @@ -0,0 +1,176 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from random import randint +from unittest import mock + +# Django +from django.core.exceptions import ValidationError +from django.test import TestCase + +# App +from project_manager.common.helpers import ProjectZipFile +from project_manager.plugins.constants import ( + PLUGIN_ALLOWED_FILE_TYPES, + PLUGIN_IMAGE_URL, + PLUGIN_LOGO_URL, + PLUGIN_PATH, + PLUGIN_RELEASE_URL, +) +from project_manager.plugins.helpers import ( + PluginZipFile, + handle_plugin_image_upload, + handle_plugin_logo_upload, + handle_plugin_zip_upload, +) +from test_utils.factories.plugins import ( + PluginFactory, + PluginImageFactory, + PluginReleaseFactory, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginZipFileTestCase(TestCase): + @staticmethod + def _get_file_list(plugin_basename): + return tuple( + reversed([ + PLUGIN_PATH.rsplit('/', i)[0] + '/' + for i in range(1, PLUGIN_PATH.count('/') + 1) + ]) + ) + ( + f'{PLUGIN_PATH}{plugin_basename}', + f'{PLUGIN_PATH}{plugin_basename}/__init__.py', + f'{PLUGIN_PATH}{plugin_basename}/{plugin_basename}.py', + f'{PLUGIN_PATH}{plugin_basename}/helpers.py', + f'{PLUGIN_PATH}{plugin_basename}/requirements.json', + ) + + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(PluginZipFile, ProjectZipFile)) + + def test_project_type(self): + self.assertEqual( + first=PluginZipFile.project_type, + second='Plugin', + ) + + def test_file_types(self): + self.assertDictEqual( + d1=PluginZipFile.file_types, + d2=PLUGIN_ALLOWED_FILE_TYPES, + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ) + def test_find_base_info(self, mock_get_file_list, _): + plugin_basename = 'test_plugin' + mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + obj = PluginZipFile('') + obj.find_base_info() + self.assertEqual( + first=obj.basename, + second=plugin_basename, + ) + + mock_get_file_list.return_value += ( + f'{PLUGIN_PATH}second_basename/__init__.py', + ) + with self.assertRaises(ValidationError) as context: + obj = PluginZipFile('') + obj.find_base_info() + + self.assertEqual( + first=context.exception.message, + second='Multiple base directories found for plugin.', + ) + self.assertEqual( + first=context.exception.code, + second='multiple', + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ) + def test_get_base_paths(self, mock_get_file_list, _): + plugin_basename = 'test_plugin' + mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + obj = PluginZipFile('') + obj.find_base_info() + self.assertListEqual( + list1=obj.get_base_paths(), + list2=[f'{PLUGIN_PATH}{plugin_basename}/{plugin_basename}.py'], + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ) + def test_get_requirement_path(self, mock_get_file_list, _): + plugin_basename = 'test_plugin' + mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + obj = PluginZipFile('') + obj.find_base_info() + self.assertEqual( + first=obj.get_requirement_path(), + second=f'{PLUGIN_PATH}{plugin_basename}/requirements.json', + ) + + +class HelperFunctionsTestCase(TestCase): + def test_handle_plugin_zip_upload(self): + obj = PluginReleaseFactory() + slug = obj.plugin.slug + self.assertEqual( + first=handle_plugin_zip_upload(obj), + second=f'{PLUGIN_RELEASE_URL}{slug}/{slug}-v{obj.version}.zip' + ) + + def test_handle_plugin_logo_upload(self): + obj = PluginFactory() + extension = 'jpg' + filename = f'test_image.{extension}' + self.assertEqual( + first=handle_plugin_logo_upload( + instance=obj, + filename=filename, + ), + second=f'{PLUGIN_LOGO_URL}{obj.slug}.{extension}', + ) + + def test_handle_plugin_image_upload(self): + obj = PluginImageFactory() + slug = obj.plugin.slug + extension = 'jpg' + filename = f'test_image.{extension}' + image_number = f'{randint(1, 10):04}' + with mock.patch( + target='project_manager.plugins.helpers.find_image_number', + return_value=image_number, + ): + self.assertEqual( + first=handle_plugin_image_upload( + instance=obj, + filename=filename, + ), + second=f'{PLUGIN_IMAGE_URL}{slug}/{image_number}.{extension}', + ) diff --git a/project_manager/sub_plugins/tests/test_helpers.py b/project_manager/sub_plugins/tests/test_helpers.py new file mode 100644 index 00000000..44205bb6 --- /dev/null +++ b/project_manager/sub_plugins/tests/test_helpers.py @@ -0,0 +1,85 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from random import randint +from unittest import mock + +# Django +from django.test import TestCase + +# App +from project_manager.sub_plugins.constants import ( + SUB_PLUGIN_IMAGE_URL, + SUB_PLUGIN_LOGO_URL, + SUB_PLUGIN_RELEASE_URL, +) +from project_manager.sub_plugins.helpers import ( + handle_sub_plugin_image_upload, + handle_sub_plugin_logo_upload, + handle_sub_plugin_zip_upload, +) +from test_utils.factories.sub_plugins import ( + SubPluginFactory, + SubPluginImageFactory, + SubPluginReleaseFactory, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginZipFileTestCase(TestCase): + # TODO: Add tests for SubPluginZipFile class + pass + + +class HelperFunctionsTestCase(TestCase): + def test_handle_sub_plugin_zip_upload(self): + obj = SubPluginReleaseFactory() + plugin_slug = obj.sub_plugin.plugin.slug + slug = obj.sub_plugin.slug + self.assertEqual( + first=handle_sub_plugin_zip_upload(obj), + second=( + f'{SUB_PLUGIN_RELEASE_URL}{plugin_slug}/{slug}/' + f'{slug}-v{obj.version}.zip' + ), + ) + + def test_handle_sub_plugin_logo_upload(self): + obj = SubPluginFactory() + plugin_slug = obj.plugin.slug + extension = 'jpg' + filename = f'test_image.{extension}' + self.assertEqual( + first=handle_sub_plugin_logo_upload( + instance=obj, + filename=filename, + ), + second=( + f'{SUB_PLUGIN_LOGO_URL}{plugin_slug}/{obj.slug}.{extension}' + ), + ) + + def test_handle_sub_plugin_image_upload(self): + obj = SubPluginImageFactory() + plugin_slug = obj.sub_plugin.plugin.slug + slug = obj.sub_plugin.slug + extension = 'jpg' + filename = f'test_image.{extension}' + image_number = f'{randint(1, 10):04}' + with mock.patch( + target='project_manager.sub_plugins.helpers.find_image_number', + return_value=image_number, + ): + self.assertEqual( + first=handle_sub_plugin_image_upload( + instance=obj, + filename=filename, + ), + second=( + f'{SUB_PLUGIN_IMAGE_URL}{plugin_slug}/{slug}/' + f'{image_number}.{extension}' + ), + ) From 3ee14b3b234360080e1b8b385725c1259dd3a522 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Fri, 5 Nov 2021 23:54:03 -0400 Subject: [PATCH 052/211] Added a few more tests. --- project_manager/common/helpers.py | 6 +- project_manager/common/tests/test_helpers.py | 249 +++++++++++++++++++ project_manager/tests/test_views.py | 7 +- 3 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 project_manager/common/tests/test_helpers.py diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 77afc29c..6e3746e7 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -5,7 +5,7 @@ # ============================================================================= # Python import json -from zipfile import ZipFile, BadZipfile +from zipfile import ZipFile, BadZipFile # Django from django.apps import apps @@ -128,10 +128,10 @@ def get_file_list(zip_obj): """Return a list of all files in the given zip file.""" try: return [x for x in zip_obj.namelist() if not x.endswith('/')] - except BadZipfile: + except BadZipFile: raise ValidationError({ 'zip_file': 'Given file is not a valid zip file.' - }) from BadZipfile + }) from BadZipFile def validate_basename(self): """Validate that the basename is not erroneous.""" diff --git a/project_manager/common/tests/test_helpers.py b/project_manager/common/tests/test_helpers.py new file mode 100644 index 00000000..d92dece7 --- /dev/null +++ b/project_manager/common/tests/test_helpers.py @@ -0,0 +1,249 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from random import sample +from unittest import mock +from zipfile import BadZipFile + +# Django +from django.core.exceptions import ValidationError +from django.test import TestCase + +# App +from project_manager.common.constants import ( + CANNOT_BE_NAMED, + CANNOT_START_WITH, +) +from project_manager.common.helpers import ( + ProjectZipFile, + find_image_number, + handle_project_image_upload, + handle_project_logo_upload, + handle_release_zip_file_upload, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ProjectZipFileTestCase(TestCase): + + def setUp(self) -> None: + super().setUp() + self.mock_zip_file = mock.patch( + target='project_manager.common.helpers.ZipFile', + ).start() + + def tearDown(self) -> None: + super().tearDown() + mock.patch.stopall() + + def test_get_file_list(self): + zip_obj = self.mock_zip_file.return_value.__enter__.return_value + name_list = zip_obj.namelist.return_value = ( + 'addons/', + 'addons/source-python/', + 'addons/source-python/plugins/', + 'addons/source-python/plugins/test_plugin/', + 'addons/source-python/plugins/test_plugin/test_plugin.py', + 'addons/source-python/plugins/test_plugin/requirements.json', + ) + zip_file = 'test.zip' + obj = ProjectZipFile(zip_file) + self.assertEqual( + first=obj.zip_file, + second=zip_file, + ) + self.assertListEqual( + list1=obj.file_list, + list2=list(name_list[4:]), + ) + + zip_obj.namelist.side_effect = BadZipFile() + with self.assertRaises(ValidationError) as context: + ProjectZipFile(zip_file) + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + errors = context.exception.message_dict['zip_file'] + self.assertEqual( + first=len(errors), + second=1, + ) + self.assertEqual( + first=errors[0], + second='Given file is not a valid zip file.', + ) + + def test_project_type_required(self): + obj = ProjectZipFile('') + with self.assertRaises(NotImplementedError) as context: + _ = obj.project_type + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_type" attribute.' + ), + ) + + def test_find_base_info_required(self): + obj = ProjectZipFile('') + with self.assertRaises(NotImplementedError) as context: + obj.find_base_info() + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"find_base_info" method.' + ), + ) + + def test_get_base_paths_required(self): + obj = ProjectZipFile('') + with self.assertRaises(NotImplementedError) as context: + obj.get_base_paths() + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"get_base_paths" method.' + ), + ) + + def test_get_requirement_path_required(self): + obj = ProjectZipFile('') + with self.assertRaises(NotImplementedError) as context: + obj.get_requirement_path() + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"get_requirement_path" method.' + ), + ) + + def test_validate_basename(self): + class TestProjectZipFile(ProjectZipFile): + project_type = 'test' + + obj = TestProjectZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_basename() + + self.assertEqual( + first=context.exception.message, + second=f'No base directory or file found for {obj.project_type}.', + ) + self.assertEqual( + first=context.exception.code, + second='not-found', + ) + + for name in CANNOT_BE_NAMED: + obj.basename = name + with self.assertRaises(ValidationError) as context: + obj.validate_basename() + + self.assertEqual( + first=context.exception.message, + second=f'{obj.project_type} basename cannot be "{obj.basename}".', + ) + self.assertEqual( + first=context.exception.code, + second='invalid', + ) + + for prefix in CANNOT_START_WITH: + obj.basename = f'{prefix}test' + with self.assertRaises(ValidationError) as context: + obj.validate_basename() + + self.assertEqual( + first=context.exception.message, + second=( + f'{obj.project_type} basename cannot start with ' + f'"{prefix}".' + ), + ) + self.assertEqual( + first=context.exception.code, + second='invalid', + ) + + obj.basename = 'base_name' + obj.validate_basename() + + +class CommonHelperFunctionsTestCase(TestCase): + + @mock.patch( + target='project_manager.common.models.settings.MEDIA_ROOT', + ) + def test_find_image_number(self, mock_media_root): + base_directory = mock_media_root.__truediv__.return_value + path = base_directory.__truediv__.return_value.__truediv__.return_value + path.isdir.return_value = False + self.assertEqual( + first=find_image_number( + directory='directory', + slug='slug', + ), + second=f'{1:04}', + ) + + path.isdir.return_value = True + existing_files = sample(range(11), 4) + max_value = max(existing_files) + path.files.return_value = ( + mock.Mock(stem=n) + for n in existing_files + ) + self.assertEqual( + first=find_image_number( + directory='directory', + slug='slug', + ), + second=f'{max_value + 1:04}', + ) + + @staticmethod + def test_handle_project_image_upload(): + obj = mock.Mock() + filename = 'test.zip' + handle_project_image_upload( + instance=obj, + filename=filename, + ) + obj.handle_image_upload.assert_called_once_with(filename) + + @staticmethod + def test_handle_project_logo_upload(): + obj = mock.Mock() + filename = 'test.zip' + handle_project_logo_upload( + instance=obj, + filename=filename, + ) + obj.handle_logo_upload.assert_called_once_with(filename) + + @staticmethod + def test_handle_release_zip_file_upload(): + obj = mock.Mock() + filename = 'test.zip' + handle_release_zip_file_upload( + instance=obj, + filename=filename, + ) + obj.handle_zip_file_upload.assert_called_once_with() diff --git a/project_manager/tests/test_views.py b/project_manager/tests/test_views.py index 92471a8b..741b2a16 100644 --- a/project_manager/tests/test_views.py +++ b/project_manager/tests/test_views.py @@ -80,7 +80,7 @@ def test_get_view(self): sub_plugin_count = 0 plugin_download_count = 0 plugin_count = randint(4, 8) - for _ in range(plugin_count): + for n in range(1, plugin_count + 1): contributors = sample(user_list, randint(2, 4)) contributing_users.update(contributors) owner = contributors.pop() @@ -101,7 +101,10 @@ def test_get_view(self): download_count=download_count, ) - if choice([True, False]): + if any([ + choice([True, False]), + (n == plugin_count and not sub_plugin_count), + ]): count = randint(1, 2) sub_plugin_count += count for _ in range(count): From 4a0f7a613cd7078eb22d1e67fcba79a51e2faf2c Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 6 Nov 2021 00:40:50 -0400 Subject: [PATCH 053/211] Changed use of add_project to instead break out functionality into its own mixin. --- project_manager/common/api/serializers/__init__.py | 9 ++++----- project_manager/common/api/serializers/mixins.py | 12 ++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index dbfb5c8e..1e2f5a7f 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -22,6 +22,7 @@ # App from project_manager.common.api.serializers.mixins import ( + AddProjectToViewMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, ProjectThroughMixin, @@ -321,8 +322,6 @@ class Meta: class ProjectImageSerializer(ProjectThroughMixin): """Base ProjectImage Serializer.""" - add_project = False - class Meta: """Define metaclass attributes.""" @@ -337,7 +336,7 @@ def create(self, validated_data): return super().create(validated_data=validated_data) -class ProjectGameSerializer(ProjectThroughMixin): +class ProjectGameSerializer(ProjectThroughMixin, AddProjectToViewMixin): """Base ProjectGame Serializer.""" game_slug = CharField( @@ -374,7 +373,7 @@ def validate(self, attrs): return super().validate(attrs=attrs) -class ProjectTagSerializer(ProjectThroughMixin): +class ProjectTagSerializer(ProjectThroughMixin, AddProjectToViewMixin): """Base ProjectTag Serializer.""" tag = CharField( @@ -410,7 +409,7 @@ def validate(self, attrs): return super().validate(attrs=attrs) -class ProjectContributorSerializer(ProjectThroughMixin): +class ProjectContributorSerializer(ProjectThroughMixin, AddProjectToViewMixin): """Base ProjectContributor Serializer.""" username = CharField( diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 1ff29145..3eb6f4be 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -15,6 +15,7 @@ # ALL DECLARATION # ============================================================================= __all__ = ( + 'AddProjectToViewMixin', 'ProjectLocaleMixin', 'ProjectReleaseCreationMixin', 'ProjectThroughMixin', @@ -219,8 +220,6 @@ def _create_group_requirements(release, project_type, group_type, group): class ProjectThroughMixin(ModelSerializer): """Mixin for through model serializers.""" - add_project = True - def get_field_names(self, declared_fields, info): """Add the 'id' field if necessary.""" field_names = super().get_field_names( @@ -234,13 +233,14 @@ def get_field_names(self, declared_fields, info): user = request.user.id if view.owner == user: return field_names + ('id',) - if user in view.contributors and not view.owner_only: + if user in view.contributors and not view.owner_only_id_access: return field_names + ('id',) return field_names + +class AddProjectToViewMixin(ModelSerializer): def validate(self, attrs): """Add the project to the validated data.""" - if self.add_project: - view = self.context['view'] - attrs[view.project_type.replace('-', '_')] = view.project + view = self.context['view'] + attrs[view.project_type.replace('-', '_')] = view.project return super().validate(attrs=attrs) From 3c706fffe22ea33c81652ec6e4a8f6c44ac6217f Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 6 Nov 2021 00:41:35 -0400 Subject: [PATCH 054/211] Renamed owner_only to owner_only_id_access to better describe the attribute. --- project_manager/common/api/views/__init__.py | 2 +- project_manager/common/api/views/mixins.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 512648ca..b6581feb 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -207,4 +207,4 @@ class ProjectContributorViewSet(ProjectThroughModelMixin): ordering_fields = ('user',) related_model_type = 'Contributor' - owner_only = True + owner_only_id_access = True diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index 12d6953f..bacddfc2 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -95,7 +95,7 @@ class ProjectThroughModelMixin(ProjectRelatedInfoMixin): http_method_names = ('get', 'post', 'delete', 'options') permission_classes = (IsAuthenticatedOrReadOnly,) - owner_only = False + owner_only_id_access = False _owner = None _contributors = None @@ -123,6 +123,6 @@ def check_permissions(self, request): is_contributor = user in self.contributors if user != self.owner and not is_contributor: raise PermissionDenied - if self.owner_only and is_contributor: + if self.owner_only_id_access and is_contributor: raise PermissionDenied return super().check_permissions(request=request) From 394ca2d8981020dcabce37c9fad8aa424a57d258 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 6 Nov 2021 00:44:00 -0400 Subject: [PATCH 055/211] Fixed inheritance for recent mixin change. --- project_manager/plugins/api/serializers/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/project_manager/plugins/api/serializers/__init__.py b/project_manager/plugins/api/serializers/__init__.py index 23d91881..5e12a2ad 100644 --- a/project_manager/plugins/api/serializers/__init__.py +++ b/project_manager/plugins/api/serializers/__init__.py @@ -16,7 +16,10 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.common.api.serializers.mixins import ProjectThroughMixin +from project_manager.common.api.serializers.mixins import ( + AddProjectToViewMixin, + ProjectThroughMixin, +) from project_manager.packages.api.serializers.common import ( ReleasePackageRequirementSerializer, ) @@ -210,7 +213,7 @@ class Meta(ProjectContributorSerializer.Meta): model = PluginContributor -class SubPluginPathSerializer(ProjectThroughMixin): +class SubPluginPathSerializer(ProjectThroughMixin, AddProjectToViewMixin): """Sub-Plugin Paths Serializer.""" class Meta: From 27f05c2778519a443eaddd6fb20dea9ea81ec751 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 6 Nov 2021 00:45:04 -0400 Subject: [PATCH 056/211] Added more tests. --- .../common/api/tests/test_serializers.py | 61 +++++++++++++++ .../packages/tests/test_helpers.py | 74 ++++++++++++------- project_manager/plugins/tests/test_helpers.py | 68 ++++++++++------- 3 files changed, 150 insertions(+), 53 deletions(-) create mode 100644 project_manager/common/api/tests/test_serializers.py diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/common/api/tests/test_serializers.py new file mode 100644 index 00000000..76c18534 --- /dev/null +++ b/project_manager/common/api/tests/test_serializers.py @@ -0,0 +1,61 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase +from django.utils import formats +from django.utils.timezone import now + +# Third Party Django +from rest_framework.serializers import ModelSerializer + +# App +from project_manager.common.api.serializers.mixins import ( + AddProjectToViewMixin, + ProjectLocaleMixin, + ProjectThroughMixin, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ProjectLocaleMixinTestCase(TestCase): + def test_get_date_time_dict(self): + self.assertDictEqual( + d1=ProjectLocaleMixin().get_date_time_dict(timestamp=None), + d2={ + 'actual': None, + 'locale': None, + 'locale_short': None, + }, + ) + timestamp = now() + self.assertDictEqual( + d1=ProjectLocaleMixin().get_date_time_dict(timestamp=timestamp), + d2={ + 'actual': timestamp, + 'locale': formats.date_format( + value=timestamp, + format='DATETIME_FORMAT', + ), + 'locale_short': formats.date_format( + value=timestamp, + format='SHORT_DATETIME_FORMAT', + ), + }, + ) + + +class ProjectThroughMixinTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectThroughMixin, ModelSerializer), + ) + + +class AddProjectToViewMixinTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(AddProjectToViewMixin, ModelSerializer), + ) diff --git a/project_manager/packages/tests/test_helpers.py b/project_manager/packages/tests/test_helpers.py index a4d3d61f..7e24ec93 100644 --- a/project_manager/packages/tests/test_helpers.py +++ b/project_manager/packages/tests/test_helpers.py @@ -35,6 +35,20 @@ # TEST CASES # ============================================================================= class PackageZipFileTestCase(TestCase): + + def setUp(self) -> None: + super().setUp() + mock.patch( + target='project_manager.common.helpers.ZipFile', + ).start() + self.mock_get_file_list = mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ).start() + + def tearDown(self) -> None: + super().tearDown() + mock.patch.stopall() + @staticmethod def _get_module_file_list(package_basename): return tuple( @@ -76,15 +90,9 @@ def test_file_types(self): d2=PACKAGE_ALLOWED_FILE_TYPES, ) - @mock.patch( - target='project_manager.common.helpers.ZipFile', - ) - @mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', - ) - def test_find_base_info(self, mock_get_file_list, _): + def test_find_base_info(self): package_basename = 'test_package_as_module' - mock_get_file_list.return_value = self._get_module_file_list( + self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, ) obj = PackageZipFile('') @@ -96,7 +104,7 @@ def test_find_base_info(self, mock_get_file_list, _): ) package_basename = 'test_package_as_package' - mock_get_file_list.return_value = self._get_package_file_list( + self.mock_get_file_list.return_value = self._get_package_file_list( package_basename=package_basename, ) obj = PackageZipFile('') @@ -107,7 +115,7 @@ def test_find_base_info(self, mock_get_file_list, _): second=package_basename, ) - mock_get_file_list.return_value += ( + self.mock_get_file_list.return_value += ( f'{PACKAGE_PATH}second_basename/__init__.py', ) with self.assertRaises(ValidationError) as context: @@ -123,15 +131,9 @@ def test_find_base_info(self, mock_get_file_list, _): second='multiple', ) - @mock.patch( - target='project_manager.common.helpers.ZipFile', - ) - @mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', - ) - def test_get_base_paths(self, mock_get_file_list, _): + def test_get_base_paths(self): package_basename = 'test_package_as_module' - mock_get_file_list.return_value = self._get_module_file_list( + self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, ) obj = PackageZipFile('') @@ -142,7 +144,7 @@ def test_get_base_paths(self, mock_get_file_list, _): ) package_basename = 'test_package_as_package' - mock_get_file_list.return_value = self._get_package_file_list( + self.mock_get_file_list.return_value = self._get_package_file_list( package_basename=package_basename, ) obj = PackageZipFile('') @@ -155,15 +157,31 @@ def test_get_base_paths(self, mock_get_file_list, _): ], ) - @mock.patch( - target='project_manager.common.helpers.ZipFile', - ) - @mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', - ) - def test_get_requirement_path(self, mock_get_file_list, _): + def test_validate_base_file_in_zip(self): + package_basename = 'test_package_as_module' + self.mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + + obj.basename = 'invalid' + with self.assertRaises(ValidationError) as context: + obj.validate_base_file_in_zip() + + self.assertEqual( + first=context.exception.message, + second='No primary file found in zip.', + ) + self.assertEqual( + first=context.exception.code, + second='not-found', + ) + + def test_get_requirement_path(self): package_basename = 'test_package_as_module' - mock_get_file_list.return_value = self._get_module_file_list( + self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, ) obj = PackageZipFile('') @@ -174,7 +192,7 @@ def test_get_requirement_path(self, mock_get_file_list, _): ) package_basename = 'test_package_as_package' - mock_get_file_list.return_value = self._get_package_file_list( + self.mock_get_file_list.return_value = self._get_package_file_list( package_basename=package_basename, ) obj = PackageZipFile('') diff --git a/project_manager/plugins/tests/test_helpers.py b/project_manager/plugins/tests/test_helpers.py index ef12a54b..1d027cdd 100644 --- a/project_manager/plugins/tests/test_helpers.py +++ b/project_manager/plugins/tests/test_helpers.py @@ -35,6 +35,20 @@ # TEST CASES # ============================================================================= class PluginZipFileTestCase(TestCase): + + def setUp(self) -> None: + super().setUp() + mock.patch( + target='project_manager.common.helpers.ZipFile', + ).start() + self.mock_get_file_list = mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ).start() + + def tearDown(self) -> None: + super().tearDown() + mock.patch.stopall() + @staticmethod def _get_file_list(plugin_basename): return tuple( @@ -65,15 +79,9 @@ def test_file_types(self): d2=PLUGIN_ALLOWED_FILE_TYPES, ) - @mock.patch( - target='project_manager.common.helpers.ZipFile', - ) - @mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', - ) - def test_find_base_info(self, mock_get_file_list, _): + def test_find_base_info(self): plugin_basename = 'test_plugin' - mock_get_file_list.return_value = self._get_file_list( + self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, ) obj = PluginZipFile('') @@ -83,7 +91,7 @@ def test_find_base_info(self, mock_get_file_list, _): second=plugin_basename, ) - mock_get_file_list.return_value += ( + self.mock_get_file_list.return_value += ( f'{PLUGIN_PATH}second_basename/__init__.py', ) with self.assertRaises(ValidationError) as context: @@ -99,15 +107,9 @@ def test_find_base_info(self, mock_get_file_list, _): second='multiple', ) - @mock.patch( - target='project_manager.common.helpers.ZipFile', - ) - @mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', - ) - def test_get_base_paths(self, mock_get_file_list, _): + def test_get_base_paths(self): plugin_basename = 'test_plugin' - mock_get_file_list.return_value = self._get_file_list( + self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, ) obj = PluginZipFile('') @@ -117,15 +119,31 @@ def test_get_base_paths(self, mock_get_file_list, _): list2=[f'{PLUGIN_PATH}{plugin_basename}/{plugin_basename}.py'], ) - @mock.patch( - target='project_manager.common.helpers.ZipFile', - ) - @mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', - ) - def test_get_requirement_path(self, mock_get_file_list, _): + def test_validate_base_file_in_zip(self): + plugin_basename = 'test_plugin' + self.mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + obj = PluginZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + + obj.basename = 'invalid' + with self.assertRaises(ValidationError) as context: + obj.validate_base_file_in_zip() + + self.assertEqual( + first=context.exception.message, + second='No primary file found in zip.', + ) + self.assertEqual( + first=context.exception.code, + second='not-found', + ) + + def test_get_requirement_path(self): plugin_basename = 'test_plugin' - mock_get_file_list.return_value = self._get_file_list( + self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, ) obj = PluginZipFile('') From e2497df18d21c72b92b3155c0a9afed88e9ce6f4 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 6 Nov 2021 00:48:18 -0400 Subject: [PATCH 057/211] Added missing docstring. --- project_manager/common/api/serializers/mixins.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 3eb6f4be..d8f5eb5d 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -239,6 +239,8 @@ def get_field_names(self, declared_fields, info): class AddProjectToViewMixin(ModelSerializer): + """Mixin used to add the project to the serialized data.""" + def validate(self, attrs): """Add the project to the validated data.""" view = self.context['view'] From 46c8ebfb2f8d2193c6862c11c0b00ff8fc2a4e77 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 6 Nov 2021 21:29:31 -0400 Subject: [PATCH 058/211] Added more serializer tests. --- .../common/api/tests/test_serializers.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/common/api/tests/test_serializers.py index 76c18534..957aac5d 100644 --- a/project_manager/common/api/tests/test_serializers.py +++ b/project_manager/common/api/tests/test_serializers.py @@ -1,6 +1,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +from unittest import mock + # Django from django.test import TestCase from django.utils import formats @@ -15,6 +18,7 @@ ProjectLocaleMixin, ProjectThroughMixin, ) +from test_utils.factories.users import ForumUserFactory # ============================================================================= @@ -48,14 +52,136 @@ def test_get_date_time_dict(self): class ProjectThroughMixinTestCase(TestCase): + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.user = ForumUserFactory() + + def setUp(self) -> None: + super().setUp() + self.field_names = ( + 'name', + ) + self.mock_get_field_names = mock.patch( + target='rest_framework.serializers.ModelSerializer.get_field_names', + return_value=self.field_names, + ).start() + + def tearDown(self) -> None: + super().tearDown() + mock.patch.stopall() + def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectThroughMixin, ModelSerializer), ) + def test_get_field_names_not_get(self): + obj = ProjectThroughMixin( + context={ + 'request': mock.Mock( + method='POST', + ), + }, + ) + self.assertTupleEqual( + tuple1=obj.get_field_names('', ''), + tuple2=self.field_names, + ) + + def test_get_field_names_no_view(self): + obj = ProjectThroughMixin( + context={ + 'request': mock.Mock( + method='GET', + ), + }, + ) + self.assertTupleEqual( + tuple1=obj.get_field_names('', ''), + tuple2=self.field_names, + ) + + def test_get_field_names_owner(self): + obj = ProjectThroughMixin( + context={ + 'request': mock.Mock( + method='GET', + user=self.user.user, + ), + 'view': mock.Mock( + owner=self.user.user.id, + ) + }, + ) + self.assertTupleEqual( + tuple1=obj.get_field_names('', ''), + tuple2=self.field_names + ('id',), + ) + + def test_get_field_names_contributor(self): + obj = ProjectThroughMixin( + context={ + 'request': mock.Mock( + method='GET', + user=self.user.user, + ), + 'view': mock.Mock( + contributors=(self.user.user.id,), + owner_only_id_access=False, + ) + }, + ) + self.assertTupleEqual( + tuple1=obj.get_field_names('', ''), + tuple2=self.field_names + ('id',), + ) + + def test_get_field_names_contributor_owner_only(self): + obj = ProjectThroughMixin( + context={ + 'request': mock.Mock( + method='GET', + user=self.user.user, + ), + 'view': mock.Mock( + contributors=(self.user.user.id,), + owner_only_id_access=True, + ), + }, + ) + self.assertTupleEqual( + tuple1=obj.get_field_names('', ''), + tuple2=self.field_names, + ) + class AddProjectToViewMixinTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( expr=issubclass(AddProjectToViewMixin, ModelSerializer), ) + + def test_validate(self): + project_type = 'test-type' + project = mock.Mock() + obj = AddProjectToViewMixin( + context={ + 'view': mock.Mock( + project_type=project_type, + project=project, + ), + }, + ) + original_attrs = { + 'field': 'value', + } + return_attrs = dict(original_attrs) + return_attrs.update({ + project_type.replace('-', '_'): project, + }) + self.assertDictEqual( + d1=obj.validate(original_attrs), + d2=return_attrs, + ) From 1446c935692da73be1cdaa41f4d52260775e08e7 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 6 Nov 2021 21:31:22 -0400 Subject: [PATCH 059/211] Added venv to prospector ignore-paths. --- prospector.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/prospector.yaml b/prospector.yaml index 9c0cb40d..9db0e2ab 100644 --- a/prospector.yaml +++ b/prospector.yaml @@ -19,6 +19,7 @@ ignore-paths: - tests - .git - .idea + - venv pep8: run: true From 1c6b4abd8b70e81d8446d6a29ba1ff8e12271649 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Mon, 8 Nov 2021 22:37:28 -0500 Subject: [PATCH 060/211] Changed default file_type to a property that validates proper value is set. --- project_manager/common/helpers.py | 39 ++++++++++++-------- project_manager/common/tests/test_helpers.py | 13 +++++++ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 6e3746e7..eca6157f 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -51,8 +51,6 @@ class ProjectZipFile: """Base ZipFile parsing class.""" - file_types = None - def __init__(self, zip_file): """Store the base attributes for the zip file.""" self.zip_file = zip_file @@ -75,6 +73,14 @@ def project_type(self): f'"project_type" attribute.' ) + @property + def file_types(self): + """Return the type of project.""" + raise NotImplementedError( + f'Class "{self.__class__.__name__}" must implement a ' + f'"file_types" attribute.' + ) + def find_base_info(self): """Store all base information for the zip file.""" raise NotImplementedError( @@ -105,22 +111,27 @@ def validate_file_paths(self): def _validate_path(self, path): """Validate the given path is ok for the extension.""" - if self.file_types is None: - raise NotImplementedError( - f'File types not set for {self.__class__.__name__}.' - ) + if path.endswith('/'): + return True - extension = path.rsplit('.')[1] - if '/' in extension: + try: + extension = path.rsplit('/', 1)[1].rsplit('.', 1)[1] + except IndexError: + print(path) return True for base_path, allowed_extensions in self.file_types.items(): if not path.startswith(base_path.format(self=self)): continue + + # extension allowed for path if extension in allowed_extensions: return True + + # extension not allowed for path return False + # File not found in any allowed paths return False @staticmethod @@ -211,19 +222,15 @@ def validate_requirements(self): self._validate_custom_requirement( item=item, ) - elif group_type == 'pypi': - self._validate_requirement( - item=item, - group_type=group_type, - field='name', - include_version=True, - ) else: + is_pypi = group_type == 'pypi' self._validate_requirement( item=item, group_type=group_type, - field='url', + field='name' if is_pypi else 'url', + include_version=is_pypi, ) + if self.requirements_errors: raise ValidationError({ 'zip_file': self.requirements_errors, diff --git a/project_manager/common/tests/test_helpers.py b/project_manager/common/tests/test_helpers.py index d92dece7..3beddc2b 100644 --- a/project_manager/common/tests/test_helpers.py +++ b/project_manager/common/tests/test_helpers.py @@ -95,6 +95,19 @@ def test_project_type_required(self): ), ) + def test_file_types_required(self): + obj = ProjectZipFile('') + with self.assertRaises(NotImplementedError) as context: + _ = obj.file_types + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"file_types" attribute.' + ), + ) + def test_find_base_info_required(self): obj = ProjectZipFile('') with self.assertRaises(NotImplementedError) as context: From 268b0fce0925c3f81a86010434dc25248b01d16a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Mon, 8 Nov 2021 22:38:38 -0500 Subject: [PATCH 061/211] Added tests for validate_file_paths for plugins/packages. --- project_manager/packages/constants.py | 2 +- .../packages/tests/test_helpers.py | 64 +++++++++++++++++++ project_manager/plugins/tests/test_helpers.py | 64 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/project_manager/packages/constants.py b/project_manager/packages/constants.py index 7165831f..dc607c71 100644 --- a/project_manager/packages/constants.py +++ b/project_manager/packages/constants.py @@ -33,7 +33,7 @@ PACKAGE_ALLOWED_FILE_TYPES = dict(ALLOWED_FILE_TYPES) PACKAGE_ALLOWED_FILE_TYPES.update({ # Just the base file if just a module - PACKAGE_PATH: ['py'], + PACKAGE_PATH: ['py'] + READABLE_DATA_FILE_TYPES, # Other files allowed if in a package PACKAGE_PATH + '{self.basename}/': ['py'] + READABLE_DATA_FILE_TYPES, diff --git a/project_manager/packages/tests/test_helpers.py b/project_manager/packages/tests/test_helpers.py index 7e24ec93..decd5d5e 100644 --- a/project_manager/packages/tests/test_helpers.py +++ b/project_manager/packages/tests/test_helpers.py @@ -202,6 +202,70 @@ def test_get_requirement_path(self): second=f'{PACKAGE_PATH}{package_basename}/requirements.json', ) + def test_validate_file_paths(self): + package_basename = 'test_package_as_module' + self.mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + obj = PackageZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + obj.validate_file_paths() + + invalid_file = f'{PACKAGE_PATH}{package_basename}.invalid' + self.mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + (invalid_file, ) + obj = PackageZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + with self.assertRaises(ValidationError) as context: + obj.validate_file_paths() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['zip_file']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['zip_file'][0], + second=f'Invalid paths found in zip: {invalid_file}', + ) + + invalid_file = f'invalid/{package_basename}.py' + self.mock_get_file_list.return_value = self._get_module_file_list( + package_basename=package_basename, + ) + (invalid_file, ) + obj = PackageZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + with self.assertRaises(ValidationError) as context: + obj.validate_file_paths() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['zip_file']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['zip_file'][0], + second=f'Invalid paths found in zip: {invalid_file}', + ) + class HelperFunctionsTestCase(TestCase): def test_handle_package_zip_upload(self): diff --git a/project_manager/plugins/tests/test_helpers.py b/project_manager/plugins/tests/test_helpers.py index 1d027cdd..f3955653 100644 --- a/project_manager/plugins/tests/test_helpers.py +++ b/project_manager/plugins/tests/test_helpers.py @@ -153,6 +153,70 @@ def test_get_requirement_path(self): second=f'{PLUGIN_PATH}{plugin_basename}/requirements.json', ) + def test_validate_file_paths(self): + plugin_basename = 'test_plugin' + self.mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + obj = PluginZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + obj.validate_file_paths() + + invalid_file = f'{PLUGIN_PATH}{plugin_basename}/{plugin_basename}.invalid' + self.mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + (invalid_file, ) + obj = PluginZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + with self.assertRaises(ValidationError) as context: + obj.validate_file_paths() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['zip_file']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['zip_file'][0], + second=f'Invalid paths found in zip: {invalid_file}', + ) + + invalid_file = f'invalid/{plugin_basename}/{plugin_basename}.py' + self.mock_get_file_list.return_value = self._get_file_list( + plugin_basename=plugin_basename, + ) + (invalid_file, ) + obj = PluginZipFile('') + obj.find_base_info() + obj.validate_base_file_in_zip() + with self.assertRaises(ValidationError) as context: + obj.validate_file_paths() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['zip_file']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['zip_file'][0], + second=f'Invalid paths found in zip: {invalid_file}', + ) + class HelperFunctionsTestCase(TestCase): def test_handle_plugin_zip_upload(self): From 3068e0cf953ff1659db6857acb7592d73dec305b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Mon, 8 Nov 2021 22:39:36 -0500 Subject: [PATCH 062/211] Updated some logic in sub_plugins helpers and added more tests for the module. --- project_manager/sub_plugins/helpers.py | 37 ++- .../sub_plugins/tests/test_helpers.py | 263 +++++++++++++++++- 2 files changed, 284 insertions(+), 16 deletions(-) diff --git a/project_manager/sub_plugins/helpers.py b/project_manager/sub_plugins/helpers.py index 155309e8..ee492d29 100644 --- a/project_manager/sub_plugins/helpers.py +++ b/project_manager/sub_plugins/helpers.py @@ -46,13 +46,12 @@ def __init__(self, zip_file, plugin): def _validate_path(self, path): """Validate the given path is ok for the extension.""" - if self.file_types is None: - raise NotImplementedError( - f'File types not set for {self.__class__.__name__}.' - ) + if path.endswith('/'): + return True - extension = path.rsplit('.')[1] - if '/' in extension: + try: + extension = path.rsplit('/', 1)[1].rsplit('.', 1)[1] + except IndexError: return True for base_path, allowed_extensions in self.file_types.items(): @@ -78,9 +77,6 @@ def find_base_info(self): plugin_path = f'{PLUGIN_PATH}{self.plugin.basename}/' paths = list(self.plugin.paths.values_list('path', flat=True)) for file_path in self.file_list: - if not file_path.endswith('.py'): - continue - if not file_path.startswith(plugin_path): # TODO: validate not another plugin path or package continue @@ -89,18 +85,24 @@ def find_base_info(self): if not current: continue + if not file_path.endswith('.py'): + continue + for current_path in paths: if not current.startswith(current_path): continue current = current.split(current_path, 1)[1] - if current.startswith('/'): + if current.startswith('/'): # pragma: no branch current = current[1:] current = current.split('/', 1)[0] - if not current: + if not current: # pragma: no cover continue + if current.endswith('.py'): + current = current[:~2] + if self.basename is None: self.basename = current @@ -138,10 +140,12 @@ def validate_base_file_in_zip(self): def _validate_base_file_in_zip(self, base_path, path_values): """Verify a base file is found in the given path.""" - if not base_path.startswith('/'): + if not base_path.startswith('/'): # pragma: no branch base_path = '/' + base_path - if not base_path.endswith('/'): + + if not base_path.endswith('/'): # pragma: no branch base_path += '/' + sub_path = f'{PLUGIN_PATH}{self.plugin.basename}{base_path}' module_found = package_found = False if path_values['allow_module']: @@ -161,7 +165,8 @@ def _validate_base_file_in_zip(self, base_path, path_values): message=( f'SubPlugin found as both a module and package in the same' f' path: "{sub_path}".' - ) + ), + code='invalid', ) if package_found or module_found: @@ -171,11 +176,13 @@ def _validate_base_file_in_zip(self, base_path, path_values): message=( f'SubPlugin not found in path, though files found within zip ' f'for directory: "{sub_path}".' - ) + ), + code='not-found', ) def get_requirement_path(self): """Return the path for the requirements json file.""" + # TODO: this is incorrect...it should take into account the sub-path if self.is_module: return ( f'{PLUGIN_PATH}{self.plugin.basename}/' diff --git a/project_manager/sub_plugins/tests/test_helpers.py b/project_manager/sub_plugins/tests/test_helpers.py index 44205bb6..a334789f 100644 --- a/project_manager/sub_plugins/tests/test_helpers.py +++ b/project_manager/sub_plugins/tests/test_helpers.py @@ -6,19 +6,25 @@ from unittest import mock # Django +from django.core.exceptions import ValidationError from django.test import TestCase # App +from project_manager.common.helpers import ProjectZipFile +from project_manager.plugins.constants import PLUGIN_PATH from project_manager.sub_plugins.constants import ( + SUB_PLUGIN_ALLOWED_FILE_TYPES, SUB_PLUGIN_IMAGE_URL, SUB_PLUGIN_LOGO_URL, SUB_PLUGIN_RELEASE_URL, ) from project_manager.sub_plugins.helpers import ( + SubPluginZipFile, handle_sub_plugin_image_upload, handle_sub_plugin_logo_upload, handle_sub_plugin_zip_upload, ) +from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory from test_utils.factories.sub_plugins import ( SubPluginFactory, SubPluginImageFactory, @@ -31,7 +37,262 @@ # ============================================================================= class SubPluginZipFileTestCase(TestCase): # TODO: Add tests for SubPluginZipFile class - pass + + base_path = plugin = sub_plugin_path = None + + @classmethod + def setUpTestData(cls): + cls.plugin = PluginFactory() + cls.sub_plugin_path = SubPluginPathFactory( + plugin=cls.plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + SubPluginPathFactory( + plugin=cls.plugin, + path='other_path', + ) + cls.base_path = f'{PLUGIN_PATH}{cls.plugin.basename}/sub_plugins/' + + def setUp(self) -> None: + super().setUp() + mock.patch( + target='project_manager.common.helpers.ZipFile', + ).start() + self.mock_get_file_list = mock.patch( + target='project_manager.common.helpers.ProjectZipFile.get_file_list', + ).start() + + def tearDown(self) -> None: + super().tearDown() + mock.patch.stopall() + + def _get_file_list(self, sub_plugin_basename): + base_path = f'{self.base_path}{sub_plugin_basename}' + return tuple( + reversed([ + base_path.rsplit('/', i)[0] + '/' + for i in range(1, base_path.count('/') + 1) + ]) + ) + ( + f'{base_path}', + f'{base_path}/__init__.py', + f'{base_path}/{sub_plugin_basename}.py', + f'{base_path}/{sub_plugin_basename}/helpers.py', + f'{base_path}/{sub_plugin_basename}/requirements.json', + ) + + def _get_module_file_list(self, sub_plugin_basename): + return tuple( + reversed([ + self.base_path.rsplit('/', i)[0] + '/' + for i in range(1, self.base_path.count('/') + 1) + ]) + ) + ( + f'{self.base_path}{sub_plugin_basename}.py', + f'{self.base_path}{sub_plugin_basename}_requirements.json', + ) + + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginZipFile, ProjectZipFile)) + + def test_project_type(self): + self.assertEqual( + first=SubPluginZipFile.project_type, + second='SubPlugin', + ) + + def test_file_types(self): + self.assertDictEqual( + d1=SubPluginZipFile.file_types, + d2=SUB_PLUGIN_ALLOWED_FILE_TYPES, + ) + + def test_find_base_info(self): + sub_plugin_basename = 'test_sub_plugin' + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + self.assertEqual( + first=obj.basename, + second=sub_plugin_basename, + ) + + self.mock_get_file_list.return_value += ( + f'{self.base_path}second_basename/__init__.py', + ) + with self.assertRaises(ValidationError) as context: + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + + self.assertEqual( + first=context.exception.message, + second='Multiple sub-plugins found in zip.', + ) + self.assertEqual( + first=context.exception.code, + second='multiple', + ) + + def test_validate_base_file_in_zip(self): + sub_plugin_basename = 'test_plugin' + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + + self.sub_plugin_path.allow_package_using_basename = False + self.sub_plugin_path.allow_package_using_init = True + self.sub_plugin_path.save() + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + + self.sub_plugin_path.allow_package_using_init = False + self.sub_plugin_path.allow_module = True + self.sub_plugin_path.save() + self.mock_get_file_list.return_value = self._get_module_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + self.assertTrue(expr=obj.is_module) + + self.sub_plugin_path.allow_package_using_basename = True + self.sub_plugin_path.save() + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + self._get_module_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + with self.assertRaises(ValidationError) as context: + obj.validate_base_file_in_zip() + + self.assertEqual( + first=context.exception.message, + second=( + f'SubPlugin found as both a module and package in the same ' + f'path: "{self.base_path}".' + ), + ) + self.assertEqual( + first=context.exception.code, + second='invalid', + ) + + obj.basename = 'invalid' + with self.assertRaises(ValidationError) as context: + obj.validate_base_file_in_zip() + + self.assertEqual( + first=context.exception.message, + second=( + f'SubPlugin not found in path, though files found within zip ' + f'for directory: "{self.base_path}".' + ), + ) + self.assertEqual( + first=context.exception.code, + second='not-found', + ) + + def test_get_requirement_path(self): + sub_plugin_basename = 'test_sub_plugin' + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + self.assertEqual( + first=obj.get_requirement_path(), + second=f'{PLUGIN_PATH}{self.plugin.basename}/{sub_plugin_basename}/requirements.json', + ) + + self.sub_plugin_path.allow_package_using_basename = False + self.sub_plugin_path.allow_module = True + self.sub_plugin_path.save() + self.mock_get_file_list.return_value = self._get_module_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + self.assertEqual( + first=obj.get_requirement_path(), + second=f'{PLUGIN_PATH}{self.plugin.basename}/{sub_plugin_basename}_requirements.json', + ) + + def test_validate_file_paths(self): + sub_plugin_basename = 'test_plugin' + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + obj.validate_file_paths() + + invalid_file = f'{self.base_path}{sub_plugin_basename}/{sub_plugin_basename}.invalid' + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + (invalid_file, ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + with self.assertRaises(ValidationError) as context: + obj.validate_file_paths() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['zip_file']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['zip_file'][0], + second=f'Invalid paths found in zip: {invalid_file}', + ) + + invalid_file = f'invalid/{sub_plugin_basename}/{sub_plugin_basename}.py' + self.mock_get_file_list.return_value = self._get_file_list( + sub_plugin_basename=sub_plugin_basename, + ) + (invalid_file, ) + obj = SubPluginZipFile('', self.plugin) + obj.find_base_info() + obj.validate_base_file_in_zip() + with self.assertRaises(ValidationError) as context: + obj.validate_file_paths() + + self.assertEqual( + first=len(context.exception.message_dict), + second=1, + ) + self.assertIn( + member='zip_file', + container=context.exception.message_dict, + ) + self.assertEqual( + first=len(context.exception.message_dict['zip_file']), + second=1, + ) + self.assertEqual( + first=context.exception.message_dict['zip_file'][0], + second=f'Invalid paths found in zip: {invalid_file}', + ) class HelperFunctionsTestCase(TestCase): From 7aa738eb3ae2e409acaab07519be0485aabb574b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Mon, 8 Nov 2021 22:40:33 -0500 Subject: [PATCH 063/211] Added tests for the common api views. --- .../common/api/tests/test_views.py | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 project_manager/common/api/tests/test_views.py diff --git a/project_manager/common/api/tests/test_views.py b/project_manager/common/api/tests/test_views.py new file mode 100644 index 00000000..4492f4d8 --- /dev/null +++ b/project_manager/common/api/tests/test_views.py @@ -0,0 +1,208 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.authentication import SessionAuthentication +from rest_framework.filters import OrderingFilter +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet + +# App +from project_manager.common.api.views import ( + ProjectAPIView, + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectReleaseViewSet, + ProjectTagViewSet, + ProjectViewSet, +) +from project_manager.common.api.views.mixins import ( + ProjectRelatedInfoMixin, + ProjectThroughModelMixin, +) +from project_manager.common.constants import RELEASE_VERSION_REGEX + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class ProjectAPIViewTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectAPIView, APIView)) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=ProjectAPIView.http_method_names, + tuple2=('get', 'options'), + ) + + +class ProjectViewSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectViewSet, ModelViewSet)) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=ProjectViewSet.authentication_classes, + tuple2=(SessionAuthentication,), + ) + self.assertTupleEqual( + tuple1=ProjectViewSet.filter_backends, + tuple2=(OrderingFilter, DjangoFilterBackend), + ) + self.assertTupleEqual( + tuple1=ProjectViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + self.assertTupleEqual( + tuple1=ProjectViewSet.ordering, + tuple2=('-releases__created',), + ) + self.assertTupleEqual( + tuple1=ProjectViewSet.ordering_fields, + tuple2=('name', 'basename', 'updated', 'created'), + ) + self.assertTupleEqual( + tuple1=ProjectViewSet.permission_classes, + tuple2=(IsAuthenticatedOrReadOnly,), + ) + + def test_creation_serializer_class_required(self): + obj = ProjectViewSet() + with self.assertRaises(NotImplementedError) as context: + _ = obj.creation_serializer_class + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"creation_serializer_class" attribute.' + ), + ) + + +class ProjectImageViewSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectImageViewSet, ProjectThroughModelMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=ProjectImageViewSet.ordering, + tuple2=('-created',), + ) + self.assertTupleEqual( + tuple1=ProjectImageViewSet.ordering_fields, + tuple2=('created',), + ) + self.assertEqual( + first=ProjectImageViewSet.related_model_type, + second='Image', + ) + + +class ProjectReleaseViewSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectReleaseViewSet, ProjectRelatedInfoMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=ProjectReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + self.assertTupleEqual( + tuple1=ProjectReleaseViewSet.ordering, + tuple2=('-created',), + ) + self.assertTupleEqual( + tuple1=ProjectReleaseViewSet.ordering_fields, + tuple2=('created',), + ) + self.assertEqual( + first=ProjectReleaseViewSet.lookup_value_regex, + second=RELEASE_VERSION_REGEX, + ) + self.assertEqual( + first=ProjectReleaseViewSet.lookup_field, + second='version', + ) + self.assertEqual( + first=ProjectReleaseViewSet.related_model_type, + second='Release', + ) + + +class ProjectGameViewSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectGameViewSet, ProjectThroughModelMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=ProjectGameViewSet.ordering, + tuple2=('-game',), + ) + self.assertTupleEqual( + tuple1=ProjectGameViewSet.ordering_fields, + tuple2=('game',), + ) + self.assertEqual( + first=ProjectGameViewSet.related_model_type, + second='Game', + ) + + +class ProjectTagViewSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectTagViewSet, ProjectThroughModelMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=ProjectTagViewSet.ordering, + tuple2=('-tag',), + ) + self.assertTupleEqual( + tuple1=ProjectTagViewSet.ordering_fields, + tuple2=('tag',), + ) + self.assertEqual( + first=ProjectTagViewSet.related_model_type, + second='Tag', + ) + + +class ProjectContributorViewSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + ProjectContributorViewSet, + ProjectThroughModelMixin, + ), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=ProjectContributorViewSet.ordering, + tuple2=('-user',), + ) + self.assertTupleEqual( + tuple1=ProjectContributorViewSet.ordering_fields, + tuple2=('user',), + ) + self.assertEqual( + first=ProjectContributorViewSet.related_model_type, + second='Contributor', + ) + self.assertTrue(expr=ProjectContributorViewSet.owner_only_id_access) From 8114b5677fd085d8936a7f7e2043c4d7deea7e08 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Mon, 8 Nov 2021 22:40:57 -0500 Subject: [PATCH 064/211] Added tests for the base admin. --- project_manager/tests/test_admin.py | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 project_manager/tests/test_admin.py diff --git a/project_manager/tests/test_admin.py b/project_manager/tests/test_admin.py new file mode 100644 index 00000000..a389b170 --- /dev/null +++ b/project_manager/tests/test_admin.py @@ -0,0 +1,48 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.contrib import admin +from django.contrib.auth.models import Group +from django.test import TestCase + +# Third Party Django +from precise_bbcode.models import BBCodeTag, SmileyTag + +# App +from project_manager.packages.models import Package +from project_manager.plugins.models import Plugin +from project_manager.sub_plugins.models import SubPlugin + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class AdminTestCase(TestCase): + def test_project_admins_are_registered(self): + self.assertIn( + member=Package, + container=admin.site._registry, + ) + self.assertIn( + member=Plugin, + container=admin.site._registry, + ) + self.assertIn( + member=SubPlugin, + container=admin.site._registry, + ) + + def test_third_party_models_not_registered(self): + self.assertNotIn( + member=Group, + container=admin.site._registry, + ) + self.assertNotIn( + member=BBCodeTag, + container=admin.site._registry, + ) + self.assertNotIn( + member=SmileyTag, + container=admin.site._registry, + ) From 567aa5c0618cb10ded58fb31c20d612a4777f154 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Mon, 8 Nov 2021 23:39:41 -0500 Subject: [PATCH 065/211] Added more tests. --- .../packages/api/tests/__init__.py | 0 .../packages/api/tests/test_filtersets.py | 30 ++ .../packages/api/tests/test_serializers.py | 405 ++++++++++++++++++ project_manager/plugins/api/tests/__init__.py | 0 .../plugins/api/tests/test_filtersets.py | 30 ++ .../plugins/api/tests/test_serializers.py | 405 ++++++++++++++++++ 6 files changed, 870 insertions(+) create mode 100644 project_manager/packages/api/tests/__init__.py create mode 100644 project_manager/packages/api/tests/test_filtersets.py create mode 100644 project_manager/packages/api/tests/test_serializers.py create mode 100644 project_manager/plugins/api/tests/__init__.py create mode 100644 project_manager/plugins/api/tests/test_filtersets.py create mode 100644 project_manager/plugins/api/tests/test_serializers.py diff --git a/project_manager/packages/api/tests/__init__.py b/project_manager/packages/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/packages/api/tests/test_filtersets.py b/project_manager/packages/api/tests/test_filtersets.py new file mode 100644 index 00000000..a3589cad --- /dev/null +++ b/project_manager/packages/api/tests/test_filtersets.py @@ -0,0 +1,30 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# App +from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.packages.api.filtersets import PackageFilterSet +from project_manager.packages.models import Package + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageFilterSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(PackageFilterSet, ProjectFilterSet)) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageFilterSet.Meta, + ProjectFilterSet.Meta, + ), + ) + self.assertEqual( + first=PackageFilterSet.Meta.model, + second=Package, + ) diff --git a/project_manager/packages/api/tests/test_serializers.py b/project_manager/packages/api/tests/test_serializers.py new file mode 100644 index 00000000..939d30d3 --- /dev/null +++ b/project_manager/packages/api/tests/test_serializers.py @@ -0,0 +1,405 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from unittest import mock + +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework.serializers import ListSerializer + +# App +from project_manager.common.api.serializers import ( + ProjectContributorSerializer, + ProjectCreateReleaseSerializer, + ProjectGameSerializer, + ProjectImageSerializer, + ProjectReleaseSerializer, + ProjectSerializer, + ProjectTagSerializer, +) +from project_manager.packages.api.serializers.common import ( + ReleasePackageRequirementSerializer, +) +from project_manager.packages.api.serializers import ( + PackageContributorSerializer, + PackageCreateReleaseSerializer, + PackageCreateSerializer, + PackageGameSerializer, + PackageImageSerializer, + PackageReleaseDownloadRequirementSerializer, + PackageReleasePackageRequirementSerializer, + PackageReleasePyPiRequirementSerializer, + PackageReleaseSerializer, + PackageReleaseVersionControlRequirementSerializer, + PackageSerializer, + PackageTagSerializer, +) +from project_manager.packages.api.serializers.mixins import PackageReleaseBase +from project_manager.packages.models import ( + Package, + PackageContributor, + PackageGame, + PackageImage, + PackageRelease, + PackageReleaseDownloadRequirement, + PackageReleasePackageRequirement, + PackageReleasePyPiRequirement, + PackageReleaseVersionControlRequirement, + PackageTag, +) +from requirements.api.serializers.common import ( + ReleaseDownloadRequirementSerializer, + ReleasePyPiRequirementSerializer, + ReleaseVersionControlRequirementSerializer, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageContributorSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageContributorSerializer, + ProjectContributorSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageContributorSerializer.Meta, + ProjectContributorSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageContributorSerializer.Meta.model, + second=PackageContributor, + ) + + +class PackageCreateReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageCreateReleaseSerializer, + ProjectCreateReleaseSerializer, + ), + ) + self.assertTrue( + expr=issubclass( + PackageCreateReleaseSerializer, + PackageReleaseBase, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageCreateReleaseSerializer.Meta, + ProjectCreateReleaseSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageCreateReleaseSerializer.Meta.model, + second=PackageRelease, + ) + + +class PackageCreateSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageCreateSerializer, PackageSerializer), + ) + + @mock.patch( + target='project_manager.common.api.serializers.ProjectSerializer.get_extra_kwargs', + return_value={}, + ) + def test_releases(self, _): + obj = PackageCreateSerializer() + self.assertIn(member='releases', container=obj.fields) + field = obj.fields['releases'] + self.assertIsInstance(obj=field, cls=PackageCreateReleaseSerializer) + self.assertTrue(expr=field.write_only) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageCreateSerializer.Meta, + PackageSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageCreateSerializer.Meta.fields, + second=PackageSerializer.Meta.fields + ('releases',), + ) + + +class PackageGameSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageGameSerializer, ProjectGameSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageGameSerializer.Meta, + ProjectGameSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageGameSerializer.Meta.model, + second=PackageGame, + ) + + +class PackageImageSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageImageSerializer, ProjectImageSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageImageSerializer.Meta, + ProjectImageSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageImageSerializer.Meta.model, + second=PackageImage, + ) + + +class PackageReleaseDownloadRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleaseDownloadRequirementSerializer, + ReleaseDownloadRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageReleaseDownloadRequirementSerializer.Meta, + ReleaseDownloadRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageReleaseDownloadRequirementSerializer.Meta.model, + second=PackageReleaseDownloadRequirement, + ) + + +class PackageReleasePackageRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleasePackageRequirementSerializer, + ReleasePackageRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageReleasePackageRequirementSerializer.Meta, + ReleasePackageRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageReleasePackageRequirementSerializer.Meta.model, + second=PackageReleasePackageRequirement, + ) + + +class PackageReleasePyPiRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleasePyPiRequirementSerializer, + ReleasePyPiRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageReleasePyPiRequirementSerializer.Meta, + ReleasePyPiRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageReleasePyPiRequirementSerializer.Meta.model, + second=PackageReleasePyPiRequirement, + ) + + +class PackageReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleaseSerializer, + ProjectReleaseSerializer, + ), + ) + self.assertTrue( + expr=issubclass(PackageReleaseSerializer, PackageReleaseBase), + ) + + def test_download_requirements(self): + obj = PackageReleaseSerializer() + self.assertIn(member='download_requirements', container=obj.fields) + field = obj.fields['download_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PackageReleaseDownloadRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='packagereleasedownloadrequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_package_requirements(self): + obj = PackageReleaseSerializer() + self.assertIn(member='package_requirements', container=obj.fields) + field = obj.fields['package_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PackageReleasePackageRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='packagereleasepackagerequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_pypi_requirements(self): + obj = PackageReleaseSerializer() + self.assertIn(member='pypi_requirements', container=obj.fields) + field = obj.fields['pypi_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PackageReleasePyPiRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='packagereleasepypirequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_vcs_requirements(self): + obj = PackageReleaseSerializer() + self.assertIn(member='vcs_requirements', container=obj.fields) + field = obj.fields['vcs_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PackageReleaseVersionControlRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='packagereleaseversioncontrolrequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageReleaseSerializer.Meta, + ProjectReleaseSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageReleaseSerializer.Meta.model, + second=PackageRelease, + ) + + +class PackageReleaseVersionControlRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageReleaseVersionControlRequirementSerializer, + ReleaseVersionControlRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageReleaseVersionControlRequirementSerializer.Meta, + ReleaseVersionControlRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageReleaseVersionControlRequirementSerializer.Meta.model, + second=PackageReleaseVersionControlRequirement, + ) + + +class PackageSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(PackageSerializer, ProjectSerializer)) + + def test_primary_attributes(self): + self.assertEqual( + first=PackageSerializer.project_type, + second='package', + ) + self.assertEqual( + first=PackageSerializer.release_model, + second=PackageRelease, + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageSerializer.Meta, + ProjectSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageSerializer.Meta.model, + second=Package, + ) + + +class PackageTagSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageTagSerializer, ProjectTagSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PackageTagSerializer.Meta, + ProjectTagSerializer.Meta, + ), + ) + self.assertEqual( + first=PackageTagSerializer.Meta.model, + second=PackageTag, + ) diff --git a/project_manager/plugins/api/tests/__init__.py b/project_manager/plugins/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/plugins/api/tests/test_filtersets.py b/project_manager/plugins/api/tests/test_filtersets.py new file mode 100644 index 00000000..5ff4c517 --- /dev/null +++ b/project_manager/plugins/api/tests/test_filtersets.py @@ -0,0 +1,30 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# App +from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.plugins.api.filtersets import PluginFilterSet +from project_manager.plugins.models import Plugin + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginFilterSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(PluginFilterSet, ProjectFilterSet)) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginFilterSet.Meta, + ProjectFilterSet.Meta, + ), + ) + self.assertEqual( + first=PluginFilterSet.Meta.model, + second=Plugin, + ) diff --git a/project_manager/plugins/api/tests/test_serializers.py b/project_manager/plugins/api/tests/test_serializers.py new file mode 100644 index 00000000..d7cb250f --- /dev/null +++ b/project_manager/plugins/api/tests/test_serializers.py @@ -0,0 +1,405 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from unittest import mock + +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework.serializers import ListSerializer + +# App +from project_manager.common.api.serializers import ( + ProjectContributorSerializer, + ProjectCreateReleaseSerializer, + ProjectGameSerializer, + ProjectImageSerializer, + ProjectReleaseSerializer, + ProjectSerializer, + ProjectTagSerializer, +) +from project_manager.packages.api.serializers.common import ( + ReleasePackageRequirementSerializer, +) +from project_manager.plugins.api.serializers import ( + PluginContributorSerializer, + PluginCreateReleaseSerializer, + PluginCreateSerializer, + PluginGameSerializer, + PluginImageSerializer, + PluginReleaseDownloadRequirementSerializer, + PluginReleasePackageRequirementSerializer, + PluginReleasePyPiRequirementSerializer, + PluginReleaseSerializer, + PluginReleaseVersionControlRequirementSerializer, + PluginSerializer, + PluginTagSerializer, +) +from project_manager.plugins.api.serializers.mixins import PluginReleaseBase +from project_manager.plugins.models import ( + Plugin, + PluginContributor, + PluginGame, + PluginImage, + PluginRelease, + PluginReleaseDownloadRequirement, + PluginReleasePackageRequirement, + PluginReleasePyPiRequirement, + PluginReleaseVersionControlRequirement, + PluginTag, +) +from requirements.api.serializers.common import ( + ReleaseDownloadRequirementSerializer, + ReleasePyPiRequirementSerializer, + ReleaseVersionControlRequirementSerializer, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginContributorSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginContributorSerializer, + ProjectContributorSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginContributorSerializer.Meta, + ProjectContributorSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginContributorSerializer.Meta.model, + second=PluginContributor, + ) + + +class PluginCreateReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginCreateReleaseSerializer, + ProjectCreateReleaseSerializer, + ), + ) + self.assertTrue( + expr=issubclass( + PluginCreateReleaseSerializer, + PluginReleaseBase, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginCreateReleaseSerializer.Meta, + ProjectCreateReleaseSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginCreateReleaseSerializer.Meta.model, + second=PluginRelease, + ) + + +class PluginCreateSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginCreateSerializer, PluginSerializer), + ) + + @mock.patch( + target='project_manager.common.api.serializers.ProjectSerializer.get_extra_kwargs', + return_value={}, + ) + def test_releases(self, _): + obj = PluginCreateSerializer() + self.assertIn(member='releases', container=obj.fields) + field = obj.fields['releases'] + self.assertIsInstance(obj=field, cls=PluginCreateReleaseSerializer) + self.assertTrue(expr=field.write_only) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginCreateSerializer.Meta, + PluginSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginCreateSerializer.Meta.fields, + second=PluginSerializer.Meta.fields + ('releases',), + ) + + +class PluginGameSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginGameSerializer, ProjectGameSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginGameSerializer.Meta, + ProjectGameSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginGameSerializer.Meta.model, + second=PluginGame, + ) + + +class PluginImageSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginImageSerializer, ProjectImageSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginImageSerializer.Meta, + ProjectImageSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginImageSerializer.Meta.model, + second=PluginImage, + ) + + +class PluginReleaseDownloadRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleaseDownloadRequirementSerializer, + ReleaseDownloadRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginReleaseDownloadRequirementSerializer.Meta, + ReleaseDownloadRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginReleaseDownloadRequirementSerializer.Meta.model, + second=PluginReleaseDownloadRequirement, + ) + + +class PluginReleasePackageRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleasePackageRequirementSerializer, + ReleasePackageRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginReleasePackageRequirementSerializer.Meta, + ReleasePackageRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginReleasePackageRequirementSerializer.Meta.model, + second=PluginReleasePackageRequirement, + ) + + +class PluginReleasePyPiRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleasePyPiRequirementSerializer, + ReleasePyPiRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginReleasePyPiRequirementSerializer.Meta, + ReleasePyPiRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginReleasePyPiRequirementSerializer.Meta.model, + second=PluginReleasePyPiRequirement, + ) + + +class PluginReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleaseSerializer, + ProjectReleaseSerializer, + ), + ) + self.assertTrue( + expr=issubclass(PluginReleaseSerializer, PluginReleaseBase), + ) + + def test_download_requirements(self): + obj = PluginReleaseSerializer() + self.assertIn(member='download_requirements', container=obj.fields) + field = obj.fields['download_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PluginReleaseDownloadRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='pluginreleasedownloadrequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_package_requirements(self): + obj = PluginReleaseSerializer() + self.assertIn(member='package_requirements', container=obj.fields) + field = obj.fields['package_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PluginReleasePackageRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='pluginreleasepackagerequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_pypi_requirements(self): + obj = PluginReleaseSerializer() + self.assertIn(member='pypi_requirements', container=obj.fields) + field = obj.fields['pypi_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PluginReleasePyPiRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='pluginreleasepypirequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_vcs_requirements(self): + obj = PluginReleaseSerializer() + self.assertIn(member='vcs_requirements', container=obj.fields) + field = obj.fields['vcs_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=PluginReleaseVersionControlRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='pluginreleaseversioncontrolrequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginReleaseSerializer.Meta, + ProjectReleaseSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginReleaseSerializer.Meta.model, + second=PluginRelease, + ) + + +class PluginReleaseVersionControlRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginReleaseVersionControlRequirementSerializer, + ReleaseVersionControlRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginReleaseVersionControlRequirementSerializer.Meta, + ReleaseVersionControlRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginReleaseVersionControlRequirementSerializer.Meta.model, + second=PluginReleaseVersionControlRequirement, + ) + + +class PluginSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(PluginSerializer, ProjectSerializer)) + + def test_primary_attributes(self): + self.assertEqual( + first=PluginSerializer.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginSerializer.release_model, + second=PluginRelease, + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginSerializer.Meta, + ProjectSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginSerializer.Meta.model, + second=Plugin, + ) + + +class PluginTagSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginTagSerializer, ProjectTagSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + PluginTagSerializer.Meta, + ProjectTagSerializer.Meta, + ), + ) + self.assertEqual( + first=PluginTagSerializer.Meta.model, + second=PluginTag, + ) From 1dc733a1dfaf98945ffc1f619c20ddc52f9333ec Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Tue, 9 Nov 2021 19:41:56 -0500 Subject: [PATCH 066/211] Updated APIViews to reuse logic. --- project_manager/common/api/views/__init__.py | 36 ++++++-------------- project_manager/sub_plugins/api/views.py | 36 ++++++-------------- 2 files changed, 20 insertions(+), 52 deletions(-) diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index b6581feb..ba88b4ff 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -52,34 +52,18 @@ class ProjectAPIView(APIView): def get(self, request): """Return all the API routes for Projects.""" + base_path = reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) return Response( data={ - 'contributors': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + ( - f'contributors/<{self.project_type}>/' - ), - 'games': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'games/<{self.project_type}>/', - 'images': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'images/<{self.project_type}>/', - 'projects': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + 'projects/', - 'releases': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'releases/<{self.project_type}>/', - 'tags': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'tags/<{self.project_type}>/', + 'contributors': base_path + f'contributors/<{self.project_type}>/', + 'games': base_path + f'games/<{self.project_type}>/', + 'images': base_path + f'images/<{self.project_type}>/', + 'projects': base_path + 'projects/', + 'releases': base_path + f'releases/<{self.project_type}>/', + 'tags': base_path + f'tags/<{self.project_type}>/', } ) diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index b3dd34c9..5735029d 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -70,34 +70,18 @@ class SubPluginAPIView(ProjectAPIView): def get(self, request): """Return all the API routes for Projects.""" + base_path = reverse( + viewname=f'api:{self.project_type}s:endpoints', + request=request, + ) return Response( data={ - 'contributors': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + ( - f'contributors//<{self.project_type}>/' - ), - 'games': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'games//<{self.project_type}>/', - 'images': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'images//<{self.project_type}>/', - 'projects': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + 'projects//', - 'releases': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'releases//<{self.project_type}>/', - 'tags': reverse( - viewname=f'api:{self.project_type}s:endpoints', - request=request, - ) + f'tags//<{self.project_type}>/', + 'contributors': base_path + f'contributors//<{self.project_type}>/', + 'games': base_path + f'games//<{self.project_type}>/', + 'images': base_path + f'images//<{self.project_type}>/', + 'projects': base_path + 'projects//', + 'releases': base_path + f'releases//<{self.project_type}>/', + 'tags': base_path + f'tags//<{self.project_type}>/', } ) From 1f82ec4049ea5399ea3c80608d5aa410081d953a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Tue, 9 Nov 2021 19:43:04 -0500 Subject: [PATCH 067/211] Updated view name to standardize with similar names. --- project_manager/packages/api/urls.py | 4 ++-- project_manager/packages/api/views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/project_manager/packages/api/urls.py b/project_manager/packages/api/urls.py index 75f0563b..7017e06a 100644 --- a/project_manager/packages/api/urls.py +++ b/project_manager/packages/api/urls.py @@ -12,7 +12,7 @@ # App from project_manager.packages.api.views import ( PackageAPIView, - PackageContributorsViewSet, + PackageContributorViewSet, PackageGameViewSet, PackageImageViewSet, PackageReleaseViewSet, @@ -52,7 +52,7 @@ ) router.register( prefix=r'^contributors/(?P[\w-]+)', - viewset=PackageContributorsViewSet, + viewset=PackageContributorViewSet, basename='contributors', ) diff --git a/project_manager/packages/api/views.py b/project_manager/packages/api/views.py index c74fbf02..706cff0f 100644 --- a/project_manager/packages/api/views.py +++ b/project_manager/packages/api/views.py @@ -45,7 +45,7 @@ # ============================================================================= __all__ = ( 'PackageAPIView', - 'PackageContributorsViewSet', + 'PackageContributorViewSet', 'PackageGameViewSet', 'PackageImageViewSet', 'PackageReleaseViewSet', @@ -203,7 +203,7 @@ class PackageTagViewSet(ProjectTagViewSet): project_model = Package -class PackageContributorsViewSet(ProjectContributorViewSet): +class PackageContributorViewSet(ProjectContributorViewSet): """Contributors listing for Packages.""" queryset = PackageContributor.objects.select_related( From abfd6d657c8cf7d0ca5198c1567476d2c92c765c Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Tue, 9 Nov 2021 19:44:08 -0500 Subject: [PATCH 068/211] Added more tests. --- project_manager/common/helpers.py | 1 - .../packages/api/tests/test_serializers.py | 92 ++++++++- .../plugins/api/tests/test_serializers.py | 193 ++++++++++++++++++ 3 files changed, 281 insertions(+), 5 deletions(-) diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index eca6157f..965583d8 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -117,7 +117,6 @@ def _validate_path(self, path): try: extension = path.rsplit('/', 1)[1].rsplit('.', 1)[1] except IndexError: - print(path) return True for base_path, allowed_extensions in self.file_types.items(): diff --git a/project_manager/packages/api/tests/test_serializers.py b/project_manager/packages/api/tests/test_serializers.py index 939d30d3..40f42774 100644 --- a/project_manager/packages/api/tests/test_serializers.py +++ b/project_manager/packages/api/tests/test_serializers.py @@ -8,7 +8,8 @@ from django.test import TestCase # Third Party Django -from rest_framework.serializers import ListSerializer +from rest_framework.fields import ReadOnlyField +from rest_framework.serializers import ListSerializer, ModelSerializer # App from project_manager.common.api.serializers import ( @@ -20,9 +21,6 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.packages.api.serializers.common import ( - ReleasePackageRequirementSerializer, -) from project_manager.packages.api.serializers import ( PackageContributorSerializer, PackageCreateReleaseSerializer, @@ -37,7 +35,11 @@ PackageSerializer, PackageTagSerializer, ) +from project_manager.packages.api.serializers.common import ( + ReleasePackageRequirementSerializer, +) from project_manager.packages.api.serializers.mixins import PackageReleaseBase +from project_manager.packages.helpers import PackageZipFile from project_manager.packages.models import ( Package, PackageContributor, @@ -209,6 +211,36 @@ def test_class_inheritance(self): ), ) + def test_name_field(self): + obj = PackageReleasePackageRequirementSerializer() + self.assertIn(member='name', container=obj.fields) + field = obj.fields['name'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='package_requirement.name', + ) + + def test_slug_field(self): + obj = PackageReleasePackageRequirementSerializer() + self.assertIn(member='slug', container=obj.fields) + field = obj.fields['slug'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='package_requirement.slug', + ) + + def test_version_field(self): + obj = PackageReleasePackageRequirementSerializer() + self.assertIn(member='version', container=obj.fields) + field = obj.fields['version'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='version', + ) + def test_meta_class(self): self.assertTrue( expr=issubclass( @@ -403,3 +435,55 @@ def test_meta_class(self): first=PackageTagSerializer.Meta.model, second=PackageTag, ) + + +class ReleasePackageRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + ReleasePackageRequirementSerializer, + ModelSerializer, + ), + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ReleasePackageRequirementSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'version', + 'optional', + ), + ) + + +class PackageReleaseBaseTestCase(TestCase): + def test_base_attributes(self): + self.assertEqual( + first=PackageReleaseBase.project_class, + second=Package, + ) + self.assertEqual( + first=PackageReleaseBase.project_type, + second='package', + ) + + def test_zip_parser(self): + self.assertEqual( + first=PackageReleaseBase().zip_parser, + second=PackageZipFile, + ) + + def test_get_project_kwargs(self): + obj = PackageReleaseBase() + slug = 'test-package' + obj.context = { + 'view': mock.Mock( + kwargs={'package_slug': slug}, + ), + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={'pk': slug}, + ) diff --git a/project_manager/plugins/api/tests/test_serializers.py b/project_manager/plugins/api/tests/test_serializers.py index d7cb250f..28da8b77 100644 --- a/project_manager/plugins/api/tests/test_serializers.py +++ b/project_manager/plugins/api/tests/test_serializers.py @@ -8,6 +8,8 @@ from django.test import TestCase # Third Party Django +from rest_framework.exceptions import ValidationError +from rest_framework.fields import ReadOnlyField from rest_framework.serializers import ListSerializer # App @@ -20,6 +22,10 @@ ProjectSerializer, ProjectTagSerializer, ) +from project_manager.common.api.serializers.mixins import ( + ProjectThroughMixin, + AddProjectToViewMixin, +) from project_manager.packages.api.serializers.common import ( ReleasePackageRequirementSerializer, ) @@ -36,8 +42,10 @@ PluginReleaseVersionControlRequirementSerializer, PluginSerializer, PluginTagSerializer, + SubPluginPathSerializer, ) from project_manager.plugins.api.serializers.mixins import PluginReleaseBase +from project_manager.plugins.helpers import PluginZipFile from project_manager.plugins.models import ( Plugin, PluginContributor, @@ -49,12 +57,14 @@ PluginReleasePyPiRequirement, PluginReleaseVersionControlRequirement, PluginTag, + SubPluginPath, ) from requirements.api.serializers.common import ( ReleaseDownloadRequirementSerializer, ReleasePyPiRequirementSerializer, ReleaseVersionControlRequirementSerializer, ) +from test_utils.factories.plugins import PluginFactory # ============================================================================= @@ -209,6 +219,36 @@ def test_class_inheritance(self): ), ) + def test_name_field(self): + obj = PluginReleasePackageRequirementSerializer() + self.assertIn(member='name', container=obj.fields) + field = obj.fields['name'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='package_requirement.name', + ) + + def test_slug_field(self): + obj = PluginReleasePackageRequirementSerializer() + self.assertIn(member='slug', container=obj.fields) + field = obj.fields['slug'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='package_requirement.slug', + ) + + def test_version_field(self): + obj = PluginReleasePackageRequirementSerializer() + self.assertIn(member='version', container=obj.fields) + field = obj.fields['version'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='version', + ) + def test_meta_class(self): self.assertTrue( expr=issubclass( @@ -403,3 +443,156 @@ def test_meta_class(self): first=PluginTagSerializer.Meta.model, second=PluginTag, ) + + +class SubPluginPathSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginPathSerializer, ProjectThroughMixin), + ) + self.assertTrue( + expr=issubclass(SubPluginPathSerializer, AddProjectToViewMixin), + ) + + def test_get_field_names(self): + obj = SubPluginPathSerializer( + context={ + 'request': mock.Mock( + method='POST', + ) + }, + ) + field_names = obj.get_field_names( + declared_fields=[], + info=mock.Mock(), + ) + self.assertTupleEqual( + tuple1=field_names, + tuple2=( + 'allow_module', + 'allow_package_using_basename', + 'allow_package_using_init', + 'path', + ), + ) + + obj = SubPluginPathSerializer( + context={ + 'request': mock.Mock( + method='PATCH', + ) + }, + ) + field_names = obj.get_field_names( + declared_fields=[], + info=mock.Mock(), + ) + self.assertTupleEqual( + tuple1=field_names, + tuple2=( + 'allow_module', + 'allow_package_using_basename', + 'allow_package_using_init', + ), + ) + + def test_validate(self): + """ + view = self.context['view'] + attrs[view.project_type.replace('-', '_')] = view.project + """ + plugin = PluginFactory() + obj = SubPluginPathSerializer( + context={ + 'view': mock.Mock( + project_type='plugin', + project=plugin, + ) + } + ) + field_names = ( + 'allow_module', + 'allow_package_using_basename', + 'allow_package_using_init', + ) + attrs = { + field_name: False for field_name in field_names + } + with self.assertRaises(ValidationError) as context: + obj.validate(attrs=attrs) + + self.assertEqual( + first=len(context.exception.detail), + second=3, + ) + for field_name in field_names: + self.assertIn( + member=field_name, + container=context.exception.detail, + ) + error = context.exception.detail[field_name] + self.assertEqual( + first=error, + second="At least one of the 'Allow' fields must be True.", + ) + self.assertEqual( + first=error.code, + second='invalid', + ) + + for field_name in field_names: + current_attrs = dict(attrs) + current_attrs.update({ + field_name: True, + }) + value = obj.validate(attrs=current_attrs) + self.assertDictEqual( + d1=value, + d2={**current_attrs, **{'plugin': plugin}}, + ) + + def test_meta_class(self): + self.assertEqual( + first=SubPluginPathSerializer.Meta.model, + second=SubPluginPath, + ) + self.assertTupleEqual( + tuple1=SubPluginPathSerializer.Meta.fields, + tuple2=( + 'allow_module', + 'allow_package_using_basename', + 'allow_package_using_init', + 'path', + ) + ) + + +class PluginReleaseBaseTestCase(TestCase): + def test_base_attributes(self): + self.assertEqual( + first=PluginReleaseBase.project_class, + second=Plugin, + ) + self.assertEqual( + first=PluginReleaseBase.project_type, + second='plugin', + ) + + def test_zip_parser(self): + self.assertEqual( + first=PluginReleaseBase().zip_parser, + second=PluginZipFile, + ) + + def test_get_project_kwargs(self): + obj = PluginReleaseBase() + slug = 'test-plugin' + obj.context = { + 'view': mock.Mock( + kwargs={'plugin_slug': slug}, + ), + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={'pk': slug}, + ) From 624cc3c6c79a3db3a80aa0261b5b4fe3c5b60700 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Tue, 9 Nov 2021 19:45:06 -0500 Subject: [PATCH 069/211] Added tests for sub_plugins that were already done for packages/plugins. --- .../sub_plugins/api/serializers/__init__.py | 6 +- .../sub_plugins/api/tests/__init__.py | 0 .../sub_plugins/api/tests/test_filtersets.py | 30 + .../sub_plugins/api/tests/test_serializers.py | 608 ++++++++++++++++++ 4 files changed, 641 insertions(+), 3 deletions(-) create mode 100644 project_manager/sub_plugins/api/tests/__init__.py create mode 100644 project_manager/sub_plugins/api/tests/test_filtersets.py create mode 100644 project_manager/sub_plugins/api/tests/test_serializers.py diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index 524e4319..5e5df780 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -179,9 +179,9 @@ def parent_project(self): try: plugin = Plugin.objects.get(slug=plugin_slug) except Plugin.DoesNotExist: - raise ValidationError( - f"Plugin '{plugin_slug}' not found." - ) from Plugin.DoesNotExist + raise ValidationError({ + 'plugin': f"Plugin '{plugin_slug}' not found." + }) from Plugin.DoesNotExist return plugin @staticmethod diff --git a/project_manager/sub_plugins/api/tests/__init__.py b/project_manager/sub_plugins/api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/project_manager/sub_plugins/api/tests/test_filtersets.py b/project_manager/sub_plugins/api/tests/test_filtersets.py new file mode 100644 index 00000000..da437533 --- /dev/null +++ b/project_manager/sub_plugins/api/tests/test_filtersets.py @@ -0,0 +1,30 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# App +from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.sub_plugins.api.filtersets import SubPluginFilterSet +from project_manager.sub_plugins.models import SubPlugin + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageFilterSetTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginFilterSet, ProjectFilterSet)) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginFilterSet.Meta, + ProjectFilterSet.Meta, + ), + ) + self.assertEqual( + first=SubPluginFilterSet.Meta.model, + second=SubPlugin, + ) diff --git a/project_manager/sub_plugins/api/tests/test_serializers.py b/project_manager/sub_plugins/api/tests/test_serializers.py new file mode 100644 index 00000000..ac8a2e83 --- /dev/null +++ b/project_manager/sub_plugins/api/tests/test_serializers.py @@ -0,0 +1,608 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +from unittest import mock + +# Django +from django.conf import settings +from django.test import TestCase + +# Third Party Django +from rest_framework.exceptions import ValidationError +from rest_framework.fields import ReadOnlyField +from rest_framework.serializers import ListSerializer + +# App +from project_manager.common.api.serializers import ( + ProjectContributorSerializer, + ProjectCreateReleaseSerializer, + ProjectGameSerializer, + ProjectImageSerializer, + ProjectReleaseSerializer, + ProjectSerializer, + ProjectTagSerializer, +) +from project_manager.packages.api.serializers.common import ( + ReleasePackageRequirementSerializer, +) +from project_manager.sub_plugins.api.serializers import ( + SubPluginContributorSerializer, + SubPluginCreateReleaseSerializer, + SubPluginCreateSerializer, + SubPluginGameSerializer, + SubPluginImageSerializer, + SubPluginReleaseDownloadRequirementSerializer, + SubPluginReleasePackageRequirementSerializer, + SubPluginReleasePyPiRequirementSerializer, + SubPluginReleaseSerializer, + SubPluginReleaseVersionControlRequirementSerializer, + SubPluginSerializer, + SubPluginTagSerializer, +) +from project_manager.sub_plugins.api.serializers.mixins import SubPluginReleaseBase +from project_manager.sub_plugins.helpers import SubPluginZipFile +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginContributor, + SubPluginGame, + SubPluginImage, + SubPluginRelease, + SubPluginReleaseDownloadRequirement, + SubPluginReleasePackageRequirement, + SubPluginReleasePyPiRequirement, + SubPluginReleaseVersionControlRequirement, + SubPluginTag, +) +from requirements.api.serializers.common import ( + ReleaseDownloadRequirementSerializer, + ReleasePyPiRequirementSerializer, + ReleaseVersionControlRequirementSerializer, +) +from test_utils.factories.plugins import PluginFactory +from test_utils.factories.sub_plugins import SubPluginReleaseFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginContributorSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginContributorSerializer, + ProjectContributorSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginContributorSerializer.Meta, + ProjectContributorSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginContributorSerializer.Meta.model, + second=SubPluginContributor, + ) + + +class SubPluginCreateReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginCreateReleaseSerializer, + ProjectCreateReleaseSerializer, + ), + ) + self.assertTrue( + expr=issubclass( + SubPluginCreateReleaseSerializer, + SubPluginReleaseBase, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginCreateReleaseSerializer.Meta, + ProjectCreateReleaseSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginCreateReleaseSerializer.Meta.model, + second=SubPluginRelease, + ) + + +class SubPluginCreateSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginCreateSerializer, SubPluginSerializer), + ) + + @mock.patch( + target='project_manager.common.api.serializers.ProjectSerializer.get_extra_kwargs', + return_value={}, + ) + def test_releases(self, _): + obj = SubPluginCreateSerializer() + self.assertIn(member='releases', container=obj.fields) + field = obj.fields['releases'] + self.assertIsInstance(obj=field, cls=SubPluginCreateReleaseSerializer) + self.assertTrue(expr=field.write_only) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginCreateSerializer.Meta, + SubPluginSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginCreateSerializer.Meta.fields, + second=SubPluginSerializer.Meta.fields + ('releases',), + ) + + +class SubPluginGameSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginGameSerializer, ProjectGameSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginGameSerializer.Meta, + ProjectGameSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginGameSerializer.Meta.model, + second=SubPluginGame, + ) + + +class SubPluginImageSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginImageSerializer, ProjectImageSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginImageSerializer.Meta, + ProjectImageSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginImageSerializer.Meta.model, + second=SubPluginImage, + ) + + +class SubPluginReleaseDownloadRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseDownloadRequirementSerializer, + ReleaseDownloadRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseDownloadRequirementSerializer.Meta, + ReleaseDownloadRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginReleaseDownloadRequirementSerializer.Meta.model, + second=SubPluginReleaseDownloadRequirement, + ) + + +class SubPluginReleasePackageRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleasePackageRequirementSerializer, + ReleasePackageRequirementSerializer, + ), + ) + + def test_name_field(self): + obj = SubPluginReleasePackageRequirementSerializer() + self.assertIn(member='name', container=obj.fields) + field = obj.fields['name'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='package_requirement.name', + ) + + def test_slug_field(self): + obj = SubPluginReleasePackageRequirementSerializer() + self.assertIn(member='slug', container=obj.fields) + field = obj.fields['slug'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='package_requirement.slug', + ) + + def test_version_field(self): + obj = SubPluginReleasePackageRequirementSerializer() + self.assertIn(member='version', container=obj.fields) + field = obj.fields['version'] + self.assertIsInstance(obj=field, cls=ReadOnlyField) + self.assertEqual( + first=field.source, + second='version', + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginReleasePackageRequirementSerializer.Meta, + ReleasePackageRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginReleasePackageRequirementSerializer.Meta.model, + second=SubPluginReleasePackageRequirement, + ) + + +class SubPluginReleasePyPiRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleasePyPiRequirementSerializer, + ReleasePyPiRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginReleasePyPiRequirementSerializer.Meta, + ReleasePyPiRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginReleasePyPiRequirementSerializer.Meta.model, + second=SubPluginReleasePyPiRequirement, + ) + + +class SubPluginReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseSerializer, + ProjectReleaseSerializer, + ), + ) + self.assertTrue( + expr=issubclass(SubPluginReleaseSerializer, SubPluginReleaseBase), + ) + + def test_download_requirements(self): + obj = SubPluginReleaseSerializer() + self.assertIn(member='download_requirements', container=obj.fields) + field = obj.fields['download_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=SubPluginReleaseDownloadRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='subpluginreleasedownloadrequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_package_requirements(self): + obj = SubPluginReleaseSerializer() + self.assertIn(member='package_requirements', container=obj.fields) + field = obj.fields['package_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=SubPluginReleasePackageRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='subpluginreleasepackagerequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_pypi_requirements(self): + obj = SubPluginReleaseSerializer() + self.assertIn(member='pypi_requirements', container=obj.fields) + field = obj.fields['pypi_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=SubPluginReleasePyPiRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='subpluginreleasepypirequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_vcs_requirements(self): + obj = SubPluginReleaseSerializer() + self.assertIn(member='vcs_requirements', container=obj.fields) + field = obj.fields['vcs_requirements'] + self.assertIsInstance(obj=field, cls=ListSerializer) + self.assertTrue(expr=field.many) + self.assertTrue(expr=field.read_only) + self.assertIsInstance( + obj=field.child, + cls=SubPluginReleaseVersionControlRequirementSerializer, + ) + self.assertEqual( + first=field.source, + second='subpluginreleaseversioncontrolrequirement_set', + ) + self.assertTrue(expr=field.child.read_only) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseSerializer.Meta, + ProjectReleaseSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginReleaseSerializer.Meta.model, + second=SubPluginRelease, + ) + + +class SubPluginReleaseVersionControlRequirementSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseVersionControlRequirementSerializer, + ReleaseVersionControlRequirementSerializer, + ), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginReleaseVersionControlRequirementSerializer.Meta, + ReleaseVersionControlRequirementSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginReleaseVersionControlRequirementSerializer.Meta.model, + second=SubPluginReleaseVersionControlRequirement, + ) + + +class SubPluginSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginSerializer, ProjectSerializer)) + + def test_primary_attributes(self): + self.assertEqual( + first=SubPluginSerializer.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginSerializer.release_model, + second=SubPluginRelease, + ) + + def test_parent_project(self): + obj = SubPluginSerializer() + invalid_plugin_slug = 'invalid' + obj.context['view'] = mock.Mock( + kwargs={'plugin_slug': invalid_plugin_slug}, + ) + with self.assertRaises(ValidationError) as context: + _ = obj.parent_project + + self.assertEqual( + first=len(context.exception.detail), + second=1, + ) + self.assertIn( + member='plugin', + container=context.exception.detail, + ) + error = context.exception.detail['plugin'] + self.assertEqual( + first=error, + second=f"Plugin '{invalid_plugin_slug}' not found.", + ) + self.assertEqual( + first=error.code, + second='invalid', + ) + + plugin_basename = 'test_plugin' + plugin_slug = plugin_basename.replace('_', '-') + plugin = PluginFactory( + basename=plugin_basename, + ) + obj.context['view'] = mock.Mock( + kwargs={'plugin_slug': plugin_slug}, + ) + parent_obj = obj.parent_project + self.assertEqual( + first=parent_obj.basename, + second=plugin.basename, + ) + + def test_get_download_kwargs(self): + release = SubPluginReleaseFactory( + zip_file=settings.MEDIA_ROOT / 'releases' / 'file_name_v1.0.0.zip', + ) + obj = release.sub_plugin + instance = SubPluginSerializer() + kwargs = instance.get_download_kwargs(obj=obj, release=release) + self.assertDictEqual( + d1=kwargs, + d2={ + 'slug': obj.plugin.slug, + 'sub_plugin_slug': obj.slug, + 'zip_file': release.file_name, + }, + ) + + def test_get_extra_validated_data(self): + forum_user = ForumUserFactory() + obj = SubPluginSerializer() + obj.context['request'] = mock.Mock( + user=forum_user.user, + ) + plugin_basename = 'test_plugin' + plugin_slug = plugin_basename.replace('_', '-') + plugin = PluginFactory( + basename=plugin_basename, + ) + obj.context['view'] = mock.Mock( + kwargs={'plugin_slug': plugin_slug}, + ) + obj_basename = 'test_sub_plugin' + obj.release_dict = { + 'basename': obj_basename, + } + original_validated_data = {} + validated_data = obj.get_extra_validated_data( + validated_data=original_validated_data, + ) + self.assertDictEqual( + d1=validated_data, + d2={ + 'owner': forum_user, + 'basename': obj_basename, + 'plugin': plugin, + } + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginSerializer.Meta, + ProjectSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginSerializer.Meta.model, + second=SubPlugin, + ) + + +class SubPluginTagSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginTagSerializer, ProjectTagSerializer), + ) + + def test_meta_class(self): + self.assertTrue( + expr=issubclass( + SubPluginTagSerializer.Meta, + ProjectTagSerializer.Meta, + ), + ) + self.assertEqual( + first=SubPluginTagSerializer.Meta.model, + second=SubPluginTag, + ) + + +class SubPluginReleaseBaseTestCase(TestCase): + def test_base_attributes(self): + self.assertEqual( + first=SubPluginReleaseBase.project_class, + second=SubPlugin, + ) + self.assertEqual( + first=SubPluginReleaseBase.project_type, + second='sub-plugin', + ) + + def test_parent_project(self): + obj = SubPluginReleaseBase() + invalid_slug = 'invalid' + obj.context = { + 'view': mock.Mock( + kwargs={'plugin_slug': invalid_slug}, + ), + } + with self.assertRaises(ValidationError) as context: + _ = obj.parent_project + + self.assertEqual( + first=len(context.exception.detail), + second=1, + ) + self.assertEqual( + first=context.exception.detail[0], + second=f"Plugin '{invalid_slug}' not found.", + ) + + plugin = PluginFactory() + obj.context = { + 'view': mock.Mock( + kwargs={'plugin_slug': plugin.slug}, + ), + } + self.assertEqual( + first=obj.parent_project, + second=plugin, + ) + + def test_zip_parser(self): + self.assertEqual( + first=SubPluginReleaseBase().zip_parser, + second=SubPluginZipFile, + ) + + def test_get_project_kwargs(self): + obj = SubPluginReleaseBase() + plugin = PluginFactory() + slug = 'test-sub-plugin' + obj.context = { + 'view': mock.Mock( + kwargs={ + 'sub_plugin_slug': slug, + 'plugin_slug': plugin.slug, + }, + ), + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={ + 'slug': slug, + 'plugin': plugin, + }, + ) + +""" + def get_project_kwargs(self): + kwargs = self.context['view'].kwargs + return { + 'slug': kwargs.get('sub_plugin_slug'), + 'plugin': self.parent_project, + } +""" \ No newline at end of file From e8bb502a0960048bd34fa56c41b644e2dcd122e4 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Tue, 9 Nov 2021 19:45:51 -0500 Subject: [PATCH 070/211] Added tests for project views. --- .../packages/api/tests/test_views.py | 337 ++++++++++++++ .../plugins/api/tests/test_views.py | 383 ++++++++++++++++ .../sub_plugins/api/tests/test_views.py | 430 ++++++++++++++++++ 3 files changed, 1150 insertions(+) create mode 100644 project_manager/packages/api/tests/test_views.py create mode 100644 project_manager/plugins/api/tests/test_views.py create mode 100644 project_manager/sub_plugins/api/tests/test_views.py diff --git a/project_manager/packages/api/tests/test_views.py b/project_manager/packages/api/tests/test_views.py new file mode 100644 index 00000000..085e65c9 --- /dev/null +++ b/project_manager/packages/api/tests/test_views.py @@ -0,0 +1,337 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ( + ProjectAPIView, + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectReleaseViewSet, + ProjectTagViewSet, + ProjectViewSet, +) +from project_manager.packages.api.filtersets import PackageFilterSet +from project_manager.packages.api.serializers import ( + PackageContributorSerializer, + PackageCreateSerializer, + PackageGameSerializer, + PackageImageSerializer, + PackageReleaseSerializer, + PackageSerializer, + PackageTagSerializer, +) +from project_manager.packages.api.views import ( + PackageAPIView, + PackageContributorViewSet, + PackageGameViewSet, + PackageImageViewSet, + PackageReleaseViewSet, + PackageTagViewSet, + PackageViewSet, +) +from project_manager.packages.models import ( + Package, + PackageContributor, + PackageGame, + PackageImage, + PackageRelease, + PackageReleaseDownloadRequirement, + PackageReleasePackageRequirement, + PackageReleasePyPiRequirement, + PackageReleaseVersionControlRequirement, + PackageTag, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageAPIViewTestCase(APITestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageAPIView, ProjectAPIView)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageAPIView.project_type, + second='package', + ) + + def test_get(self): + response = self.client.get(path='/api/packages/') + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + base_path = reverse( + viewname=f'api:packages:endpoints', + request=response.wsgi_request, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'contributors': base_path + f'contributors//', + 'games': base_path + f'games//', + 'images': base_path + f'images//', + 'projects': base_path + f'projects/', + 'releases': base_path + f'releases//', + 'tags': base_path + f'tags//', + } + ) + + +class PackageContributorViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageContributorViewSet, + ProjectContributorViewSet, + ), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageContributorViewSet.serializer_class, + second=PackageContributorSerializer, + ) + self.assertEqual( + first=PackageContributorViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageContributorViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageContributorViewSet.queryset.model, + expr2=PackageContributor, + ) + self.assertDictEqual( + d1=PackageContributorViewSet.queryset.query.select_related, + d2={'user': {'user': {}}, 'package': {}} + ) + + +class PackageGameViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageGameViewSet, ProjectGameViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageGameViewSet.serializer_class, + second=PackageGameSerializer, + ) + self.assertEqual( + first=PackageGameViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageGameViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageGameViewSet.queryset.model, + expr2=PackageGame, + ) + self.assertDictEqual( + d1=PackageGameViewSet.queryset.query.select_related, + d2={'game': {}, 'package': {}} + ) + + +class PackageImageViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageImageViewSet.serializer_class, + second=PackageImageSerializer, + ) + self.assertEqual( + first=PackageImageViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageImageViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageImageViewSet.queryset.model, + expr2=PackageImage, + ) + self.assertDictEqual( + d1=PackageImageViewSet.queryset.query.select_related, + d2={'package': {}}, + ) + + +class PackageReleaseViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageReleaseViewSet.serializer_class, + second=PackageReleaseSerializer, + ) + self.assertEqual( + first=PackageReleaseViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageReleaseViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageReleaseViewSet.queryset.model, + expr2=PackageRelease, + ) + self.assertDictEqual( + d1=PackageReleaseViewSet.queryset.query.select_related, + d2={'package': {}}, + ) + prefetch_lookups = PackageReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + +class PackageTagViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageTagViewSet.serializer_class, + second=PackageTagSerializer, + ) + self.assertEqual( + first=PackageTagViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageTagViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageTagViewSet.queryset.model, + expr2=PackageTag, + ) + self.assertDictEqual( + d1=PackageTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'package': {}} + ) + + +class PackageViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageViewSet.filterset_class, + second=PackageFilterSet, + ) + self.assertEqual( + first=PackageViewSet.serializer_class, + second=PackageSerializer, + ) + self.assertEqual( + first=PackageViewSet.creation_serializer_class, + second=PackageCreateSerializer, + ) + self.assertIs(expr1=PackageViewSet.queryset.model, expr2=Package) + prefetch_lookups = PackageViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=PackageViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}}, + ) diff --git a/project_manager/plugins/api/tests/test_views.py b/project_manager/plugins/api/tests/test_views.py new file mode 100644 index 00000000..7adb319c --- /dev/null +++ b/project_manager/plugins/api/tests/test_views.py @@ -0,0 +1,383 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework import status +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ( + ProjectAPIView, + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectReleaseViewSet, + ProjectTagViewSet, + ProjectViewSet, +) +from project_manager.common.api.views.mixins import ProjectThroughModelMixin +from project_manager.plugins.api.filtersets import PluginFilterSet +from project_manager.plugins.api.serializers import ( + PluginContributorSerializer, + PluginCreateSerializer, + PluginGameSerializer, + PluginImageSerializer, + PluginReleaseSerializer, + PluginSerializer, + PluginTagSerializer, + SubPluginPathSerializer, +) +from project_manager.plugins.api.views import ( + PluginAPIView, + PluginContributorViewSet, + PluginGameViewSet, + PluginImageViewSet, + PluginReleaseViewSet, + PluginTagViewSet, + PluginViewSet, + SubPluginPathViewSet, +) +from project_manager.plugins.models import ( + Plugin, + PluginContributor, + PluginGame, + PluginImage, + PluginRelease, + PluginReleaseDownloadRequirement, + PluginReleasePackageRequirement, + PluginReleasePyPiRequirement, + PluginReleaseVersionControlRequirement, + PluginTag, + SubPluginPath, +) + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginAPIViewTestCase(APITestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginAPIView, ProjectAPIView)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginAPIView.project_type, + second='plugin', + ) + + def test_get(self): + response = self.client.get(path='/api/plugins/') + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + base_path = reverse( + viewname=f'api:plugins:endpoints', + request=response.wsgi_request, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'contributors': base_path + f'contributors//', + 'games': base_path + f'games//', + 'images': base_path + f'images//', + 'projects': base_path + f'projects/', + 'releases': base_path + f'releases//', + 'tags': base_path + f'tags//', + 'paths': base_path + f'paths//', + } + ) + + +class PluginContributorViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginContributorViewSet, + ProjectContributorViewSet, + ), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginContributorViewSet.serializer_class, + second=PluginContributorSerializer, + ) + self.assertEqual( + first=PluginContributorViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginContributorViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginContributorViewSet.queryset.model, + expr2=PluginContributor, + ) + self.assertDictEqual( + d1=PluginContributorViewSet.queryset.query.select_related, + d2={'user': {'user': {}}, 'plugin': {}} + ) + + +class PluginGameViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginGameViewSet, ProjectGameViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginGameViewSet.serializer_class, + second=PluginGameSerializer, + ) + self.assertEqual( + first=PluginGameViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginGameViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginGameViewSet.queryset.model, + expr2=PluginGame, + ) + self.assertDictEqual( + d1=PluginGameViewSet.queryset.query.select_related, + d2={'game': {}, 'plugin': {}} + ) + + +class PluginImageViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginImageViewSet.serializer_class, + second=PluginImageSerializer, + ) + self.assertEqual( + first=PluginImageViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginImageViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginImageViewSet.queryset.model, + expr2=PluginImage, + ) + self.assertDictEqual( + d1=PluginImageViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + + +class PluginReleaseViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginReleaseViewSet.serializer_class, + second=PluginReleaseSerializer, + ) + self.assertEqual( + first=PluginReleaseViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginReleaseViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginReleaseViewSet.queryset.model, + expr2=PluginRelease, + ) + self.assertDictEqual( + d1=PluginReleaseViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + prefetch_lookups = PluginReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + +class PluginTagViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginTagViewSet.serializer_class, + second=PluginTagSerializer, + ) + self.assertEqual( + first=PluginTagViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginTagViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginTagViewSet.queryset.model, + expr2=PluginTag, + ) + self.assertDictEqual( + d1=PluginTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'plugin': {}} + ) + + +class PluginViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginViewSet.filterset_class, + second=PluginFilterSet, + ) + self.assertEqual( + first=PluginViewSet.serializer_class, + second=PluginSerializer, + ) + self.assertEqual( + first=PluginViewSet.creation_serializer_class, + second=PluginCreateSerializer, + ) + self.assertIs(expr1=PluginViewSet.queryset.model, expr2=Plugin) + prefetch_lookups = PluginViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=PluginViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}}, + ) + + +class SubPluginPathViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginPathViewSet, ProjectThroughModelMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=SubPluginPathViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'delete', 'options'), + ) + self.assertTupleEqual( + tuple1=SubPluginPathViewSet.ordering, + tuple2=('path',), + ) + self.assertEqual( + first=SubPluginPathViewSet.serializer_class, + second=SubPluginPathSerializer, + ) + self.assertEqual( + first=SubPluginPathViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=SubPluginPathViewSet.project_model, + second=Plugin, + ) + self.assertEqual( + first=SubPluginPathViewSet.related_model_type, + second='Sub-Plugin Path', + ) + self.assertIs( + expr1=SubPluginPathViewSet.queryset.model, + expr2=SubPluginPath, + ) + self.assertDictEqual( + d1=SubPluginPathViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) diff --git a/project_manager/sub_plugins/api/tests/test_views.py b/project_manager/sub_plugins/api/tests/test_views.py new file mode 100644 index 00000000..61a4bcf7 --- /dev/null +++ b/project_manager/sub_plugins/api/tests/test_views.py @@ -0,0 +1,430 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.test import TestCase + +# Third Party Django +from rest_framework import status +from rest_framework.parsers import ParseError +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ( + ProjectAPIView, + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectReleaseViewSet, + ProjectTagViewSet, + ProjectViewSet, +) +from project_manager.sub_plugins.api.filtersets import SubPluginFilterSet +from project_manager.sub_plugins.api.serializers import ( + SubPluginContributorSerializer, + SubPluginCreateSerializer, + SubPluginGameSerializer, + SubPluginImageSerializer, + SubPluginReleaseSerializer, + SubPluginSerializer, + SubPluginTagSerializer, +) +from project_manager.sub_plugins.api.views import ( + SubPluginAPIView, + SubPluginContributorViewSet, + SubPluginGameViewSet, + SubPluginImageViewSet, + SubPluginReleaseViewSet, + SubPluginTagViewSet, + SubPluginViewSet, +) +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginContributor, + SubPluginGame, + SubPluginImage, + SubPluginRelease, + SubPluginReleaseDownloadRequirement, + SubPluginReleasePackageRequirement, + SubPluginReleasePyPiRequirement, + SubPluginReleaseVersionControlRequirement, + SubPluginTag, +) +from test_utils.factories.plugins import PluginFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginAPIViewTestCase(APITestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginAPIView, ProjectAPIView)) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginAPIView.project_type, + second='sub-plugin', + ) + + def test_get(self): + response = self.client.get(path='/api/sub-plugins/') + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + base_path = reverse( + viewname=f'api:sub-plugins:endpoints', + request=response.wsgi_request, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'contributors': base_path + f'contributors///', + 'games': base_path + f'games///', + 'images': base_path + f'images///', + 'projects': base_path + f'projects//', + 'releases': base_path + f'releases///', + 'tags': base_path + f'tags///', + } + ) + + +class SubPluginContributorViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginContributorViewSet, + ProjectContributorViewSet, + ), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginContributorViewSet.serializer_class, + second=SubPluginContributorSerializer, + ) + self.assertEqual( + first=SubPluginContributorViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginContributorViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginContributorViewSet.queryset.model, + expr2=SubPluginContributor, + ) + self.assertDictEqual( + d1=SubPluginContributorViewSet.queryset.query.select_related, + d2={'user': {'user': {}}, 'sub_plugin': {}} + ) + + +class SubPluginGameViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginGameViewSet, ProjectGameViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginGameViewSet.serializer_class, + second=SubPluginGameSerializer, + ) + self.assertEqual( + first=SubPluginGameViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginGameViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginGameViewSet.queryset.model, + expr2=SubPluginGame, + ) + self.assertDictEqual( + d1=SubPluginGameViewSet.queryset.query.select_related, + d2={'game': {}, 'sub_plugin': {}} + ) + + +class SubPluginImageViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginImageViewSet.serializer_class, + second=SubPluginImageSerializer, + ) + self.assertEqual( + first=SubPluginImageViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginImageViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginImageViewSet.queryset.model, + expr2=SubPluginImage, + ) + self.assertDictEqual( + d1=SubPluginImageViewSet.queryset.query.select_related, + d2={'sub_plugin': {}}, + ) + + def test_parent_project(self): + obj = SubPluginImageViewSet() + invalid_slug = 'invalid' + obj.kwargs = {'plugin_slug': invalid_slug} + with self.assertRaises(ParseError) as context: + _ = obj.parent_project + + self.assertEqual( + first=context.exception.detail, + second=f"Plugin '{invalid_slug}' not found.", + ) + + plugin = PluginFactory() + obj.kwargs = {'plugin_slug': plugin.slug} + self.assertEqual( + first=obj.parent_project, + second=plugin, + ) + + def test_get_project_kwargs(self): + obj = SubPluginImageViewSet() + plugin = PluginFactory() + sub_plugin_slug = 'test-sub-plugin' + obj.kwargs = { + 'sub_plugin_slug': sub_plugin_slug, + 'plugin_slug': plugin.slug, + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={ + 'slug': sub_plugin_slug, + 'plugin': plugin, + } + ) + + +class SubPluginReleaseViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginReleaseViewSet.serializer_class, + second=SubPluginReleaseSerializer, + ) + self.assertEqual( + first=SubPluginReleaseViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginReleaseViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginReleaseViewSet.queryset.model, + expr2=SubPluginRelease, + ) + self.assertDictEqual( + d1=SubPluginReleaseViewSet.queryset.query.select_related, + d2={'sub_plugin': {}}, + ) + prefetch_lookups = SubPluginReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + def test_parent_project(self): + obj = SubPluginReleaseViewSet() + invalid_slug = 'invalid' + obj.kwargs = {'plugin_slug': invalid_slug} + with self.assertRaises(ParseError) as context: + _ = obj.parent_project + + self.assertEqual( + first=context.exception.detail, + second=f"Plugin '{invalid_slug}' not found.", + ) + + plugin = PluginFactory() + obj.kwargs = {'plugin_slug': plugin.slug} + self.assertEqual( + first=obj.parent_project, + second=plugin, + ) + + def test_get_project_kwargs(self): + obj = SubPluginReleaseViewSet() + plugin = PluginFactory() + sub_plugin_slug = 'test-sub-plugin' + obj.kwargs = { + 'sub_plugin_slug': sub_plugin_slug, + 'plugin_slug': plugin.slug, + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={ + 'slug': sub_plugin_slug, + 'plugin': plugin, + } + ) + + +class SubPluginTagViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginTagViewSet.serializer_class, + second=SubPluginTagSerializer, + ) + self.assertEqual( + first=SubPluginTagViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginTagViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginTagViewSet.queryset.model, + expr2=SubPluginTag, + ) + self.assertDictEqual( + d1=SubPluginTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'sub_plugin': {}} + ) + + +class SubPluginViewSetTestCase(TestCase): + def test_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginViewSet.filterset_class, + second=SubPluginFilterSet, + ) + self.assertEqual( + first=SubPluginViewSet.serializer_class, + second=SubPluginSerializer, + ) + self.assertEqual( + first=SubPluginViewSet.creation_serializer_class, + second=SubPluginCreateSerializer, + ) + self.assertIs(expr1=SubPluginViewSet.queryset.model, expr2=SubPlugin) + prefetch_lookups = SubPluginViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=SubPluginViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}, 'plugin': {}}, + ) + + def test_get_queryset(self): + with self.assertRaises(ParseError) as context: + obj = SubPluginViewSet() + obj.kwargs = {} + obj.get_queryset() + + self.assertEqual( + first=context.exception.detail, + second='Invalid plugin_slug.', + ) + + # TODO: validate the query returns the correct data + plugin = PluginFactory() + obj.kwargs = {'plugin_slug': plugin.slug} + obj.get_queryset() + + # TODO: validate the query returns the correct data + obj.kwargs = {} + obj.plugin = plugin + obj.get_queryset() From 2bb53e7d66f87812eb1db0d1d751223b00acf54b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:00:45 -0500 Subject: [PATCH 071/211] Removed requirements that are no longer used. --- SPPM/settings/base.py | 3 --- pip-requirements/base.txt | 2 -- 2 files changed, 5 deletions(-) diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index 0eafbb94..a5c23e35 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -53,7 +53,6 @@ 'rest_framework', 'embed_video', 'precise_bbcode', - 'crispy_forms', 'django_extensions', 'django_filters', 'project_manager', @@ -206,6 +205,4 @@ DOWNLOAD_URL = 'http://downloads.sourcepython.com/' -CRISPY_TEMPLATE_PACK = 'bootstrap' - DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index 336a6f79..dbf72015 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,6 +1,4 @@ django==3.2.9 -django-braces==1.14.0 -django-crispy-forms==1.13.0 django-embed-video==1.4.0 django-extensions==3.1.3 django-filter==21.1 From 81c6060992f6ce2624aa5b8ed7fd93eee194105b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:01:07 -0500 Subject: [PATCH 072/211] Added more fixtures for testing. --- .../test-package/test-package-invalid-v1.0.0.zip | Bin 0 -> 1534 bytes .../test-plugin/test-plugin-invalid-v1.0.0.zip | Bin 0 -> 1514 bytes .../test-sub-plugin-invalid-v1.0.0.zip | Bin 0 -> 2190 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 fixtures/releases/packages/test-package/test-package-invalid-v1.0.0.zip create mode 100644 fixtures/releases/plugins/test-plugin/test-plugin-invalid-v1.0.0.zip create mode 100644 fixtures/releases/sub-plugins/test-plugin/test-sub-plugin/test-sub-plugin-invalid-v1.0.0.zip diff --git a/fixtures/releases/packages/test-package/test-package-invalid-v1.0.0.zip b/fixtures/releases/packages/test-package/test-package-invalid-v1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..5602612c7e25e714efa0ea3be939ac45ea61c3da GIT binary patch literal 1534 zcmWIWW@h1H0D&T>lwdFeO0YA?FeIj=Wo(&Cc*T!^#Z z07C$TafZUwupE?NG$PjclGNgoc&J|>cAf0)OS}feIPK~W%?Um!Gvz)9FdjhInJl}C zQVUBni&Aq_^Gb^KvWoNbz|noLyD#w+rsbJ=Wr;bNDO{jv2Sq;z!*pPH{+cdhUb9#LzV5ib?B_A7}QQ zne?Ng_tBL(CcZ0JI7CEJpH0nJ(Vmhbrp*wz$cZyx@w{DX`r^Q}%E%lwdFeO0YA?FeIj=aeir0a;k1YWl2VU9>lmimzxto7^iWw(TtPDWn4i{X?iBimCt}i zf-p{FC9`r+ys1gZ*pk%Zl6Z(65SLF{-tz+^=YazsWanf|J2Ugj5_2+BxImEriVO~h>A+b0HC@KQ4#@KWVsldLM)Im& zK_%E?xeLvS2i9SD*87~Nj$Rmtr>@?{M^A$onxpx(;gD^(K zFfz$8<4WWbz(4{40fx7ZAR3C0AU4YMnJaWIUy^M(h4wJ0IdThQ9OYKvJN#CK5c4lIVdpV@>d^Y`{=u PUlwdFeO0YA?FeIj=aeir0a;k1YWl2VU9>lmimzxto7^iWw(TtPDWn4i{X?iBimCt}i zf-p{F6?f;LcvF**u_dX+CGikDATF1+>rd1JVw_g|Ky$e%DOMDhCP8h3hWnH&&50n4 z(>iH1AGwli9oVM``ysv+1Nu}IpKq;ma)M9FOu5eiObj3#MlI`$QVUBni&Aq_^Gb^K zvWoNbz{zmZ=DtL6O!s8wl_ln6rf?yW<4Uwx2%wg`;^Q;(GE3s)^$IG%F8T*dZXk>$ zfr-ZwOoQ0VK4{DU1(0^#0(idK(`-4Pt1T za>bTfLAK5dSSVu+ESQgwtwUsVP(cH< z8fzhg@F`|7gKYIqa;-*7!w{!{3LQM5fLY`~oC0(M!;(e|CNe_--6>cKAYd?o3Lrdg z!z^}S!31*K1!}nsr4RzT3TpubiacCJ5ORn`F;gkT&_WPgAORf*Dv&UOjFk--TMV2) N=+DBypb5kb3;>$$OJ@K8 literal 0 HcmV?d00001 From 2a8a1227af8e00afdeafee0c69cbd0e1d6b03f0a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:01:46 -0500 Subject: [PATCH 073/211] Added more tests for games. --- games/api/tests/test_views.py | 34 +++++++++++++++++++--------------- games/api/views.py | 1 + 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/games/api/tests/test_views.py b/games/api/tests/test_views.py index 3429f605..b415e448 100644 --- a/games/api/tests/test_views.py +++ b/games/api/tests/test_views.py @@ -18,7 +18,9 @@ # ============================================================================= # TEST CASES # ============================================================================= -class GameViewSetAPITestCase(APITestCase): +class GameViewSetTestCase(APITestCase): + + api_path = '/api/games/' def test_filter_backends(self): self.assertTupleEqual( @@ -44,8 +46,14 @@ def test_ordering_fields(self): tuple2=('basename', 'name',), ) - def test_can_list(self): - response = self.client.get(path='/api/games/') + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=GameViewSet.http_method_names, + tuple2=('get', 'options'), + ) + + def test_get(self): + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -56,7 +64,7 @@ def test_can_list(self): ) call_command('create_game_instances') - response = self.client.get(path='/api/games/') + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -66,17 +74,13 @@ def test_can_list(self): second=len(GAMES), ) - def test_cannot_post(self): - game = list(GAMES)[0] - response = self.client.post( - path='/api/games/', - data={ - 'basename': game, - 'icon': f'games/{game}.png', - 'name': GAMES[game], - } - ) + def test_options(self): + response = self.client.options(path=self.api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_405_METHOD_NOT_ALLOWED, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['name'], + second='Game List', ) diff --git a/games/api/views.py b/games/api/views.py index 5820bc39..3993f67d 100644 --- a/games/api/views.py +++ b/games/api/views.py @@ -43,3 +43,4 @@ class GameViewSet(ListModelMixin, GenericViewSet): queryset = Game.objects.all() ordering = ('name',) ordering_fields = ('basename', 'name') + http_method_names = ('get', 'options') From e48c7db780b011b3f5d32a342b5d3d1459a87dfb Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:02:01 -0500 Subject: [PATCH 074/211] Added more tests for tags. --- tags/api/tests/test_views.py | 31 +++++++++++++++++++------------ tags/api/views.py | 1 + 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/tags/api/tests/test_views.py b/tags/api/tests/test_views.py index 3b448436..1aa9c0a5 100644 --- a/tags/api/tests/test_views.py +++ b/tags/api/tests/test_views.py @@ -17,7 +17,9 @@ # ============================================================================= # TEST CASES # ============================================================================= -class TagViewSetAPITestCase(APITestCase): +class TagViewSetTestCase(APITestCase): + + api_path = '/api/tags/' def test_filter_backends(self): self.assertTupleEqual( @@ -49,8 +51,14 @@ def test_ordering_fields(self): tuple2=('name',), ) - def test_can_list(self): - response = self.client.get(path='/api/tags/') + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=TagViewSet.http_method_names, + tuple2=('get', 'options'), + ) + + def test_get(self): + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -61,7 +69,7 @@ def test_can_list(self): ) tag = TagFactory() - response = self.client.get(path='/api/tags/') + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -76,14 +84,13 @@ def test_can_list(self): second=tag.name, ) - def test_cannot_post(self): - response = self.client.post( - path='/api/tags/', - data={ - 'name': 'test', - } - ) + def test_options(self): + response = self.client.options(path=self.api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_405_METHOD_NOT_ALLOWED, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['name'], + second='Tag List', ) diff --git a/tags/api/views.py b/tags/api/views.py index 64b34149..93ada6de 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -56,3 +56,4 @@ class TagViewSet(ListModelMixin, GenericViewSet): ) ordering = ('name',) ordering_fields = ('name',) + http_method_names = ('get', 'options') From 335628b9356b20c30e5228c29f847fc08de66bce Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:03:36 -0500 Subject: [PATCH 075/211] Updated how raise-from is handled to raise from the previous exception instead of just the previous exception type. --- users/management/commands/associate_super_user.py | 4 ++-- users/management/commands/create_test_user.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/users/management/commands/associate_super_user.py b/users/management/commands/associate_super_user.py index 00aeb59c..3d3ac7c2 100644 --- a/users/management/commands/associate_super_user.py +++ b/users/management/commands/associate_super_user.py @@ -53,10 +53,10 @@ def handle(self, *args, **options): username = options['username'] try: user = User.objects.get(username=username) - except User.DoesNotExist: + except User.DoesNotExist as exception: raise CommandError( f'User with the username "{username}" was not found.' - ) from User.DoesNotExist + ) from exception forum_id = options['forum_id'] if ForumUser.objects.filter( diff --git a/users/management/commands/create_test_user.py b/users/management/commands/create_test_user.py index b084030a..14884a46 100644 --- a/users/management/commands/create_test_user.py +++ b/users/management/commands/create_test_user.py @@ -72,10 +72,10 @@ def handle(self, *args, **options): username=username, password=options['password'], ) - except Exception as error: + except Exception as exception: raise CommandError( - f'Unable to create User due to: {error}' - ) from Exception + f'Unable to create User due to: {exception}' + ) from exception ForumUser.objects.create( user=user, From 57db2df75c8cffb6189c249469b2dd3f6b068115 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:04:32 -0500 Subject: [PATCH 076/211] Added more tests for users. --- users/api/tests/test_views.py | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/users/api/tests/test_views.py b/users/api/tests/test_views.py index e150c875..d4a2e34f 100644 --- a/users/api/tests/test_views.py +++ b/users/api/tests/test_views.py @@ -17,7 +17,9 @@ # ============================================================================= # TEST CASES # ============================================================================= -class ForumUserViewSetAPITestCase(APITestCase): +class ForumUserViewSetTestCase(APITestCase): + + api_path = '/api/users/' def test_filter_backends(self): self.assertTupleEqual( @@ -55,9 +57,9 @@ def test_ordering_fields(self): tuple2=('forum_id', 'user__username'), ) - def test_get(self): + def test_get_list(self): user = ForumUserFactory() - response = self.client.get(path='/api/users/') + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -77,9 +79,26 @@ def test_get(self): second=user.user.username, ) + def test_get_details(self): + user = ForumUserFactory() + response = self.client.get(path=f'{self.api_path}{user.forum_id}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual( + first=content['forum_id'], + second=user.forum_id, + ) + self.assertEqual( + first=content['username'], + second=user.user.username, + ) + def test_get_filter(self): user = ForumUserFactory() - response = self.client.get(path='/api/users/?has_contributions=true') + response = self.client.get(path=f'{self.api_path}?has_contributions=true') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -89,7 +108,7 @@ def test_get_filter(self): second=0, ) - response = self.client.get(path='/api/users/?has_contributions=false') + response = self.client.get(path=f'{self.api_path}?has_contributions=false') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -108,3 +127,14 @@ def test_get_filter(self): first=content_user['username'], second=user.user.username, ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['name'], + second='Forum User List', + ) From a9c90be20017a2ca75aad950d616e1746329b722 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:05:29 -0500 Subject: [PATCH 077/211] Added more tests for the statistics api view. --- project_manager/tests/test_views.py | 23 +++++++++++++++++++++-- project_manager/views.py | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/project_manager/tests/test_views.py b/project_manager/tests/test_views.py index 741b2a16..da90b5e8 100644 --- a/project_manager/tests/test_views.py +++ b/project_manager/tests/test_views.py @@ -36,18 +36,26 @@ # ============================================================================= class StatisticsViewTestCase(TestCase): + api_path = '/statistics/' + def test_class_inheritance(self): self.assertTrue( expr=issubclass(StatisticsView, TemplateView), ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=StatisticsView.http_method_names, + tuple2=('get', 'options'), + ) + def test_template_name(self): self.assertEqual( first=StatisticsView.template_name, second='statistics.html', ) - def test_get_view(self): + def test_get(self): contributing_users = set() total_users = randint(20, 30) user_list = [ForumUserFactory() for _ in range(total_users)] @@ -129,7 +137,7 @@ def test_get_view(self): download_count=download_count, ) - response = self.client.get('/statistics/') + response = self.client.get(self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -158,3 +166,14 @@ def test_get_view(self): ]) } ) + + def test_options(self): + response = self.client.get(self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertIn( + member='Source.Python Project Manager Statistics', + container=str(response.content), + ) diff --git a/project_manager/views.py b/project_manager/views.py index b75de9be..0ec51584 100644 --- a/project_manager/views.py +++ b/project_manager/views.py @@ -29,6 +29,7 @@ class StatisticsView(TemplateView): """View for total Project statistics.""" template_name = 'statistics.html' + http_method_names = ('get', 'options') def get_context_data(self, **kwargs): """Return all statistical context data.""" From d680b0bd88e5c957ed4aeb866e0a6b0b4c4749cc Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:51:44 -0500 Subject: [PATCH 078/211] Removed some unused code. More updates for raise-from changes. Added a get_requirements_file_contents method that can be overwritten in sub_plugins later. --- .../common/api/serializers/__init__.py | 31 ++------ .../common/api/serializers/mixins.py | 43 ++++------- project_manager/common/helpers.py | 73 +++++++++++++------ 3 files changed, 69 insertions(+), 78 deletions(-) diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index 1e2f5a7f..3783da8a 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -212,15 +212,6 @@ def get_requirements(release): 'download_requirements': download_requirements, } - def get_extra_kwargs(self): - """Set the 'name' field to read-only when updating.""" - extra_kwargs = super().get_extra_kwargs() - if self.context['view'].action == 'update': - name_kwargs = extra_kwargs.get('name', {}) - name_kwargs['read_only'] = True - extra_kwargs['name'] = name_kwargs - return extra_kwargs - def get_extra_validated_data(self, validated_data): """Add any extra data to be used on create.""" validated_data['owner'] = self.context['request'].user.forum_user @@ -234,18 +225,6 @@ def get_updated(self, obj): def validate(self, attrs): """Validate the given field values.""" self.release_dict = attrs.pop('releases', {}) - version = self.release_dict.get('version', '') - zip_file = self.release_dict.get('zip_file') - if ( - self.context['request'].method == 'POST' and - not all([version, zip_file]) - ): - raise ValidationError({ - 'releases': ( - 'Version and Zip File are required when using POST or PUT ' - f'for creating/updating a {self.project_type}.' - ) - }) return attrs def update(self, instance, validated_data): @@ -365,10 +344,10 @@ def validate(self, attrs): }) try: game = Game.objects.get(basename=name) - except Game.DoesNotExist: + except Game.DoesNotExist as exception: raise ValidationError({ 'game': f'Invalid game "{name}".' - }) from Game.DoesNotExist + }) from exception attrs['game'] = game return super().validate(attrs=attrs) @@ -448,9 +427,9 @@ def validate(self, attrs): }) try: user = ForumUser.objects.get(user__username=username) - except ForumUser.DoesNotExist: + except ForumUser.DoesNotExist as exception: raise ValidationError({ - 'user': f'No user named "{username}".' - }) from ForumUser.DoesNotExist + 'username': f'No user named "{username}".' + }) from exception attrs['user'] = user return super().validate(attrs=attrs) diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index d8f5eb5d..99ee274b 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -10,6 +10,9 @@ from rest_framework.exceptions import ValidationError from rest_framework.serializers import ModelSerializer +# App +from project_manager.common.helpers import GROUP_QUERYSET_NAMES + # ============================================================================= # ALL DECLARATION @@ -22,17 +25,6 @@ ) -# ============================================================================= -# GLOBAL VARIABLES -# ============================================================================= -GROUP_QUERYSET_NAMES = { - 'custom': 'package', - 'pypi': 'pypi', - 'vcs': 'versioncontrol', - 'download': 'download', -} - - # ============================================================================= # MIXINS # ============================================================================= @@ -93,21 +85,15 @@ def zip_parser(self): def get_project_kwargs(self): """Return kwargs for the project.""" - return { - 'pk': self.context['view'].kwargs.get('pk') - } + raise NotImplementedError( + f'Class "{self.__class__.__name__}" must implement a ' + '"get_project_kwargs" method.' + ) def validate(self, attrs): """Validate that the new release can be created.""" version = attrs.get('version', '') zip_file = attrs.get('zip_file') - if any([version, zip_file]) and not all([version, zip_file]): - raise ValidationError({ - '__all__': ( - "If either 'version' or 'zip_file' are provided, " - "must be provided." - ) - }) # Validate the version is new for the project kwargs = self.get_project_kwargs() @@ -120,7 +106,7 @@ def validate(self, attrs): ) project_basename = getattr(project, 'basename', None) - args = (zip_file,) + args = self.get_zip_file_args(zip_file=zip_file) zip_validator = self.zip_parser(*args) self.run_zip_file_validation( @@ -136,6 +122,11 @@ def validate(self, attrs): return attrs + @staticmethod + def get_zip_file_args(zip_file): + """Return the arguments necessary to instantiate the ZipFile class.""" + return [zip_file] + def get_project(self, kwargs): """Return the Project for the given kwargs.""" try: @@ -167,7 +158,7 @@ def run_zip_file_validation(self, zip_validator, project_basename): 'zip_file': ( f"Basename in zip '{zip_validator.basename}' does " f"not match basename for {self.project_type} " - f"'{project_basename}'" + f"'{project_basename}'." ) }) @@ -189,7 +180,7 @@ def create(self, validated_data): def _create_requirements(self, release): """Create all requirements for the release.""" - if self.requirements is None: + if not self.requirements: return # TODO: look into bulk_create @@ -205,10 +196,6 @@ def _create_requirements(self, release): @staticmethod def _create_group_requirements(release, project_type, group_type, group): queryset_group_name = GROUP_QUERYSET_NAMES.get(group_type) - if not queryset_group_name: - # TODO: should we care if they have invalid groupings? - pass - for requirement in group: requirement_set = getattr( release, diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 965583d8..0ceab923 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -5,6 +5,8 @@ # ============================================================================= # Python import json +import logging +from collections import defaultdict from zipfile import ZipFile, BadZipFile # Django @@ -20,6 +22,7 @@ # ALL DECLARATION # ============================================================================= __all__ = ( + 'GROUP_QUERYSET_NAMES', 'ProjectZipFile', 'find_image_number', 'handle_project_image_upload', @@ -43,6 +46,13 @@ app_label='requirements', model_name='VersionControlRequirement', ) +logger = logging.getLogger(__name__) +GROUP_QUERYSET_NAMES = { + 'custom': 'package', + 'pypi': 'pypi', + 'vcs': 'versioncontrol', + 'download': 'download', +} # ============================================================================= @@ -57,12 +67,7 @@ def __init__(self, zip_file): with ZipFile(self.zip_file) as zip_obj: self.file_list = self.get_file_list(zip_obj) self.basename = None - self.requirements = { - 'custom': [], - 'pypi': [], - 'vcs': [], - 'download': [], - } + self.requirements = defaultdict(list) self.requirements_errors = [] @property @@ -138,10 +143,10 @@ def get_file_list(zip_obj): """Return a list of all files in the given zip file.""" try: return [x for x in zip_obj.namelist() if not x.endswith('/')] - except BadZipFile: + except BadZipFile as exception: raise ValidationError({ 'zip_file': 'Given file is not a valid zip file.' - }) from BadZipFile + }) from exception def validate_basename(self): """Validate that the basename is not erroneous.""" @@ -183,33 +188,25 @@ def validate_base_file_in_zip(self): def validate_requirements(self): """Return the requirements for the release.""" - requirement_path = self.get_requirement_path() - try: - with ZipFile(self.zip_file).open(requirement_path) as requirement_file: - contents = json.load(requirement_file) - except KeyError: + contents = self.get_requirements_file_contents() + if contents is None: return - except json.JSONDecodeError: - raise ValidationError({ - 'zip_file': 'Requirements json file cannot be decoded.' - }) from json.JSONDecodeError - if not isinstance(contents, dict): - raise ValidationError({ - 'zip_file': 'Invalid requirements json file.' - }) + for group_type, group in contents.items(): - if group_type not in self.requirements: + if group_type not in GROUP_QUERYSET_NAMES: self.requirements_errors.append( f'Invalid group name "{group_type}" found in ' f'requirements json file.' ) continue + if not isinstance(group, list): self.requirements_errors.append( f'Invalid group values for "{group_type}" found in ' f'requirements json file.' ) continue + for item in group: if not isinstance(item, dict): self.requirements_errors.append( @@ -217,6 +214,7 @@ def validate_requirements(self): f'requirements json file.' ) continue + if group_type == 'custom': self._validate_custom_requirement( item=item, @@ -235,6 +233,27 @@ def validate_requirements(self): 'zip_file': self.requirements_errors, }) + def get_requirements_file_contents(self): + """Return the contents of the requirements.json file.""" + requirement_path = self.get_requirement_path() + try: + with ZipFile(self.zip_file).open(requirement_path) as requirement_file: + contents = json.load(requirement_file) + except KeyError: + logger.debug('No requirement file found.') + return None + except json.decoder.JSONDecodeError as exception: + raise ValidationError({ + 'zip_file': 'Requirements json file cannot be decoded.' + }) from exception + + if not isinstance(contents, dict): + raise ValidationError({ + 'zip_file': 'Invalid requirements json file.' + }) + + return contents + def get_requirement_path(self): """Return the path for the requirements json file.""" raise NotImplementedError( @@ -257,6 +276,8 @@ def _validate_custom_requirement(self, item): model_name='Package', ) try: + # TODO: should this be retrieving via basename instead of slug? + # or should the field in the requirements file be called slug? package = package_model.objects.get(slug=basename) except package_model.DoesNotExist: self.requirements_errors.append( @@ -264,6 +285,7 @@ def _validate_custom_requirement(self, item): f'json file not found.' ) return + version = item.get('version') # TODO: update this logic to work with all version operators available_versions = package.releases.values_list( @@ -276,6 +298,7 @@ def _validate_custom_requirement(self, item): f'from requirements json file, not found.' ) return + self.requirements['custom'].append({ 'package_requirement': package, 'version': version, @@ -294,14 +317,15 @@ def _validate_requirement( 'vcs': VersionControlRequirement, }.get(group_type) value = item.get(field) - instance, created = model.objects.get_or_create(**{field: value}) - key = f'{group_type}_requirement' if value is None: self.requirements_errors.append( f'No {field} found for object in "{group_type}" listing in ' f'requirements json file.' ) return + + instance, created = model.objects.get_or_create(**{field: value}) + key = f'{group_type}_requirement' requirement_dict = { key: instance, 'optional': item.get('optional', False), @@ -310,6 +334,7 @@ def _validate_requirement( requirement_dict.update({ 'version': item.get('version'), }) + self.requirements[group_type].append(requirement_dict) From 76c24477073536d1a9acfb98d972c14acff02163 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:52:52 -0500 Subject: [PATCH 079/211] Implemented get_requirements_file_contents for sub_plugins. Updated get_requirement_paths to return a list of all possible paths for sub_plugins. --- project_manager/sub_plugins/helpers.py | 86 +++++++++++++++++--------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/project_manager/sub_plugins/helpers.py b/project_manager/sub_plugins/helpers.py index ee492d29..0a222389 100644 --- a/project_manager/sub_plugins/helpers.py +++ b/project_manager/sub_plugins/helpers.py @@ -4,6 +4,10 @@ # IMPORTS # ============================================================================= # Django +import json +import logging +from zipfile import ZipFile + from django.core.exceptions import ValidationError # App @@ -28,6 +32,12 @@ ) +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +logger = logging.getLogger(__name__) + + # ============================================================================= # CLASSES # ============================================================================= @@ -36,7 +46,7 @@ class SubPluginZipFile(ProjectZipFile): project_type = 'SubPlugin' file_types = SUB_PLUGIN_ALLOWED_FILE_TYPES - paths = set() + paths = None is_module = False def __init__(self, zip_file, plugin): @@ -66,8 +76,10 @@ def _validate_path(self, path): ) ): continue + if extension in allowed_extensions: return True + return False return False @@ -75,7 +87,8 @@ def _validate_path(self, path): def find_base_info(self): """Store all base information for the zip file.""" plugin_path = f'{PLUGIN_PATH}{self.plugin.basename}/' - paths = list(self.plugin.paths.values_list('path', flat=True)) + paths = list(self.plugin.paths.all()) + self.paths = set() for file_path in self.file_list: if not file_path.startswith(plugin_path): # TODO: validate not another plugin path or package @@ -89,10 +102,11 @@ def find_base_info(self): continue for current_path in paths: - if not current.startswith(current_path): + path = current_path.path + if not current.startswith(path): continue - current = current.split(current_path, 1)[1] + current = current.split(path, 1)[1] if current.startswith('/'): # pragma: no branch current = current[1:] @@ -117,25 +131,16 @@ def find_base_info(self): def validate_base_file_in_zip(self): """Verify that there is a base file within the zip file.""" plugin_paths = { - values['path']: { - 'allow_module': values['allow_module'], - 'allow_package_using_basename': values[ - 'allow_package_using_basename' - ], - 'allow_package_using_init': values['allow_package_using_init'], - } for values in self.plugin.paths.filter( - path__in=self.paths, - ).values( - 'path', - 'allow_module', - 'allow_package_using_basename', - 'allow_package_using_init', - ) + path.path: { + 'allow_module': path.allow_module, + 'allow_package_using_basename': path.allow_package_using_basename, + 'allow_package_using_init': path.allow_package_using_init, + } for path in self.paths } - for path in self.paths: + for path, path_values in plugin_paths.items(): self._validate_base_file_in_zip( base_path=path, - path_values=plugin_paths[path] + path_values=path_values, ) def _validate_base_file_in_zip(self, base_path, path_values): @@ -180,18 +185,43 @@ def _validate_base_file_in_zip(self, base_path, path_values): code='not-found', ) - def get_requirement_path(self): + def get_requirements_file_contents(self): + """Return the contents of the requirements.json file.""" + requirement_paths = self.get_requirement_paths() + for requirement_path in requirement_paths: + try: + with ZipFile(self.zip_file).open(requirement_path) as requirement_file: + contents = json.load(requirement_file) + except KeyError: + continue + except json.decoder.JSONDecodeError as exception: + raise ValidationError({ + 'zip_file': 'Requirements json file cannot be decoded.' + }) from exception + + if not isinstance(contents, dict): + raise ValidationError({ + 'zip_file': 'Invalid requirements json file.' + }) + + return contents + + logger.debug('No requirement file found.') + return None + + def get_requirement_paths(self): """Return the path for the requirements json file.""" - # TODO: this is incorrect...it should take into account the sub-path if self.is_module: - return ( - f'{PLUGIN_PATH}{self.plugin.basename}/' + return [ + f'{PLUGIN_PATH}{self.plugin.basename}/{sub_plugin_path.path}/' f'{self.basename}_requirements.json' - ) - return ( - f'{PLUGIN_PATH}{self.plugin.basename}/' + for sub_plugin_path in self.paths + ] + return [ + f'{PLUGIN_PATH}{self.plugin.basename}/{sub_plugin_path.path}/' f'{self.basename}/requirements.json' - ) + for sub_plugin_path in self.paths + ] # ============================================================================= From c9949f97a32ed4bd21fd26a6bdee5fe9d1c8c605 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:53:32 -0500 Subject: [PATCH 080/211] More updates for raise-from. --- project_manager/sub_plugins/api/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index 5735029d..485afcac 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -154,8 +154,8 @@ def get_queryset(self): try: self.plugin = Plugin.objects.get(slug=plugin_slug) return queryset.filter(plugin=self.plugin) - except Plugin.DoesNotExist: - raise ParseError('Invalid plugin_slug.') from Plugin.DoesNotExist + except Plugin.DoesNotExist as exception: + raise ParseError('Invalid plugin_slug.') from exception class SubPluginImageViewSet(ProjectImageViewSet): @@ -175,10 +175,10 @@ def parent_project(self): plugin_slug = self.kwargs.get('plugin_slug') try: plugin = Plugin.objects.get(slug=plugin_slug) - except Plugin.DoesNotExist: + except Plugin.DoesNotExist as exception: raise ParseError( f"Plugin '{plugin_slug}' not found." - ) from Plugin.DoesNotExist + ) from exception return plugin def get_project_kwargs(self): @@ -242,10 +242,10 @@ def parent_project(self): plugin_slug = self.kwargs.get('plugin_slug') try: plugin = Plugin.objects.get(slug=plugin_slug) - except Plugin.DoesNotExist: + except Plugin.DoesNotExist as exception: raise ParseError( f"Plugin '{plugin_slug}' not found." - ) from Plugin.DoesNotExist + ) from exception return plugin def get_project_kwargs(self): From 4a6cb519540c8f265e3d6a4310ee2cca8b21470d Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:54:38 -0500 Subject: [PATCH 081/211] Updated SubPluginPathSerializer to take into account the existing boolean attributes during validation of PATCH. --- .../plugins/api/serializers/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/project_manager/plugins/api/serializers/__init__.py b/project_manager/plugins/api/serializers/__init__.py index 5e12a2ad..14544669 100644 --- a/project_manager/plugins/api/serializers/__init__.py +++ b/project_manager/plugins/api/serializers/__init__.py @@ -242,9 +242,18 @@ def get_field_names(self, declared_fields, info): def validate(self, attrs): """Validate that at least one of the 'Allow' fields is True.""" if not any([ - attrs['allow_module'], - attrs['allow_package_using_basename'], - attrs['allow_package_using_init'], + attrs.get( + 'allow_module', + getattr(self.instance, 'allow_module', None), + ), + attrs.get( + 'allow_package_using_basename', + getattr(self.instance, 'allow_package_using_basename', None), + ), + attrs.get( + 'allow_package_using_init', + getattr(self.instance, 'allow_package_using_init', None), + ), ]): message = "At least one of the 'Allow' fields must be True." raise ValidationError({ From 0e6587b468c05466902f05d6ab8bb01c1601dfd6 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:56:39 -0500 Subject: [PATCH 082/211] Added sub_plugin overrides for get_zip_file_args. --- .../sub_plugins/api/serializers/__init__.py | 12 ++++++++++-- .../sub_plugins/api/serializers/mixins.py | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index 5e5df780..cd096896 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -148,6 +148,10 @@ class Meta(ProjectReleaseSerializer.Meta): model = SubPluginRelease + def get_zip_file_args(self, zip_file): + """Return the arguments necessary to instantiate the ZipFile class.""" + return [zip_file, self.parent_project] + class SubPluginCreateReleaseSerializer( SubPluginReleaseBase, ProjectCreateReleaseSerializer @@ -159,6 +163,10 @@ class Meta(ProjectCreateReleaseSerializer.Meta): model = SubPluginRelease + def get_zip_file_args(self, zip_file): + """Return the arguments necessary to instantiate the ZipFile class.""" + return [zip_file, self.parent_project] + class SubPluginSerializer(ProjectSerializer): """Serializer for updating and listing SubPlugins.""" @@ -178,10 +186,10 @@ def parent_project(self): plugin_slug = kwargs.get('plugin_slug') try: plugin = Plugin.objects.get(slug=plugin_slug) - except Plugin.DoesNotExist: + except Plugin.DoesNotExist as exception: raise ValidationError({ 'plugin': f"Plugin '{plugin_slug}' not found." - }) from Plugin.DoesNotExist + }) from exception return plugin @staticmethod diff --git a/project_manager/sub_plugins/api/serializers/mixins.py b/project_manager/sub_plugins/api/serializers/mixins.py index a6ddf348..6365d659 100644 --- a/project_manager/sub_plugins/api/serializers/mixins.py +++ b/project_manager/sub_plugins/api/serializers/mixins.py @@ -36,10 +36,10 @@ def parent_project(self): plugin_slug = kwargs.get('plugin_slug') try: plugin = Plugin.objects.get(slug=plugin_slug) - except Plugin.DoesNotExist: + except Plugin.DoesNotExist as exception: raise ValidationError( f"Plugin '{plugin_slug}' not found." - ) from Plugin.DoesNotExist + ) from exception return plugin @property From b4492a5527eabd603c3b257ba72148efee82d2b0 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 11:59:28 -0500 Subject: [PATCH 083/211] Removed more unused code. Updated more instances of raise-from. Moved owner/contributor properties from ProjectThroughModelMixin to the inherited ProjectRelatedInfoMixin class for use with releases. Added check_permissions to ProjectReleaseViewSet that utilize owner/contributor. --- project_manager/common/api/views/__init__.py | 48 ++++++++------------ project_manager/common/api/views/mixins.py | 44 +++++++++--------- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index ba88b4ff..690a08d9 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -9,12 +9,11 @@ # Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.authentication import SessionAuthentication -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.filters import OrderingFilter from rest_framework.permissions import IsAuthenticatedOrReadOnly, SAFE_METHODS from rest_framework.response import Response from rest_framework.reverse import reverse -from rest_framework import status from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet @@ -82,10 +81,6 @@ class ProjectViewSet(ModelViewSet): ordering_fields = ('name', 'basename', 'updated', 'created') permission_classes = (IsAuthenticatedOrReadOnly,) - stored_contributors = None - stored_supported_games = None - stored_tags = None - @property def creation_serializer_class(self): """Return the serializer class to use ONLY when creating a project.""" @@ -117,19 +112,12 @@ def check_permissions(self, request): def create(self, request, *args, **kwargs): """Store the many-to-many fields before creation.""" - self.store_many_to_many_fields(request=request) try: return super().create(request, *args, **kwargs) - except IntegrityError: - return Response( - data={ - 'error': ( - f'{self.queryset.model.__name__} already exists. ' - 'Cannot create.' - ) - }, - status=status.HTTP_400_BAD_REQUEST, - ) + except IntegrityError as exception: + raise ValidationError({ + 'basename': f'{self.queryset.model.__name__} already exists. Cannot create.' + }) from exception def get_serializer_class(self): """Return the serializer class for the current method.""" @@ -137,17 +125,6 @@ def get_serializer_class(self): return self.creation_serializer_class return super().get_serializer_class() - def store_many_to_many_fields(self, request): - """Store the many-to-many fields.""" - self.stored_contributors = request.data.pop('contributors', None) - self.stored_supported_games = request.data.pop('supported_games', None) - self.stored_tags = request.data.pop('tags', None) - - def update(self, request, *args, **kwargs): - """Store the many-to-many fields before updating.""" - self.store_many_to_many_fields(request=request) - return super().update(request, *args, **kwargs) - class ProjectImageViewSet(ProjectThroughModelMixin): """Base Image View.""" @@ -167,6 +144,21 @@ class ProjectReleaseViewSet(ProjectRelatedInfoMixin): lookup_field = 'version' related_model_type = 'Release' + def check_permissions(self, request): + """Only allow the owner and contributors to create releases.""" + if request.method not in SAFE_METHODS: + if not hasattr(request.user, 'forum_user'): + print('no forum_user') + raise PermissionDenied + + user = request.user.id + is_contributor = user in self.contributors + if user != self.owner and not is_contributor: + print('not owner or contributor') + raise PermissionDenied + + return super().check_permissions(request=request) + class ProjectGameViewSet(ProjectThroughModelMixin): """Base Game Support ViewSet.""" diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index bacddfc2..3bc94616 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -32,6 +32,25 @@ class ProjectRelatedInfoMixin(ModelViewSet): related_model_type = None _project = None + _owner = None + _contributors = None + + @property + def owner(self): + """Return the project's owner.""" + if self._owner is None: + self._owner = self.project.owner.user_id + return self._owner + + @property + def contributors(self): + """Return a Queryset for the project's contributors.""" + if self._contributors is None: + self._contributors = self.project.contributors.values_list( + 'user', + flat=True, + ) + return self._contributors @property def project(self): @@ -43,10 +62,10 @@ def project(self): self._project = self.project_model.objects.select_related( 'owner__user' ).get(**kwargs) - except self.project_model.DoesNotExist: + except self.project_model.DoesNotExist as exception: raise ParseError( f"Invalid {self.project_type.replace('-', '_')}_slug." - ) from self.project_model.DoesNotExist + ) from exception return self._project @property @@ -96,28 +115,9 @@ class ProjectThroughModelMixin(ProjectRelatedInfoMixin): permission_classes = (IsAuthenticatedOrReadOnly,) owner_only_id_access = False - _owner = None - _contributors = None - - @property - def owner(self): - """Return the project's owner.""" - if self._owner is None: - self._owner = self.project.owner.user_id - return self._owner - - @property - def contributors(self): - """Return a Queryset for the project's contributors.""" - if self._contributors is None: - self._contributors = self.project.contributors.values_list( - 'user', - flat=True, - ) - return self._contributors def check_permissions(self, request): - """Only allow the owner and contributors to add game support.""" + """Only allow the owner and contributors to add data relationships.""" if request.method not in SAFE_METHODS or self.action == 'retrieve': user = request.user.id is_contributor = user in self.contributors From dedddf570cc67a94c110660bd6e1d471713ef65d Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 12:01:06 -0500 Subject: [PATCH 084/211] Added more tests. --- project_manager/api/tests/test_views.py | 15 +- .../packages/tests/test_helpers.py | 296 ++++++++++++++- project_manager/plugins/tests/test_helpers.py | 298 ++++++++++++++- .../sub_plugins/tests/test_helpers.py | 350 +++++++++++++++++- 4 files changed, 926 insertions(+), 33 deletions(-) diff --git a/project_manager/api/tests/test_views.py b/project_manager/api/tests/test_views.py index 1bbe3834..26f63037 100644 --- a/project_manager/api/tests/test_views.py +++ b/project_manager/api/tests/test_views.py @@ -5,12 +5,25 @@ from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APITestCase +from rest_framework.views import APIView + +# App +from project_manager.api.views import ProjectManagerAPIView # ============================================================================= # TEST CASES # ============================================================================= -class ProjectManagerAPIViewAPITestCase(APITestCase): +class ProjectManagerAPIViewTestCase(APITestCase): + + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectManagerAPIView, APIView)) + + def test_allowed_methods(self): + self.assertListEqual( + list1=ProjectManagerAPIView().allowed_methods, + list2=['GET', 'OPTIONS'], + ) def test_get(self): response = self.client.get(path='/api/') diff --git a/project_manager/packages/tests/test_helpers.py b/project_manager/packages/tests/test_helpers.py index decd5d5e..6084b739 100644 --- a/project_manager/packages/tests/test_helpers.py +++ b/project_manager/packages/tests/test_helpers.py @@ -6,6 +6,7 @@ from unittest import mock # Django +from django.conf import settings from django.core.exceptions import ValidationError from django.test import TestCase @@ -34,13 +35,14 @@ # ============================================================================= # TEST CASES # ============================================================================= +from test_utils.factories.requirements import PyPiRequirementFactory, DownloadRequirementFactory, \ + VersionControlRequirementFactory + + class PackageZipFileTestCase(TestCase): def setUp(self) -> None: super().setUp() - mock.patch( - target='project_manager.common.helpers.ZipFile', - ).start() self.mock_get_file_list = mock.patch( target='project_manager.common.helpers.ProjectZipFile.get_file_list', ).start() @@ -90,7 +92,10 @@ def test_file_types(self): d2=PACKAGE_ALLOWED_FILE_TYPES, ) - def test_find_base_info(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_find_base_info(self, _): package_basename = 'test_package_as_module' self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, @@ -131,7 +136,10 @@ def test_find_base_info(self): second='multiple', ) - def test_get_base_paths(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_get_base_paths(self, _): package_basename = 'test_package_as_module' self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, @@ -157,7 +165,10 @@ def test_get_base_paths(self): ], ) - def test_validate_base_file_in_zip(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_base_file_in_zip(self, _): package_basename = 'test_package_as_module' self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, @@ -179,7 +190,10 @@ def test_validate_base_file_in_zip(self): second='not-found', ) - def test_get_requirement_path(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_get_requirement_path(self, _): package_basename = 'test_package_as_module' self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, @@ -202,7 +216,10 @@ def test_get_requirement_path(self): second=f'{PACKAGE_PATH}{package_basename}/requirements.json', ) - def test_validate_file_paths(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_file_paths(self, _): package_basename = 'test_package_as_module' self.mock_get_file_list.return_value = self._get_module_file_list( package_basename=package_basename, @@ -266,6 +283,269 @@ def test_validate_file_paths(self): second=f'Invalid paths found in zip: {invalid_file}', ) + @mock.patch( + target='project_manager.common.helpers.logger', + ) + def test_validate_requirements_file_failures(self, mock_logger): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' + self.mock_get_file_list.return_value = [] + obj = PackageZipFile(zip_file=file_path) + obj.basename = 'invalid' + obj.validate_requirements() + mock_logger.debug.assert_called_once_with('No requirement file found.') + + file_path = base_path / 'test-package' / 'test-package-invalid-v1.0.0.zip' + self.mock_get_file_list.return_value = [ + 'addons/source-python/packages/custom/test_package/test_package_requirements.json', + ] + obj = PackageZipFile(zip_file=file_path) + obj.basename = 'test_package' + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'zip_file': ['Requirements json file cannot be decoded.']}, + ) + + @mock.patch( + target='project_manager.common.helpers.json.loads', + ) + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_requirements_file_item_failures(self, _, mock_json_loads): + custom_package_basename = 'test_custom_package' + custom_package_slug = custom_package_basename.replace('_', '-') + custom_package = PackageFactory( + basename=custom_package_basename, + ) + custom_package_release = PackageReleaseFactory( + package=custom_package, + version='1.0.0', + ) + download_requirement_url = 'http://example.com/some_file.zip' + download_requirement = DownloadRequirementFactory( + url=download_requirement_url, + ) + pypi_requirement_name = 'some-pypi-package' + pypi_requirement = PyPiRequirementFactory( + name=pypi_requirement_name, + ) + vcs_requirement_url = 'git://git.some-project.org/SomeProject.git' + vcs_requirement = VersionControlRequirementFactory( + url=vcs_requirement_url, + ) + + mock_json_loads.return_value = [] + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'zip_file': ['Invalid requirements json file.']}, + ) + + group_type = 'invalid' + mock_json_loads.return_value = { + group_type: {}, + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid group name "{group_type}" found in requirements ' + f'json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: { + 'key': 'value', + }, + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid group values for "{group_type}" found in ' + f'requirements json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: [ + 'package', + ], + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid object found in "{group_type}" listing in ' + f'requirements json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: [ + {'key': 'value'}, + ], + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + 'No basename found for object in "custom" listing in ' + 'requirements json file.' + ], + }, + ) + + group_type = 'custom' + invalid_basename = 'invalid' + mock_json_loads.return_value = { + group_type: [ + {'basename': invalid_basename}, + ], + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Custom Package "{invalid_basename}" from requirements ' + f'json file not found.' + ], + }, + ) + + group_type = 'custom' + version = '1.0.1' + mock_json_loads.return_value = { + group_type: [ + { + 'basename': custom_package_slug, + 'version': version, + }, + ], + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Custom Package "{custom_package_slug}" version ' + f'"{version}", from requirements json file, not found.' + ], + }, + ) + + for group_type, required_field in { + 'download': 'url', + 'pypi': 'name', + 'vcs': 'url', + }.items(): + mock_json_loads.return_value = { + group_type: [ + { + 'key': 'value', + }, + ], + } + obj = PackageZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'No {required_field} found for object in ' + f'"{group_type}" listing in requirements json file.' + ], + }, + ) + + mock_json_loads.return_value = { + 'custom': [ + { + 'basename': custom_package_slug, + 'version': custom_package_release.version, + }, + ], + 'download': [ + { + 'url': download_requirement_url, + } + ], + 'pypi': [ + { + 'name': pypi_requirement_name, + } + ], + 'vcs': [ + { + 'url': vcs_requirement_url, + } + ], + } + obj = PackageZipFile('') + obj.validate_requirements() + self.assertDictEqual( + d1=obj.requirements, + d2={ + 'custom': [{ + 'package_requirement': custom_package, + 'version': custom_package_release.version, + 'optional': False, + }], + 'download': [{ + 'download_requirement': download_requirement, + 'optional': False, + }], + 'pypi': [{ + 'pypi_requirement': pypi_requirement, + 'version': None, + 'optional': False, + }], + 'vcs': [{ + 'vcs_requirement': vcs_requirement, + 'optional': False, + }], + } + ) + class HelperFunctionsTestCase(TestCase): def test_handle_package_zip_upload(self): diff --git a/project_manager/plugins/tests/test_helpers.py b/project_manager/plugins/tests/test_helpers.py index f3955653..c8fcc790 100644 --- a/project_manager/plugins/tests/test_helpers.py +++ b/project_manager/plugins/tests/test_helpers.py @@ -6,6 +6,7 @@ from unittest import mock # Django +from django.conf import settings from django.core.exceptions import ValidationError from django.test import TestCase @@ -24,11 +25,17 @@ handle_plugin_logo_upload, handle_plugin_zip_upload, ) +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory from test_utils.factories.plugins import ( PluginFactory, PluginImageFactory, PluginReleaseFactory, ) +from test_utils.factories.requirements import ( + DownloadRequirementFactory, + VersionControlRequirementFactory, + PyPiRequirementFactory, +) # ============================================================================= @@ -38,9 +45,6 @@ class PluginZipFileTestCase(TestCase): def setUp(self) -> None: super().setUp() - mock.patch( - target='project_manager.common.helpers.ZipFile', - ).start() self.mock_get_file_list = mock.patch( target='project_manager.common.helpers.ProjectZipFile.get_file_list', ).start() @@ -79,7 +83,10 @@ def test_file_types(self): d2=PLUGIN_ALLOWED_FILE_TYPES, ) - def test_find_base_info(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_find_base_info(self, _): plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, @@ -107,7 +114,10 @@ def test_find_base_info(self): second='multiple', ) - def test_get_base_paths(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_get_base_paths(self, _): plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, @@ -119,7 +129,10 @@ def test_get_base_paths(self): list2=[f'{PLUGIN_PATH}{plugin_basename}/{plugin_basename}.py'], ) - def test_validate_base_file_in_zip(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_base_file_in_zip(self, _): plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, @@ -141,7 +154,10 @@ def test_validate_base_file_in_zip(self): second='not-found', ) - def test_get_requirement_path(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_get_requirement_path(self, _): plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, @@ -153,7 +169,10 @@ def test_get_requirement_path(self): second=f'{PLUGIN_PATH}{plugin_basename}/requirements.json', ) - def test_validate_file_paths(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_file_paths(self, _): plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( plugin_basename=plugin_basename, @@ -217,6 +236,269 @@ def test_validate_file_paths(self): second=f'Invalid paths found in zip: {invalid_file}', ) + @mock.patch( + target='project_manager.common.helpers.logger', + ) + def test_validate_requirements_file_failures(self, mock_logger): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' + self.mock_get_file_list.return_value = [] + obj = PluginZipFile(zip_file=file_path) + obj.basename = 'invalid' + obj.validate_requirements() + mock_logger.debug.assert_called_once_with('No requirement file found.') + + file_path = base_path / 'test-plugin' / 'test-plugin-invalid-v1.0.0.zip' + self.mock_get_file_list.return_value = [ + 'addons/source-python/plugins/test_plugin/requirements.json', + ] + obj = PluginZipFile(zip_file=file_path) + obj.basename = 'test_plugin' + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'zip_file': ['Requirements json file cannot be decoded.']}, + ) + + @mock.patch( + target='project_manager.common.helpers.json.loads', + ) + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_requirements_file_item_failures(self, _, mock_json_loads): + custom_package_basename = 'test_custom_package' + custom_package_slug = custom_package_basename.replace('_', '-') + custom_package = PackageFactory( + basename=custom_package_basename, + ) + custom_package_release = PackageReleaseFactory( + package=custom_package, + version='1.0.0', + ) + download_requirement_url = 'http://example.com/some_file.zip' + download_requirement = DownloadRequirementFactory( + url=download_requirement_url, + ) + pypi_requirement_name = 'some-pypi-package' + pypi_requirement = PyPiRequirementFactory( + name=pypi_requirement_name, + ) + vcs_requirement_url = 'git://git.some-project.org/SomeProject.git' + vcs_requirement = VersionControlRequirementFactory( + url=vcs_requirement_url, + ) + + mock_json_loads.return_value = [] + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'zip_file': ['Invalid requirements json file.']}, + ) + + group_type = 'invalid' + mock_json_loads.return_value = { + group_type: {}, + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid group name "{group_type}" found in requirements ' + f'json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: { + 'key': 'value', + }, + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid group values for "{group_type}" found in ' + f'requirements json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: [ + 'package', + ], + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid object found in "{group_type}" listing in ' + f'requirements json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: [ + {'key': 'value'}, + ], + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + 'No basename found for object in "custom" listing in ' + 'requirements json file.' + ], + }, + ) + + group_type = 'custom' + invalid_basename = 'invalid' + mock_json_loads.return_value = { + group_type: [ + {'basename': invalid_basename}, + ], + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Custom Package "{invalid_basename}" from requirements ' + f'json file not found.' + ], + }, + ) + + group_type = 'custom' + version = '1.0.1' + mock_json_loads.return_value = { + group_type: [ + { + 'basename': custom_package_slug, + 'version': version, + }, + ], + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Custom Package "{custom_package_slug}" version ' + f'"{version}", from requirements json file, not found.' + ], + }, + ) + + for group_type, required_field in { + 'download': 'url', + 'pypi': 'name', + 'vcs': 'url', + }.items(): + mock_json_loads.return_value = { + group_type: [ + { + 'key': 'value', + }, + ], + } + obj = PluginZipFile('') + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'No {required_field} found for object in ' + f'"{group_type}" listing in requirements json file.' + ], + }, + ) + + mock_json_loads.return_value = { + 'custom': [ + { + 'basename': custom_package_slug, + 'version': custom_package_release.version, + }, + ], + 'download': [ + { + 'url': download_requirement_url, + } + ], + 'pypi': [ + { + 'name': pypi_requirement_name, + } + ], + 'vcs': [ + { + 'url': vcs_requirement_url, + } + ], + } + obj = PluginZipFile('') + obj.validate_requirements() + self.assertDictEqual( + d1=obj.requirements, + d2={ + 'custom': [{ + 'package_requirement': custom_package, + 'version': custom_package_release.version, + 'optional': False, + }], + 'download': [{ + 'download_requirement': download_requirement, + 'optional': False, + }], + 'pypi': [{ + 'pypi_requirement': pypi_requirement, + 'version': None, + 'optional': False, + }], + 'vcs': [{ + 'vcs_requirement': vcs_requirement, + 'optional': False, + }], + } + ) + class HelperFunctionsTestCase(TestCase): def test_handle_plugin_zip_upload(self): diff --git a/project_manager/sub_plugins/tests/test_helpers.py b/project_manager/sub_plugins/tests/test_helpers.py index a334789f..30337bdc 100644 --- a/project_manager/sub_plugins/tests/test_helpers.py +++ b/project_manager/sub_plugins/tests/test_helpers.py @@ -6,6 +6,7 @@ from unittest import mock # Django +from django.conf import settings from django.core.exceptions import ValidationError from django.test import TestCase @@ -24,7 +25,13 @@ handle_sub_plugin_logo_upload, handle_sub_plugin_zip_upload, ) +from test_utils.factories.packages import PackageReleaseFactory, PackageFactory from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory +from test_utils.factories.requirements import ( + VersionControlRequirementFactory, + PyPiRequirementFactory, + DownloadRequirementFactory, +) from test_utils.factories.sub_plugins import ( SubPluginFactory, SubPluginImageFactory, @@ -36,7 +43,6 @@ # TEST CASES # ============================================================================= class SubPluginZipFileTestCase(TestCase): - # TODO: Add tests for SubPluginZipFile class base_path = plugin = sub_plugin_path = None @@ -56,9 +62,6 @@ def setUpTestData(cls): def setUp(self) -> None: super().setUp() - mock.patch( - target='project_manager.common.helpers.ZipFile', - ).start() self.mock_get_file_list = mock.patch( target='project_manager.common.helpers.ProjectZipFile.get_file_list', ).start() @@ -108,7 +111,10 @@ def test_file_types(self): d2=SUB_PLUGIN_ALLOWED_FILE_TYPES, ) - def test_find_base_info(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_find_base_info(self, _): sub_plugin_basename = 'test_sub_plugin' self.mock_get_file_list.return_value = self._get_file_list( sub_plugin_basename=sub_plugin_basename, @@ -136,7 +142,10 @@ def test_find_base_info(self): second='multiple', ) - def test_validate_base_file_in_zip(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_base_file_in_zip(self, _): sub_plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( sub_plugin_basename=sub_plugin_basename, @@ -203,7 +212,10 @@ def test_validate_base_file_in_zip(self): second='not-found', ) - def test_get_requirement_path(self): + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_get_requirement_paths(self, _): sub_plugin_basename = 'test_sub_plugin' self.mock_get_file_list.return_value = self._get_file_list( sub_plugin_basename=sub_plugin_basename, @@ -211,9 +223,12 @@ def test_get_requirement_path(self): obj = SubPluginZipFile('', self.plugin) obj.find_base_info() obj.validate_base_file_in_zip() - self.assertEqual( - first=obj.get_requirement_path(), - second=f'{PLUGIN_PATH}{self.plugin.basename}/{sub_plugin_basename}/requirements.json', + self.assertListEqual( + list1=obj.get_requirement_paths(), + list2=[ + f'{PLUGIN_PATH}{self.plugin.basename}/{self.sub_plugin_path.path}/' + f'{sub_plugin_basename}/requirements.json' + ], ) self.sub_plugin_path.allow_package_using_basename = False @@ -225,12 +240,18 @@ def test_get_requirement_path(self): obj = SubPluginZipFile('', self.plugin) obj.find_base_info() obj.validate_base_file_in_zip() - self.assertEqual( - first=obj.get_requirement_path(), - second=f'{PLUGIN_PATH}{self.plugin.basename}/{sub_plugin_basename}_requirements.json', - ) - - def test_validate_file_paths(self): + self.assertListEqual( + list1=obj.get_requirement_paths(), + list2=[ + f'{PLUGIN_PATH}{self.plugin.basename}/{self.sub_plugin_path.path}/' + f'{sub_plugin_basename}_requirements.json' + ], + ) + + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + def test_validate_file_paths(self, _): sub_plugin_basename = 'test_plugin' self.mock_get_file_list.return_value = self._get_file_list( sub_plugin_basename=sub_plugin_basename, @@ -294,6 +315,303 @@ def test_validate_file_paths(self): second=f'Invalid paths found in zip: {invalid_file}', ) + @mock.patch( + target='project_manager.sub_plugins.helpers.logger', + ) + def test_validate_requirements_file_failures(self, mock_logger): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' / 'test-plugin' + file_path = base_path / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' + self.mock_get_file_list.return_value = [] + plugin = PluginFactory( + basename='test_plugin', + ) + sub_plugin_path = SubPluginPathFactory( + plugin=plugin, + allow_package_using_init=True, + path='sub_plugins', + ) + obj = SubPluginZipFile( + zip_file=file_path, + plugin=plugin, + ) + obj.basename = 'invalid' + obj.paths = {sub_plugin_path} + obj.validate_requirements() + mock_logger.debug.assert_called_once_with('No requirement file found.') + + file_path = base_path / 'test-sub-plugin' / 'test-sub-plugin-invalid-v1.0.0.zip' + self.mock_get_file_list.return_value = [ + 'addons/source-python/plugins/test_plugin/sub_plugins/test_sub_plugin/requirements.json', + ] + obj = SubPluginZipFile( + zip_file=file_path, + plugin=plugin, + ) + obj.basename = 'test_sub_plugin' + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'zip_file': ['Requirements json file cannot be decoded.']}, + ) + + @mock.patch( + target='project_manager.common.helpers.json.loads', + ) + @mock.patch( + target='project_manager.common.helpers.ZipFile', + ) + @mock.patch( + target='project_manager.sub_plugins.helpers.ZipFile', + ) + def test_validate_requirements_file_item_failures(self, _, __, mock_json_loads): + plugin = PluginFactory() + custom_package_basename = 'test_custom_package' + custom_package_slug = custom_package_basename.replace('_', '-') + custom_package = PackageFactory( + basename=custom_package_basename, + ) + custom_package_release = PackageReleaseFactory( + package=custom_package, + version='1.0.0', + ) + download_requirement_url = 'http://example.com/some_file.zip' + download_requirement = DownloadRequirementFactory( + url=download_requirement_url, + ) + pypi_requirement_name = 'some-pypi-package' + pypi_requirement = PyPiRequirementFactory( + name=pypi_requirement_name, + ) + vcs_requirement_url = 'git://git.some-project.org/SomeProject.git' + vcs_requirement = VersionControlRequirementFactory( + url=vcs_requirement_url, + ) + + mock_json_loads.return_value = [] + obj = SubPluginZipFile('', plugin=plugin) + sub_plugin_path = SubPluginPathFactory( + plugin=plugin, + allow_package_using_init=True, + path='sub_plugins', + ) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'zip_file': ['Invalid requirements json file.']}, + ) + + group_type = 'invalid' + mock_json_loads.return_value = { + group_type: {}, + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid group name "{group_type}" found in requirements ' + f'json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: { + 'key': 'value', + }, + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid group values for "{group_type}" found in ' + f'requirements json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: [ + 'package', + ], + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Invalid object found in "{group_type}" listing in ' + f'requirements json file.' + ], + }, + ) + + group_type = 'custom' + mock_json_loads.return_value = { + group_type: [ + {'key': 'value'}, + ], + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + 'No basename found for object in "custom" listing in ' + 'requirements json file.' + ], + }, + ) + + group_type = 'custom' + invalid_basename = 'invalid' + mock_json_loads.return_value = { + group_type: [ + {'basename': invalid_basename}, + ], + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Custom Package "{invalid_basename}" from requirements ' + f'json file not found.' + ], + }, + ) + + group_type = 'custom' + version = '1.0.1' + mock_json_loads.return_value = { + group_type: [ + { + 'basename': custom_package_slug, + 'version': version, + }, + ], + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'Custom Package "{custom_package_slug}" version ' + f'"{version}", from requirements json file, not found.' + ], + }, + ) + + for group_type, required_field in { + 'download': 'url', + 'pypi': 'name', + 'vcs': 'url', + }.items(): + mock_json_loads.return_value = { + group_type: [ + { + 'key': 'value', + }, + ], + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + with self.assertRaises(ValidationError) as context: + obj.validate_requirements() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={ + 'zip_file': [ + f'No {required_field} found for object in ' + f'"{group_type}" listing in requirements json file.' + ], + }, + ) + + mock_json_loads.return_value = { + 'custom': [ + { + 'basename': custom_package_slug, + 'version': custom_package_release.version, + }, + ], + 'download': [ + { + 'url': download_requirement_url, + } + ], + 'pypi': [ + { + 'name': pypi_requirement_name, + } + ], + 'vcs': [ + { + 'url': vcs_requirement_url, + } + ], + } + obj = SubPluginZipFile('', plugin=plugin) + obj.paths = {sub_plugin_path} + obj.validate_requirements() + self.assertDictEqual( + d1=obj.requirements, + d2={ + 'custom': [{ + 'package_requirement': custom_package, + 'version': custom_package_release.version, + 'optional': False, + }], + 'download': [{ + 'download_requirement': download_requirement, + 'optional': False, + }], + 'pypi': [{ + 'pypi_requirement': pypi_requirement, + 'version': None, + 'optional': False, + }], + 'vcs': [{ + 'vcs_requirement': vcs_requirement, + 'optional': False, + }], + } + ) + class HelperFunctionsTestCase(TestCase): def test_handle_sub_plugin_zip_upload(self): From c6fd359ca2481efefea38deaf2be2a5e03b8cb1a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 12:02:50 -0500 Subject: [PATCH 085/211] Added more tests. --- project_manager/packages/tests/test_views.py | 70 ++++++------- project_manager/plugins/tests/test_views.py | 72 +++++++------- .../sub_plugins/api/tests/test_serializers.py | 9 -- .../sub_plugins/tests/test_views.py | 97 +++++++++---------- 4 files changed, 120 insertions(+), 128 deletions(-) diff --git a/project_manager/packages/tests/test_views.py b/project_manager/packages/tests/test_views.py index 00484b09..2ed2dfb3 100644 --- a/project_manager/packages/tests/test_views.py +++ b/project_manager/packages/tests/test_views.py @@ -22,12 +22,37 @@ # ============================================================================= # TEST CASES # ============================================================================= +@override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') class PackageReleaseDownloadViewTestCase(TestCase): + + basename = package = zip_file = None + + @classmethod + def setUpTestData(cls): + cls.basename = 'test_package' + cls.package = PackageFactory( + basename=cls.basename, + ) + version = '1.0.0' + cls.zip_file = f'{cls.package.slug}-v{version}.zip' + cls.release = PackageReleaseFactory( + package=cls.package, + version=version, + zip_file=cls.zip_file, + ) + cls.api_path = f'/media/{PACKAGE_RELEASE_URL}{cls.package.slug}/{cls.zip_file}' + def test_model_inheritance(self): self.assertTrue( expr=issubclass(PackageReleaseDownloadView, DownloadMixin), ) + def test__allowed_methods(self): + self.assertListEqual( + list1=PackageReleaseDownloadView()._allowed_methods(), + list2=['GET', 'OPTIONS'], + ) + def test_base_attributes(self): self.assertEqual( first=PackageReleaseDownloadView.model, @@ -49,60 +74,39 @@ def test_base_attributes(self): @mock.patch( target='project_manager.common.mixins.DownloadMixin.full_path', ) - @override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') def test_get_failure(self, mock_full_path): - basename = 'test_package' - package = PackageFactory( - basename=basename, - ) - version = '1.0.0' - zip_file = f'{package.slug}-v{version}.zip' - PackageReleaseFactory( - package=package, - version=version, - zip_file=zip_file, - ) mock_full_path.isfile.return_value = False - response = self.client.get( - path=f'/media/{PACKAGE_RELEASE_URL}{package.slug}/{zip_file}' - ) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_404_NOT_FOUND, ) mock_full_path.isfile.assert_called_once_with() - @override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') def test_get_success(self): - basename = 'test_package' - package = PackageFactory( - basename=basename, - ) - version = '1.0.0' - zip_file = f'{package.slug}-v{version}.zip' - release = PackageReleaseFactory( - package=package, - version=version, - zip_file=zip_file, - ) self.assertEqual( - first=PackageRelease.objects.get(pk=release.pk).download_count, + first=PackageRelease.objects.get(pk=self.release.pk).download_count, second=0, ) - response = self.client.get( - path=f'/media/{PACKAGE_RELEASE_URL}{package.slug}/{zip_file}' - ) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) self.assertIn( member=( - f'addons/source-python/packages/custom/{basename}/__init__.py' + f'addons/source-python/packages/custom/{self.basename}/__init__.py' ), container=str(response.content), ) self.assertEqual( - first=PackageRelease.objects.get(pk=release.pk).download_count, + first=PackageRelease.objects.get(pk=self.release.pk).download_count, second=1, ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) diff --git a/project_manager/plugins/tests/test_views.py b/project_manager/plugins/tests/test_views.py index e7e12049..043e0c60 100644 --- a/project_manager/plugins/tests/test_views.py +++ b/project_manager/plugins/tests/test_views.py @@ -22,12 +22,37 @@ # ============================================================================= # TEST CASES # ============================================================================= +@override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') class PluginReleaseDownloadViewTestCase(TestCase): + + basename = plugin = zip_file = None + + @classmethod + def setUpTestData(cls): + cls.basename = 'test_plugin' + cls.plugin = PluginFactory( + basename=cls.basename, + ) + version = '1.0.0' + cls.zip_file = f'{cls.plugin.slug}-v{version}.zip' + cls.release = PluginReleaseFactory( + plugin=cls.plugin, + version=version, + zip_file=cls.zip_file, + ) + cls.api_path = f'/media/{PLUGIN_RELEASE_URL}{cls.plugin.slug}/{cls.zip_file}' + def test_model_inheritance(self): self.assertTrue( expr=issubclass(PluginReleaseDownloadView, DownloadMixin), ) + def test__allowed_methods(self): + self.assertListEqual( + list1=PluginReleaseDownloadView()._allowed_methods(), + list2=['GET', 'OPTIONS'], + ) + def test_base_attributes(self): self.assertEqual( first=PluginReleaseDownloadView.model, @@ -49,66 +74,45 @@ def test_base_attributes(self): @mock.patch( target='project_manager.common.mixins.DownloadMixin.full_path', ) - @override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') def test_get_failure(self, mock_full_path): - basename = 'test_plugin' - plugin = PluginFactory( - basename=basename, - ) - version = '1.0.0' - zip_file = f'{plugin.slug}-v{version}.zip' - PluginReleaseFactory( - plugin=plugin, - version=version, - zip_file=zip_file, - ) mock_full_path.isfile.return_value = False - response = self.client.get( - path=f'/media/{PLUGIN_RELEASE_URL}{plugin.slug}/{zip_file}' - ) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_404_NOT_FOUND, ) mock_full_path.isfile.assert_called_once_with() - @override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') def test_get_success(self): - basename = 'test_plugin' - plugin = PluginFactory( - basename=basename, - ) - version = '1.0.0' - zip_file = f'{plugin.slug}-v{version}.zip' - release = PluginReleaseFactory( - plugin=plugin, - version=version, - zip_file=zip_file, - ) self.assertEqual( - first=PluginRelease.objects.get(pk=release.pk).download_count, + first=PluginRelease.objects.get(pk=self.release.pk).download_count, second=0, ) - response = self.client.get( - path=f'/media/{PLUGIN_RELEASE_URL}{plugin.slug}/{zip_file}' - ) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) self.assertIn( member=( - f'addons/source-python/plugins/{basename}/{basename}.py' + f'addons/source-python/plugins/{self.basename}/{self.basename}.py' ), container=str(response.content), ) self.assertIn( member=( - f'addons/source-python/plugins/{basename}/__init__.py' + f'addons/source-python/plugins/{self.basename}/__init__.py' ), container=str(response.content), ) self.assertEqual( - first=PluginRelease.objects.get(pk=release.pk).download_count, + first=PluginRelease.objects.get(pk=self.release.pk).download_count, second=1, ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) diff --git a/project_manager/sub_plugins/api/tests/test_serializers.py b/project_manager/sub_plugins/api/tests/test_serializers.py index ac8a2e83..495ed51e 100644 --- a/project_manager/sub_plugins/api/tests/test_serializers.py +++ b/project_manager/sub_plugins/api/tests/test_serializers.py @@ -597,12 +597,3 @@ def test_get_project_kwargs(self): 'plugin': plugin, }, ) - -""" - def get_project_kwargs(self): - kwargs = self.context['view'].kwargs - return { - 'slug': kwargs.get('sub_plugin_slug'), - 'plugin': self.parent_project, - } -""" \ No newline at end of file diff --git a/project_manager/sub_plugins/tests/test_views.py b/project_manager/sub_plugins/tests/test_views.py index bf343d90..e7848aee 100644 --- a/project_manager/sub_plugins/tests/test_views.py +++ b/project_manager/sub_plugins/tests/test_views.py @@ -27,12 +27,42 @@ # ============================================================================= # TEST CASES # ============================================================================= +@override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') class SubPluginReleaseDownloadViewTestCase(TestCase): + + basename = plugin_basename = sub_plugin = zip_file = None + + @classmethod + def setUpTestData(cls): + cls.plugin_basename = 'test_plugin' + plugin = PluginFactory( + basename=cls.plugin_basename, + ) + cls.basename = 'test_sub_plugin' + cls.sub_plugin = SubPluginFactory( + plugin=plugin, + basename=cls.basename, + ) + version = '1.0.0' + cls.zip_file = f'{cls.sub_plugin.slug}-v{version}.zip' + cls.release = SubPluginReleaseFactory( + sub_plugin=cls.sub_plugin, + version=version, + zip_file=cls.zip_file, + ) + cls.api_path = f'/media/{SUB_PLUGIN_RELEASE_URL}{plugin.slug}/{cls.sub_plugin.slug}/{cls.zip_file}' + def test_model_inheritance(self): self.assertTrue( expr=issubclass(SubPluginReleaseDownloadView, DownloadMixin), ) + def test__allowed_methods(self): + self.assertListEqual( + list1=SubPluginReleaseDownloadView()._allowed_methods(), + list2=['GET', 'OPTIONS'], + ) + def test_base_attributes(self): self.assertEqual( first=SubPluginReleaseDownloadView.model, @@ -54,84 +84,47 @@ def test_base_attributes(self): @mock.patch( target='project_manager.common.mixins.DownloadMixin.full_path', ) - @override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') def test_get_failure(self, mock_full_path): - plugin_basename = 'test_plugin' - plugin = PluginFactory( - basename=plugin_basename, - ) - basename = 'test_sub_plugin' - sub_plugin = SubPluginFactory( - plugin=plugin, - basename=basename, - ) - version = '1.0.0' - zip_file = f'{sub_plugin.slug}-v{version}.zip' - SubPluginReleaseFactory( - sub_plugin=sub_plugin, - version=version, - zip_file=zip_file, - ) mock_full_path.isfile.return_value = False - response = self.client.get( - path=( - f'/media/{SUB_PLUGIN_RELEASE_URL}{plugin.slug}/' - f'{sub_plugin.slug}/{zip_file}' - ), - ) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_404_NOT_FOUND, ) mock_full_path.isfile.assert_called_once_with() - @override_settings(MEDIA_ROOT=settings.BASE_DIR / 'fixtures') def test_get_success(self): - plugin_basename = 'test_plugin' - plugin = PluginFactory( - basename=plugin_basename, - ) - basename = 'test_sub_plugin' - sub_plugin = SubPluginFactory( - plugin=plugin, - basename=basename, - ) - version = '1.0.0' - zip_file = f'{sub_plugin.slug}-v{version}.zip' - release = SubPluginReleaseFactory( - sub_plugin=sub_plugin, - version=version, - zip_file=zip_file, - ) self.assertEqual( - first=SubPluginRelease.objects.get(pk=release.pk).download_count, + first=SubPluginRelease.objects.get(pk=self.release.pk).download_count, second=0, ) - response = self.client.get( - path=( - f'/media/{SUB_PLUGIN_RELEASE_URL}{plugin.slug}/' - f'{sub_plugin.slug}/{zip_file}' - ), - ) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) self.assertIn( member=( - f'addons/source-python/plugins/{plugin_basename}/sub_plugins/' - f'{basename}/__init__.py' + f'addons/source-python/plugins/{self.plugin_basename}/sub_plugins/' + f'{self.basename}/__init__.py' ), container=str(response.content), ) self.assertIn( member=( - f'addons/source-python/plugins/{plugin_basename}/sub_plugins/' - f'{basename}/{basename}.py' + f'addons/source-python/plugins/{self.plugin_basename}/sub_plugins/' + f'{self.basename}/{self.basename}.py' ), container=str(response.content), ) self.assertEqual( - first=SubPluginRelease.objects.get(pk=release.pk).download_count, + first=SubPluginRelease.objects.get(pk=self.release.pk).download_count, second=1, ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) From 51d6ed088d63172cc6aef1e5313505670e81040f Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 12:03:26 -0500 Subject: [PATCH 086/211] Added remaining serializer tests. --- .../common/api/tests/test_serializers.py | 451 ++++++++++++++++++ 1 file changed, 451 insertions(+) diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/common/api/tests/test_serializers.py index 957aac5d..aa1b4c3d 100644 --- a/project_manager/common/api/tests/test_serializers.py +++ b/project_manager/common/api/tests/test_serializers.py @@ -10,15 +10,40 @@ from django.utils.timezone import now # Third Party Django +from rest_framework.fields import ( + CharField, + FileField, + IntegerField, + SerializerMethodField, +) from rest_framework.serializers import ModelSerializer # App +from games.api.serializers import GameSerializer +from games.constants import GAME_SLUG_MAX_LENGTH +from project_manager.common.api.serializers import ( + ProjectContributorSerializer, + ProjectCreateReleaseSerializer, + ProjectGameSerializer, + ProjectImageSerializer, + ProjectReleaseSerializer, + ProjectSerializer, + ProjectTagSerializer, +) from project_manager.common.api.serializers.mixins import ( AddProjectToViewMixin, ProjectLocaleMixin, + ProjectReleaseCreationMixin, ProjectThroughMixin, ) +from project_manager.common.constants import ( + RELEASE_NOTES_MAX_LENGTH, + RELEASE_VERSION_MAX_LENGTH, +) +from tags.constants import TAG_NAME_MAX_LENGTH from test_utils.factories.users import ForumUserFactory +from users.api.serializers.common import ForumUserContributorSerializer +from users.constants import USER_USERNAME_MAX_LENGTH # ============================================================================= @@ -157,6 +182,65 @@ def test_get_field_names_contributor_owner_only(self): ) +class ProjectReleaseCreationMixinTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectReleaseCreationMixin, ModelSerializer), + ) + + def test_project_class_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectReleaseCreationMixin.project_class.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_class" attribute.' + ), + ) + + def test_project_type_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectReleaseCreationMixin.project_type.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_type" attribute.' + ), + ) + + def test_zip_parser_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectReleaseCreationMixin.zip_parser.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"zip_parser" attribute.' + ), + ) + + def test_get_project_kwargs_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectReleaseCreationMixin.get_project_kwargs(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"get_project_kwargs" method.' + ), + ) + + class AddProjectToViewMixinTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( @@ -185,3 +269,370 @@ def test_validate(self): d1=obj.validate(original_attrs), d2=return_attrs, ) + + +class ProjectContributorSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectContributorSerializer, ProjectThroughMixin), + ) + self.assertTrue( + expr=issubclass(ProjectContributorSerializer, AddProjectToViewMixin), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectContributorSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=2, + ) + + self.assertIn( + member='username', + container=declared_fields, + ) + field = declared_fields['username'] + self.assertIsInstance( + obj=field, + cls=CharField, + ) + self.assertEqual( + first=field.max_length, + second=USER_USERNAME_MAX_LENGTH, + ) + self.assertTrue(expr=field.write_only) + + self.assertIn( + member='user', + container=declared_fields, + ) + field = declared_fields['user'] + self.assertIsInstance( + obj=field, + cls=ForumUserContributorSerializer, + ) + self.assertTrue(expr=field.read_only) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectContributorSerializer.Meta.fields, + tuple2=('username', 'user'), + ) + + +class ProjectCreateReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectCreateReleaseSerializer, ProjectReleaseCreationMixin), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectCreateReleaseSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=3, + ) + + self.assertIn( + member='notes', + container=declared_fields, + ) + field = declared_fields['notes'] + self.assertIsInstance( + obj=field, + cls=CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_NOTES_MAX_LENGTH, + ) + self.assertTrue(expr=field.allow_blank) + + self.assertIn( + member='version', + container=declared_fields, + ) + field = declared_fields['version'] + self.assertIsInstance( + obj=field, + cls=CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertTrue(expr=field.allow_blank) + + self.assertIn( + member='zip_file', + container=declared_fields, + ) + field = declared_fields['zip_file'] + self.assertIsInstance( + obj=field, + cls=FileField, + ) + self.assertTrue(expr=field.allow_null) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectCreateReleaseSerializer.Meta.fields, + tuple2=('notes', 'zip_file', 'version'), + ) + + +class ProjectGameSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectGameSerializer, ProjectThroughMixin), + ) + self.assertTrue( + expr=issubclass(ProjectGameSerializer, AddProjectToViewMixin), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectGameSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=2, + ) + + self.assertIn( + member='game_slug', + container=declared_fields, + ) + field = declared_fields['game_slug'] + self.assertIsInstance( + obj=field, + cls=CharField, + ) + self.assertEqual( + first=field.max_length, + second=GAME_SLUG_MAX_LENGTH, + ) + self.assertTrue(expr=field.write_only) + + self.assertIn( + member='game', + container=declared_fields, + ) + field = declared_fields['game'] + self.assertIsInstance( + obj=field, + cls=GameSerializer, + ) + self.assertTrue(expr=field.read_only) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectGameSerializer.Meta.fields, + tuple2=('game_slug', 'game'), + ) + + +class ProjectImageSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectImageSerializer, ProjectThroughMixin), + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectImageSerializer.Meta.fields, + tuple2=('image',), + ) + + +class ProjectReleaseSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectReleaseSerializer, ProjectReleaseCreationMixin), + ) + self.assertTrue( + expr=issubclass(ProjectReleaseSerializer, ProjectLocaleMixin), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectReleaseSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=2, + ) + + self.assertIn( + member='created', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['created'], + cls=SerializerMethodField, + ) + + self.assertIn( + member='download_count', + container=declared_fields, + ) + field = declared_fields['download_count'] + self.assertIsInstance( + obj=field, + cls=IntegerField, + ) + self.assertTrue(expr=field.read_only) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectReleaseSerializer.Meta.fields, + tuple2=( + 'notes', + 'zip_file', + 'version', + 'created', + 'download_count', + 'download_requirements', + 'package_requirements', + 'pypi_requirements', + 'vcs_requirements', + 'id', + ), + ) + + +class ProjectSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectSerializer, ModelSerializer), + ) + self.assertTrue( + expr=issubclass(ProjectSerializer, ProjectLocaleMixin), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=4, + ) + + self.assertIn( + member='current_release', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['current_release'], + cls=SerializerMethodField, + ) + + self.assertIn( + member='owner', + container=declared_fields, + ) + field = declared_fields['owner'] + self.assertIsInstance( + obj=field, + cls=ForumUserContributorSerializer, + ) + self.assertTrue(expr=field.read_only) + + self.assertIn( + member='created', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['created'], + cls=SerializerMethodField, + ) + + self.assertIn( + member='updated', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['updated'], + cls=SerializerMethodField, + ) + + def test_project_type_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectSerializer.project_type.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_type" attribute.' + ), + ) + + def test_release_model_required(self): + obj = '' + with self.assertRaises(NotImplementedError) as context: + ProjectSerializer.release_model.fget(obj) + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"release_model" attribute.' + ), + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'total_downloads', + 'current_release', + 'created', + 'updated', + 'synopsis', + 'description', + 'configuration', + 'logo', + 'video', + 'owner', + ), + ) + self.assertTupleEqual( + tuple1=ProjectSerializer.Meta.read_only_fields, + tuple2=('slug',), + ) + + +class ProjectTagSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectTagSerializer, ProjectThroughMixin), + ) + self.assertTrue( + expr=issubclass(ProjectTagSerializer, AddProjectToViewMixin), + ) + + def test_declared_fields(self): + declared_fields = getattr(ProjectTagSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=1, + ) + + self.assertIn( + member='tag', + container=declared_fields, + ) + field = declared_fields['tag'] + self.assertIsInstance( + obj=field, + cls=CharField, + ) + self.assertEqual( + first=field.max_length, + second=TAG_NAME_MAX_LENGTH, + ) + + def test_meta_class(self): + self.assertTupleEqual( + tuple1=ProjectTagSerializer.Meta.fields, + tuple2=('tag',), + ) From a4d1f5c402fe4cfd99860d1dd83030a34148723a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 12:04:02 -0500 Subject: [PATCH 087/211] Added remaining common view tests. --- .../common/api/tests/test_views.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/project_manager/common/api/tests/test_views.py b/project_manager/common/api/tests/test_views.py index 4492f4d8..21f09f43 100644 --- a/project_manager/common/api/tests/test_views.py +++ b/project_manager/common/api/tests/test_views.py @@ -32,6 +32,62 @@ # ============================================================================= # TEST CASES # ============================================================================= +class ProjectRelatedInfoMixinTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectRelatedInfoMixin, ModelViewSet)) + + def test_primary_attributes(self): + self.assertTupleEqual( + tuple1=ProjectRelatedInfoMixin.filter_backends, + tuple2=(OrderingFilter, DjangoFilterBackend), + ) + + def test_project_type_required(self): + obj = ProjectRelatedInfoMixin() + with self.assertRaises(NotImplementedError) as context: + _ = obj.project_type + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_type" attribute.' + ), + ) + + def test_project_model_required(self): + obj = ProjectRelatedInfoMixin() + with self.assertRaises(NotImplementedError) as context: + _ = obj.project_model + + self.assertEqual( + first=str(context.exception), + second=( + f'Class "{obj.__class__.__name__}" must implement a ' + f'"project_model" attribute.' + ), + ) + + +class ProjectThroughModelMixinTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectThroughModelMixin, ModelViewSet)) + + def test_primary_attributes(self): + self.assertTupleEqual( + tuple1=ProjectThroughModelMixin.authentication_classes, + tuple2=(SessionAuthentication,), + ) + self.assertTupleEqual( + tuple1=ProjectThroughModelMixin.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + self.assertTupleEqual( + tuple1=ProjectThroughModelMixin.permission_classes, + tuple2=(IsAuthenticatedOrReadOnly,), + ) + + class ProjectAPIViewTestCase(TestCase): def test_class_inheritance(self): self.assertTrue(expr=issubclass(ProjectAPIView, APIView)) From 6d0815399894a3e1d9e20a92ac636da8d6a8e8d0 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 14:34:29 -0500 Subject: [PATCH 088/211] Added more tests for project API views. --- .../packages/api/tests/test_views.py | 2274 +++++++++++++- .../plugins/api/tests/test_views.py | 2661 ++++++++++++++++- .../sub_plugins/api/tests/test_views.py | 2084 ++++++++++++- 3 files changed, 6715 insertions(+), 304 deletions(-) diff --git a/project_manager/packages/api/tests/test_views.py b/project_manager/packages/api/tests/test_views.py index 085e65c9..9700656a 100644 --- a/project_manager/packages/api/tests/test_views.py +++ b/project_manager/packages/api/tests/test_views.py @@ -1,10 +1,21 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +import shutil +import tempfile + # Django -from django.test import TestCase +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path # Third Party Django +from PIL import Image from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APITestCase @@ -50,12 +61,26 @@ PackageReleaseVersionControlRequirement, PackageTag, ) +from test_utils.factories.games import GameFactory +from test_utils.factories.packages import ( + PackageFactory, + PackageContributorFactory, + PackageGameFactory, + PackageImageFactory, + PackageReleaseFactory, + PackageTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory # ============================================================================= # TEST CASES # ============================================================================= class PackageAPIViewTestCase(APITestCase): + + api_path = '/api/packages/' + def test_inheritance(self): self.assertTrue(expr=issubclass(PackageAPIView, ProjectAPIView)) @@ -65,8 +90,14 @@ def test_base_attributes(self): second='package', ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=ProjectAPIView.http_method_names, + tuple2=('get', 'options'), + ) + def test_get(self): - response = self.client.get(path='/api/packages/') + response = self.client.get(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) base_path = reverse( viewname=f'api:packages:endpoints', @@ -84,8 +115,32 @@ def test_get(self): } ) + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual(first=response.json()['name'], second='Package APIs') + + +class PackageContributorViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/contributors/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + cls.package_contributor = PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.new_contributor = ForumUserFactory() + cls.regular_user = ForumUserFactory() -class PackageContributorViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue( expr=issubclass( @@ -116,222 +171,2179 @@ def test_base_attributes(self): d2={'user': {'user': {}}, 'package': {}} ) - -class PackageGameViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PackageGameViewSet, ProjectGameViewSet), + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageContributorViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), ) - def test_base_attributes(self): + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageGameViewSet.serializer_class, - second=PackageGameSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertEqual( - first=PackageGameViewSet.project_type, - second='package', + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageGameViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageGameViewSet.queryset.model, - expr2=PackageGame, + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor self.assertDictEqual( - d1=PackageGameViewSet.queryset.query.select_related, - d2={'game': {}, 'package': {}} - ) - - -class PackageImageViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PackageImageViewSet, ProjectImageViewSet), + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, ) - def test_base_attributes(self): + # Verify that contributors can see results but not 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageImageViewSet.serializer_class, - second=PackageImageSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertEqual( - first=PackageImageViewSet.project_type, - second='package', + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageImageViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageImageViewSet.queryset.model, - expr2=PackageImage, + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual(first=content['count'], second=1) self.assertDictEqual( - d1=PackageImageViewSet.queryset.query.select_related, - d2={'package': {}}, + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + 'id': str(self.package_contributor.id), + }, ) - -class PackageReleaseViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PackageReleaseViewSet, ProjectReleaseViewSet), + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_contributor.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - def test_base_attributes(self): + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) self.assertEqual( - first=PackageReleaseViewSet.serializer_class, - second=PackageReleaseSerializer, + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) + + # Verify that contributors cannot see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) self.assertEqual( - first=PackageReleaseViewSet.project_type, - second='package', + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) self.assertEqual( - first=PackageReleaseViewSet.project_model, - second=Package, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertIs( - expr1=PackageReleaseViewSet.queryset.model, - expr2=PackageRelease, + self.assertDictEqual( + d1=response.json(), + d2={ + 'user': { + 'forum_id': self.package_contributor.user.forum_id, + 'username': self.package_contributor.user.user.username, + }, + 'id': str(self.package_contributor.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, ) self.assertDictEqual( - d1=PackageReleaseViewSet.queryset.query.select_related, - d2={'package': {}}, + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, ) - prefetch_lookups = PackageReleaseViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=4) - lookup = prefetch_lookups[0] + def test_post(self): + # Verify that non logged in user cannot add a contributor + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) self.assertEqual( - first=lookup.prefetch_to, - second='packagereleasepackagerequirement_set', + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleasePackageRequirement, + + # Verify that regular user cannot add a contributor + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('package_requirement__name',), + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot add a contributor + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, ) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'package_requirement': {}}, + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - lookup = prefetch_lookups[1] + # Verify that owner can add a contributor + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) self.assertEqual( - first=lookup.prefetch_to, - second='packagereleasedownloadrequirement_set', + first=response.status_code, + second=status.HTTP_201_CREATED, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleaseDownloadRequirement, + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing contributor cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.contributor.user.username}, ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('download_requirement__url',), + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, + ) + + # Verify owner cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.owner.user.username}, ) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'download_requirement': {}}, + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, ) - lookup = prefetch_lookups[2] + # Verify unknown username cannot be added + invalid_username = 'invalid' + response = self.client.post( + path=self.api_path, + data={'username': invalid_username}, + ) self.assertEqual( - first=lookup.prefetch_to, - second='packagereleasepypirequirement_set', + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleasePyPiRequirement, + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'No user named "{invalid_username}".']}, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a contributor + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('pypi_requirement__name',), + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', ) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'pypi_requirement': {}}, + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - lookup = prefetch_lookups[3] + # Verify that contributor cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', + ) self.assertEqual( - first=lookup.prefetch_to, - second='packagereleaseversioncontrolrequirement_set', + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleaseVersionControlRequirement, + + # Verify that owner can delete a contributor + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('vcs_requirement__url',), + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'vcs_requirement': {}}, + first=response.json()['name'], + second=f'{self.package} - Contributor', ) -class PackageTagViewSetTestCase(TestCase): +class PackageGameViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = game_1 = game_2 = package = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/games/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.game_1 = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + cls.game_2 = GameFactory( + name='Game2', + basename='game2', + icon='icon2.jpg', + ) + cls.game_3 = GameFactory( + name='Game3', + basename='game3', + icon='icon3.jpg', + ) + cls.game_4 = GameFactory( + name='Game4', + basename='game4', + icon='icon4.jpg', + ) + cls.package_game_1 = PackageGameFactory( + package=cls.package, + game=cls.game_1, + ) + cls.package_game_2 = PackageGameFactory( + package=cls.package, + game=cls.game_2, + ) + cls.regular_user = ForumUserFactory() + def test_inheritance(self): - self.assertTrue(expr=issubclass(PackageTagViewSet, ProjectTagViewSet)) + self.assertTrue( + expr=issubclass(PackageGameViewSet, ProjectGameViewSet), + ) def test_base_attributes(self): self.assertEqual( - first=PackageTagViewSet.serializer_class, - second=PackageTagSerializer, + first=PackageGameViewSet.serializer_class, + second=PackageGameSerializer, ) self.assertEqual( - first=PackageTagViewSet.project_type, + first=PackageGameViewSet.project_type, second='package', ) self.assertEqual( - first=PackageTagViewSet.project_model, + first=PackageGameViewSet.project_model, second=Package, ) self.assertIs( - expr1=PackageTagViewSet.queryset.model, - expr2=PackageTag, + expr1=PackageGameViewSet.queryset.model, + expr2=PackageGame, ) self.assertDictEqual( - d1=PackageTagViewSet.queryset.query.select_related, - d2={'tag': {}, 'package': {}} + d1=PackageGameViewSet.queryset.query.select_related, + d2={'game': {}, 'package': {}} ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageGameViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) -class PackageViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue(expr=issubclass(PackageViewSet, ProjectViewSet)) + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) - def test_base_attributes(self): + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageViewSet.filterset_class, - second=PackageFilterSet, + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageViewSet.serializer_class, - second=PackageSerializer, + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.package_game_2.id), + }, ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PackageViewSet.creation_serializer_class, - second=PackageCreateSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertIs(expr1=PackageViewSet.queryset.model, expr2=Package) - prefetch_lookups = PackageViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=1) - lookup = prefetch_lookups[0] - self.assertEqual(first=lookup.prefetch_to, second='releases') + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.package_game_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_game_1.id}/' + response = self.client.get(path=api_path) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('-created',), + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, ) self.assertDictEqual( - d1=PackageViewSet.queryset.query.select_related, - d2={'owner': {'user': {}}}, + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.package_game_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.package_game_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a game + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a game + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a game + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a game + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_4.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated game cannot be added + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_1.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Game already linked to {PackageGameViewSet.project_type}.']} + ) + + # Verify non-existing game cannot be added + invalid_slug = 'invalid' + response = self.client.post( + path=self.api_path, + data={'game_slug': invalid_slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Invalid game "{invalid_slug}".']} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a game + response = self.client.delete( + path=self.api_path + f'{self.package_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a game + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.package_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a game + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a game + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_game_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Game', + ) + + +class PackageImageViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/images/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.package_image_1 = PackageImageFactory( + package=cls.package, + ) + cls.package_image_2 = PackageImageFactory( + package=cls.package, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageImageViewSet.serializer_class, + second=PackageImageSerializer, + ) + self.assertEqual( + first=PackageImageViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageImageViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageImageViewSet.queryset.model, + expr2=PackageImage, + ) + self.assertDictEqual( + d1=PackageImageViewSet.queryset.query.select_related, + d2={'package': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageImageViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.package_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.package_image_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.package_image_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_image_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.package_image_1.image.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.package_image_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.package_image_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify that regular user cannot add an image + self.client.force_login(self.regular_user.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add an image + self.client.force_login(self.contributor.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add an image + self.client.force_login(self.owner.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_delete(self): + # Verify that regular user cannot delete an image + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.package_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete an image + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete an image + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_image_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Image', + ) + + +class PackageReleaseViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/releases/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.package_release = PackageReleaseFactory( + package=cls.package, + zip_file='release_v1.0.0.zip', + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageReleaseViewSet.serializer_class, + second=PackageReleaseSerializer, + ) + self.assertEqual( + first=PackageReleaseViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageReleaseViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageReleaseViewSet.queryset.model, + expr2=PackageRelease, + ) + self.assertDictEqual( + d1=PackageReleaseViewSet.queryset.query.select_related, + d2={'package': {}}, + ) + prefetch_lookups = PackageReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + timestamp = self.package_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' + payload = { + 'notes': self.package_release.notes, + 'zip_file': zip_file, + 'version': self.package_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.package_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.package_release.id), + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.package_release.version}/' + response = self.client.get(path=api_path) + timestamp = self.package_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' + payload = { + 'notes': self.package_release.notes, + 'zip_file': zip_file, + 'version': self.package_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.package_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.package_release.id), + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + package = PackageFactory( + basename='test_package', + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + PackageContributorFactory( + package=package, + user=self.contributor, + ) + api_path = f'{self.base_api_path}{package.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' + + # Verify that non logged in user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.contributor.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=package.releases.count(), + second=2, + ) + content = response.json() + release = package.releases.get(id=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that owner can create a release + version = '1.0.2' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.owner.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=package.releases.count(), + second=3, + ) + content = response.json() + release = package.releases.get(id=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that the same version cannot be created twice + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'version': ['Given version matches existing version.']}, + ) + + # Verify that the basename in the zip file is being verified against + # the basename from the url path + zip_basename = package.basename + package = PackageFactory( + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{package.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'zip_file': [ + f"Basename in zip '{zip_basename}' does not match basename" + f" for package '{package.basename}'.", + ], + } + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Release', + ) + + +class PackageTagViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/tags/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.package_tag_1 = PackageTagFactory( + package=cls.package, + ) + cls.package_tag_2 = PackageTagFactory( + package=cls.package, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageTagViewSet.serializer_class, + second=PackageTagSerializer, + ) + self.assertEqual( + first=PackageTagViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageTagViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageTagViewSet.queryset.model, + expr2=PackageTag, + ) + self.assertDictEqual( + d1=PackageTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'package': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageTagViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + 'id': str(self.package_tag_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + 'id': str(self.package_tag_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_tag_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.package_tag_1.tag.name, + 'id': str(self.package_tag_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.package_tag_1.tag.name, + 'id': str(self.package_tag_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a tag + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a tag + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a tag + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a tag + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-2'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated tag cannot be added + response = self.client.post( + path=self.api_path, + data={'tag': self.package_tag_1.tag}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f'Tag already linked to {PackageTagViewSet.project_type}.']} + ) + + # Verify black-listed tag cannot be added + tag = TagFactory( + black_listed=True, + ) + response = self.client.post( + path=self.api_path, + data={'tag': tag.name}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a tag + response = self.client.delete( + path=self.api_path + f'{self.package_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a tag + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.package_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a tag + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a tag + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_tag_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Tag', + ) + + +class PackageViewSetTestCase(APITestCase): + + contributor = owner = package = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + logo='logo.jpg', + ) + cls.package_release = PackageReleaseFactory( + package=cls.package, + zip_file='/media/release_v1.0.0.zip', + ) + cls.api_path = f'/api/packages/projects/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageViewSet.filterset_class, + second=PackageFilterSet, + ) + self.assertEqual( + first=PackageViewSet.serializer_class, + second=PackageSerializer, + ) + self.assertEqual( + first=PackageViewSet.creation_serializer_class, + second=PackageCreateSerializer, + ) + self.assertIs(expr1=PackageViewSet.queryset.model, expr2=Package) + prefetch_lookups = PackageViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=PackageViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.package_release.get_absolute_url()}' + logo = f'{domain}{self.package.logo.url}' + created_timestamp = self.package.created + updated_timestamp = self.package.updated + payload = { + 'name': self.package.name, + 'slug': self.package.slug, + 'total_downloads': self.package.total_downloads, + 'current_release': { + 'version': self.package_release.version, + 'notes': self.package_release.notes, + 'zip_file': zip_file, + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.package.synopsis, + 'description': self.package.description, + 'configuration': self.package.configuration, + 'logo': logo, + 'video': self.package.video, + 'owner': { + 'forum_id': self.package.owner.forum_id, + 'username': self.package.owner.user.username, + } + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_list_filters(self): + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate tag filtering + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + tag = TagFactory(name='test_tag') + PackageTagFactory( + package=self.package, + tag=tag, + ) + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + game = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + PackageGameFactory( + package=self.package, + game=game, + ) + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get( + path=f'{self.api_path}?user={self.regular_user.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.contributor.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.owner.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.package.slug}/' + response = self.client.get(path=api_path) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.package_release.get_absolute_url()}' + logo = f'{domain}{self.package.logo.url}' + created_timestamp = self.package.created + updated_timestamp = self.package.updated + payload = { + 'name': self.package.name, + 'slug': self.package.slug, + 'total_downloads': self.package.total_downloads, + 'current_release': { + 'version': self.package_release.version, + 'notes': self.package_release.notes, + 'zip_file': zip_file, + 'package_requirements': [], + 'pypi_requirements': [], + 'version_control_requirements': [], + 'download_requirements': [], + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.package.synopsis, + 'description': self.package.description, + 'configuration': self.package.configuration, + 'logo': logo, + 'video': self.package.video, + 'owner': { + 'forum_id': self.package.owner.forum_id, + 'username': self.package.owner.user.username, + } + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify non logged in user cannot create a package + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' + version = '1.0.0' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that a logged in user can create a package + self.assertEqual( + first=Package.objects.count(), + second=1, + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=Package.objects.count(), + second=2, + ) + content = response.json() + package = Package.objects.get(slug=content['slug']) + self.assertEqual( + first=package.releases.count(), + second=1, + ) + release = package.releases.get() + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify cannot create a package where the basename already exists + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'basename': 'Package already exists. Cannot create.'} + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.package.slug}/' + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'New Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second='Package List', ) diff --git a/project_manager/plugins/api/tests/test_views.py b/project_manager/plugins/api/tests/test_views.py index 7adb319c..cfb6d3c4 100644 --- a/project_manager/plugins/api/tests/test_views.py +++ b/project_manager/plugins/api/tests/test_views.py @@ -1,10 +1,21 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +import shutil +import tempfile + # Django -from django.test import TestCase +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path # Third Party Django +from PIL import Image from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APITestCase @@ -54,12 +65,27 @@ PluginTag, SubPluginPath, ) +from test_utils.factories.games import GameFactory +from test_utils.factories.plugins import ( + PluginContributorFactory, + PluginFactory, + PluginGameFactory, + PluginImageFactory, + PluginReleaseFactory, + PluginTagFactory, + SubPluginPathFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory # ============================================================================= # TEST CASES # ============================================================================= class PluginAPIViewTestCase(APITestCase): + + api_path = '/api/plugins/' + def test_inheritance(self): self.assertTrue(expr=issubclass(PluginAPIView, ProjectAPIView)) @@ -69,8 +95,14 @@ def test_base_attributes(self): second='plugin', ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginAPIView.http_method_names, + tuple2=('get', 'options'), + ) + def test_get(self): - response = self.client.get(path='/api/plugins/') + response = self.client.get(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) base_path = reverse( viewname=f'api:plugins:endpoints', @@ -89,8 +121,32 @@ def test_get(self): } ) + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual(first=response.json()['name'], second='Plugin APIs') + + +class PluginContributorViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/contributors/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + cls.plugin_contributor = PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.new_contributor = ForumUserFactory() + cls.regular_user = ForumUserFactory() -class PluginContributorViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue( expr=issubclass( @@ -121,259 +177,2238 @@ def test_base_attributes(self): d2={'user': {'user': {}}, 'plugin': {}} ) - -class PluginGameViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PluginGameViewSet, ProjectGameViewSet), + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginContributorViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), ) - def test_base_attributes(self): + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginGameViewSet.serializer_class, - second=PluginGameSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertEqual( - first=PluginGameViewSet.project_type, - second='plugin', + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginGameViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginGameViewSet.queryset.model, - expr2=PluginGame, + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor self.assertDictEqual( - d1=PluginGameViewSet.queryset.query.select_related, - d2={'game': {}, 'plugin': {}} - ) - - -class PluginImageViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PluginImageViewSet, ProjectImageViewSet), + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, ) - def test_base_attributes(self): + # Verify that contributors can see results but not 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginImageViewSet.serializer_class, - second=PluginImageSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertEqual( - first=PluginImageViewSet.project_type, - second='plugin', + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginImageViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginImageViewSet.queryset.model, - expr2=PluginImage, + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual(first=content['count'], second=1) self.assertDictEqual( - d1=PluginImageViewSet.queryset.query.select_related, - d2={'plugin': {}}, + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + 'id': str(self.plugin_contributor.id), + }, ) - -class PluginReleaseViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PluginReleaseViewSet, ProjectReleaseViewSet), + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_contributor.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - def test_base_attributes(self): + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) self.assertEqual( - first=PluginReleaseViewSet.serializer_class, - second=PluginReleaseSerializer, + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) + + # Verify that contributors cannot see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) self.assertEqual( - first=PluginReleaseViewSet.project_type, - second='plugin', + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) self.assertEqual( - first=PluginReleaseViewSet.project_model, - second=Plugin, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertIs( - expr1=PluginReleaseViewSet.queryset.model, - expr2=PluginRelease, + self.assertDictEqual( + d1=response.json(), + d2={ + 'user': { + 'forum_id': self.plugin_contributor.user.forum_id, + 'username': self.plugin_contributor.user.user.username, + }, + 'id': str(self.plugin_contributor.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, ) self.assertDictEqual( - d1=PluginReleaseViewSet.queryset.query.select_related, - d2={'plugin': {}}, + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, ) - prefetch_lookups = PluginReleaseViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=4) - lookup = prefetch_lookups[0] + def test_post(self): + # Verify that non logged in user cannot add a contributor + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleasepackagerequirement_set', + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleasePackageRequirement, + + # Verify that regular user cannot add a contributor + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('package_requirement__name',), + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot add a contributor + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, ) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'package_requirement': {}}, + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - lookup = prefetch_lookups[1] + # Verify that owner can add a contributor + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleasedownloadrequirement_set', + first=response.status_code, + second=status.HTTP_201_CREATED, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleaseDownloadRequirement, + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing contributor cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.contributor.user.username}, ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('download_requirement__url',), + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, + ) + + # Verify owner cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.owner.user.username}, ) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'download_requirement': {}}, + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, ) - lookup = prefetch_lookups[2] + # Verify unknown username cannot be added + invalid_username = 'invalid' + response = self.client.post( + path=self.api_path, + data={'username': invalid_username}, + ) self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleasepypirequirement_set', + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleasePyPiRequirement, + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'No user named "{invalid_username}".']}, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a contributor + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('pypi_requirement__name',), + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', ) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'pypi_requirement': {}}, + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - lookup = prefetch_lookups[3] + # Verify that contributor cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', + ) self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleaseversioncontrolrequirement_set', + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleaseVersionControlRequirement, + + # Verify that owner can delete a contributor + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', ) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('vcs_requirement__url',), + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( - first=lookup.queryset.query.select_related, - second={'vcs_requirement': {}}, + first=response.json()['name'], + second=f'{self.plugin} - Contributor', ) -class PluginTagViewSetTestCase(TestCase): +class PluginGameViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = game_1 = game_2 = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/games/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.game_1 = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + cls.game_2 = GameFactory( + name='Game2', + basename='game2', + icon='icon2.jpg', + ) + cls.game_3 = GameFactory( + name='Game3', + basename='game3', + icon='icon3.jpg', + ) + cls.game_4 = GameFactory( + name='Game4', + basename='game4', + icon='icon4.jpg', + ) + cls.plugin_game_1 = PluginGameFactory( + plugin=cls.plugin, + game=cls.game_1, + ) + cls.plugin_game_2 = PluginGameFactory( + plugin=cls.plugin, + game=cls.game_2, + ) + cls.regular_user = ForumUserFactory() + def test_inheritance(self): - self.assertTrue(expr=issubclass(PluginTagViewSet, ProjectTagViewSet)) + self.assertTrue( + expr=issubclass(PluginGameViewSet, ProjectGameViewSet), + ) def test_base_attributes(self): self.assertEqual( - first=PluginTagViewSet.serializer_class, - second=PluginTagSerializer, + first=PluginGameViewSet.serializer_class, + second=PluginGameSerializer, ) self.assertEqual( - first=PluginTagViewSet.project_type, + first=PluginGameViewSet.project_type, second='plugin', ) self.assertEqual( - first=PluginTagViewSet.project_model, + first=PluginGameViewSet.project_model, second=Plugin, ) self.assertIs( - expr1=PluginTagViewSet.queryset.model, - expr2=PluginTag, + expr1=PluginGameViewSet.queryset.model, + expr2=PluginGame, ) self.assertDictEqual( - d1=PluginTagViewSet.queryset.query.select_related, - d2={'tag': {}, 'plugin': {}} + d1=PluginGameViewSet.queryset.query.select_related, + d2={'game': {}, 'plugin': {}} ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginGameViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) -class PluginViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue(expr=issubclass(PluginViewSet, ProjectViewSet)) - - def test_base_attributes(self): + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginViewSet.filterset_class, - second=PluginFilterSet, + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginViewSet.serializer_class, - second=PluginSerializer, + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=PluginViewSet.creation_serializer_class, - second=PluginCreateSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertIs(expr1=PluginViewSet.queryset.model, expr2=Plugin) - prefetch_lookups = PluginViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=1) - lookup = prefetch_lookups[0] - self.assertEqual(first=lookup.prefetch_to, second='releases') + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) self.assertEqual( - first=lookup.queryset.query.order_by, - second=('-created',), + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual(first=content['count'], second=2) self.assertDictEqual( - d1=PluginViewSet.queryset.query.select_related, - d2={'owner': {'user': {}}}, + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_2.id), + }, ) + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_game_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) -class SubPluginPathViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginPathViewSet, ProjectThroughModelMixin), + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, ) - def test_base_attributes(self): - self.assertTupleEqual( - tuple1=SubPluginPathViewSet.http_method_names, - tuple2=('get', 'post', 'patch', 'delete', 'options'), + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertTupleEqual( - tuple1=SubPluginPathViewSet.ordering, - tuple2=('path',), + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_1.id), + }, ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) self.assertEqual( - first=SubPluginPathViewSet.serializer_class, - second=SubPluginPathSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertEqual( - first=SubPluginPathViewSet.project_type, - second='plugin', + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_1.id), + }, ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) self.assertEqual( - first=SubPluginPathViewSet.project_model, - second=Plugin, + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, ) - self.assertEqual( - first=SubPluginPathViewSet.related_model_type, - second='Sub-Plugin Path', + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, ) - self.assertIs( + + def test_post(self): + # Verify that non logged in user cannot add a game + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a game + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a game + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a game + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_4.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated game cannot be added + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_1.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Game already linked to {PluginGameViewSet.project_type}.']} + ) + + # Verify non-existing game cannot be added + invalid_slug = 'invalid' + response = self.client.post( + path=self.api_path, + data={'game_slug': invalid_slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Invalid game "{invalid_slug}".']} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a game + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a game + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a game + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a game + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Game', + ) + + +class PluginImageViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/images/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.plugin_image_1 = PluginImageFactory( + plugin=cls.plugin, + ) + cls.plugin_image_2 = PluginImageFactory( + plugin=cls.plugin, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginImageViewSet.serializer_class, + second=PluginImageSerializer, + ) + self.assertEqual( + first=PluginImageViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginImageViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginImageViewSet.queryset.model, + expr2=PluginImage, + ) + self.assertDictEqual( + d1=PluginImageViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginImageViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.plugin_image_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.plugin_image_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_image_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.plugin_image_1.image.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.plugin_image_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.plugin_image_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify that regular user cannot add an image + self.client.force_login(self.regular_user.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add an image + self.client.force_login(self.contributor.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add an image + self.client.force_login(self.owner.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_delete(self): + # Verify that regular user cannot delete an image + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete an image + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete an image + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_image_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Image', + ) + + +class PluginReleaseViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/releases/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.plugin_release = PluginReleaseFactory( + plugin=cls.plugin, + zip_file='release_v1.0.0.zip', + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginReleaseViewSet.serializer_class, + second=PluginReleaseSerializer, + ) + self.assertEqual( + first=PluginReleaseViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginReleaseViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginReleaseViewSet.queryset.model, + expr2=PluginRelease, + ) + self.assertDictEqual( + d1=PluginReleaseViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + prefetch_lookups = PluginReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + timestamp = self.plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' + payload = { + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + 'version': self.plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.plugin_release.id), + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.plugin_release.version}/' + response = self.client.get(path=api_path) + timestamp = self.plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' + payload = { + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + 'version': self.plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.plugin_release.id), + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + plugin = PluginFactory( + basename='test_plugin', + owner=self.owner, + ) + PluginReleaseFactory( + plugin=plugin, + version='1.0.0', + ) + PluginContributorFactory( + plugin=plugin, + user=self.contributor, + ) + api_path = f'{self.base_api_path}{plugin.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' + + # Verify that non logged in user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.contributor.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=plugin.releases.count(), + second=2, + ) + content = response.json() + release = plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that owner can create a release + version = '1.0.2' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.owner.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=plugin.releases.count(), + second=3, + ) + content = response.json() + release = plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that the same version cannot be created twice + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'version': ['Given version matches existing version.']}, + ) + + # Verify that the basename in the zip file is being verified against + # the basename from the url path + zip_basename = plugin.basename + plugin = PluginFactory( + owner=self.owner, + ) + PluginReleaseFactory( + plugin=plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'zip_file': [ + f"Basename in zip '{zip_basename}' does not match basename" + f" for plugin '{plugin.basename}'.", + ], + } + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Release', + ) + + +class PluginTagViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/tags/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.plugin_tag_1 = PluginTagFactory( + plugin=cls.plugin, + ) + cls.plugin_tag_2 = PluginTagFactory( + plugin=cls.plugin, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginTagViewSet.serializer_class, + second=PluginTagSerializer, + ) + self.assertEqual( + first=PluginTagViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginTagViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginTagViewSet.queryset.model, + expr2=PluginTag, + ) + self.assertDictEqual( + d1=PluginTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginTagViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + 'id': str(self.plugin_tag_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + 'id': str(self.plugin_tag_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_tag_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.plugin_tag_1.tag.name, + 'id': str(self.plugin_tag_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.plugin_tag_1.tag.name, + 'id': str(self.plugin_tag_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a tag + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a tag + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a tag + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a tag + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-2'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated tag cannot be added + response = self.client.post( + path=self.api_path, + data={'tag': self.plugin_tag_1.tag}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f'Tag already linked to {PluginTagViewSet.project_type}.']} + ) + + # Verify black-listed tag cannot be added + tag = TagFactory( + black_listed=True, + ) + response = self.client.post( + path=self.api_path, + data={'tag': tag.name}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a tag + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a tag + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a tag + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a tag + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Tag', + ) + + +class PluginViewSetTestCase(APITestCase): + + contributor = owner = plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + logo='logo.jpg', + ) + cls.plugin_release = PluginReleaseFactory( + plugin=cls.plugin, + zip_file='/media/release_v1.0.0.zip', + ) + cls.api_path = f'/api/plugins/projects/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginViewSet.filterset_class, + second=PluginFilterSet, + ) + self.assertEqual( + first=PluginViewSet.serializer_class, + second=PluginSerializer, + ) + self.assertEqual( + first=PluginViewSet.creation_serializer_class, + second=PluginCreateSerializer, + ) + self.assertIs(expr1=PluginViewSet.queryset.model, expr2=Plugin) + prefetch_lookups = PluginViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=PluginViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.plugin_release.get_absolute_url()}' + logo = f'{domain}{self.plugin.logo.url}' + self.assertEqual(first=content['count'], second=1) + created_timestamp = self.plugin.created + updated_timestamp = self.plugin.updated + payload = { + 'name': self.plugin.name, + 'slug': self.plugin.slug, + 'total_downloads': self.plugin.total_downloads, + 'current_release': { + 'version': self.plugin_release.version, + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.plugin.synopsis, + 'description': self.plugin.description, + 'configuration': self.plugin.configuration, + 'logo': logo, + 'video': self.plugin.video, + 'owner': { + 'forum_id': self.plugin.owner.forum_id, + 'username': self.plugin.owner.user.username, + } + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_list_filters(self): + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate tag filtering + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + tag = TagFactory(name='test_tag') + PluginTagFactory( + plugin=self.plugin, + tag=tag, + ) + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + game = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + PluginGameFactory( + plugin=self.plugin, + game=game, + ) + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get( + path=f'{self.api_path}?user={self.regular_user.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.contributor.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.owner.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.plugin.slug}/' + response = self.client.get(path=api_path) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.plugin_release.get_absolute_url()}' + logo = f'{domain}{self.plugin.logo.url}' + created_timestamp = self.plugin.created + updated_timestamp = self.plugin.updated + payload = { + 'name': self.plugin.name, + 'slug': self.plugin.slug, + 'total_downloads': self.plugin.total_downloads, + 'current_release': { + 'version': self.plugin_release.version, + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + 'package_requirements': [], + 'pypi_requirements': [], + 'version_control_requirements': [], + 'download_requirements': [], + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.plugin.synopsis, + 'description': self.plugin.description, + 'configuration': self.plugin.configuration, + 'logo': logo, + 'video': self.plugin.video, + 'owner': { + 'forum_id': self.plugin.owner.forum_id, + 'username': self.plugin.owner.user.username, + } + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify non logged in user cannot create a plugin + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' + version = '1.0.0' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that a logged in user can create a plugin + self.assertEqual( + first=Plugin.objects.count(), + second=1, + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=Plugin.objects.count(), + second=2, + ) + content = response.json() + plugin = Plugin.objects.get(slug=content['slug']) + self.assertEqual( + first=plugin.releases.count(), + second=1, + ) + release = plugin.releases.get() + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify cannot create a plugin where the basename already exists + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'basename': 'Plugin already exists. Cannot create.'} + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.plugin.slug}/' + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'New Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second='Plugin List', + ) + + +class SubPluginPathViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/paths/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.sub_plugin_path_1 = SubPluginPathFactory( + plugin=cls.plugin, + allow_module=True, + ) + cls.sub_plugin_path_2 = SubPluginPathFactory( + plugin=cls.plugin, + allow_package_using_basename=True, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginPathViewSet, ProjectThroughModelMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=SubPluginPathViewSet.ordering, + tuple2=('path',), + ) + self.assertEqual( + first=SubPluginPathViewSet.serializer_class, + second=SubPluginPathSerializer, + ) + self.assertEqual( + first=SubPluginPathViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=SubPluginPathViewSet.project_model, + second=Plugin, + ) + self.assertEqual( + first=SubPluginPathViewSet.related_model_type, + second='Sub-Plugin Path', + ) + self.assertIs( expr1=SubPluginPathViewSet.queryset.model, expr2=SubPluginPath, ) @@ -381,3 +2416,321 @@ def test_base_attributes(self): d1=SubPluginPathViewSet.queryset.query.select_related, d2={'plugin': {}}, ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginPathViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_path_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a path + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-1', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': False, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a path + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-1', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': False, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a path + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-1', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': False, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a path + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-2', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': True, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.sub_plugin_path_1.id}/' + response = self.client.patch( + path=api_path, + data={ + 'allow_module': False, + 'allow_package_using_init': True, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'allow_module': False, + 'allow_package_using_init': True, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'allow_module': False, + 'allow_package_using_init': True, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'allow_module': True, + 'allow_package_using_init': False, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a path + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a path + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a path + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a path + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Sub-Plugin Path', + ) diff --git a/project_manager/sub_plugins/api/tests/test_views.py b/project_manager/sub_plugins/api/tests/test_views.py index 61a4bcf7..eb13c068 100644 --- a/project_manager/sub_plugins/api/tests/test_views.py +++ b/project_manager/sub_plugins/api/tests/test_views.py @@ -1,10 +1,21 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +import shutil +import tempfile + # Django -from django.test import TestCase +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path # Third Party Django +from PIL import Image from rest_framework import status from rest_framework.parsers import ParseError from rest_framework.reverse import reverse @@ -51,13 +62,27 @@ SubPluginReleaseVersionControlRequirement, SubPluginTag, ) -from test_utils.factories.plugins import PluginFactory +from test_utils.factories.games import GameFactory +from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory +from test_utils.factories.sub_plugins import ( + SubPluginContributorFactory, + SubPluginFactory, + SubPluginGameFactory, + SubPluginImageFactory, + SubPluginReleaseFactory, + SubPluginTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory # ============================================================================= # TEST CASES # ============================================================================= class SubPluginAPIViewTestCase(APITestCase): + + api_path = '/api/sub-plugins/' + def test_inheritance(self): self.assertTrue(expr=issubclass(SubPluginAPIView, ProjectAPIView)) @@ -67,8 +92,14 @@ def test_base_attributes(self): second='sub-plugin', ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginAPIView.http_method_names, + tuple2=('get', 'options'), + ) + def test_get(self): - response = self.client.get(path='/api/sub-plugins/') + response = self.client.get(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) base_path = reverse( viewname=f'api:sub-plugins:endpoints', @@ -86,8 +117,34 @@ def test_get(self): } ) + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual(first=response.json()['name'], second='Sub-Plugin APIs') + + +class SubPluginContributorViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + plugin=cls.plugin, + owner=cls.owner, + ) + cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + cls.sub_plugin_contributor = SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.new_contributor = ForumUserFactory() + cls.regular_user = ForumUserFactory() -class SubPluginContributorViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue( expr=issubclass( @@ -118,8 +175,334 @@ def test_base_attributes(self): d2={'user': {'user': {}}, 'sub_plugin': {}} ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginContributorViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that contributors can see results but not 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + 'id': str(self.sub_plugin_contributor.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_contributor.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors cannot see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'user': { + 'forum_id': self.sub_plugin_contributor.user.forum_id, + 'username': self.sub_plugin_contributor.user.user.username, + }, + 'id': str(self.sub_plugin_contributor.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a contributor + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a contributor + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot add a contributor + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can add a contributor + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing contributor cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, + ) + + # Verify owner cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.owner.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, + ) + + # Verify unknown username cannot be added + invalid_username = 'invalid' + response = self.client.post( + path=self.api_path, + data={'username': invalid_username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'No user named "{invalid_username}".']}, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a contributor + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can delete a contributor + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Contributor', + ) + + +class SubPluginGameViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = game_1 = game_2 = plugin = sub_plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + cls.package_contributor = SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.game_1 = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + cls.game_2 = GameFactory( + name='Game2', + basename='game2', + icon='icon2.jpg', + ) + cls.game_3 = GameFactory( + name='Game3', + basename='game3', + icon='icon3.jpg', + ) + cls.game_4 = GameFactory( + name='Game4', + basename='game4', + icon='icon4.jpg', + ) + cls.sub_plugin_game_1 = SubPluginGameFactory( + sub_plugin=cls.sub_plugin, + game=cls.game_1, + ) + cls.sub_plugin_game_2 = SubPluginGameFactory( + sub_plugin=cls.sub_plugin, + game=cls.game_2, + ) + cls.regular_user = ForumUserFactory() -class SubPluginGameViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue( expr=issubclass(SubPluginGameViewSet, ProjectGameViewSet), @@ -147,8 +530,323 @@ def test_base_attributes(self): d2={'game': {}, 'sub_plugin': {}} ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginGameViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_game_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a game + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a game + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a game + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a game + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_4.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated game cannot be added + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_1.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Game already linked to {SubPluginGameViewSet.project_type}.']} + ) + + # Verify non-existing game cannot be added + invalid_slug = 'invalid' + response = self.client.post( + path=self.api_path, + data={'game_slug': invalid_slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Invalid game "{invalid_slug}".']} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a game + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a game + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a game + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a game + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Game', + ) + + +class SubPluginImageViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.sub_plugin_image_1 = SubPluginImageFactory( + sub_plugin=cls.sub_plugin, + ) + cls.sub_plugin_image_2 = SubPluginImageFactory( + sub_plugin=cls.sub_plugin, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() -class SubPluginImageViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue( expr=issubclass(SubPluginImageViewSet, ProjectImageViewSet), @@ -211,21 +909,274 @@ def test_get_project_kwargs(self): } ) - -class SubPluginReleaseViewSetTestCase(TestCase): - def test_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginReleaseViewSet, ProjectReleaseViewSet), + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginImageViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), ) - def test_base_attributes(self): + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) self.assertEqual( - first=SubPluginReleaseViewSet.serializer_class, - second=SubPluginReleaseSerializer, + first=response.status_code, + second=status.HTTP_200_OK, ) - self.assertEqual( - first=SubPluginReleaseViewSet.project_type, - second='sub-plugin', + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_image_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_1.image.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify that regular user cannot add an image + self.client.force_login(self.regular_user.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add an image + self.client.force_login(self.contributor.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add an image + self.client.force_login(self.owner.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_delete(self): + # Verify that regular user cannot delete an image + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete an image + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete an image + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_image_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Image', + ) + + +class SubPluginReleaseViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/releases/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.sub_plugin_release = SubPluginReleaseFactory( + sub_plugin=cls.sub_plugin, + zip_file='release_v1.0.0.zip', + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginReleaseViewSet.serializer_class, + second=SubPluginReleaseSerializer, + ) + self.assertEqual( + first=SubPluginReleaseViewSet.project_type, + second='sub-plugin', ) self.assertEqual( first=SubPluginReleaseViewSet.project_model, @@ -349,8 +1300,377 @@ def test_get_project_kwargs(self): } ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + timestamp = self.sub_plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' + payload = { + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + 'version': self.sub_plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.sub_plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.sub_plugin_release.id), + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.sub_plugin_release.version}/' + response = self.client.get(path=api_path) + timestamp = self.sub_plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' + payload = { + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + 'version': self.sub_plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.sub_plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.sub_plugin_release.id), + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}{self.plugin.slug}/invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + sub_plugin = SubPluginFactory( + plugin=plugin, + basename='test_sub_plugin', + owner=self.owner, + ) + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version='1.0.0', + ) + SubPluginContributorFactory( + sub_plugin=sub_plugin, + user=self.contributor, + ) + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' + + # Verify that non logged in user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify contributor can create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.contributor.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=sub_plugin.releases.count(), + second=2, + ) + content = response.json() + release = sub_plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify owner can create a release + version = '1.0.2' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.owner.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=sub_plugin.releases.count(), + second=3, + ) + content = response.json() + release = sub_plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that the same version cannot be created twice + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'version': ['Given version matches existing version.']}, + ) + + # Verify that the basename in the zip file is being verified against + # the basename from the url path + zip_basename = sub_plugin.basename + sub_plugin = SubPluginFactory( + plugin=plugin, + owner=self.owner, + ) + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'zip_file': [ + f"Basename in zip '{zip_basename}' does not match basename" + f" for sub-plugin '{sub_plugin.basename}'.", + ], + } + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Release', + ) + + +class SubPluginTagViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.sub_plugin_tag_1 = SubPluginTagFactory( + sub_plugin=cls.sub_plugin, + ) + cls.sub_plugin_tag_2 = SubPluginTagFactory( + sub_plugin=cls.sub_plugin, + ) + cls.regular_user = ForumUserFactory() -class SubPluginTagViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue(expr=issubclass(SubPluginTagViewSet, ProjectTagViewSet)) @@ -376,8 +1696,296 @@ def test_base_attributes(self): d2={'tag': {}, 'sub_plugin': {}} ) + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginTagViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + 'id': str(self.sub_plugin_tag_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + 'id': str(self.sub_plugin_tag_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_tag_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.sub_plugin_tag_1.tag.name, + 'id': str(self.sub_plugin_tag_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.sub_plugin_tag_1.tag.name, + 'id': str(self.sub_plugin_tag_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a tag + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a tag + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a tag + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a tag + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-2'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated tag cannot be added + response = self.client.post( + path=self.api_path, + data={'tag': self.sub_plugin_tag_1.tag}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f'Tag already linked to {SubPluginTagViewSet.project_type}.']} + ) + + # Verify black-listed tag cannot be added + tag = TagFactory( + black_listed=True, + ) + response = self.client.post( + path=self.api_path, + data={'tag': tag.name}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a tag + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a tag + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a tag + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a tag + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Tag', + ) + + +class SubPluginViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = sub_plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=plugin, + logo='logo.jpg', + ) + cls.sub_plugin_release = SubPluginReleaseFactory( + sub_plugin=cls.sub_plugin, + zip_file='/media/release_v1.0.0.zip', + ) + cls.base_api_path = f'/api/sub-plugins/projects/' + cls.api_path = f'{cls.base_api_path}{plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() -class SubPluginViewSetTestCase(TestCase): def test_inheritance(self): self.assertTrue(expr=issubclass(SubPluginViewSet, ProjectViewSet)) @@ -428,3 +2036,441 @@ def test_get_queryset(self): obj.kwargs = {} obj.plugin = plugin obj.get_queryset() + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.sub_plugin_release.get_absolute_url()}' + logo = f'{domain}{self.sub_plugin.logo.url}' + self.assertEqual(first=content['count'], second=1) + created_timestamp = self.sub_plugin.created + updated_timestamp = self.sub_plugin.updated + payload = { + 'name': self.sub_plugin.name, + 'slug': self.sub_plugin.slug, + 'total_downloads': self.sub_plugin.total_downloads, + 'current_release': { + 'version': self.sub_plugin_release.version, + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.sub_plugin.synopsis, + 'description': self.sub_plugin.description, + 'configuration': self.sub_plugin.configuration, + 'logo': logo, + 'video': self.sub_plugin.video, + 'owner': { + 'forum_id': self.sub_plugin.owner.forum_id, + 'username': self.sub_plugin.owner.user.username, + } + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_list_filters(self): + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate tag filtering + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + tag = TagFactory(name='test_tag') + SubPluginTagFactory( + sub_plugin=self.sub_plugin, + tag=tag, + ) + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + game = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + SubPluginGameFactory( + sub_plugin=self.sub_plugin, + game=game, + ) + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get( + path=f'{self.api_path}?user={self.regular_user.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.contributor.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.owner.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.sub_plugin.slug}/' + response = self.client.get(path=api_path) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.sub_plugin_release.get_absolute_url()}' + logo = f'{domain}{self.sub_plugin.logo.url}' + created_timestamp = self.sub_plugin.created + updated_timestamp = self.sub_plugin.updated + payload = { + 'name': self.sub_plugin.name, + 'slug': self.sub_plugin.slug, + 'total_downloads': self.sub_plugin.total_downloads, + 'current_release': { + 'version': self.sub_plugin_release.version, + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + 'package_requirements': [], + 'pypi_requirements': [], + 'version_control_requirements': [], + 'download_requirements': [], + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.sub_plugin.synopsis, + 'description': self.sub_plugin.description, + 'configuration': self.sub_plugin.configuration, + 'logo': logo, + 'video': self.sub_plugin.video, + 'owner': { + 'forum_id': self.sub_plugin.owner.forum_id, + 'username': self.sub_plugin.owner.user.username, + } + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify non logged in user cannot create a sub-plugin + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' + version = '1.0.0' + api_path = f'{self.base_api_path}{plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'name': 'Test SubPlugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that a logged in user can create a sub-plugin + self.assertEqual( + first=SubPlugin.objects.count(), + second=1, + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'name': 'Test SubPlugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=SubPlugin.objects.count(), + second=2, + ) + content = response.json() + sub_plugin = SubPlugin.objects.get(slug=content['slug']) + self.assertEqual( + first=sub_plugin.releases.count(), + second=1, + ) + release = sub_plugin.releases.get() + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify cannot create a sub-plugin where the basename already exists + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'name': 'Test SubPlugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'basename': 'SubPlugin already exists. Cannot create.'} + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.sub_plugin.slug}/' + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'New Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second='Sub Plugin List', + ) From bddfa48eec730023ce028762da9c2293249015b9 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 16:47:19 -0500 Subject: [PATCH 089/211] Added more fixtures for testing. --- .../test-package-requirements-v1.0.0.zip | Bin 0 -> 1749 bytes .../test-plugin-requirements-v1.0.0.zip | Bin 0 -> 1729 bytes .../test-sub-plugin-requirements-v1.0.0.zip | Bin 0 -> 2405 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 fixtures/releases/packages/test-package/test-package-requirements-v1.0.0.zip create mode 100644 fixtures/releases/plugins/test-plugin/test-plugin-requirements-v1.0.0.zip create mode 100644 fixtures/releases/sub-plugins/test-plugin/test-sub-plugin/test-sub-plugin-requirements-v1.0.0.zip diff --git a/fixtures/releases/packages/test-package/test-package-requirements-v1.0.0.zip b/fixtures/releases/packages/test-package/test-package-requirements-v1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..bcee8b74015141c2ec7f1d713a473c496f53e1f5 GIT binary patch literal 1749 zcmWIWW@h1H0D&T>lwdFeO0YA?FeIj=Wo(&Cc*T!^#Z z07C$TafU)=MJ`G(8WC%JNosLPJk&1`yVRs6Bsv2z7P~+p&%pq)>u#-f@qHlAoSA{a znJl}CQVUBni&Aq_^Gb^KvWoNbz|jq|oJRt~^0k4!s}C6n?D?(jbk1z6ZPizPn~Ddn zOOEWgwr6^Y^Q2#$|L?91W7!&T^z@;3K553*#~G7jt|~5inrybXZ{y>omueK5LoP8X z>Mkz``g>ZatA{^-4eyTy8?!uSN_1Y7brk2E(6-=3_KqI^!}qK|nU}9w!ueKc)#tWD z(|4@pIrVh&&RgdBKdju${eo|JzUJX*HRH`#7?H_5(<0;hyt#6#Dwn^~X3l&6<@57= z@$3sr_J)7j>JzuBUb(lIYp&xIt4l{$a3!gpdn~^~a6L(`NiCaphyoY`|`(vOPXM_1;U_^x2#5D`gzHZ@~KdrFF!HbdYd zC(eMy^LDA}ivzPDBa<96u3RhubUX+MFuZjH(eOOY3dz$LsSCGZq72A}flPxMhMvDk zH(CzG=n1%tM#}4i?ZC|G$j*O3&<=RM2U>+S&m%$#GuIlwdFeO0YA?FeIj=aeir0a;k1YWl2VU9>lmimzxto7^iWw(TtPDWn4i{X?iBimCt}i zf-p{F*Hq-9cvF**u_dX+CGikDATDo_nvggRh_P4!3J?wkkQH}pwTtfqdFIRv3|6FA zQIuL(npu>Zo0?Zrtd~`sp9cttfM&Rgti4QvUl|OAHHY($-I2c63(|mt3J0Kn!aN# z -IciuA3|6%23?iYN+^ED4gs~K;`!iY@fnHCw}=gpN{Rk{3?Hgn$lFQ1?1i)UY0 zvN!zGR-d?4^~$}yTyq_#SY0~0f-6b&+++C_8W*O&Ecx`iIM>+mhkN6f!1LBu<@fOT zrGwHIBuJ+N6X&n#G6r_QAoTzRD=9&W6oYyNmEiD{yU>g+JiX6(>ga`WcNjTGk+sn@tlwqNNEL_+kw`B zavs*e!j;z{)&Yx0h9!+&KniPM!F+^l9hRI=*nZ56fo%U=CKBvNatk==0WAk*6Re(x sBslwdFeO0YA?FeIj=aeir0a;k1YWl2VU9>lmimzxto7^iWw(TtPDWn4i{X?iBimCt}i zf-p{F6?f;LcvF**u_dX+CGikDATF1+>rd1JVw_g|Ky$e%DOMDhCP8h3hWnH&&50n4 z)4Ed?xhOtzCD%HzPZ9P*e7jp}LgGar#$r7v`Z*Xt*59qwF1`=snKLsmgi*`-qSV6D z%%arX)Vz{ny{zK=Ja94uxn~Qed)5Z}u0CWSu;;h7(>b%PwpCyGZ7LqPE;+K}+Mekp z&Xaz1{=d66jAd)U(bI?C`J@?JA7@OCxvIG6X|mbkzKxHUUaC=K4!OjnsJpx%=b|VMHeLOpA=~^XAH}s$Bj`n>p|Om(S1h#j`Ig*&F_8 zt54jjdgb0;uDOm=tS%j0!Ih+X?y>v|jSJIXmVEkMoNMg(!@coK;Cbt-@_Tsv(h<3A zC0gPNpjL3l$7kkcmc+;F6;y%)`X4YCfiTX9pAIZAeodD#umeWC2M~u-%Oz;(5auAc z3(bjJF=O8QoTrXn7>B2>-o{5ygBY5oTrp|=@#D;%Gn0N)^gg;W$HaF93x|kE>a(dC zE80_1#IzX#7ddeTES|SZOXd@(Y~G2pK&K-}tH5Bw={88-z$|uP!31*Ka$vbnRt%xL4W$qQx(aIn1d2RJ rh+!5%$RTFPLM2zBxe{C;0UZY_kT9Z?l?@nM44gpd544_v6~qGoGNHb4 literal 0 HcmV?d00001 From dfea6ae85e74376d0b6d541b2d9876ebd5c3d59c Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 16:54:12 -0500 Subject: [PATCH 090/211] Moved requirement relationship creation to its own mixin. Updated serializers to properly create the requirement relationships for both project creation and release creation. --- .../common/api/serializers/__init__.py | 11 +++- .../common/api/serializers/mixins.py | 61 +++++++++++-------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index 3783da8a..17219d4e 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -23,6 +23,7 @@ # App from project_manager.common.api.serializers.mixins import ( AddProjectToViewMixin, + CreateRequirementsMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, ProjectThroughMixin, @@ -60,7 +61,11 @@ # ============================================================================= # SERIALIZERS # ============================================================================= -class ProjectSerializer(ModelSerializer, ProjectLocaleMixin): +class ProjectSerializer( + CreateRequirementsMixin, + ModelSerializer, + ProjectLocaleMixin +): """Base Project Serializer.""" current_release = SerializerMethodField() @@ -115,6 +120,7 @@ def create(self, validated_data): current_time = now() validated_data['created'] = validated_data['updated'] = current_time instance = super().create(validated_data) + self.requirements = self.release_dict.pop('requirements') version = self.release_dict['version'] zip_file = self.release_dict['zip_file'] notes = self.release_dict['notes'] @@ -125,7 +131,8 @@ def create(self, validated_data): 'version': version, 'zip_file': zip_file, } - self.release_model.objects.create(**kwargs) + release = self.release_model.objects.create(**kwargs) + self._create_requirements(release=release) return instance def get_created(self, obj): diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 99ee274b..d08a8829 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -19,6 +19,7 @@ # ============================================================================= __all__ = ( 'AddProjectToViewMixin', + 'CreateRequirementsMixin', 'ProjectLocaleMixin', 'ProjectReleaseCreationMixin', 'ProjectThroughMixin', @@ -54,7 +55,37 @@ def get_date_display(date, date_format): ) if date else date -class ProjectReleaseCreationMixin(ModelSerializer): +class CreateRequirementsMixin: + + requirements = None + + def _create_requirements(self, release): + """Create all requirements for the release.""" + if not self.requirements: + return + + # TODO: look into bulk_create + project_type = release.__class__.__name__.lower() + for group_type, group in self.requirements.items(): + self._create_group_requirements( + release=release, + project_type=project_type, + group_type=group_type, + group=group, + ) + + @staticmethod + def _create_group_requirements(release, project_type, group_type, group): + queryset_group_name = GROUP_QUERYSET_NAMES.get(group_type) + for requirement in group: + requirement_set = getattr( + release, + f'{project_type}{queryset_group_name}requirement_set' + ) + requirement_set.create(**requirement) + + +class ProjectReleaseCreationMixin(CreateRequirementsMixin, ModelSerializer): """Mixin for validation/creation of a project release.""" requirements = None @@ -116,6 +147,7 @@ def validate(self, attrs): # This needs added for project creation attrs['basename'] = zip_validator.basename + attrs['requirements'] = zip_validator.requirements if project is not None: attrs[self.project_type.replace('-', '_')] = project @@ -152,7 +184,6 @@ def run_zip_file_validation(self, zip_validator, project_basename): zip_validator.validate_basename() zip_validator.validate_base_file_in_zip() zip_validator.validate_requirements() - self.requirements = zip_validator.requirements if project_basename not in (zip_validator.basename, None): raise ValidationError({ 'zip_file': ( @@ -166,6 +197,7 @@ def create(self, validated_data): """Update the project's updated datetime when release is created.""" # Remove the basename before creating the release del validated_data['basename'] + self.requirements = validated_data.pop('requirements') instance = super().create(validated_data=validated_data) project_type = self.project_type.replace('-', '_') @@ -178,31 +210,6 @@ def create(self, validated_data): self._create_requirements(instance) return instance - def _create_requirements(self, release): - """Create all requirements for the release.""" - if not self.requirements: - return - - # TODO: look into bulk_create - project_type = release.__class__.__name__.lower() - for group_type, group in self.requirements.items(): - self._create_group_requirements( - release=release, - project_type=project_type, - group_type=group_type, - group=group, - ) - - @staticmethod - def _create_group_requirements(release, project_type, group_type, group): - queryset_group_name = GROUP_QUERYSET_NAMES.get(group_type) - for requirement in group: - requirement_set = getattr( - release, - f'{project_type}{queryset_group_name}requirement_set' - ) - requirement_set.create(**requirement) - class ProjectThroughMixin(ModelSerializer): """Mixin for through model serializers.""" From 67b418d5d57f587d610fb5576214f9926f9db9a1 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 16:55:08 -0500 Subject: [PATCH 091/211] Updated tests for mixin changes. Removed debug print statements. --- project_manager/common/api/tests/test_serializers.py | 4 ++++ project_manager/common/api/views/__init__.py | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/common/api/tests/test_serializers.py index aa1b4c3d..c5be61c5 100644 --- a/project_manager/common/api/tests/test_serializers.py +++ b/project_manager/common/api/tests/test_serializers.py @@ -32,6 +32,7 @@ ) from project_manager.common.api.serializers.mixins import ( AddProjectToViewMixin, + CreateRequirementsMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, ProjectThroughMixin, @@ -187,6 +188,9 @@ def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectReleaseCreationMixin, ModelSerializer), ) + self.assertTrue( + expr=issubclass(ProjectReleaseCreationMixin, CreateRequirementsMixin), + ) def test_project_class_required(self): obj = '' diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 690a08d9..66753913 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -148,13 +148,11 @@ def check_permissions(self, request): """Only allow the owner and contributors to create releases.""" if request.method not in SAFE_METHODS: if not hasattr(request.user, 'forum_user'): - print('no forum_user') raise PermissionDenied user = request.user.id is_contributor = user in self.contributors if user != self.owner and not is_contributor: - print('not owner or contributor') raise PermissionDenied return super().check_permissions(request=request) From 97744d78ff253157e5a610db4b4261258d8c933b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 16:55:53 -0500 Subject: [PATCH 092/211] Changed custom package requirement validation to retrieve the package via the basename instead of slug. --- project_manager/common/helpers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 0ceab923..3203c25b 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -276,9 +276,7 @@ def _validate_custom_requirement(self, item): model_name='Package', ) try: - # TODO: should this be retrieving via basename instead of slug? - # or should the field in the requirements file be called slug? - package = package_model.objects.get(slug=basename) + package = package_model.objects.get(basename=basename) except package_model.DoesNotExist: self.requirements_errors.append( f'Custom Package "{basename}" from requirements ' From 0c7359506f68abbbcb134c6b3abcbd7cf4a931a8 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 16:57:09 -0500 Subject: [PATCH 093/211] Added tests to validate requirement relationship creation on project/release POST calls. --- .../packages/api/tests/test_views.py | 162 +++++++++++++++ .../plugins/api/tests/test_views.py | 163 +++++++++++++++ .../sub_plugins/api/tests/test_views.py | 189 ++++++++++++++++++ 3 files changed, 514 insertions(+) diff --git a/project_manager/packages/api/tests/test_views.py b/project_manager/packages/api/tests/test_views.py index 9700656a..f3692bdf 100644 --- a/project_manager/packages/api/tests/test_views.py +++ b/project_manager/packages/api/tests/test_views.py @@ -61,6 +61,11 @@ PackageReleaseVersionControlRequirement, PackageTag, ) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) from test_utils.factories.games import GameFactory from test_utils.factories.packages import ( PackageFactory, @@ -1543,6 +1548,87 @@ def test_post(self): } ) + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-requirements-v1.0.0.zip' + version = '1.0.1' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + package = PackageFactory( + basename='test_package', + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{package.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + release = PackageRelease.objects.get(pk=response.json()['id']) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) @@ -2287,6 +2373,82 @@ def test_post(self): d2={'basename': 'Package already exists. Cannot create.'} ) + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-requirements-v1.0.0.zip' + version = '1.0.0' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + contents = response.json() + package = Package.objects.get(slug=contents['slug']) + release = PackageRelease.objects.get(package=package) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + def test_patch(self): # Verify that non logged in user cannot update a path api_path = f'{self.api_path}{self.package.slug}/' diff --git a/project_manager/plugins/api/tests/test_views.py b/project_manager/plugins/api/tests/test_views.py index cfb6d3c4..eef0dedd 100644 --- a/project_manager/plugins/api/tests/test_views.py +++ b/project_manager/plugins/api/tests/test_views.py @@ -65,7 +65,13 @@ PluginTag, SubPluginPath, ) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) from test_utils.factories.games import GameFactory +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory from test_utils.factories.plugins import ( PluginContributorFactory, PluginFactory, @@ -1549,6 +1555,87 @@ def test_post(self): } ) + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-requirements-v1.0.0.zip' + version = '1.0.1' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + plugin = PluginFactory( + basename='test_plugin', + owner=self.owner, + ) + PluginReleaseFactory( + plugin=plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + release = PluginRelease.objects.get(pk=response.json()['id']) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) @@ -2293,6 +2380,82 @@ def test_post(self): d2={'basename': 'Plugin already exists. Cannot create.'} ) + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-requirements-v1.0.0.zip' + version = '1.0.0' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + contents = response.json() + plugin = Plugin.objects.get(slug=contents['slug']) + release = PluginRelease.objects.get(plugin=plugin) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + def test_patch(self): # Verify that non logged in user cannot update a path api_path = f'{self.api_path}{self.plugin.slug}/' diff --git a/project_manager/sub_plugins/api/tests/test_views.py b/project_manager/sub_plugins/api/tests/test_views.py index eb13c068..f992735d 100644 --- a/project_manager/sub_plugins/api/tests/test_views.py +++ b/project_manager/sub_plugins/api/tests/test_views.py @@ -62,7 +62,13 @@ SubPluginReleaseVersionControlRequirement, SubPluginTag, ) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) from test_utils.factories.games import GameFactory +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory from test_utils.factories.sub_plugins import ( SubPluginContributorFactory, @@ -1635,6 +1641,104 @@ def test_post(self): } ) + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + sub_plugin = SubPluginFactory( + plugin=plugin, + basename='test_sub_plugin', + owner=self.owner, + ) + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' + version = '1.0.1' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + package = PackageFactory( + basename='test_package', + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + release = SubPluginRelease.objects.get(pk=response.json()['id']) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) @@ -2414,6 +2518,91 @@ def test_post(self): d2={'basename': 'SubPlugin already exists. Cannot create.'} ) + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' + version = '1.0.0' + api_path = f'{self.base_api_path}{plugin.slug}/' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + contents = response.json() + sub_plugin = SubPlugin.objects.get(slug=contents['slug'], plugin=plugin) + release = SubPluginRelease.objects.get(sub_plugin=sub_plugin) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + def test_patch(self): # Verify that non logged in user cannot update a path api_path = f'{self.api_path}{self.sub_plugin.slug}/' From 379745351d9ebc856ddccb4c9d4f607c57f918b7 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 16:57:53 -0500 Subject: [PATCH 094/211] Updated tests for change to custom package requirement validation. --- project_manager/packages/tests/test_helpers.py | 7 +++---- project_manager/plugins/tests/test_helpers.py | 7 +++---- project_manager/sub_plugins/tests/test_helpers.py | 7 +++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/project_manager/packages/tests/test_helpers.py b/project_manager/packages/tests/test_helpers.py index 6084b739..fea4f802 100644 --- a/project_manager/packages/tests/test_helpers.py +++ b/project_manager/packages/tests/test_helpers.py @@ -317,7 +317,6 @@ def test_validate_requirements_file_failures(self, mock_logger): ) def test_validate_requirements_file_item_failures(self, _, mock_json_loads): custom_package_basename = 'test_custom_package' - custom_package_slug = custom_package_basename.replace('_', '-') custom_package = PackageFactory( basename=custom_package_basename, ) @@ -452,7 +451,7 @@ def test_validate_requirements_file_item_failures(self, _, mock_json_loads): mock_json_loads.return_value = { group_type: [ { - 'basename': custom_package_slug, + 'basename': custom_package_basename, 'version': version, }, ], @@ -465,7 +464,7 @@ def test_validate_requirements_file_item_failures(self, _, mock_json_loads): d1=context.exception.message_dict, d2={ 'zip_file': [ - f'Custom Package "{custom_package_slug}" version ' + f'Custom Package "{custom_package_basename}" version ' f'"{version}", from requirements json file, not found.' ], }, @@ -500,7 +499,7 @@ def test_validate_requirements_file_item_failures(self, _, mock_json_loads): mock_json_loads.return_value = { 'custom': [ { - 'basename': custom_package_slug, + 'basename': custom_package_basename, 'version': custom_package_release.version, }, ], diff --git a/project_manager/plugins/tests/test_helpers.py b/project_manager/plugins/tests/test_helpers.py index c8fcc790..e2496b91 100644 --- a/project_manager/plugins/tests/test_helpers.py +++ b/project_manager/plugins/tests/test_helpers.py @@ -270,7 +270,6 @@ def test_validate_requirements_file_failures(self, mock_logger): ) def test_validate_requirements_file_item_failures(self, _, mock_json_loads): custom_package_basename = 'test_custom_package' - custom_package_slug = custom_package_basename.replace('_', '-') custom_package = PackageFactory( basename=custom_package_basename, ) @@ -405,7 +404,7 @@ def test_validate_requirements_file_item_failures(self, _, mock_json_loads): mock_json_loads.return_value = { group_type: [ { - 'basename': custom_package_slug, + 'basename': custom_package_basename, 'version': version, }, ], @@ -418,7 +417,7 @@ def test_validate_requirements_file_item_failures(self, _, mock_json_loads): d1=context.exception.message_dict, d2={ 'zip_file': [ - f'Custom Package "{custom_package_slug}" version ' + f'Custom Package "{custom_package_basename}" version ' f'"{version}", from requirements json file, not found.' ], }, @@ -453,7 +452,7 @@ def test_validate_requirements_file_item_failures(self, _, mock_json_loads): mock_json_loads.return_value = { 'custom': [ { - 'basename': custom_package_slug, + 'basename': custom_package_basename, 'version': custom_package_release.version, }, ], diff --git a/project_manager/sub_plugins/tests/test_helpers.py b/project_manager/sub_plugins/tests/test_helpers.py index 30337bdc..f7b355ad 100644 --- a/project_manager/sub_plugins/tests/test_helpers.py +++ b/project_manager/sub_plugins/tests/test_helpers.py @@ -369,7 +369,6 @@ def test_validate_requirements_file_failures(self, mock_logger): def test_validate_requirements_file_item_failures(self, _, __, mock_json_loads): plugin = PluginFactory() custom_package_basename = 'test_custom_package' - custom_package_slug = custom_package_basename.replace('_', '-') custom_package = PackageFactory( basename=custom_package_basename, ) @@ -515,7 +514,7 @@ def test_validate_requirements_file_item_failures(self, _, __, mock_json_loads): mock_json_loads.return_value = { group_type: [ { - 'basename': custom_package_slug, + 'basename': custom_package_basename, 'version': version, }, ], @@ -529,7 +528,7 @@ def test_validate_requirements_file_item_failures(self, _, __, mock_json_loads): d1=context.exception.message_dict, d2={ 'zip_file': [ - f'Custom Package "{custom_package_slug}" version ' + f'Custom Package "{custom_package_basename}" version ' f'"{version}", from requirements json file, not found.' ], }, @@ -565,7 +564,7 @@ def test_validate_requirements_file_item_failures(self, _, __, mock_json_loads): mock_json_loads.return_value = { 'custom': [ { - 'basename': custom_package_slug, + 'basename': custom_package_basename, 'version': custom_package_release.version, }, ], From 208c75025a7fadb6fe13b8649bc8cce904e05914 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 17:30:41 -0500 Subject: [PATCH 095/211] Added missing doc-string. Broke the API tests out into multiple modules. --- .../common/api/serializers/mixins.py | 1 + .../packages/api/tests/test_project_views.py | 615 ++++ .../packages/api/tests/test_related_views.py | 1332 ++++++++ .../packages/api/tests/test_release_views.py | 591 ++++ .../packages/api/tests/test_views.py | 2457 +------------- .../plugins/api/tests/test_project_views.py | 616 ++++ .../plugins/api/tests/test_related_views.py | 1720 ++++++++++ .../plugins/api/tests/test_release_views.py | 590 ++++ .../plugins/api/tests/test_views.py | 2844 +---------------- .../api/tests/test_project_views.py | 660 ++++ .../api/tests/test_related_views.py | 1379 ++++++++ .../api/tests/test_release_views.py | 658 ++++ .../sub_plugins/api/tests/test_views.py | 2611 +-------------- 13 files changed, 8168 insertions(+), 7906 deletions(-) create mode 100644 project_manager/packages/api/tests/test_project_views.py create mode 100644 project_manager/packages/api/tests/test_related_views.py create mode 100644 project_manager/packages/api/tests/test_release_views.py create mode 100644 project_manager/plugins/api/tests/test_project_views.py create mode 100644 project_manager/plugins/api/tests/test_related_views.py create mode 100644 project_manager/plugins/api/tests/test_release_views.py create mode 100644 project_manager/sub_plugins/api/tests/test_project_views.py create mode 100644 project_manager/sub_plugins/api/tests/test_related_views.py create mode 100644 project_manager/sub_plugins/api/tests/test_release_views.py diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index d08a8829..00aae026 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -56,6 +56,7 @@ def get_date_display(date, date_format): class CreateRequirementsMixin: + """Mixin for creating the requirement relationships for releases.""" requirements = None diff --git a/project_manager/packages/api/tests/test_project_views.py b/project_manager/packages/api/tests/test_project_views.py new file mode 100644 index 00000000..835736b8 --- /dev/null +++ b/project_manager/packages/api/tests/test_project_views.py @@ -0,0 +1,615 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path + +# Third Party Django +from rest_framework import status +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ProjectViewSet +from project_manager.packages.api.filtersets import PackageFilterSet +from project_manager.packages.api.serializers import ( + PackageCreateSerializer, + PackageSerializer, +) +from project_manager.packages.api.views import PackageViewSet +from project_manager.packages.models import ( + Package, + PackageRelease, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.games import GameFactory +from test_utils.factories.packages import ( + PackageFactory, + PackageContributorFactory, + PackageGameFactory, + PackageReleaseFactory, + PackageTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageViewSetTestCase(APITestCase): + + contributor = owner = package = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + logo='logo.jpg', + ) + cls.package_release = PackageReleaseFactory( + package=cls.package, + zip_file='/media/release_v1.0.0.zip', + ) + cls.api_path = f'/api/packages/projects/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageViewSet.filterset_class, + second=PackageFilterSet, + ) + self.assertEqual( + first=PackageViewSet.serializer_class, + second=PackageSerializer, + ) + self.assertEqual( + first=PackageViewSet.creation_serializer_class, + second=PackageCreateSerializer, + ) + self.assertIs(expr1=PackageViewSet.queryset.model, expr2=Package) + prefetch_lookups = PackageViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=PackageViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.package_release.get_absolute_url()}' + logo = f'{domain}{self.package.logo.url}' + created_timestamp = self.package.created + updated_timestamp = self.package.updated + payload = { + 'name': self.package.name, + 'slug': self.package.slug, + 'total_downloads': self.package.total_downloads, + 'current_release': { + 'version': self.package_release.version, + 'notes': self.package_release.notes, + 'zip_file': zip_file, + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.package.synopsis, + 'description': self.package.description, + 'configuration': self.package.configuration, + 'logo': logo, + 'video': self.package.video, + 'owner': { + 'forum_id': self.package.owner.forum_id, + 'username': self.package.owner.user.username, + } + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_list_filters(self): + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate tag filtering + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + tag = TagFactory(name='test_tag') + PackageTagFactory( + package=self.package, + tag=tag, + ) + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + game = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + PackageGameFactory( + package=self.package, + game=game, + ) + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get( + path=f'{self.api_path}?user={self.regular_user.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.contributor.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.owner.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.package.slug}/' + response = self.client.get(path=api_path) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.package_release.get_absolute_url()}' + logo = f'{domain}{self.package.logo.url}' + created_timestamp = self.package.created + updated_timestamp = self.package.updated + payload = { + 'name': self.package.name, + 'slug': self.package.slug, + 'total_downloads': self.package.total_downloads, + 'current_release': { + 'version': self.package_release.version, + 'notes': self.package_release.notes, + 'zip_file': zip_file, + 'package_requirements': [], + 'pypi_requirements': [], + 'version_control_requirements': [], + 'download_requirements': [], + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.package.synopsis, + 'description': self.package.description, + 'configuration': self.package.configuration, + 'logo': logo, + 'video': self.package.video, + 'owner': { + 'forum_id': self.package.owner.forum_id, + 'username': self.package.owner.user.username, + } + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify non logged in user cannot create a package + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' + version = '1.0.0' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that a logged in user can create a package + self.assertEqual( + first=Package.objects.count(), + second=1, + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=Package.objects.count(), + second=2, + ) + content = response.json() + package = Package.objects.get(slug=content['slug']) + self.assertEqual( + first=package.releases.count(), + second=1, + ) + release = package.releases.get() + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify cannot create a package where the basename already exists + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'basename': 'Package already exists. Cannot create.'} + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-requirements-v1.0.0.zip' + version = '1.0.0' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + contents = response.json() + package = Package.objects.get(slug=contents['slug']) + release = PackageRelease.objects.get(package=package) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.package.slug}/' + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'New Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second='Package List', + ) diff --git a/project_manager/packages/api/tests/test_related_views.py b/project_manager/packages/api/tests/test_related_views.py new file mode 100644 index 00000000..8f2a0759 --- /dev/null +++ b/project_manager/packages/api/tests/test_related_views.py @@ -0,0 +1,1332 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.test import override_settings + +# Third Party Python +from path import Path + +# Third Party Django +from PIL import Image +from rest_framework import status +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ( + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectTagViewSet, +) +from project_manager.packages.api.serializers import ( + PackageContributorSerializer, + PackageGameSerializer, + PackageImageSerializer, + PackageTagSerializer, +) +from project_manager.packages.api.views import ( + PackageContributorViewSet, + PackageGameViewSet, + PackageImageViewSet, + PackageTagViewSet, +) +from project_manager.packages.models import ( + Package, + PackageContributor, + PackageGame, + PackageImage, + PackageTag, +) +from test_utils.factories.games import GameFactory +from test_utils.factories.packages import ( + PackageFactory, + PackageContributorFactory, + PackageGameFactory, + PackageImageFactory, + PackageTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageContributorViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/contributors/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + cls.package_contributor = PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.new_contributor = ForumUserFactory() + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass( + PackageContributorViewSet, + ProjectContributorViewSet, + ), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageContributorViewSet.serializer_class, + second=PackageContributorSerializer, + ) + self.assertEqual( + first=PackageContributorViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageContributorViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageContributorViewSet.queryset.model, + expr2=PackageContributor, + ) + self.assertDictEqual( + d1=PackageContributorViewSet.queryset.query.select_related, + d2={'user': {'user': {}}, 'package': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageContributorViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that contributors can see results but not 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + 'id': str(self.package_contributor.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_contributor.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors cannot see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'user': { + 'forum_id': self.package_contributor.user.forum_id, + 'username': self.package_contributor.user.user.username, + }, + 'id': str(self.package_contributor.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a contributor + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a contributor + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot add a contributor + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can add a contributor + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing contributor cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, + ) + + # Verify owner cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.owner.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, + ) + + # Verify unknown username cannot be added + invalid_username = 'invalid' + response = self.client.post( + path=self.api_path, + data={'username': invalid_username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'No user named "{invalid_username}".']}, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a contributor + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can delete a contributor + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Contributor', + ) + + +class PackageGameViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = game_1 = game_2 = package = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/games/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.game_1 = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + cls.game_2 = GameFactory( + name='Game2', + basename='game2', + icon='icon2.jpg', + ) + cls.game_3 = GameFactory( + name='Game3', + basename='game3', + icon='icon3.jpg', + ) + cls.game_4 = GameFactory( + name='Game4', + basename='game4', + icon='icon4.jpg', + ) + cls.package_game_1 = PackageGameFactory( + package=cls.package, + game=cls.game_1, + ) + cls.package_game_2 = PackageGameFactory( + package=cls.package, + game=cls.game_2, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageGameViewSet, ProjectGameViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageGameViewSet.serializer_class, + second=PackageGameSerializer, + ) + self.assertEqual( + first=PackageGameViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageGameViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageGameViewSet.queryset.model, + expr2=PackageGame, + ) + self.assertDictEqual( + d1=PackageGameViewSet.queryset.query.select_related, + d2={'game': {}, 'package': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageGameViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.package_game_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.package_game_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_game_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.package_game_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.package_game_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a game + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a game + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a game + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a game + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_4.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated game cannot be added + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_1.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Game already linked to {PackageGameViewSet.project_type}.']} + ) + + # Verify non-existing game cannot be added + invalid_slug = 'invalid' + response = self.client.post( + path=self.api_path, + data={'game_slug': invalid_slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Invalid game "{invalid_slug}".']} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a game + response = self.client.delete( + path=self.api_path + f'{self.package_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a game + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.package_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a game + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a game + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_game_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Game', + ) + + +class PackageImageViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/images/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.package_image_1 = PackageImageFactory( + package=cls.package, + ) + cls.package_image_2 = PackageImageFactory( + package=cls.package, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageImageViewSet.serializer_class, + second=PackageImageSerializer, + ) + self.assertEqual( + first=PackageImageViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageImageViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageImageViewSet.queryset.model, + expr2=PackageImage, + ) + self.assertDictEqual( + d1=PackageImageViewSet.queryset.query.select_related, + d2={'package': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageImageViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.package_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.package_image_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.package_image_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_image_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.package_image_1.image.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.package_image_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.package_image_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify that regular user cannot add an image + self.client.force_login(self.regular_user.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add an image + self.client.force_login(self.contributor.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add an image + self.client.force_login(self.owner.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_delete(self): + # Verify that regular user cannot delete an image + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.package_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete an image + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete an image + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_image_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Image', + ) + + +class PackageTagViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/tags/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.package_tag_1 = PackageTagFactory( + package=cls.package, + ) + cls.package_tag_2 = PackageTagFactory( + package=cls.package, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PackageTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PackageTagViewSet.serializer_class, + second=PackageTagSerializer, + ) + self.assertEqual( + first=PackageTagViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageTagViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageTagViewSet.queryset.model, + expr2=PackageTag, + ) + self.assertDictEqual( + d1=PackageTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'package': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageTagViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + 'id': str(self.package_tag_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.package_tag_2.tag.name, + 'id': str(self.package_tag_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.package_tag_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.package_tag_1.tag.name, + 'id': str(self.package_tag_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.package_tag_1.tag.name, + 'id': str(self.package_tag_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a tag + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a tag + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a tag + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a tag + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-2'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated tag cannot be added + response = self.client.post( + path=self.api_path, + data={'tag': self.package_tag_1.tag}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f'Tag already linked to {PackageTagViewSet.project_type}.']} + ) + + # Verify black-listed tag cannot be added + tag = TagFactory( + black_listed=True, + ) + response = self.client.post( + path=self.api_path, + data={'tag': tag.name}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a tag + response = self.client.delete( + path=self.api_path + f'{self.package_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a tag + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.package_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a tag + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.package_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a tag + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.package_tag_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Tag', + ) diff --git a/project_manager/packages/api/tests/test_release_views.py b/project_manager/packages/api/tests/test_release_views.py new file mode 100644 index 00000000..4a8870b3 --- /dev/null +++ b/project_manager/packages/api/tests/test_release_views.py @@ -0,0 +1,591 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path + +# Third Party Django +from rest_framework import status +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ProjectReleaseViewSet +from project_manager.packages.api.serializers import PackageReleaseSerializer +from project_manager.packages.api.views import PackageReleaseViewSet +from project_manager.packages.models import ( + Package, + PackageRelease, + PackageReleaseDownloadRequirement, + PackageReleasePackageRequirement, + PackageReleasePyPiRequirement, + PackageReleaseVersionControlRequirement, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.packages import ( + PackageFactory, + PackageContributorFactory, + PackageReleaseFactory, +) +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PackageReleaseViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = package = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.package = PackageFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/packages/releases/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.contributor = ForumUserFactory() + PackageContributorFactory( + package=cls.package, + user=cls.contributor, + ) + cls.package_release = PackageReleaseFactory( + package=cls.package, + zip_file='release_v1.0.0.zip', + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PackageReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PackageReleaseViewSet.serializer_class, + second=PackageReleaseSerializer, + ) + self.assertEqual( + first=PackageReleaseViewSet.project_type, + second='package', + ) + self.assertEqual( + first=PackageReleaseViewSet.project_model, + second=Package, + ) + self.assertIs( + expr1=PackageReleaseViewSet.queryset.model, + expr2=PackageRelease, + ) + self.assertDictEqual( + d1=PackageReleaseViewSet.queryset.query.select_related, + d2={'package': {}}, + ) + prefetch_lookups = PackageReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='packagereleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PackageReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PackageReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + timestamp = self.package_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' + payload = { + 'notes': self.package_release.notes, + 'zip_file': zip_file, + 'version': self.package_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.package_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.package_release.id), + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.package_release.version}/' + response = self.client.get(path=api_path) + timestamp = self.package_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' + payload = { + 'notes': self.package_release.notes, + 'zip_file': zip_file, + 'version': self.package_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.package_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.package_release.id), + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid package_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + package = PackageFactory( + basename='test_package', + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + PackageContributorFactory( + package=package, + user=self.contributor, + ) + api_path = f'{self.base_api_path}{package.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' + + # Verify that non logged in user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.contributor.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=package.releases.count(), + second=2, + ) + content = response.json() + release = package.releases.get(id=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that owner can create a release + version = '1.0.2' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.owner.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=package.releases.count(), + second=3, + ) + content = response.json() + release = package.releases.get(id=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that the same version cannot be created twice + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'version': ['Given version matches existing version.']}, + ) + + # Verify that the basename in the zip file is being verified against + # the basename from the url path + zip_basename = package.basename + package = PackageFactory( + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{package.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'zip_file': [ + f"Basename in zip '{zip_basename}' does not match basename" + f" for package '{package.basename}'.", + ], + } + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' + file_path = base_path / 'test-package' / 'test-package-requirements-v1.0.0.zip' + version = '1.0.1' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + package = PackageFactory( + basename='test_package', + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{package.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + release = PackageRelease.objects.get(pk=response.json()['id']) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.package} - Release', + ) diff --git a/project_manager/packages/api/tests/test_views.py b/project_manager/packages/api/tests/test_views.py index f3692bdf..a32b2e4c 100644 --- a/project_manager/packages/api/tests/test_views.py +++ b/project_manager/packages/api/tests/test_views.py @@ -1,82 +1,14 @@ # ============================================================================= # IMPORTS # ============================================================================= -# Python -import shutil -import tempfile - -# Django -from django.conf import settings -from django.core.files.uploadedfile import UploadedFile -from django.test import override_settings -from django.utils import formats - -# Third Party Python -from path import Path - # Third Party Django -from PIL import Image from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ( - ProjectAPIView, - ProjectContributorViewSet, - ProjectGameViewSet, - ProjectImageViewSet, - ProjectReleaseViewSet, - ProjectTagViewSet, - ProjectViewSet, -) -from project_manager.packages.api.filtersets import PackageFilterSet -from project_manager.packages.api.serializers import ( - PackageContributorSerializer, - PackageCreateSerializer, - PackageGameSerializer, - PackageImageSerializer, - PackageReleaseSerializer, - PackageSerializer, - PackageTagSerializer, -) -from project_manager.packages.api.views import ( - PackageAPIView, - PackageContributorViewSet, - PackageGameViewSet, - PackageImageViewSet, - PackageReleaseViewSet, - PackageTagViewSet, - PackageViewSet, -) -from project_manager.packages.models import ( - Package, - PackageContributor, - PackageGame, - PackageImage, - PackageRelease, - PackageReleaseDownloadRequirement, - PackageReleasePackageRequirement, - PackageReleasePyPiRequirement, - PackageReleaseVersionControlRequirement, - PackageTag, -) -from requirements.models import ( - DownloadRequirement, - PyPiRequirement, - VersionControlRequirement, -) -from test_utils.factories.games import GameFactory -from test_utils.factories.packages import ( - PackageFactory, - PackageContributorFactory, - PackageGameFactory, - PackageImageFactory, - PackageReleaseFactory, - PackageTagFactory, -) -from test_utils.factories.tags import TagFactory -from test_utils.factories.users import ForumUserFactory +from project_manager.common.api.views import ProjectAPIView +from project_manager.packages.api.views import PackageAPIView # ============================================================================= @@ -124,2388 +56,3 @@ def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual(first=response.json()['name'], second='Package APIs') - - -class PackageContributorViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = package = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.package = PackageFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/packages/contributors/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' - cls.contributor = ForumUserFactory() - cls.package_contributor = PackageContributorFactory( - package=cls.package, - user=cls.contributor, - ) - cls.new_contributor = ForumUserFactory() - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass( - PackageContributorViewSet, - ProjectContributorViewSet, - ), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PackageContributorViewSet.serializer_class, - second=PackageContributorSerializer, - ) - self.assertEqual( - first=PackageContributorViewSet.project_type, - second='package', - ) - self.assertEqual( - first=PackageContributorViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageContributorViewSet.queryset.model, - expr2=PackageContributor, - ) - self.assertDictEqual( - d1=PackageContributorViewSet.queryset.query.select_related, - d2={'user': {'user': {}}, 'package': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PackageContributorViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - user = self.contributor - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - user = self.contributor - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that contributors can see results but not 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - 'id': str(self.package_contributor.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.package_contributor.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors cannot see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'user': { - 'forum_id': self.package_contributor.user.forum_id, - 'username': self.package_contributor.user.user.username, - }, - 'id': str(self.package_contributor.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid package_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a contributor - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a contributor - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor cannot add a contributor - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that owner can add a contributor - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing contributor cannot be added - response = self.client.post( - path=self.api_path, - data={'username': self.contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, - ) - - # Verify owner cannot be added - response = self.client.post( - path=self.api_path, - data={'username': self.owner.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, - ) - - # Verify unknown username cannot be added - invalid_username = 'invalid' - response = self.client.post( - path=self.api_path, - data={'username': invalid_username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'No user named "{invalid_username}".']}, - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a contributor - response = self.client.delete( - path=self.api_path + f'{self.package_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a contributor - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.package_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor cannot delete a contributor - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.package_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that owner can delete a contributor - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.package_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.package} - Contributor', - ) - - -class PackageGameViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = game_1 = game_2 = package = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.package = PackageFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/packages/games/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' - cls.contributor = ForumUserFactory() - PackageContributorFactory( - package=cls.package, - user=cls.contributor, - ) - cls.game_1 = GameFactory( - name='Game1', - basename='game1', - icon='icon1.jpg', - ) - cls.game_2 = GameFactory( - name='Game2', - basename='game2', - icon='icon2.jpg', - ) - cls.game_3 = GameFactory( - name='Game3', - basename='game3', - icon='icon3.jpg', - ) - cls.game_4 = GameFactory( - name='Game4', - basename='game4', - icon='icon4.jpg', - ) - cls.package_game_1 = PackageGameFactory( - package=cls.package, - game=cls.game_1, - ) - cls.package_game_2 = PackageGameFactory( - package=cls.package, - game=cls.game_2, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PackageGameViewSet, ProjectGameViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PackageGameViewSet.serializer_class, - second=PackageGameSerializer, - ) - self.assertEqual( - first=PackageGameViewSet.project_type, - second='package', - ) - self.assertEqual( - first=PackageGameViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageGameViewSet.queryset.model, - expr2=PackageGame, - ) - self.assertDictEqual( - d1=PackageGameViewSet.queryset.query.select_related, - d2={'game': {}, 'package': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PackageGameViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - 'id': str(self.package_game_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - 'id': str(self.package_game_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.package_game_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - request = response.wsgi_request - icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'game': { - 'name': self.game_1.name, - 'slug': self.game_1.slug, - 'icon': icon, - }, - 'id': str(self.package_game_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'game': { - 'name': self.game_1.name, - 'slug': self.game_1.slug, - 'icon': icon, - }, - 'id': str(self.package_game_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid package_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a game - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a game - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a game - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a game - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_4.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing affiliated game cannot be added - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_1.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'game': [f'Game already linked to {PackageGameViewSet.project_type}.']} - ) - - # Verify non-existing game cannot be added - invalid_slug = 'invalid' - response = self.client.post( - path=self.api_path, - data={'game_slug': invalid_slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'game': [f'Invalid game "{invalid_slug}".']} - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a game - response = self.client.delete( - path=self.api_path + f'{self.package_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a game - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.package_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a game - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.package_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a game - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.package_game_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.package} - Game', - ) - - -class PackageImageViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = package = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.package = PackageFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/packages/images/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' - cls.contributor = ForumUserFactory() - PackageContributorFactory( - package=cls.package, - user=cls.contributor, - ) - cls.package_image_1 = PackageImageFactory( - package=cls.package, - ) - cls.package_image_2 = PackageImageFactory( - package=cls.package, - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PackageImageViewSet, ProjectImageViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PackageImageViewSet.serializer_class, - second=PackageImageSerializer, - ) - self.assertEqual( - first=PackageImageViewSet.project_type, - second='package', - ) - self.assertEqual( - first=PackageImageViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageImageViewSet.queryset.model, - expr2=PackageImage, - ) - self.assertDictEqual( - d1=PackageImageViewSet.queryset.query.select_related, - d2={'package': {}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PackageImageViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that a non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.package_image_2.image.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - 'id': str(self.package_image_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - 'id': str(self.package_image_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.package_image_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.package_image_1.image.url}' - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'image': image, - 'id': str(self.package_image_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'image': image, - 'id': str(self.package_image_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid package_slug.'}, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - # Verify that regular user cannot add an image - self.client.force_login(self.regular_user.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add an image - self.client.force_login(self.contributor.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add an image - self.client.force_login(self.owner.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_delete(self): - # Verify that regular user cannot delete an image - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.package_image_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete an image - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.package_image_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete an image - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.package_image_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.package} - Image', - ) - - -class PackageReleaseViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = package = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.package = PackageFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/packages/releases/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' - cls.contributor = ForumUserFactory() - PackageContributorFactory( - package=cls.package, - user=cls.contributor, - ) - cls.package_release = PackageReleaseFactory( - package=cls.package, - zip_file='release_v1.0.0.zip', - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PackageReleaseViewSet, ProjectReleaseViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PackageReleaseViewSet.serializer_class, - second=PackageReleaseSerializer, - ) - self.assertEqual( - first=PackageReleaseViewSet.project_type, - second='package', - ) - self.assertEqual( - first=PackageReleaseViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageReleaseViewSet.queryset.model, - expr2=PackageRelease, - ) - self.assertDictEqual( - d1=PackageReleaseViewSet.queryset.query.select_related, - d2={'package': {}}, - ) - prefetch_lookups = PackageReleaseViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=4) - - lookup = prefetch_lookups[0] - self.assertEqual( - first=lookup.prefetch_to, - second='packagereleasepackagerequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleasePackageRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('package_requirement__name',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'package_requirement': {}}, - ) - - lookup = prefetch_lookups[1] - self.assertEqual( - first=lookup.prefetch_to, - second='packagereleasedownloadrequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleaseDownloadRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('download_requirement__url',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'download_requirement': {}}, - ) - - lookup = prefetch_lookups[2] - self.assertEqual( - first=lookup.prefetch_to, - second='packagereleasepypirequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleasePyPiRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('pypi_requirement__name',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'pypi_requirement': {}}, - ) - - lookup = prefetch_lookups[3] - self.assertEqual( - first=lookup.prefetch_to, - second='packagereleaseversioncontrolrequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PackageReleaseVersionControlRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('vcs_requirement__url',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'vcs_requirement': {}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PackageReleaseViewSet.http_method_names, - tuple2=('get', 'post', 'options'), - ) - - def test_get_list(self): - # Verify that a non logged in user can see results - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - timestamp = self.package_release.created - request = response.wsgi_request - zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' - payload = { - 'notes': self.package_release.notes, - 'zip_file': zip_file, - 'version': self.package_release.version, - 'created': { - 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'download_count': self.package_release.download_count, - 'download_requirements': [], - 'package_requirements': [], - 'pypi_requirements': [], - 'vcs_requirements': [], - 'id': str(self.package_release.id), - } - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that regular user can see results - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that contributors can see results - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that the owner can see results - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - def test_get_details(self): - # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.package_release.version}/' - response = self.client.get(path=api_path) - timestamp = self.package_release.created - request = response.wsgi_request - zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' - payload = { - 'notes': self.package_release.notes, - 'zip_file': zip_file, - 'version': self.package_release.version, - 'created': { - 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'download_count': self.package_release.download_count, - 'download_requirements': [], - 'package_requirements': [], - 'pypi_requirements': [], - 'vcs_requirements': [], - 'id': str(self.package_release.id), - } - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that regular user can see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid package_slug.'}, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - package = PackageFactory( - basename='test_package', - owner=self.owner, - ) - PackageReleaseFactory( - package=package, - version='1.0.0', - ) - PackageContributorFactory( - package=package, - user=self.contributor, - ) - api_path = f'{self.base_api_path}{package.slug}/' - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' - file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' - - # Verify that non logged in user cannot create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.contributor.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=package.releases.count(), - second=2, - ) - content = response.json() - release = package.releases.get(id=content['id']) - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify that owner can create a release - version = '1.0.2' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.owner.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=package.releases.count(), - second=3, - ) - content = response.json() - release = package.releases.get(id=content['id']) - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify that the same version cannot be created twice - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'version': ['Given version matches existing version.']}, - ) - - # Verify that the basename in the zip file is being verified against - # the basename from the url path - zip_basename = package.basename - package = PackageFactory( - owner=self.owner, - ) - PackageReleaseFactory( - package=package, - version='1.0.0', - ) - api_path = f'{self.base_api_path}{package.slug}/' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'zip_file': [ - f"Basename in zip '{zip_basename}' does not match basename" - f" for package '{package.basename}'.", - ], - } - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post_with_requirements(self): - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' - file_path = base_path / 'test-package' / 'test-package-requirements-v1.0.0.zip' - version = '1.0.1' - custom_package_1 = PackageFactory( - basename='custom_package_1', - ) - PackageReleaseFactory( - package=custom_package_1, - version='1.0.0', - ) - custom_package_2 = PackageFactory( - basename='custom_package_2', - ) - PackageReleaseFactory( - package=custom_package_2, - version='1.0.0', - ) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=0, - ) - self.client.force_login(self.owner.user) - package = PackageFactory( - basename='test_package', - owner=self.owner, - ) - PackageReleaseFactory( - package=package, - version='1.0.0', - ) - api_path = f'{self.base_api_path}{package.slug}/' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - release = PackageRelease.objects.get(pk=response.json()['id']) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.download_requirements.count(), - second=2, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.pypi_requirements.count(), - second=2, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.vcs_requirements.count(), - second=2, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.package} - Release', - ) - - -class PackageTagViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = package = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.package = PackageFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/packages/tags/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' - cls.contributor = ForumUserFactory() - PackageContributorFactory( - package=cls.package, - user=cls.contributor, - ) - cls.package_tag_1 = PackageTagFactory( - package=cls.package, - ) - cls.package_tag_2 = PackageTagFactory( - package=cls.package, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue(expr=issubclass(PackageTagViewSet, ProjectTagViewSet)) - - def test_base_attributes(self): - self.assertEqual( - first=PackageTagViewSet.serializer_class, - second=PackageTagSerializer, - ) - self.assertEqual( - first=PackageTagViewSet.project_type, - second='package', - ) - self.assertEqual( - first=PackageTagViewSet.project_model, - second=Package, - ) - self.assertIs( - expr1=PackageTagViewSet.queryset.model, - expr2=PackageTag, - ) - self.assertDictEqual( - d1=PackageTagViewSet.queryset.query.select_related, - d2={'tag': {}, 'package': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PackageTagViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.package_tag_2.tag.name, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.package_tag_2.tag.name, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.package_tag_2.tag.name, - 'id': str(self.package_tag_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.package_tag_2.tag.name, - 'id': str(self.package_tag_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.package_tag_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'tag': self.package_tag_1.tag.name, - 'id': str(self.package_tag_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'tag': self.package_tag_1.tag.name, - 'id': str(self.package_tag_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid package_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a tag - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a tag - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a tag - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a tag - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-2'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing affiliated tag cannot be added - response = self.client.post( - path=self.api_path, - data={'tag': self.package_tag_1.tag}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'tag': [f'Tag already linked to {PackageTagViewSet.project_type}.']} - ) - - # Verify black-listed tag cannot be added - tag = TagFactory( - black_listed=True, - ) - response = self.client.post( - path=self.api_path, - data={'tag': tag.name}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a tag - response = self.client.delete( - path=self.api_path + f'{self.package_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a tag - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.package_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a tag - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.package_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a tag - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.package_tag_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.package} - Tag', - ) - - -class PackageViewSetTestCase(APITestCase): - - contributor = owner = package = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.package = PackageFactory( - owner=cls.owner, - logo='logo.jpg', - ) - cls.package_release = PackageReleaseFactory( - package=cls.package, - zip_file='/media/release_v1.0.0.zip', - ) - cls.api_path = f'/api/packages/projects/' - cls.contributor = ForumUserFactory() - PackageContributorFactory( - package=cls.package, - user=cls.contributor, - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue(expr=issubclass(PackageViewSet, ProjectViewSet)) - - def test_base_attributes(self): - self.assertEqual( - first=PackageViewSet.filterset_class, - second=PackageFilterSet, - ) - self.assertEqual( - first=PackageViewSet.serializer_class, - second=PackageSerializer, - ) - self.assertEqual( - first=PackageViewSet.creation_serializer_class, - second=PackageCreateSerializer, - ) - self.assertIs(expr1=PackageViewSet.queryset.model, expr2=Package) - prefetch_lookups = PackageViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=1) - lookup = prefetch_lookups[0] - self.assertEqual(first=lookup.prefetch_to, second='releases') - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('-created',), - ) - self.assertDictEqual( - d1=PackageViewSet.queryset.query.select_related, - d2={'owner': {'user': {}}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PackageViewSet.http_method_names, - tuple2=('get', 'post', 'patch', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - request = response.wsgi_request - domain = f'{request.scheme}://{request.get_host()}' - zip_file = f'{domain}{self.package_release.get_absolute_url()}' - logo = f'{domain}{self.package.logo.url}' - created_timestamp = self.package.created - updated_timestamp = self.package.updated - payload = { - 'name': self.package.name, - 'slug': self.package.slug, - 'total_downloads': self.package.total_downloads, - 'current_release': { - 'version': self.package_release.version, - 'notes': self.package_release.notes, - 'zip_file': zip_file, - }, - 'created': { - 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'updated': { - 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'synopsis': self.package.synopsis, - 'description': self.package.description, - 'configuration': self.package.configuration, - 'logo': logo, - 'video': self.package.video, - 'owner': { - 'forum_id': self.package.owner.forum_id, - 'username': self.package.owner.user.username, - } - } - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - def test_get_list_filters(self): - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate tag filtering - response = self.client.get(path=f'{self.api_path}?tag=test_tag') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - tag = TagFactory(name='test_tag') - PackageTagFactory( - package=self.package, - tag=tag, - ) - response = self.client.get(path=f'{self.api_path}?tag=test_tag') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate game filtering - response = self.client.get(path=f'{self.api_path}?game=game1') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - game = GameFactory( - name='Game1', - basename='game1', - icon='icon1.jpg', - ) - PackageGameFactory( - package=self.package, - game=game, - ) - response = self.client.get(path=f'{self.api_path}?game=game1') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate game filtering - response = self.client.get( - path=f'{self.api_path}?user={self.regular_user.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - response = self.client.get( - path=f'{self.api_path}?user={self.contributor.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - response = self.client.get( - path=f'{self.api_path}?user={self.owner.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - def test_get_details(self): - # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.package.slug}/' - response = self.client.get(path=api_path) - request = response.wsgi_request - domain = f'{request.scheme}://{request.get_host()}' - zip_file = f'{domain}{self.package_release.get_absolute_url()}' - logo = f'{domain}{self.package.logo.url}' - created_timestamp = self.package.created - updated_timestamp = self.package.updated - payload = { - 'name': self.package.name, - 'slug': self.package.slug, - 'total_downloads': self.package.total_downloads, - 'current_release': { - 'version': self.package_release.version, - 'notes': self.package_release.notes, - 'zip_file': zip_file, - 'package_requirements': [], - 'pypi_requirements': [], - 'version_control_requirements': [], - 'download_requirements': [], - }, - 'created': { - 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'updated': { - 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'synopsis': self.package.synopsis, - 'description': self.package.description, - 'configuration': self.package.configuration, - 'logo': logo, - 'video': self.package.video, - 'owner': { - 'forum_id': self.package.owner.forum_id, - 'username': self.package.owner.user.username, - } - } - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that regular user can see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - # Verify non logged in user cannot create a package - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' - file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' - version = '1.0.0' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Package', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that a logged in user can create a package - self.assertEqual( - first=Package.objects.count(), - second=1, - ) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Package', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=Package.objects.count(), - second=2, - ) - content = response.json() - package = Package.objects.get(slug=content['slug']) - self.assertEqual( - first=package.releases.count(), - second=1, - ) - release = package.releases.get() - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify cannot create a package where the basename already exists - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Package', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'basename': 'Package already exists. Cannot create.'} - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post_with_requirements(self): - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' - file_path = base_path / 'test-package' / 'test-package-requirements-v1.0.0.zip' - version = '1.0.0' - custom_package_1 = PackageFactory( - basename='custom_package_1', - ) - PackageReleaseFactory( - package=custom_package_1, - version='1.0.0', - ) - custom_package_2 = PackageFactory( - basename='custom_package_2', - ) - PackageReleaseFactory( - package=custom_package_2, - version='1.0.0', - ) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=0, - ) - self.client.force_login(self.owner.user) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Package', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - contents = response.json() - package = Package.objects.get(slug=contents['slug']) - release = PackageRelease.objects.get(package=package) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.download_requirements.count(), - second=2, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.pypi_requirements.count(), - second=2, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.vcs_requirements.count(), - second=2, - ) - - def test_patch(self): - # Verify that non logged in user cannot update a path - api_path = f'{self.api_path}{self.package.slug}/' - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot update a path - self.client.force_login(self.regular_user.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can update a path - self.client.force_login(self.contributor.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - # Verify that owner can update a path - self.client.force_login(self.owner.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'New Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second='Package List', - ) diff --git a/project_manager/plugins/api/tests/test_project_views.py b/project_manager/plugins/api/tests/test_project_views.py new file mode 100644 index 00000000..9e7be739 --- /dev/null +++ b/project_manager/plugins/api/tests/test_project_views.py @@ -0,0 +1,616 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path + +# Third Party Django +from rest_framework import status +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ProjectViewSet +from project_manager.plugins.api.filtersets import PluginFilterSet +from project_manager.plugins.api.serializers import ( + PluginCreateSerializer, + PluginSerializer, +) +from project_manager.plugins.api.views import PluginViewSet +from project_manager.plugins.models import ( + Plugin, + PluginRelease, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.games import GameFactory +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory +from test_utils.factories.plugins import ( + PluginContributorFactory, + PluginFactory, + PluginGameFactory, + PluginReleaseFactory, + PluginTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginViewSetTestCase(APITestCase): + + contributor = owner = plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + logo='logo.jpg', + ) + cls.plugin_release = PluginReleaseFactory( + plugin=cls.plugin, + zip_file='/media/release_v1.0.0.zip', + ) + cls.api_path = f'/api/plugins/projects/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginViewSet.filterset_class, + second=PluginFilterSet, + ) + self.assertEqual( + first=PluginViewSet.serializer_class, + second=PluginSerializer, + ) + self.assertEqual( + first=PluginViewSet.creation_serializer_class, + second=PluginCreateSerializer, + ) + self.assertIs(expr1=PluginViewSet.queryset.model, expr2=Plugin) + prefetch_lookups = PluginViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=PluginViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.plugin_release.get_absolute_url()}' + logo = f'{domain}{self.plugin.logo.url}' + self.assertEqual(first=content['count'], second=1) + created_timestamp = self.plugin.created + updated_timestamp = self.plugin.updated + payload = { + 'name': self.plugin.name, + 'slug': self.plugin.slug, + 'total_downloads': self.plugin.total_downloads, + 'current_release': { + 'version': self.plugin_release.version, + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.plugin.synopsis, + 'description': self.plugin.description, + 'configuration': self.plugin.configuration, + 'logo': logo, + 'video': self.plugin.video, + 'owner': { + 'forum_id': self.plugin.owner.forum_id, + 'username': self.plugin.owner.user.username, + } + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_list_filters(self): + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate tag filtering + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + tag = TagFactory(name='test_tag') + PluginTagFactory( + plugin=self.plugin, + tag=tag, + ) + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + game = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + PluginGameFactory( + plugin=self.plugin, + game=game, + ) + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get( + path=f'{self.api_path}?user={self.regular_user.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.contributor.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.owner.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.plugin.slug}/' + response = self.client.get(path=api_path) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.plugin_release.get_absolute_url()}' + logo = f'{domain}{self.plugin.logo.url}' + created_timestamp = self.plugin.created + updated_timestamp = self.plugin.updated + payload = { + 'name': self.plugin.name, + 'slug': self.plugin.slug, + 'total_downloads': self.plugin.total_downloads, + 'current_release': { + 'version': self.plugin_release.version, + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + 'package_requirements': [], + 'pypi_requirements': [], + 'version_control_requirements': [], + 'download_requirements': [], + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.plugin.synopsis, + 'description': self.plugin.description, + 'configuration': self.plugin.configuration, + 'logo': logo, + 'video': self.plugin.video, + 'owner': { + 'forum_id': self.plugin.owner.forum_id, + 'username': self.plugin.owner.user.username, + } + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify non logged in user cannot create a plugin + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' + version = '1.0.0' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that a logged in user can create a plugin + self.assertEqual( + first=Plugin.objects.count(), + second=1, + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=Plugin.objects.count(), + second=2, + ) + content = response.json() + plugin = Plugin.objects.get(slug=content['slug']) + self.assertEqual( + first=plugin.releases.count(), + second=1, + ) + release = plugin.releases.get() + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify cannot create a plugin where the basename already exists + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'basename': 'Plugin already exists. Cannot create.'} + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-requirements-v1.0.0.zip' + version = '1.0.0' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=self.api_path, + data={ + 'name': 'Test Plugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + contents = response.json() + plugin = Plugin.objects.get(slug=contents['slug']) + release = PluginRelease.objects.get(plugin=plugin) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.plugin.slug}/' + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'New Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second='Plugin List', + ) diff --git a/project_manager/plugins/api/tests/test_related_views.py b/project_manager/plugins/api/tests/test_related_views.py new file mode 100644 index 00000000..a59a1bbb --- /dev/null +++ b/project_manager/plugins/api/tests/test_related_views.py @@ -0,0 +1,1720 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.test import override_settings + +# Third Party Python +from path import Path + +# Third Party Django +from PIL import Image +from rest_framework import status +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ( + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectTagViewSet, +) +from project_manager.common.api.views.mixins import ProjectThroughModelMixin +from project_manager.plugins.api.serializers import ( + PluginContributorSerializer, + PluginGameSerializer, + PluginImageSerializer, + PluginTagSerializer, + SubPluginPathSerializer, +) +from project_manager.plugins.api.views import ( + PluginContributorViewSet, + PluginGameViewSet, + PluginImageViewSet, + PluginTagViewSet, + SubPluginPathViewSet, +) +from project_manager.plugins.models import ( + Plugin, + PluginContributor, + PluginGame, + PluginImage, + PluginTag, + SubPluginPath, +) +from test_utils.factories.games import GameFactory +from test_utils.factories.plugins import ( + PluginContributorFactory, + PluginFactory, + PluginGameFactory, + PluginImageFactory, + PluginTagFactory, + SubPluginPathFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginContributorViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/contributors/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + cls.plugin_contributor = PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.new_contributor = ForumUserFactory() + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass( + PluginContributorViewSet, + ProjectContributorViewSet, + ), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginContributorViewSet.serializer_class, + second=PluginContributorSerializer, + ) + self.assertEqual( + first=PluginContributorViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginContributorViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginContributorViewSet.queryset.model, + expr2=PluginContributor, + ) + self.assertDictEqual( + d1=PluginContributorViewSet.queryset.query.select_related, + d2={'user': {'user': {}}, 'plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginContributorViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that contributors can see results but not 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + 'id': str(self.plugin_contributor.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_contributor.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors cannot see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'user': { + 'forum_id': self.plugin_contributor.user.forum_id, + 'username': self.plugin_contributor.user.user.username, + }, + 'id': str(self.plugin_contributor.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a contributor + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a contributor + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot add a contributor + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can add a contributor + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing contributor cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, + ) + + # Verify owner cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.owner.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, + ) + + # Verify unknown username cannot be added + invalid_username = 'invalid' + response = self.client.post( + path=self.api_path, + data={'username': invalid_username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'No user named "{invalid_username}".']}, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a contributor + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can delete a contributor + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Contributor', + ) + + +class PluginGameViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = game_1 = game_2 = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/games/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.game_1 = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + cls.game_2 = GameFactory( + name='Game2', + basename='game2', + icon='icon2.jpg', + ) + cls.game_3 = GameFactory( + name='Game3', + basename='game3', + icon='icon3.jpg', + ) + cls.game_4 = GameFactory( + name='Game4', + basename='game4', + icon='icon4.jpg', + ) + cls.plugin_game_1 = PluginGameFactory( + plugin=cls.plugin, + game=cls.game_1, + ) + cls.plugin_game_2 = PluginGameFactory( + plugin=cls.plugin, + game=cls.game_2, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginGameViewSet, ProjectGameViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginGameViewSet.serializer_class, + second=PluginGameSerializer, + ) + self.assertEqual( + first=PluginGameViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginGameViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginGameViewSet.queryset.model, + expr2=PluginGame, + ) + self.assertDictEqual( + d1=PluginGameViewSet.queryset.query.select_related, + d2={'game': {}, 'plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginGameViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_game_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.plugin_game_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a game + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a game + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a game + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a game + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_4.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated game cannot be added + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_1.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Game already linked to {PluginGameViewSet.project_type}.']} + ) + + # Verify non-existing game cannot be added + invalid_slug = 'invalid' + response = self.client.post( + path=self.api_path, + data={'game_slug': invalid_slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Invalid game "{invalid_slug}".']} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a game + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a game + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a game + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a game + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_game_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Game', + ) + + +class PluginImageViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/images/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.plugin_image_1 = PluginImageFactory( + plugin=cls.plugin, + ) + cls.plugin_image_2 = PluginImageFactory( + plugin=cls.plugin, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginImageViewSet.serializer_class, + second=PluginImageSerializer, + ) + self.assertEqual( + first=PluginImageViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginImageViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginImageViewSet.queryset.model, + expr2=PluginImage, + ) + self.assertDictEqual( + d1=PluginImageViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginImageViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.plugin_image_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.plugin_image_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_image_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.plugin_image_1.image.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.plugin_image_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.plugin_image_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify that regular user cannot add an image + self.client.force_login(self.regular_user.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add an image + self.client.force_login(self.contributor.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add an image + self.client.force_login(self.owner.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_delete(self): + # Verify that regular user cannot delete an image + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete an image + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete an image + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_image_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Image', + ) + + +class PluginTagViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/tags/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.plugin_tag_1 = PluginTagFactory( + plugin=cls.plugin, + ) + cls.plugin_tag_2 = PluginTagFactory( + plugin=cls.plugin, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(PluginTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=PluginTagViewSet.serializer_class, + second=PluginTagSerializer, + ) + self.assertEqual( + first=PluginTagViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginTagViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginTagViewSet.queryset.model, + expr2=PluginTag, + ) + self.assertDictEqual( + d1=PluginTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginTagViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + 'id': str(self.plugin_tag_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.plugin_tag_2.tag.name, + 'id': str(self.plugin_tag_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.plugin_tag_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.plugin_tag_1.tag.name, + 'id': str(self.plugin_tag_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.plugin_tag_1.tag.name, + 'id': str(self.plugin_tag_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a tag + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a tag + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a tag + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a tag + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-2'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated tag cannot be added + response = self.client.post( + path=self.api_path, + data={'tag': self.plugin_tag_1.tag}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f'Tag already linked to {PluginTagViewSet.project_type}.']} + ) + + # Verify black-listed tag cannot be added + tag = TagFactory( + black_listed=True, + ) + response = self.client.post( + path=self.api_path, + data={'tag': tag.name}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a tag + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a tag + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a tag + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a tag + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.plugin_tag_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Tag', + ) + + +class SubPluginPathViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/paths/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.sub_plugin_path_1 = SubPluginPathFactory( + plugin=cls.plugin, + allow_module=True, + ) + cls.sub_plugin_path_2 = SubPluginPathFactory( + plugin=cls.plugin, + allow_package_using_basename=True, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginPathViewSet, ProjectThroughModelMixin), + ) + + def test_base_attributes(self): + self.assertTupleEqual( + tuple1=SubPluginPathViewSet.ordering, + tuple2=('path',), + ) + self.assertEqual( + first=SubPluginPathViewSet.serializer_class, + second=SubPluginPathSerializer, + ) + self.assertEqual( + first=SubPluginPathViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=SubPluginPathViewSet.project_model, + second=Plugin, + ) + self.assertEqual( + first=SubPluginPathViewSet.related_model_type, + second='Sub-Plugin Path', + ) + self.assertIs( + expr1=SubPluginPathViewSet.queryset.model, + expr2=SubPluginPath, + ) + self.assertDictEqual( + d1=SubPluginPathViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginPathViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_path_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'path': self.sub_plugin_path_1.path, + 'allow_module': self.sub_plugin_path_1.allow_module, + 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, + 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, + 'id': str(self.sub_plugin_path_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a path + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-1', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': False, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a path + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-1', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': False, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a path + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-1', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': False, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a path + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={ + 'path': 'new-path-2', + 'allow_module': False, + 'allow_package_using_init': True, + 'allow_package_using_basename': True, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.sub_plugin_path_1.id}/' + response = self.client.patch( + path=api_path, + data={ + 'allow_module': False, + 'allow_package_using_init': True, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'allow_module': False, + 'allow_package_using_init': True, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'allow_module': False, + 'allow_package_using_init': True, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'allow_module': True, + 'allow_package_using_init': False, + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a path + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a path + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a path + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a path + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_path_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Sub-Plugin Path', + ) diff --git a/project_manager/plugins/api/tests/test_release_views.py b/project_manager/plugins/api/tests/test_release_views.py new file mode 100644 index 00000000..2c93ddd2 --- /dev/null +++ b/project_manager/plugins/api/tests/test_release_views.py @@ -0,0 +1,590 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path + +# Third Party Django +from rest_framework import status +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ProjectReleaseViewSet +from project_manager.plugins.api.serializers import PluginReleaseSerializer +from project_manager.plugins.api.views import PluginReleaseViewSet +from project_manager.plugins.models import ( + Plugin, + PluginRelease, + PluginReleaseDownloadRequirement, + PluginReleasePackageRequirement, + PluginReleasePyPiRequirement, + PluginReleaseVersionControlRequirement, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory +from test_utils.factories.plugins import ( + PluginContributorFactory, + PluginFactory, + PluginReleaseFactory, +) +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class PluginReleaseViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory( + owner=cls.owner, + ) + cls.base_api_path = f'/api/plugins/releases/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.contributor = ForumUserFactory() + PluginContributorFactory( + plugin=cls.plugin, + user=cls.contributor, + ) + cls.plugin_release = PluginReleaseFactory( + plugin=cls.plugin, + zip_file='release_v1.0.0.zip', + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(PluginReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=PluginReleaseViewSet.serializer_class, + second=PluginReleaseSerializer, + ) + self.assertEqual( + first=PluginReleaseViewSet.project_type, + second='plugin', + ) + self.assertEqual( + first=PluginReleaseViewSet.project_model, + second=Plugin, + ) + self.assertIs( + expr1=PluginReleaseViewSet.queryset.model, + expr2=PluginRelease, + ) + self.assertDictEqual( + d1=PluginReleaseViewSet.queryset.query.select_related, + d2={'plugin': {}}, + ) + prefetch_lookups = PluginReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='pluginreleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=PluginReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=PluginReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + timestamp = self.plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' + payload = { + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + 'version': self.plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.plugin_release.id), + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.plugin_release.version}/' + response = self.client.get(path=api_path) + timestamp = self.plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' + payload = { + 'notes': self.plugin_release.notes, + 'zip_file': zip_file, + 'version': self.plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.plugin_release.id), + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + plugin = PluginFactory( + basename='test_plugin', + owner=self.owner, + ) + PluginReleaseFactory( + plugin=plugin, + version='1.0.0', + ) + PluginContributorFactory( + plugin=plugin, + user=self.contributor, + ) + api_path = f'{self.base_api_path}{plugin.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' + + # Verify that non logged in user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.contributor.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=plugin.releases.count(), + second=2, + ) + content = response.json() + release = plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that owner can create a release + version = '1.0.2' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.owner.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=plugin.releases.count(), + second=3, + ) + content = response.json() + release = plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that the same version cannot be created twice + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'version': ['Given version matches existing version.']}, + ) + + # Verify that the basename in the zip file is being verified against + # the basename from the url path + zip_basename = plugin.basename + plugin = PluginFactory( + owner=self.owner, + ) + PluginReleaseFactory( + plugin=plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'zip_file': [ + f"Basename in zip '{zip_basename}' does not match basename" + f" for plugin '{plugin.basename}'.", + ], + } + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' + file_path = base_path / 'test-plugin' / 'test-plugin-requirements-v1.0.0.zip' + version = '1.0.1' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + plugin = PluginFactory( + basename='test_plugin', + owner=self.owner, + ) + PluginReleaseFactory( + plugin=plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + release = PluginRelease.objects.get(pk=response.json()['id']) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.plugin} - Release', + ) diff --git a/project_manager/plugins/api/tests/test_views.py b/project_manager/plugins/api/tests/test_views.py index eef0dedd..4a151c2a 100644 --- a/project_manager/plugins/api/tests/test_views.py +++ b/project_manager/plugins/api/tests/test_views.py @@ -1,88 +1,14 @@ # ============================================================================= # IMPORTS # ============================================================================= -# Python -import shutil -import tempfile - -# Django -from django.conf import settings -from django.core.files.uploadedfile import UploadedFile -from django.test import override_settings -from django.utils import formats - -# Third Party Python -from path import Path - # Third Party Django -from PIL import Image from rest_framework import status from rest_framework.reverse import reverse from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ( - ProjectAPIView, - ProjectContributorViewSet, - ProjectGameViewSet, - ProjectImageViewSet, - ProjectReleaseViewSet, - ProjectTagViewSet, - ProjectViewSet, -) -from project_manager.common.api.views.mixins import ProjectThroughModelMixin -from project_manager.plugins.api.filtersets import PluginFilterSet -from project_manager.plugins.api.serializers import ( - PluginContributorSerializer, - PluginCreateSerializer, - PluginGameSerializer, - PluginImageSerializer, - PluginReleaseSerializer, - PluginSerializer, - PluginTagSerializer, - SubPluginPathSerializer, -) -from project_manager.plugins.api.views import ( - PluginAPIView, - PluginContributorViewSet, - PluginGameViewSet, - PluginImageViewSet, - PluginReleaseViewSet, - PluginTagViewSet, - PluginViewSet, - SubPluginPathViewSet, -) -from project_manager.plugins.models import ( - Plugin, - PluginContributor, - PluginGame, - PluginImage, - PluginRelease, - PluginReleaseDownloadRequirement, - PluginReleasePackageRequirement, - PluginReleasePyPiRequirement, - PluginReleaseVersionControlRequirement, - PluginTag, - SubPluginPath, -) -from requirements.models import ( - DownloadRequirement, - PyPiRequirement, - VersionControlRequirement, -) -from test_utils.factories.games import GameFactory -from test_utils.factories.packages import PackageFactory, PackageReleaseFactory -from test_utils.factories.plugins import ( - PluginContributorFactory, - PluginFactory, - PluginGameFactory, - PluginImageFactory, - PluginReleaseFactory, - PluginTagFactory, - SubPluginPathFactory, -) -from test_utils.factories.tags import TagFactory -from test_utils.factories.users import ForumUserFactory +from project_manager.common.api.views import ProjectAPIView +from project_manager.plugins.api.views import PluginAPIView # ============================================================================= @@ -131,2769 +57,3 @@ def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual(first=response.json()['name'], second='Plugin APIs') - - -class PluginContributorViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/plugins/contributors/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' - cls.contributor = ForumUserFactory() - cls.plugin_contributor = PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.new_contributor = ForumUserFactory() - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass( - PluginContributorViewSet, - ProjectContributorViewSet, - ), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PluginContributorViewSet.serializer_class, - second=PluginContributorSerializer, - ) - self.assertEqual( - first=PluginContributorViewSet.project_type, - second='plugin', - ) - self.assertEqual( - first=PluginContributorViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginContributorViewSet.queryset.model, - expr2=PluginContributor, - ) - self.assertDictEqual( - d1=PluginContributorViewSet.queryset.query.select_related, - d2={'user': {'user': {}}, 'plugin': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PluginContributorViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - user = self.contributor - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - user = self.contributor - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that contributors can see results but not 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - 'id': str(self.plugin_contributor.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.plugin_contributor.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors cannot see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'user': { - 'forum_id': self.plugin_contributor.user.forum_id, - 'username': self.plugin_contributor.user.user.username, - }, - 'id': str(self.plugin_contributor.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a contributor - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a contributor - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor cannot add a contributor - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that owner can add a contributor - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing contributor cannot be added - response = self.client.post( - path=self.api_path, - data={'username': self.contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, - ) - - # Verify owner cannot be added - response = self.client.post( - path=self.api_path, - data={'username': self.owner.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, - ) - - # Verify unknown username cannot be added - invalid_username = 'invalid' - response = self.client.post( - path=self.api_path, - data={'username': invalid_username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'No user named "{invalid_username}".']}, - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a contributor - response = self.client.delete( - path=self.api_path + f'{self.plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a contributor - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor cannot delete a contributor - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that owner can delete a contributor - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.plugin} - Contributor', - ) - - -class PluginGameViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = game_1 = game_2 = plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/plugins/games/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' - cls.contributor = ForumUserFactory() - PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.game_1 = GameFactory( - name='Game1', - basename='game1', - icon='icon1.jpg', - ) - cls.game_2 = GameFactory( - name='Game2', - basename='game2', - icon='icon2.jpg', - ) - cls.game_3 = GameFactory( - name='Game3', - basename='game3', - icon='icon3.jpg', - ) - cls.game_4 = GameFactory( - name='Game4', - basename='game4', - icon='icon4.jpg', - ) - cls.plugin_game_1 = PluginGameFactory( - plugin=cls.plugin, - game=cls.game_1, - ) - cls.plugin_game_2 = PluginGameFactory( - plugin=cls.plugin, - game=cls.game_2, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PluginGameViewSet, ProjectGameViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PluginGameViewSet.serializer_class, - second=PluginGameSerializer, - ) - self.assertEqual( - first=PluginGameViewSet.project_type, - second='plugin', - ) - self.assertEqual( - first=PluginGameViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginGameViewSet.queryset.model, - expr2=PluginGame, - ) - self.assertDictEqual( - d1=PluginGameViewSet.queryset.query.select_related, - d2={'game': {}, 'plugin': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PluginGameViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - 'id': str(self.plugin_game_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - 'id': str(self.plugin_game_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.plugin_game_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - request = response.wsgi_request - icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'game': { - 'name': self.game_1.name, - 'slug': self.game_1.slug, - 'icon': icon, - }, - 'id': str(self.plugin_game_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'game': { - 'name': self.game_1.name, - 'slug': self.game_1.slug, - 'icon': icon, - }, - 'id': str(self.plugin_game_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a game - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a game - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a game - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a game - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_4.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing affiliated game cannot be added - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_1.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'game': [f'Game already linked to {PluginGameViewSet.project_type}.']} - ) - - # Verify non-existing game cannot be added - invalid_slug = 'invalid' - response = self.client.post( - path=self.api_path, - data={'game_slug': invalid_slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'game': [f'Invalid game "{invalid_slug}".']} - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a game - response = self.client.delete( - path=self.api_path + f'{self.plugin_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a game - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a game - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a game - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_game_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.plugin} - Game', - ) - - -class PluginImageViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/plugins/images/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' - cls.contributor = ForumUserFactory() - PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.plugin_image_1 = PluginImageFactory( - plugin=cls.plugin, - ) - cls.plugin_image_2 = PluginImageFactory( - plugin=cls.plugin, - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PluginImageViewSet, ProjectImageViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PluginImageViewSet.serializer_class, - second=PluginImageSerializer, - ) - self.assertEqual( - first=PluginImageViewSet.project_type, - second='plugin', - ) - self.assertEqual( - first=PluginImageViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginImageViewSet.queryset.model, - expr2=PluginImage, - ) - self.assertDictEqual( - d1=PluginImageViewSet.queryset.query.select_related, - d2={'plugin': {}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PluginImageViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - 'id': str(self.plugin_image_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - 'id': str(self.plugin_image_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.plugin_image_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.plugin_image_1.image.url}' - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'image': image, - 'id': str(self.plugin_image_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'image': image, - 'id': str(self.plugin_image_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid plugin_slug.'}, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - # Verify that regular user cannot add an image - self.client.force_login(self.regular_user.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add an image - self.client.force_login(self.contributor.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add an image - self.client.force_login(self.owner.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_delete(self): - # Verify that regular user cannot delete an image - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_image_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete an image - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_image_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete an image - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_image_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.plugin} - Image', - ) - - -class PluginReleaseViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/plugins/releases/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' - cls.contributor = ForumUserFactory() - PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.plugin_release = PluginReleaseFactory( - plugin=cls.plugin, - zip_file='release_v1.0.0.zip', - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(PluginReleaseViewSet, ProjectReleaseViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=PluginReleaseViewSet.serializer_class, - second=PluginReleaseSerializer, - ) - self.assertEqual( - first=PluginReleaseViewSet.project_type, - second='plugin', - ) - self.assertEqual( - first=PluginReleaseViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginReleaseViewSet.queryset.model, - expr2=PluginRelease, - ) - self.assertDictEqual( - d1=PluginReleaseViewSet.queryset.query.select_related, - d2={'plugin': {}}, - ) - prefetch_lookups = PluginReleaseViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=4) - - lookup = prefetch_lookups[0] - self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleasepackagerequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleasePackageRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('package_requirement__name',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'package_requirement': {}}, - ) - - lookup = prefetch_lookups[1] - self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleasedownloadrequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleaseDownloadRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('download_requirement__url',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'download_requirement': {}}, - ) - - lookup = prefetch_lookups[2] - self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleasepypirequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleasePyPiRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('pypi_requirement__name',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'pypi_requirement': {}}, - ) - - lookup = prefetch_lookups[3] - self.assertEqual( - first=lookup.prefetch_to, - second='pluginreleaseversioncontrolrequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=PluginReleaseVersionControlRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('vcs_requirement__url',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'vcs_requirement': {}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PluginReleaseViewSet.http_method_names, - tuple2=('get', 'post', 'options'), - ) - - def test_get_list(self): - # Verify that a non logged in user can see results - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - timestamp = self.plugin_release.created - request = response.wsgi_request - zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' - payload = { - 'notes': self.plugin_release.notes, - 'zip_file': zip_file, - 'version': self.plugin_release.version, - 'created': { - 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'download_count': self.plugin_release.download_count, - 'download_requirements': [], - 'package_requirements': [], - 'pypi_requirements': [], - 'vcs_requirements': [], - 'id': str(self.plugin_release.id), - } - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that regular user can see results - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that contributors can see results - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that the owner can see results - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - def test_get_details(self): - # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.plugin_release.version}/' - response = self.client.get(path=api_path) - timestamp = self.plugin_release.created - request = response.wsgi_request - zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' - payload = { - 'notes': self.plugin_release.notes, - 'zip_file': zip_file, - 'version': self.plugin_release.version, - 'created': { - 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'download_count': self.plugin_release.download_count, - 'download_requirements': [], - 'package_requirements': [], - 'pypi_requirements': [], - 'vcs_requirements': [], - 'id': str(self.plugin_release.id), - } - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that regular user can see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid plugin_slug.'}, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - plugin = PluginFactory( - basename='test_plugin', - owner=self.owner, - ) - PluginReleaseFactory( - plugin=plugin, - version='1.0.0', - ) - PluginContributorFactory( - plugin=plugin, - user=self.contributor, - ) - api_path = f'{self.base_api_path}{plugin.slug}/' - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' - file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' - - # Verify that non logged in user cannot create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.contributor.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=plugin.releases.count(), - second=2, - ) - content = response.json() - release = plugin.releases.get(pk=content['id']) - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify that owner can create a release - version = '1.0.2' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.owner.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=plugin.releases.count(), - second=3, - ) - content = response.json() - release = plugin.releases.get(pk=content['id']) - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify that the same version cannot be created twice - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'version': ['Given version matches existing version.']}, - ) - - # Verify that the basename in the zip file is being verified against - # the basename from the url path - zip_basename = plugin.basename - plugin = PluginFactory( - owner=self.owner, - ) - PluginReleaseFactory( - plugin=plugin, - version='1.0.0', - ) - api_path = f'{self.base_api_path}{plugin.slug}/' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'zip_file': [ - f"Basename in zip '{zip_basename}' does not match basename" - f" for plugin '{plugin.basename}'.", - ], - } - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post_with_requirements(self): - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' - file_path = base_path / 'test-plugin' / 'test-plugin-requirements-v1.0.0.zip' - version = '1.0.1' - custom_package_1 = PackageFactory( - basename='custom_package_1', - ) - PackageReleaseFactory( - package=custom_package_1, - version='1.0.0', - ) - custom_package_2 = PackageFactory( - basename='custom_package_2', - ) - PackageReleaseFactory( - package=custom_package_2, - version='1.0.0', - ) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=0, - ) - self.client.force_login(self.owner.user) - plugin = PluginFactory( - basename='test_plugin', - owner=self.owner, - ) - PluginReleaseFactory( - plugin=plugin, - version='1.0.0', - ) - api_path = f'{self.base_api_path}{plugin.slug}/' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - release = PluginRelease.objects.get(pk=response.json()['id']) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.download_requirements.count(), - second=2, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.pypi_requirements.count(), - second=2, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.vcs_requirements.count(), - second=2, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.plugin} - Release', - ) - - -class PluginTagViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/plugins/tags/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' - cls.contributor = ForumUserFactory() - PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.plugin_tag_1 = PluginTagFactory( - plugin=cls.plugin, - ) - cls.plugin_tag_2 = PluginTagFactory( - plugin=cls.plugin, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue(expr=issubclass(PluginTagViewSet, ProjectTagViewSet)) - - def test_base_attributes(self): - self.assertEqual( - first=PluginTagViewSet.serializer_class, - second=PluginTagSerializer, - ) - self.assertEqual( - first=PluginTagViewSet.project_type, - second='plugin', - ) - self.assertEqual( - first=PluginTagViewSet.project_model, - second=Plugin, - ) - self.assertIs( - expr1=PluginTagViewSet.queryset.model, - expr2=PluginTag, - ) - self.assertDictEqual( - d1=PluginTagViewSet.queryset.query.select_related, - d2={'tag': {}, 'plugin': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PluginTagViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.plugin_tag_2.tag.name, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.plugin_tag_2.tag.name, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.plugin_tag_2.tag.name, - 'id': str(self.plugin_tag_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.plugin_tag_2.tag.name, - 'id': str(self.plugin_tag_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.plugin_tag_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'tag': self.plugin_tag_1.tag.name, - 'id': str(self.plugin_tag_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'tag': self.plugin_tag_1.tag.name, - 'id': str(self.plugin_tag_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a tag - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a tag - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a tag - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a tag - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-2'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing affiliated tag cannot be added - response = self.client.post( - path=self.api_path, - data={'tag': self.plugin_tag_1.tag}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'tag': [f'Tag already linked to {PluginTagViewSet.project_type}.']} - ) - - # Verify black-listed tag cannot be added - tag = TagFactory( - black_listed=True, - ) - response = self.client.post( - path=self.api_path, - data={'tag': tag.name}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a tag - response = self.client.delete( - path=self.api_path + f'{self.plugin_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a tag - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a tag - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a tag - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.plugin_tag_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.plugin} - Tag', - ) - - -class PluginViewSetTestCase(APITestCase): - - contributor = owner = plugin = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - logo='logo.jpg', - ) - cls.plugin_release = PluginReleaseFactory( - plugin=cls.plugin, - zip_file='/media/release_v1.0.0.zip', - ) - cls.api_path = f'/api/plugins/projects/' - cls.contributor = ForumUserFactory() - PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue(expr=issubclass(PluginViewSet, ProjectViewSet)) - - def test_base_attributes(self): - self.assertEqual( - first=PluginViewSet.filterset_class, - second=PluginFilterSet, - ) - self.assertEqual( - first=PluginViewSet.serializer_class, - second=PluginSerializer, - ) - self.assertEqual( - first=PluginViewSet.creation_serializer_class, - second=PluginCreateSerializer, - ) - self.assertIs(expr1=PluginViewSet.queryset.model, expr2=Plugin) - prefetch_lookups = PluginViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=1) - lookup = prefetch_lookups[0] - self.assertEqual(first=lookup.prefetch_to, second='releases') - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('-created',), - ) - self.assertDictEqual( - d1=PluginViewSet.queryset.query.select_related, - d2={'owner': {'user': {}}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=PluginViewSet.http_method_names, - tuple2=('get', 'post', 'patch', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - request = response.wsgi_request - domain = f'{request.scheme}://{request.get_host()}' - zip_file = f'{domain}{self.plugin_release.get_absolute_url()}' - logo = f'{domain}{self.plugin.logo.url}' - self.assertEqual(first=content['count'], second=1) - created_timestamp = self.plugin.created - updated_timestamp = self.plugin.updated - payload = { - 'name': self.plugin.name, - 'slug': self.plugin.slug, - 'total_downloads': self.plugin.total_downloads, - 'current_release': { - 'version': self.plugin_release.version, - 'notes': self.plugin_release.notes, - 'zip_file': zip_file, - }, - 'created': { - 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'updated': { - 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'synopsis': self.plugin.synopsis, - 'description': self.plugin.description, - 'configuration': self.plugin.configuration, - 'logo': logo, - 'video': self.plugin.video, - 'owner': { - 'forum_id': self.plugin.owner.forum_id, - 'username': self.plugin.owner.user.username, - } - } - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - def test_get_list_filters(self): - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate tag filtering - response = self.client.get(path=f'{self.api_path}?tag=test_tag') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - tag = TagFactory(name='test_tag') - PluginTagFactory( - plugin=self.plugin, - tag=tag, - ) - response = self.client.get(path=f'{self.api_path}?tag=test_tag') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate game filtering - response = self.client.get(path=f'{self.api_path}?game=game1') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - game = GameFactory( - name='Game1', - basename='game1', - icon='icon1.jpg', - ) - PluginGameFactory( - plugin=self.plugin, - game=game, - ) - response = self.client.get(path=f'{self.api_path}?game=game1') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate game filtering - response = self.client.get( - path=f'{self.api_path}?user={self.regular_user.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - response = self.client.get( - path=f'{self.api_path}?user={self.contributor.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - response = self.client.get( - path=f'{self.api_path}?user={self.owner.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - def test_get_details(self): - # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.plugin.slug}/' - response = self.client.get(path=api_path) - request = response.wsgi_request - domain = f'{request.scheme}://{request.get_host()}' - zip_file = f'{domain}{self.plugin_release.get_absolute_url()}' - logo = f'{domain}{self.plugin.logo.url}' - created_timestamp = self.plugin.created - updated_timestamp = self.plugin.updated - payload = { - 'name': self.plugin.name, - 'slug': self.plugin.slug, - 'total_downloads': self.plugin.total_downloads, - 'current_release': { - 'version': self.plugin_release.version, - 'notes': self.plugin_release.notes, - 'zip_file': zip_file, - 'package_requirements': [], - 'pypi_requirements': [], - 'version_control_requirements': [], - 'download_requirements': [], - }, - 'created': { - 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'updated': { - 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'synopsis': self.plugin.synopsis, - 'description': self.plugin.description, - 'configuration': self.plugin.configuration, - 'logo': logo, - 'video': self.plugin.video, - 'owner': { - 'forum_id': self.plugin.owner.forum_id, - 'username': self.plugin.owner.user.username, - } - } - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that regular user can see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - # Verify non logged in user cannot create a plugin - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' - file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' - version = '1.0.0' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Plugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that a logged in user can create a plugin - self.assertEqual( - first=Plugin.objects.count(), - second=1, - ) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Plugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=Plugin.objects.count(), - second=2, - ) - content = response.json() - plugin = Plugin.objects.get(slug=content['slug']) - self.assertEqual( - first=plugin.releases.count(), - second=1, - ) - release = plugin.releases.get() - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify cannot create a plugin where the basename already exists - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Plugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'basename': 'Plugin already exists. Cannot create.'} - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post_with_requirements(self): - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' - file_path = base_path / 'test-plugin' / 'test-plugin-requirements-v1.0.0.zip' - version = '1.0.0' - custom_package_1 = PackageFactory( - basename='custom_package_1', - ) - PackageReleaseFactory( - package=custom_package_1, - version='1.0.0', - ) - custom_package_2 = PackageFactory( - basename='custom_package_2', - ) - PackageReleaseFactory( - package=custom_package_2, - version='1.0.0', - ) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=0, - ) - self.client.force_login(self.owner.user) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=self.api_path, - data={ - 'name': 'Test Plugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - contents = response.json() - plugin = Plugin.objects.get(slug=contents['slug']) - release = PluginRelease.objects.get(plugin=plugin) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.download_requirements.count(), - second=2, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.pypi_requirements.count(), - second=2, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.vcs_requirements.count(), - second=2, - ) - - def test_patch(self): - # Verify that non logged in user cannot update a path - api_path = f'{self.api_path}{self.plugin.slug}/' - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot update a path - self.client.force_login(self.regular_user.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can update a path - self.client.force_login(self.contributor.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - # Verify that owner can update a path - self.client.force_login(self.owner.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'New Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second='Plugin List', - ) - - -class SubPluginPathViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory( - owner=cls.owner, - ) - cls.base_api_path = f'/api/plugins/paths/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' - cls.contributor = ForumUserFactory() - PluginContributorFactory( - plugin=cls.plugin, - user=cls.contributor, - ) - cls.sub_plugin_path_1 = SubPluginPathFactory( - plugin=cls.plugin, - allow_module=True, - ) - cls.sub_plugin_path_2 = SubPluginPathFactory( - plugin=cls.plugin, - allow_package_using_basename=True, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginPathViewSet, ProjectThroughModelMixin), - ) - - def test_base_attributes(self): - self.assertTupleEqual( - tuple1=SubPluginPathViewSet.ordering, - tuple2=('path',), - ) - self.assertEqual( - first=SubPluginPathViewSet.serializer_class, - second=SubPluginPathSerializer, - ) - self.assertEqual( - first=SubPluginPathViewSet.project_type, - second='plugin', - ) - self.assertEqual( - first=SubPluginPathViewSet.project_model, - second=Plugin, - ) - self.assertEqual( - first=SubPluginPathViewSet.related_model_type, - second='Sub-Plugin Path', - ) - self.assertIs( - expr1=SubPluginPathViewSet.queryset.model, - expr2=SubPluginPath, - ) - self.assertDictEqual( - d1=SubPluginPathViewSet.queryset.query.select_related, - d2={'plugin': {}}, - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginPathViewSet.http_method_names, - tuple2=('get', 'post', 'patch', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'path': self.sub_plugin_path_1.path, - 'allow_module': self.sub_plugin_path_1.allow_module, - 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, - 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'path': self.sub_plugin_path_1.path, - 'allow_module': self.sub_plugin_path_1.allow_module, - 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, - 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'path': self.sub_plugin_path_1.path, - 'allow_module': self.sub_plugin_path_1.allow_module, - 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, - 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, - 'id': str(self.sub_plugin_path_1.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'path': self.sub_plugin_path_1.path, - 'allow_module': self.sub_plugin_path_1.allow_module, - 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, - 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, - 'id': str(self.sub_plugin_path_1.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_path_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'path': self.sub_plugin_path_1.path, - 'allow_module': self.sub_plugin_path_1.allow_module, - 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, - 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, - 'id': str(self.sub_plugin_path_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'path': self.sub_plugin_path_1.path, - 'allow_module': self.sub_plugin_path_1.allow_module, - 'allow_package_using_init': self.sub_plugin_path_1.allow_package_using_init, - 'allow_package_using_basename': self.sub_plugin_path_1.allow_package_using_basename, - 'id': str(self.sub_plugin_path_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a path - response = self.client.post( - path=self.api_path, - data={ - 'path': 'new-path-1', - 'allow_module': False, - 'allow_package_using_init': True, - 'allow_package_using_basename': False, - }, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a path - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={ - 'path': 'new-path-1', - 'allow_module': False, - 'allow_package_using_init': True, - 'allow_package_using_basename': False, - }, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a path - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={ - 'path': 'new-path-1', - 'allow_module': False, - 'allow_package_using_init': True, - 'allow_package_using_basename': False, - }, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a path - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={ - 'path': 'new-path-2', - 'allow_module': False, - 'allow_package_using_init': True, - 'allow_package_using_basename': True, - }, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_patch(self): - # Verify that non logged in user cannot update a path - api_path = f'{self.api_path}{self.sub_plugin_path_1.id}/' - response = self.client.patch( - path=api_path, - data={ - 'allow_module': False, - 'allow_package_using_init': True, - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot update a path - self.client.force_login(self.regular_user.user) - response = self.client.patch( - path=api_path, - data={ - 'allow_module': False, - 'allow_package_using_init': True, - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can update a path - self.client.force_login(self.contributor.user) - response = self.client.patch( - path=api_path, - data={ - 'allow_module': False, - 'allow_package_using_init': True, - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - # Verify that owner can update a path - self.client.force_login(self.owner.user) - response = self.client.patch( - path=api_path, - data={ - 'allow_module': True, - 'allow_package_using_init': False, - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a path - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_path_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a path - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_path_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a path - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_path_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a path - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_path_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.plugin} - Sub-Plugin Path', - ) diff --git a/project_manager/sub_plugins/api/tests/test_project_views.py b/project_manager/sub_plugins/api/tests/test_project_views.py new file mode 100644 index 00000000..2d48fa3d --- /dev/null +++ b/project_manager/sub_plugins/api/tests/test_project_views.py @@ -0,0 +1,660 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path + +# Third Party Django +from rest_framework import status +from rest_framework.parsers import ParseError +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ProjectViewSet +from project_manager.sub_plugins.api.filtersets import SubPluginFilterSet +from project_manager.sub_plugins.api.serializers import ( + SubPluginCreateSerializer, + SubPluginSerializer, +) +from project_manager.sub_plugins.api.views import SubPluginViewSet +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginRelease, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.games import GameFactory +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory +from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory +from test_utils.factories.sub_plugins import ( + SubPluginContributorFactory, + SubPluginFactory, + SubPluginGameFactory, + SubPluginReleaseFactory, + SubPluginTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = sub_plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=plugin, + logo='logo.jpg', + ) + cls.sub_plugin_release = SubPluginReleaseFactory( + sub_plugin=cls.sub_plugin, + zip_file='/media/release_v1.0.0.zip', + ) + cls.base_api_path = f'/api/sub-plugins/projects/' + cls.api_path = f'{cls.base_api_path}{plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginViewSet, ProjectViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginViewSet.filterset_class, + second=SubPluginFilterSet, + ) + self.assertEqual( + first=SubPluginViewSet.serializer_class, + second=SubPluginSerializer, + ) + self.assertEqual( + first=SubPluginViewSet.creation_serializer_class, + second=SubPluginCreateSerializer, + ) + self.assertIs(expr1=SubPluginViewSet.queryset.model, expr2=SubPlugin) + prefetch_lookups = SubPluginViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=1) + lookup = prefetch_lookups[0] + self.assertEqual(first=lookup.prefetch_to, second='releases') + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('-created',), + ) + self.assertDictEqual( + d1=SubPluginViewSet.queryset.query.select_related, + d2={'owner': {'user': {}}, 'plugin': {}}, + ) + + def test_get_queryset(self): + with self.assertRaises(ParseError) as context: + obj = SubPluginViewSet() + obj.kwargs = {} + obj.get_queryset() + + self.assertEqual( + first=context.exception.detail, + second='Invalid plugin_slug.', + ) + + # TODO: validate the query returns the correct data + plugin = PluginFactory() + obj.kwargs = {'plugin_slug': plugin.slug} + obj.get_queryset() + + # TODO: validate the query returns the correct data + obj.kwargs = {} + obj.plugin = plugin + obj.get_queryset() + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginViewSet.http_method_names, + tuple2=('get', 'post', 'patch', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.sub_plugin_release.get_absolute_url()}' + logo = f'{domain}{self.sub_plugin.logo.url}' + self.assertEqual(first=content['count'], second=1) + created_timestamp = self.sub_plugin.created + updated_timestamp = self.sub_plugin.updated + payload = { + 'name': self.sub_plugin.name, + 'slug': self.sub_plugin.slug, + 'total_downloads': self.sub_plugin.total_downloads, + 'current_release': { + 'version': self.sub_plugin_release.version, + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.sub_plugin.synopsis, + 'description': self.sub_plugin.description, + 'configuration': self.sub_plugin.configuration, + 'logo': logo, + 'video': self.sub_plugin.video, + 'owner': { + 'forum_id': self.sub_plugin.owner.forum_id, + 'username': self.sub_plugin.owner.user.username, + } + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_list_filters(self): + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate tag filtering + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + tag = TagFactory(name='test_tag') + SubPluginTagFactory( + sub_plugin=self.sub_plugin, + tag=tag, + ) + response = self.client.get(path=f'{self.api_path}?tag=test_tag') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + game = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + SubPluginGameFactory( + sub_plugin=self.sub_plugin, + game=game, + ) + response = self.client.get(path=f'{self.api_path}?game=game1') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + # Validate game filtering + response = self.client.get( + path=f'{self.api_path}?user={self.regular_user.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=0, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.contributor.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + response = self.client.get( + path=f'{self.api_path}?user={self.owner.user.username}', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertEqual( + first=response.json()['count'], + second=1, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.sub_plugin.slug}/' + response = self.client.get(path=api_path) + request = response.wsgi_request + domain = f'{request.scheme}://{request.get_host()}' + zip_file = f'{domain}{self.sub_plugin_release.get_absolute_url()}' + logo = f'{domain}{self.sub_plugin.logo.url}' + created_timestamp = self.sub_plugin.created + updated_timestamp = self.sub_plugin.updated + payload = { + 'name': self.sub_plugin.name, + 'slug': self.sub_plugin.slug, + 'total_downloads': self.sub_plugin.total_downloads, + 'current_release': { + 'version': self.sub_plugin_release.version, + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + 'package_requirements': [], + 'pypi_requirements': [], + 'version_control_requirements': [], + 'download_requirements': [], + }, + 'created': { + 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'updated': { + 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'synopsis': self.sub_plugin.synopsis, + 'description': self.sub_plugin.description, + 'configuration': self.sub_plugin.configuration, + 'logo': logo, + 'video': self.sub_plugin.video, + 'owner': { + 'forum_id': self.sub_plugin.owner.forum_id, + 'username': self.sub_plugin.owner.user.username, + } + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify non logged in user cannot create a sub-plugin + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' + version = '1.0.0' + api_path = f'{self.base_api_path}{plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'name': 'Test SubPlugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that a logged in user can create a sub-plugin + self.assertEqual( + first=SubPlugin.objects.count(), + second=1, + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'name': 'Test SubPlugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=SubPlugin.objects.count(), + second=2, + ) + content = response.json() + sub_plugin = SubPlugin.objects.get(slug=content['slug']) + self.assertEqual( + first=sub_plugin.releases.count(), + second=1, + ) + release = sub_plugin.releases.get() + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify cannot create a sub-plugin where the basename already exists + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'name': 'Test SubPlugin', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'basename': 'SubPlugin already exists. Cannot create.'} + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' + version = '1.0.0' + api_path = f'{self.base_api_path}{plugin.slug}/' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'name': 'Test Package', + 'releases.notes': '', + 'releases.version': version, + 'releases.zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + contents = response.json() + sub_plugin = SubPlugin.objects.get(slug=contents['slug'], plugin=plugin) + release = SubPluginRelease.objects.get(sub_plugin=sub_plugin) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + + def test_patch(self): + # Verify that non logged in user cannot update a path + api_path = f'{self.api_path}{self.sub_plugin.slug}/' + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot update a path + self.client.force_login(self.regular_user.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can update a path + self.client.force_login(self.contributor.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + # Verify that owner can update a path + self.client.force_login(self.owner.user) + response = self.client.patch( + path=api_path, + data={ + 'synopsis': 'New Test Synopsis', + } + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second='Sub Plugin List', + ) diff --git a/project_manager/sub_plugins/api/tests/test_related_views.py b/project_manager/sub_plugins/api/tests/test_related_views.py new file mode 100644 index 00000000..b365add7 --- /dev/null +++ b/project_manager/sub_plugins/api/tests/test_related_views.py @@ -0,0 +1,1379 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.test import override_settings + +# Third Party Python +from path import Path + +# Third Party Django +from PIL import Image +from rest_framework import status +from rest_framework.parsers import ParseError +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ( + ProjectContributorViewSet, + ProjectGameViewSet, + ProjectImageViewSet, + ProjectTagViewSet, +) +from project_manager.sub_plugins.api.serializers import ( + SubPluginContributorSerializer, + SubPluginGameSerializer, + SubPluginImageSerializer, + SubPluginTagSerializer, +) +from project_manager.sub_plugins.api.views import ( + SubPluginContributorViewSet, + SubPluginGameViewSet, + SubPluginImageViewSet, + SubPluginTagViewSet, +) +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginContributor, + SubPluginGame, + SubPluginImage, + SubPluginTag, +) +from test_utils.factories.games import GameFactory +from test_utils.factories.plugins import PluginFactory +from test_utils.factories.sub_plugins import ( + SubPluginContributorFactory, + SubPluginFactory, + SubPluginGameFactory, + SubPluginImageFactory, + SubPluginTagFactory, +) +from test_utils.factories.tags import TagFactory +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginContributorViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + plugin=cls.plugin, + owner=cls.owner, + ) + cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + cls.sub_plugin_contributor = SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.new_contributor = ForumUserFactory() + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass( + SubPluginContributorViewSet, + ProjectContributorViewSet, + ), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginContributorViewSet.serializer_class, + second=SubPluginContributorSerializer, + ) + self.assertEqual( + first=SubPluginContributorViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginContributorViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginContributorViewSet.queryset.model, + expr2=SubPluginContributor, + ) + self.assertDictEqual( + d1=SubPluginContributorViewSet.queryset.query.select_related, + d2={'user': {'user': {}}, 'sub_plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginContributorViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + user = self.contributor + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that contributors can see results but not 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'user': { + 'forum_id': user.forum_id, + 'username': user.user.username, + }, + 'id': str(self.sub_plugin_contributor.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_contributor.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors cannot see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'user': { + 'forum_id': self.sub_plugin_contributor.user.forum_id, + 'username': self.sub_plugin_contributor.user.user.username, + }, + 'id': str(self.sub_plugin_contributor.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a contributor + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a contributor + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot add a contributor + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can add a contributor + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'username': self.new_contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing contributor cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.contributor.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, + ) + + # Verify owner cannot be added + response = self.client.post( + path=self.api_path, + data={'username': self.owner.user.username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, + ) + + # Verify unknown username cannot be added + invalid_username = 'invalid' + response = self.client.post( + path=self.api_path, + data={'username': invalid_username}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'username': [f'No user named "{invalid_username}".']}, + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a contributor + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor cannot delete a contributor + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that owner can delete a contributor + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_contributor.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Contributor', + ) + + +class SubPluginGameViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = game_1 = game_2 = plugin = sub_plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + cls.package_contributor = SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.game_1 = GameFactory( + name='Game1', + basename='game1', + icon='icon1.jpg', + ) + cls.game_2 = GameFactory( + name='Game2', + basename='game2', + icon='icon2.jpg', + ) + cls.game_3 = GameFactory( + name='Game3', + basename='game3', + icon='icon3.jpg', + ) + cls.game_4 = GameFactory( + name='Game4', + basename='game4', + icon='icon4.jpg', + ) + cls.sub_plugin_game_1 = SubPluginGameFactory( + sub_plugin=cls.sub_plugin, + game=cls.game_1, + ) + cls.sub_plugin_game_2 = SubPluginGameFactory( + sub_plugin=cls.sub_plugin, + game=cls.game_2, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginGameViewSet, ProjectGameViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginGameViewSet.serializer_class, + second=SubPluginGameSerializer, + ) + self.assertEqual( + first=SubPluginGameViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginGameViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginGameViewSet.queryset.model, + expr2=SubPluginGame, + ) + self.assertDictEqual( + d1=SubPluginGameViewSet.queryset.query.select_related, + d2={'game': {}, 'sub_plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginGameViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'game': { + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_game_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'game': { + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': icon, + }, + 'id': str(self.sub_plugin_game_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a game + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a game + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a game + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_3.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a game + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_4.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated game cannot be added + response = self.client.post( + path=self.api_path, + data={'game_slug': self.game_1.slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Game already linked to {SubPluginGameViewSet.project_type}.']} + ) + + # Verify non-existing game cannot be added + invalid_slug = 'invalid' + response = self.client.post( + path=self.api_path, + data={'game_slug': invalid_slug}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'game': [f'Invalid game "{invalid_slug}".']} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a game + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a game + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a game + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a game + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_game_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Game', + ) + + +class SubPluginImageViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.sub_plugin_image_1 = SubPluginImageFactory( + sub_plugin=cls.sub_plugin, + ) + cls.sub_plugin_image_2 = SubPluginImageFactory( + sub_plugin=cls.sub_plugin, + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginImageViewSet, ProjectImageViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginImageViewSet.serializer_class, + second=SubPluginImageSerializer, + ) + self.assertEqual( + first=SubPluginImageViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginImageViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginImageViewSet.queryset.model, + expr2=SubPluginImage, + ) + self.assertDictEqual( + d1=SubPluginImageViewSet.queryset.query.select_related, + d2={'sub_plugin': {}}, + ) + + def test_parent_project(self): + obj = SubPluginImageViewSet() + invalid_slug = 'invalid' + obj.kwargs = {'plugin_slug': invalid_slug} + with self.assertRaises(ParseError) as context: + _ = obj.parent_project + + self.assertEqual( + first=context.exception.detail, + second=f"Plugin '{invalid_slug}' not found.", + ) + + plugin = PluginFactory() + obj.kwargs = {'plugin_slug': plugin.slug} + self.assertEqual( + first=obj.parent_project, + second=plugin, + ) + + def test_get_project_kwargs(self): + obj = SubPluginImageViewSet() + plugin = PluginFactory() + sub_plugin_slug = 'test-sub-plugin' + obj.kwargs = { + 'sub_plugin_slug': sub_plugin_slug, + 'plugin_slug': plugin.slug, + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={ + 'slug': sub_plugin_slug, + 'plugin': plugin, + } + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginImageViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_image_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + request = response.wsgi_request + image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_1.image.url}' + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'image': image, + 'id': str(self.sub_plugin_image_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + # Verify that regular user cannot add an image + self.client.force_login(self.regular_user.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add an image + self.client.force_login(self.contributor.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add an image + self.client.force_login(self.owner.user) + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=self.api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_delete(self): + # Verify that regular user cannot delete an image + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete an image + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_image_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete an image + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_image_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Image', + ) + + +class SubPluginTagViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.sub_plugin_tag_1 = SubPluginTagFactory( + sub_plugin=cls.sub_plugin, + ) + cls.sub_plugin_tag_2 = SubPluginTagFactory( + sub_plugin=cls.sub_plugin, + ) + cls.regular_user = ForumUserFactory() + + def test_inheritance(self): + self.assertTrue(expr=issubclass(SubPluginTagViewSet, ProjectTagViewSet)) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginTagViewSet.serializer_class, + second=SubPluginTagSerializer, + ) + self.assertEqual( + first=SubPluginTagViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginTagViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginTagViewSet.queryset.model, + expr2=SubPluginTag, + ) + self.assertDictEqual( + d1=SubPluginTagViewSet.queryset.query.select_related, + d2={'tag': {}, 'sub_plugin': {}} + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginTagViewSet.http_method_names, + tuple2=('get', 'post', 'delete', 'options'), + ) + + def test_get_list(self): + # Verify that non logged in user can see results but not 'id' + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + }, + ) + + # Verify that regular user can see results but not 'id' + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + }, + ) + + # Verify that contributors can see results AND 'id' + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + 'id': str(self.sub_plugin_tag_2.id), + }, + ) + + # Verify that the owner can see results AND 'id' + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=2) + self.assertDictEqual( + d1=content['results'][0], + d2={ + 'tag': self.sub_plugin_tag_2.tag.name, + 'id': str(self.sub_plugin_tag_2.id), + }, + ) + + def test_get_details(self): + # Verify that non logged in user cannot see details + api_path = f'{self.api_path}{self.sub_plugin_tag_1.id}/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.sub_plugin_tag_1.tag.name, + 'id': str(self.sub_plugin_tag_1.id), + }, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'tag': self.sub_plugin_tag_1.tag.name, + 'id': str(self.sub_plugin_tag_1.id), + }, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + def test_post(self): + # Verify that non logged in user cannot add a tag + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot add a tag + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can add a tag + self.client.force_login(self.contributor.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-1'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + # Verify that owner can add a tag + self.client.force_login(self.owner.user) + response = self.client.post( + path=self.api_path, + data={'tag': 'new-tag-2'}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + + def test_post_failure(self): + self.client.force_login(self.owner.user) + + # Verify existing affiliated tag cannot be added + response = self.client.post( + path=self.api_path, + data={'tag': self.sub_plugin_tag_1.tag}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f'Tag already linked to {SubPluginTagViewSet.project_type}.']} + ) + + # Verify black-listed tag cannot be added + tag = TagFactory( + black_listed=True, + ) + response = self.client.post( + path=self.api_path, + data={'tag': tag.name}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} + ) + + def test_delete(self): + # Verify that non logged in user cannot delete a tag + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot delete a tag + self.client.force_login(self.regular_user.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that contributor can delete a tag + self.client.force_login(self.contributor.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_1.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + # Verify that owner can delete a tag + self.client.force_login(self.owner.user) + response = self.client.delete( + path=self.api_path + f'{self.sub_plugin_tag_2.id}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_204_NO_CONTENT, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Tag', + ) diff --git a/project_manager/sub_plugins/api/tests/test_release_views.py b/project_manager/sub_plugins/api/tests/test_release_views.py new file mode 100644 index 00000000..553f2a42 --- /dev/null +++ b/project_manager/sub_plugins/api/tests/test_release_views.py @@ -0,0 +1,658 @@ +# ============================================================================= +# IMPORTS +# ============================================================================= +# Python +import shutil +import tempfile + +# Django +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile +from django.test import override_settings +from django.utils import formats + +# Third Party Python +from path import Path + +# Third Party Django +from rest_framework import status +from rest_framework.parsers import ParseError +from rest_framework.test import APITestCase + +# App +from project_manager.common.api.views import ProjectReleaseViewSet +from project_manager.sub_plugins.api.serializers import SubPluginReleaseSerializer +from project_manager.sub_plugins.api.views import SubPluginReleaseViewSet +from project_manager.sub_plugins.models import ( + SubPlugin, + SubPluginRelease, + SubPluginReleaseDownloadRequirement, + SubPluginReleasePackageRequirement, + SubPluginReleasePyPiRequirement, + SubPluginReleaseVersionControlRequirement, +) +from requirements.models import ( + DownloadRequirement, + PyPiRequirement, + VersionControlRequirement, +) +from test_utils.factories.packages import PackageFactory, PackageReleaseFactory +from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory +from test_utils.factories.sub_plugins import ( + SubPluginContributorFactory, + SubPluginFactory, + SubPluginReleaseFactory, +) +from test_utils.factories.users import ForumUserFactory + + +# ============================================================================= +# TEST CASES +# ============================================================================= +class SubPluginReleaseViewSetTestCase(APITestCase): + + base_api_path = contributor = owner = plugin = sub_plugin = None + MEDIA_ROOT = Path(tempfile.mkdtemp()) + + @classmethod + def setUpTestData(cls): + cls.owner = ForumUserFactory() + cls.plugin = PluginFactory() + cls.sub_plugin = SubPluginFactory( + owner=cls.owner, + plugin=cls.plugin, + ) + cls.base_api_path = f'/api/sub-plugins/releases/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/{cls.sub_plugin.slug}/' + cls.contributor = ForumUserFactory() + SubPluginContributorFactory( + sub_plugin=cls.sub_plugin, + user=cls.contributor, + ) + cls.sub_plugin_release = SubPluginReleaseFactory( + sub_plugin=cls.sub_plugin, + zip_file='release_v1.0.0.zip', + ) + cls.regular_user = ForumUserFactory() + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) + super().tearDownClass() + + def test_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginReleaseViewSet, ProjectReleaseViewSet), + ) + + def test_base_attributes(self): + self.assertEqual( + first=SubPluginReleaseViewSet.serializer_class, + second=SubPluginReleaseSerializer, + ) + self.assertEqual( + first=SubPluginReleaseViewSet.project_type, + second='sub-plugin', + ) + self.assertEqual( + first=SubPluginReleaseViewSet.project_model, + second=SubPlugin, + ) + self.assertIs( + expr1=SubPluginReleaseViewSet.queryset.model, + expr2=SubPluginRelease, + ) + self.assertDictEqual( + d1=SubPluginReleaseViewSet.queryset.query.select_related, + d2={'sub_plugin': {}}, + ) + prefetch_lookups = SubPluginReleaseViewSet.queryset._prefetch_related_lookups + self.assertEqual(first=len(prefetch_lookups), second=4) + + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleasepackagerequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleasePackageRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('package_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'package_requirement': {}}, + ) + + lookup = prefetch_lookups[1] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleasedownloadrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleaseDownloadRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('download_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'download_requirement': {}}, + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleasepypirequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleasePyPiRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('pypi_requirement__name',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'pypi_requirement': {}}, + ) + + lookup = prefetch_lookups[3] + self.assertEqual( + first=lookup.prefetch_to, + second='subpluginreleaseversioncontrolrequirement_set', + ) + self.assertIs( + expr1=lookup.queryset.model, + expr2=SubPluginReleaseVersionControlRequirement, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('vcs_requirement__url',), + ) + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'vcs_requirement': {}}, + ) + + def test_parent_project(self): + obj = SubPluginReleaseViewSet() + invalid_slug = 'invalid' + obj.kwargs = {'plugin_slug': invalid_slug} + with self.assertRaises(ParseError) as context: + _ = obj.parent_project + + self.assertEqual( + first=context.exception.detail, + second=f"Plugin '{invalid_slug}' not found.", + ) + + plugin = PluginFactory() + obj.kwargs = {'plugin_slug': plugin.slug} + self.assertEqual( + first=obj.parent_project, + second=plugin, + ) + + def test_get_project_kwargs(self): + obj = SubPluginReleaseViewSet() + plugin = PluginFactory() + sub_plugin_slug = 'test-sub-plugin' + obj.kwargs = { + 'sub_plugin_slug': sub_plugin_slug, + 'plugin_slug': plugin.slug, + } + self.assertDictEqual( + d1=obj.get_project_kwargs(), + d2={ + 'slug': sub_plugin_slug, + 'plugin': plugin, + } + ) + + def test_http_method_names(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseViewSet.http_method_names, + tuple2=('get', 'post', 'options'), + ) + + def test_get_list(self): + # Verify that a non logged in user can see results + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + timestamp = self.sub_plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' + payload = { + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + 'version': self.sub_plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.sub_plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.sub_plugin_release.id), + } + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that regular user can see results + self.client.force_login(self.regular_user.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that contributors can see results + self.client.force_login(self.contributor.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + # Verify that the owner can see results + self.client.force_login(self.owner.user) + response = self.client.get(path=self.api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual(first=content['count'], second=1) + self.assertDictEqual( + d1=content['results'][0], + d2=payload, + ) + + def test_get_details(self): + # Verify that non logged in user can see details + api_path = f'{self.api_path}{self.sub_plugin_release.version}/' + response = self.client.get(path=api_path) + timestamp = self.sub_plugin_release.created + request = response.wsgi_request + zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' + payload = { + 'notes': self.sub_plugin_release.notes, + 'zip_file': zip_file, + 'version': self.sub_plugin_release.version, + 'created': { + 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), + 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), + }, + 'download_count': self.sub_plugin_release.download_count, + 'download_requirements': [], + 'package_requirements': [], + 'pypi_requirements': [], + 'vcs_requirements': [], + 'id': str(self.sub_plugin_release.id), + } + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that regular user can see details + self.client.force_login(self.regular_user.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that contributors can see details + self.client.force_login(self.contributor.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + # Verify that the owner can see details + self.client.force_login(self.owner.user) + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2=payload, + ) + + def test_get_details_failure(self): + api_path = f'{self.base_api_path}{self.plugin.slug}/invalid/' + response = self.client.get(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'detail': 'Invalid sub_plugin_slug.'}, + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post(self): + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + sub_plugin = SubPluginFactory( + plugin=plugin, + basename='test_sub_plugin', + owner=self.owner, + ) + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version='1.0.0', + ) + SubPluginContributorFactory( + sub_plugin=sub_plugin, + user=self.contributor, + ) + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' + + # Verify that non logged in user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify that regular user cannot create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.regular_user.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + + # Verify contributor can create a release + version = '1.0.1' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.contributor.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=sub_plugin.releases.count(), + second=2, + ) + content = response.json() + release = sub_plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify owner can create a release + version = '1.0.2' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + self.client.force_login(self.owner.user) + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + self.assertEqual( + first=sub_plugin.releases.count(), + second=3, + ) + content = response.json() + release = sub_plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.version, + second=version, + ) + + # Verify that the same version cannot be created twice + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={'version': ['Given version matches existing version.']}, + ) + + # Verify that the basename in the zip file is being verified against + # the basename from the url path + zip_basename = sub_plugin.basename + sub_plugin = SubPluginFactory( + plugin=plugin, + owner=self.owner, + ) + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_400_BAD_REQUEST, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'zip_file': [ + f"Basename in zip '{zip_basename}' does not match basename" + f" for sub-plugin '{sub_plugin.basename}'.", + ], + } + ) + + @override_settings(MEDIA_ROOT=MEDIA_ROOT) + def test_post_with_requirements(self): + plugin = PluginFactory( + basename='test_plugin', + ) + SubPluginPathFactory( + plugin=plugin, + path='sub_plugins', + allow_package_using_basename=True, + ) + sub_plugin = SubPluginFactory( + plugin=plugin, + basename='test_sub_plugin', + owner=self.owner, + ) + SubPluginReleaseFactory( + sub_plugin=sub_plugin, + version='1.0.0', + ) + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' + file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' + version = '1.0.1' + custom_package_1 = PackageFactory( + basename='custom_package_1', + ) + PackageReleaseFactory( + package=custom_package_1, + version='1.0.0', + ) + custom_package_2 = PackageFactory( + basename='custom_package_2', + ) + PackageReleaseFactory( + package=custom_package_2, + version='1.0.0', + ) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=0, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=0, + ) + self.client.force_login(self.owner.user) + package = PackageFactory( + basename='test_package', + owner=self.owner, + ) + PackageReleaseFactory( + package=package, + version='1.0.0', + ) + with file_path.open('rb') as open_file: + zip_file = UploadedFile(open_file, content_type='application/zip') + response = self.client.post( + path=api_path, + data={ + 'version': version, + 'zip_file': zip_file, + }, + ) + + self.assertEqual( + first=response.status_code, + second=status.HTTP_201_CREATED, + ) + release = SubPluginRelease.objects.get(pk=response.json()['id']) + self.assertEqual( + first=DownloadRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.download_requirements.count(), + second=2, + ) + self.assertEqual( + first=PyPiRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.pypi_requirements.count(), + second=2, + ) + self.assertEqual( + first=VersionControlRequirement.objects.count(), + second=2, + ) + self.assertEqual( + first=release.vcs_requirements.count(), + second=2, + ) + + def test_options(self): + response = self.client.options(path=self.api_path) + self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) + self.assertEqual( + first=response.json()['name'], + second=f'{self.sub_plugin} - Release', + ) diff --git a/project_manager/sub_plugins/api/tests/test_views.py b/project_manager/sub_plugins/api/tests/test_views.py index f992735d..4e02f0d5 100644 --- a/project_manager/sub_plugins/api/tests/test_views.py +++ b/project_manager/sub_plugins/api/tests/test_views.py @@ -1,85 +1,14 @@ # ============================================================================= # IMPORTS # ============================================================================= -# Python -import shutil -import tempfile - -# Django -from django.conf import settings -from django.core.files.uploadedfile import UploadedFile -from django.test import override_settings -from django.utils import formats - -# Third Party Python -from path import Path - # Third Party Django -from PIL import Image from rest_framework import status -from rest_framework.parsers import ParseError from rest_framework.reverse import reverse from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ( - ProjectAPIView, - ProjectContributorViewSet, - ProjectGameViewSet, - ProjectImageViewSet, - ProjectReleaseViewSet, - ProjectTagViewSet, - ProjectViewSet, -) -from project_manager.sub_plugins.api.filtersets import SubPluginFilterSet -from project_manager.sub_plugins.api.serializers import ( - SubPluginContributorSerializer, - SubPluginCreateSerializer, - SubPluginGameSerializer, - SubPluginImageSerializer, - SubPluginReleaseSerializer, - SubPluginSerializer, - SubPluginTagSerializer, -) -from project_manager.sub_plugins.api.views import ( - SubPluginAPIView, - SubPluginContributorViewSet, - SubPluginGameViewSet, - SubPluginImageViewSet, - SubPluginReleaseViewSet, - SubPluginTagViewSet, - SubPluginViewSet, -) -from project_manager.sub_plugins.models import ( - SubPlugin, - SubPluginContributor, - SubPluginGame, - SubPluginImage, - SubPluginRelease, - SubPluginReleaseDownloadRequirement, - SubPluginReleasePackageRequirement, - SubPluginReleasePyPiRequirement, - SubPluginReleaseVersionControlRequirement, - SubPluginTag, -) -from requirements.models import ( - DownloadRequirement, - PyPiRequirement, - VersionControlRequirement, -) -from test_utils.factories.games import GameFactory -from test_utils.factories.packages import PackageFactory, PackageReleaseFactory -from test_utils.factories.plugins import PluginFactory, SubPluginPathFactory -from test_utils.factories.sub_plugins import ( - SubPluginContributorFactory, - SubPluginFactory, - SubPluginGameFactory, - SubPluginImageFactory, - SubPluginReleaseFactory, - SubPluginTagFactory, -) -from test_utils.factories.tags import TagFactory -from test_utils.factories.users import ForumUserFactory +from project_manager.common.api.views import ProjectAPIView +from project_manager.sub_plugins.api.views import SubPluginAPIView # ============================================================================= @@ -127,2539 +56,3 @@ def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual(first=response.json()['name'], second='Sub-Plugin APIs') - - -class SubPluginContributorViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = sub_plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory() - cls.sub_plugin = SubPluginFactory( - plugin=cls.plugin, - owner=cls.owner, - ) - cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' - cls.contributor = ForumUserFactory() - cls.sub_plugin_contributor = SubPluginContributorFactory( - sub_plugin=cls.sub_plugin, - user=cls.contributor, - ) - cls.new_contributor = ForumUserFactory() - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass( - SubPluginContributorViewSet, - ProjectContributorViewSet, - ), - ) - - def test_base_attributes(self): - self.assertEqual( - first=SubPluginContributorViewSet.serializer_class, - second=SubPluginContributorSerializer, - ) - self.assertEqual( - first=SubPluginContributorViewSet.project_type, - second='sub-plugin', - ) - self.assertEqual( - first=SubPluginContributorViewSet.project_model, - second=SubPlugin, - ) - self.assertIs( - expr1=SubPluginContributorViewSet.queryset.model, - expr2=SubPluginContributor, - ) - self.assertDictEqual( - d1=SubPluginContributorViewSet.queryset.query.select_related, - d2={'user': {'user': {}}, 'sub_plugin': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginContributorViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - user = self.contributor - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - user = self.contributor - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that contributors can see results but not 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'user': { - 'forum_id': user.forum_id, - 'username': user.user.username, - }, - 'id': str(self.sub_plugin_contributor.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_contributor.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors cannot see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'user': { - 'forum_id': self.sub_plugin_contributor.user.forum_id, - 'username': self.sub_plugin_contributor.user.user.username, - }, - 'id': str(self.sub_plugin_contributor.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid sub_plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a contributor - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a contributor - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor cannot add a contributor - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that owner can add a contributor - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'username': self.new_contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing contributor cannot be added - response = self.client.post( - path=self.api_path, - data={'username': self.contributor.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'User {self.contributor.user.username} is already a contributor']}, - ) - - # Verify owner cannot be added - response = self.client.post( - path=self.api_path, - data={'username': self.owner.user.username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'User {self.owner.user.username} is the owner, cannot add as a contributor']}, - ) - - # Verify unknown username cannot be added - invalid_username = 'invalid' - response = self.client.post( - path=self.api_path, - data={'username': invalid_username}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'username': [f'No user named "{invalid_username}".']}, - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a contributor - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a contributor - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor cannot delete a contributor - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that owner can delete a contributor - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.sub_plugin} - Contributor', - ) - - -class SubPluginGameViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = game_1 = game_2 = plugin = sub_plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory() - cls.sub_plugin = SubPluginFactory( - owner=cls.owner, - plugin=cls.plugin, - ) - cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' - cls.contributor = ForumUserFactory() - cls.package_contributor = SubPluginContributorFactory( - sub_plugin=cls.sub_plugin, - user=cls.contributor, - ) - cls.game_1 = GameFactory( - name='Game1', - basename='game1', - icon='icon1.jpg', - ) - cls.game_2 = GameFactory( - name='Game2', - basename='game2', - icon='icon2.jpg', - ) - cls.game_3 = GameFactory( - name='Game3', - basename='game3', - icon='icon3.jpg', - ) - cls.game_4 = GameFactory( - name='Game4', - basename='game4', - icon='icon4.jpg', - ) - cls.sub_plugin_game_1 = SubPluginGameFactory( - sub_plugin=cls.sub_plugin, - game=cls.game_1, - ) - cls.sub_plugin_game_2 = SubPluginGameFactory( - sub_plugin=cls.sub_plugin, - game=cls.game_2, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginGameViewSet, ProjectGameViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=SubPluginGameViewSet.serializer_class, - second=SubPluginGameSerializer, - ) - self.assertEqual( - first=SubPluginGameViewSet.project_type, - second='sub-plugin', - ) - self.assertEqual( - first=SubPluginGameViewSet.project_model, - second=SubPlugin, - ) - self.assertIs( - expr1=SubPluginGameViewSet.queryset.model, - expr2=SubPluginGame, - ) - self.assertDictEqual( - d1=SubPluginGameViewSet.queryset.query.select_related, - d2={'game': {}, 'sub_plugin': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginGameViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - icon = f'{request.scheme}://{request.get_host()}{self.game_2.icon.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - 'id': str(self.sub_plugin_game_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'game': { - 'name': self.game_2.name, - 'slug': self.game_2.slug, - 'icon': icon, - }, - 'id': str(self.sub_plugin_game_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_game_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - request = response.wsgi_request - icon = f'{request.scheme}://{request.get_host()}{self.game_1.icon.url}' - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'game': { - 'name': self.game_1.name, - 'slug': self.game_1.slug, - 'icon': icon, - }, - 'id': str(self.sub_plugin_game_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'game': { - 'name': self.game_1.name, - 'slug': self.game_1.slug, - 'icon': icon, - }, - 'id': str(self.sub_plugin_game_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid sub_plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a game - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a game - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a game - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_3.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a game - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_4.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing affiliated game cannot be added - response = self.client.post( - path=self.api_path, - data={'game_slug': self.game_1.slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'game': [f'Game already linked to {SubPluginGameViewSet.project_type}.']} - ) - - # Verify non-existing game cannot be added - invalid_slug = 'invalid' - response = self.client.post( - path=self.api_path, - data={'game_slug': invalid_slug}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'game': [f'Invalid game "{invalid_slug}".']} - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a game - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a game - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a game - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a game - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.sub_plugin} - Game', - ) - - -class SubPluginImageViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = sub_plugin = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory() - cls.sub_plugin = SubPluginFactory( - owner=cls.owner, - plugin=cls.plugin, - ) - cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' - cls.contributor = ForumUserFactory() - SubPluginContributorFactory( - sub_plugin=cls.sub_plugin, - user=cls.contributor, - ) - cls.sub_plugin_image_1 = SubPluginImageFactory( - sub_plugin=cls.sub_plugin, - ) - cls.sub_plugin_image_2 = SubPluginImageFactory( - sub_plugin=cls.sub_plugin, - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginImageViewSet, ProjectImageViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=SubPluginImageViewSet.serializer_class, - second=SubPluginImageSerializer, - ) - self.assertEqual( - first=SubPluginImageViewSet.project_type, - second='sub-plugin', - ) - self.assertEqual( - first=SubPluginImageViewSet.project_model, - second=SubPlugin, - ) - self.assertIs( - expr1=SubPluginImageViewSet.queryset.model, - expr2=SubPluginImage, - ) - self.assertDictEqual( - d1=SubPluginImageViewSet.queryset.query.select_related, - d2={'sub_plugin': {}}, - ) - - def test_parent_project(self): - obj = SubPluginImageViewSet() - invalid_slug = 'invalid' - obj.kwargs = {'plugin_slug': invalid_slug} - with self.assertRaises(ParseError) as context: - _ = obj.parent_project - - self.assertEqual( - first=context.exception.detail, - second=f"Plugin '{invalid_slug}' not found.", - ) - - plugin = PluginFactory() - obj.kwargs = {'plugin_slug': plugin.slug} - self.assertEqual( - first=obj.parent_project, - second=plugin, - ) - - def test_get_project_kwargs(self): - obj = SubPluginImageViewSet() - plugin = PluginFactory() - sub_plugin_slug = 'test-sub-plugin' - obj.kwargs = { - 'sub_plugin_slug': sub_plugin_slug, - 'plugin_slug': plugin.slug, - } - self.assertDictEqual( - d1=obj.get_project_kwargs(), - d2={ - 'slug': sub_plugin_slug, - 'plugin': plugin, - } - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginImageViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - 'id': str(self.sub_plugin_image_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'image': image, - 'id': str(self.sub_plugin_image_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_image_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_1.image.url}' - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'image': image, - 'id': str(self.sub_plugin_image_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'image': image, - 'id': str(self.sub_plugin_image_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid sub_plugin_slug.'}, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - # Verify that regular user cannot add an image - self.client.force_login(self.regular_user.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add an image - self.client.force_login(self.contributor.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add an image - self.client.force_login(self.owner.user) - image = Image.new('RGB', (100, 100)) - tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') - image.save(tmp_file) - tmp_file.seek(0) - response = self.client.post( - path=self.api_path, - data={'image': tmp_file}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_delete(self): - # Verify that regular user cannot delete an image - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_image_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete an image - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_image_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete an image - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_image_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.sub_plugin} - Image', - ) - - -class SubPluginReleaseViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = sub_plugin = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory() - cls.sub_plugin = SubPluginFactory( - owner=cls.owner, - plugin=cls.plugin, - ) - cls.base_api_path = f'/api/sub-plugins/releases/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/{cls.sub_plugin.slug}/' - cls.contributor = ForumUserFactory() - SubPluginContributorFactory( - sub_plugin=cls.sub_plugin, - user=cls.contributor, - ) - cls.sub_plugin_release = SubPluginReleaseFactory( - sub_plugin=cls.sub_plugin, - zip_file='release_v1.0.0.zip', - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginReleaseViewSet, ProjectReleaseViewSet), - ) - - def test_base_attributes(self): - self.assertEqual( - first=SubPluginReleaseViewSet.serializer_class, - second=SubPluginReleaseSerializer, - ) - self.assertEqual( - first=SubPluginReleaseViewSet.project_type, - second='sub-plugin', - ) - self.assertEqual( - first=SubPluginReleaseViewSet.project_model, - second=SubPlugin, - ) - self.assertIs( - expr1=SubPluginReleaseViewSet.queryset.model, - expr2=SubPluginRelease, - ) - self.assertDictEqual( - d1=SubPluginReleaseViewSet.queryset.query.select_related, - d2={'sub_plugin': {}}, - ) - prefetch_lookups = SubPluginReleaseViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=4) - - lookup = prefetch_lookups[0] - self.assertEqual( - first=lookup.prefetch_to, - second='subpluginreleasepackagerequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=SubPluginReleasePackageRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('package_requirement__name',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'package_requirement': {}}, - ) - - lookup = prefetch_lookups[1] - self.assertEqual( - first=lookup.prefetch_to, - second='subpluginreleasedownloadrequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=SubPluginReleaseDownloadRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('download_requirement__url',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'download_requirement': {}}, - ) - - lookup = prefetch_lookups[2] - self.assertEqual( - first=lookup.prefetch_to, - second='subpluginreleasepypirequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=SubPluginReleasePyPiRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('pypi_requirement__name',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'pypi_requirement': {}}, - ) - - lookup = prefetch_lookups[3] - self.assertEqual( - first=lookup.prefetch_to, - second='subpluginreleaseversioncontrolrequirement_set', - ) - self.assertIs( - expr1=lookup.queryset.model, - expr2=SubPluginReleaseVersionControlRequirement, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('vcs_requirement__url',), - ) - self.assertEqual( - first=lookup.queryset.query.select_related, - second={'vcs_requirement': {}}, - ) - - def test_parent_project(self): - obj = SubPluginReleaseViewSet() - invalid_slug = 'invalid' - obj.kwargs = {'plugin_slug': invalid_slug} - with self.assertRaises(ParseError) as context: - _ = obj.parent_project - - self.assertEqual( - first=context.exception.detail, - second=f"Plugin '{invalid_slug}' not found.", - ) - - plugin = PluginFactory() - obj.kwargs = {'plugin_slug': plugin.slug} - self.assertEqual( - first=obj.parent_project, - second=plugin, - ) - - def test_get_project_kwargs(self): - obj = SubPluginReleaseViewSet() - plugin = PluginFactory() - sub_plugin_slug = 'test-sub-plugin' - obj.kwargs = { - 'sub_plugin_slug': sub_plugin_slug, - 'plugin_slug': plugin.slug, - } - self.assertDictEqual( - d1=obj.get_project_kwargs(), - d2={ - 'slug': sub_plugin_slug, - 'plugin': plugin, - } - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginReleaseViewSet.http_method_names, - tuple2=('get', 'post', 'options'), - ) - - def test_get_list(self): - # Verify that a non logged in user can see results - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - timestamp = self.sub_plugin_release.created - request = response.wsgi_request - zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' - payload = { - 'notes': self.sub_plugin_release.notes, - 'zip_file': zip_file, - 'version': self.sub_plugin_release.version, - 'created': { - 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'download_count': self.sub_plugin_release.download_count, - 'download_requirements': [], - 'package_requirements': [], - 'pypi_requirements': [], - 'vcs_requirements': [], - 'id': str(self.sub_plugin_release.id), - } - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that regular user can see results - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that contributors can see results - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that the owner can see results - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - def test_get_details(self): - # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.sub_plugin_release.version}/' - response = self.client.get(path=api_path) - timestamp = self.sub_plugin_release.created - request = response.wsgi_request - zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' - payload = { - 'notes': self.sub_plugin_release.notes, - 'zip_file': zip_file, - 'version': self.sub_plugin_release.version, - 'created': { - 'actual': timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'download_count': self.sub_plugin_release.download_count, - 'download_requirements': [], - 'package_requirements': [], - 'pypi_requirements': [], - 'vcs_requirements': [], - 'id': str(self.sub_plugin_release.id), - } - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that regular user can see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}{self.plugin.slug}/invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid sub_plugin_slug.'}, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - plugin = PluginFactory( - basename='test_plugin', - ) - SubPluginPathFactory( - plugin=plugin, - path='sub_plugins', - allow_package_using_basename=True, - ) - sub_plugin = SubPluginFactory( - plugin=plugin, - basename='test_sub_plugin', - owner=self.owner, - ) - SubPluginReleaseFactory( - sub_plugin=sub_plugin, - version='1.0.0', - ) - SubPluginContributorFactory( - sub_plugin=sub_plugin, - user=self.contributor, - ) - api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' - file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' - - # Verify that non logged in user cannot create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify contributor can create a release - version = '1.0.1' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.contributor.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=sub_plugin.releases.count(), - second=2, - ) - content = response.json() - release = sub_plugin.releases.get(pk=content['id']) - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify owner can create a release - version = '1.0.2' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.owner.user) - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=sub_plugin.releases.count(), - second=3, - ) - content = response.json() - release = sub_plugin.releases.get(pk=content['id']) - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify that the same version cannot be created twice - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'version': ['Given version matches existing version.']}, - ) - - # Verify that the basename in the zip file is being verified against - # the basename from the url path - zip_basename = sub_plugin.basename - sub_plugin = SubPluginFactory( - plugin=plugin, - owner=self.owner, - ) - SubPluginReleaseFactory( - sub_plugin=sub_plugin, - version='1.0.0', - ) - api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'zip_file': [ - f"Basename in zip '{zip_basename}' does not match basename" - f" for sub-plugin '{sub_plugin.basename}'.", - ], - } - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post_with_requirements(self): - plugin = PluginFactory( - basename='test_plugin', - ) - SubPluginPathFactory( - plugin=plugin, - path='sub_plugins', - allow_package_using_basename=True, - ) - sub_plugin = SubPluginFactory( - plugin=plugin, - basename='test_sub_plugin', - owner=self.owner, - ) - SubPluginReleaseFactory( - sub_plugin=sub_plugin, - version='1.0.0', - ) - api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' - file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' - version = '1.0.1' - custom_package_1 = PackageFactory( - basename='custom_package_1', - ) - PackageReleaseFactory( - package=custom_package_1, - version='1.0.0', - ) - custom_package_2 = PackageFactory( - basename='custom_package_2', - ) - PackageReleaseFactory( - package=custom_package_2, - version='1.0.0', - ) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=0, - ) - self.client.force_login(self.owner.user) - package = PackageFactory( - basename='test_package', - owner=self.owner, - ) - PackageReleaseFactory( - package=package, - version='1.0.0', - ) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'version': version, - 'zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - release = SubPluginRelease.objects.get(pk=response.json()['id']) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.download_requirements.count(), - second=2, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.pypi_requirements.count(), - second=2, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.vcs_requirements.count(), - second=2, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.sub_plugin} - Release', - ) - - -class SubPluginTagViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = plugin = sub_plugin = None - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - cls.plugin = PluginFactory() - cls.sub_plugin = SubPluginFactory( - owner=cls.owner, - plugin=cls.plugin, - ) - cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' - cls.contributor = ForumUserFactory() - SubPluginContributorFactory( - sub_plugin=cls.sub_plugin, - user=cls.contributor, - ) - cls.sub_plugin_tag_1 = SubPluginTagFactory( - sub_plugin=cls.sub_plugin, - ) - cls.sub_plugin_tag_2 = SubPluginTagFactory( - sub_plugin=cls.sub_plugin, - ) - cls.regular_user = ForumUserFactory() - - def test_inheritance(self): - self.assertTrue(expr=issubclass(SubPluginTagViewSet, ProjectTagViewSet)) - - def test_base_attributes(self): - self.assertEqual( - first=SubPluginTagViewSet.serializer_class, - second=SubPluginTagSerializer, - ) - self.assertEqual( - first=SubPluginTagViewSet.project_type, - second='sub-plugin', - ) - self.assertEqual( - first=SubPluginTagViewSet.project_model, - second=SubPlugin, - ) - self.assertIs( - expr1=SubPluginTagViewSet.queryset.model, - expr2=SubPluginTag, - ) - self.assertDictEqual( - d1=SubPluginTagViewSet.queryset.query.select_related, - d2={'tag': {}, 'sub_plugin': {}} - ) - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginTagViewSet.http_method_names, - tuple2=('get', 'post', 'delete', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.sub_plugin_tag_2.tag.name, - }, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.sub_plugin_tag_2.tag.name, - }, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.sub_plugin_tag_2.tag.name, - 'id': str(self.sub_plugin_tag_2.id), - }, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=2) - self.assertDictEqual( - d1=content['results'][0], - d2={ - 'tag': self.sub_plugin_tag_2.tag.name, - 'id': str(self.sub_plugin_tag_2.id), - }, - ) - - def test_get_details(self): - # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_tag_1.id}/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'tag': self.sub_plugin_tag_1.tag.name, - 'id': str(self.sub_plugin_tag_1.id), - }, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2={ - 'tag': self.sub_plugin_tag_1.tag.name, - 'id': str(self.sub_plugin_tag_1.id), - }, - ) - - def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'detail': 'Invalid sub_plugin_slug.'}, - ) - - def test_post(self): - # Verify that non logged in user cannot add a tag - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot add a tag - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can add a tag - self.client.force_login(self.contributor.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-1'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - # Verify that owner can add a tag - self.client.force_login(self.owner.user) - response = self.client.post( - path=self.api_path, - data={'tag': 'new-tag-2'}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - - def test_post_failure(self): - self.client.force_login(self.owner.user) - - # Verify existing affiliated tag cannot be added - response = self.client.post( - path=self.api_path, - data={'tag': self.sub_plugin_tag_1.tag}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'tag': [f'Tag already linked to {SubPluginTagViewSet.project_type}.']} - ) - - # Verify black-listed tag cannot be added - tag = TagFactory( - black_listed=True, - ) - response = self.client.post( - path=self.api_path, - data={'tag': tag.name}, - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'tag': [f"Tag '{tag.name}' is black-listed, unable to add."]} - ) - - def test_delete(self): - # Verify that non logged in user cannot delete a tag - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot delete a tag - self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can delete a tag - self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_1.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - # Verify that owner can delete a tag - self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_2.id}/', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_204_NO_CONTENT, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second=f'{self.sub_plugin} - Tag', - ) - - -class SubPluginViewSetTestCase(APITestCase): - - base_api_path = contributor = owner = sub_plugin = None - MEDIA_ROOT = Path(tempfile.mkdtemp()) - - @classmethod - def setUpTestData(cls): - cls.owner = ForumUserFactory() - plugin = PluginFactory() - cls.sub_plugin = SubPluginFactory( - owner=cls.owner, - plugin=plugin, - logo='logo.jpg', - ) - cls.sub_plugin_release = SubPluginReleaseFactory( - sub_plugin=cls.sub_plugin, - zip_file='/media/release_v1.0.0.zip', - ) - cls.base_api_path = f'/api/sub-plugins/projects/' - cls.api_path = f'{cls.base_api_path}{plugin.slug}/' - cls.contributor = ForumUserFactory() - SubPluginContributorFactory( - sub_plugin=cls.sub_plugin, - user=cls.contributor, - ) - cls.regular_user = ForumUserFactory() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls.MEDIA_ROOT, ignore_errors=True) - super().tearDownClass() - - def test_inheritance(self): - self.assertTrue(expr=issubclass(SubPluginViewSet, ProjectViewSet)) - - def test_base_attributes(self): - self.assertEqual( - first=SubPluginViewSet.filterset_class, - second=SubPluginFilterSet, - ) - self.assertEqual( - first=SubPluginViewSet.serializer_class, - second=SubPluginSerializer, - ) - self.assertEqual( - first=SubPluginViewSet.creation_serializer_class, - second=SubPluginCreateSerializer, - ) - self.assertIs(expr1=SubPluginViewSet.queryset.model, expr2=SubPlugin) - prefetch_lookups = SubPluginViewSet.queryset._prefetch_related_lookups - self.assertEqual(first=len(prefetch_lookups), second=1) - lookup = prefetch_lookups[0] - self.assertEqual(first=lookup.prefetch_to, second='releases') - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('-created',), - ) - self.assertDictEqual( - d1=SubPluginViewSet.queryset.query.select_related, - d2={'owner': {'user': {}}, 'plugin': {}}, - ) - - def test_get_queryset(self): - with self.assertRaises(ParseError) as context: - obj = SubPluginViewSet() - obj.kwargs = {} - obj.get_queryset() - - self.assertEqual( - first=context.exception.detail, - second='Invalid plugin_slug.', - ) - - # TODO: validate the query returns the correct data - plugin = PluginFactory() - obj.kwargs = {'plugin_slug': plugin.slug} - obj.get_queryset() - - # TODO: validate the query returns the correct data - obj.kwargs = {} - obj.plugin = plugin - obj.get_queryset() - - def test_http_method_names(self): - self.assertTupleEqual( - tuple1=SubPluginViewSet.http_method_names, - tuple2=('get', 'post', 'patch', 'options'), - ) - - def test_get_list(self): - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - request = response.wsgi_request - domain = f'{request.scheme}://{request.get_host()}' - zip_file = f'{domain}{self.sub_plugin_release.get_absolute_url()}' - logo = f'{domain}{self.sub_plugin.logo.url}' - self.assertEqual(first=content['count'], second=1) - created_timestamp = self.sub_plugin.created - updated_timestamp = self.sub_plugin.updated - payload = { - 'name': self.sub_plugin.name, - 'slug': self.sub_plugin.slug, - 'total_downloads': self.sub_plugin.total_downloads, - 'current_release': { - 'version': self.sub_plugin_release.version, - 'notes': self.sub_plugin_release.notes, - 'zip_file': zip_file, - }, - 'created': { - 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'updated': { - 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'synopsis': self.sub_plugin.synopsis, - 'description': self.sub_plugin.description, - 'configuration': self.sub_plugin.configuration, - 'logo': logo, - 'video': self.sub_plugin.video, - 'owner': { - 'forum_id': self.sub_plugin.owner.forum_id, - 'username': self.sub_plugin.owner.user.username, - } - } - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that regular user can see results but not 'id' - self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that contributors can see results AND 'id' - self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - # Verify that the owner can see results AND 'id' - self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - content = response.json() - self.assertEqual(first=content['count'], second=1) - self.assertDictEqual( - d1=content['results'][0], - d2=payload, - ) - - def test_get_list_filters(self): - response = self.client.get(path=self.api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate tag filtering - response = self.client.get(path=f'{self.api_path}?tag=test_tag') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - tag = TagFactory(name='test_tag') - SubPluginTagFactory( - sub_plugin=self.sub_plugin, - tag=tag, - ) - response = self.client.get(path=f'{self.api_path}?tag=test_tag') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate game filtering - response = self.client.get(path=f'{self.api_path}?game=game1') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - game = GameFactory( - name='Game1', - basename='game1', - icon='icon1.jpg', - ) - SubPluginGameFactory( - sub_plugin=self.sub_plugin, - game=game, - ) - response = self.client.get(path=f'{self.api_path}?game=game1') - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - # Validate game filtering - response = self.client.get( - path=f'{self.api_path}?user={self.regular_user.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=0, - ) - response = self.client.get( - path=f'{self.api_path}?user={self.contributor.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - response = self.client.get( - path=f'{self.api_path}?user={self.owner.user.username}', - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertEqual( - first=response.json()['count'], - second=1, - ) - - def test_get_details(self): - # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.sub_plugin.slug}/' - response = self.client.get(path=api_path) - request = response.wsgi_request - domain = f'{request.scheme}://{request.get_host()}' - zip_file = f'{domain}{self.sub_plugin_release.get_absolute_url()}' - logo = f'{domain}{self.sub_plugin.logo.url}' - created_timestamp = self.sub_plugin.created - updated_timestamp = self.sub_plugin.updated - payload = { - 'name': self.sub_plugin.name, - 'slug': self.sub_plugin.slug, - 'total_downloads': self.sub_plugin.total_downloads, - 'current_release': { - 'version': self.sub_plugin_release.version, - 'notes': self.sub_plugin_release.notes, - 'zip_file': zip_file, - 'package_requirements': [], - 'pypi_requirements': [], - 'version_control_requirements': [], - 'download_requirements': [], - }, - 'created': { - 'actual': created_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(created_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(created_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'updated': { - 'actual': updated_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'locale': formats.date_format(updated_timestamp, 'DATETIME_FORMAT'), - 'locale_short': formats.date_format(updated_timestamp, 'SHORT_DATETIME_FORMAT'), - }, - 'synopsis': self.sub_plugin.synopsis, - 'description': self.sub_plugin.description, - 'configuration': self.sub_plugin.configuration, - 'logo': logo, - 'video': self.sub_plugin.video, - 'owner': { - 'forum_id': self.sub_plugin.owner.forum_id, - 'username': self.sub_plugin.owner.user.username, - } - } - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that regular user can see details - self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that contributors can see details - self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - # Verify that the owner can see details - self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - self.assertDictEqual( - d1=response.json(), - d2=payload, - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post(self): - # Verify non logged in user cannot create a sub-plugin - plugin = PluginFactory( - basename='test_plugin', - ) - SubPluginPathFactory( - plugin=plugin, - path='sub_plugins', - allow_package_using_basename=True, - ) - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' - file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' - version = '1.0.0' - api_path = f'{self.base_api_path}{plugin.slug}/' - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'name': 'Test SubPlugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that a logged in user can create a sub-plugin - self.assertEqual( - first=SubPlugin.objects.count(), - second=1, - ) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - self.client.force_login(self.regular_user.user) - response = self.client.post( - path=api_path, - data={ - 'name': 'Test SubPlugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - self.assertEqual( - first=SubPlugin.objects.count(), - second=2, - ) - content = response.json() - sub_plugin = SubPlugin.objects.get(slug=content['slug']) - self.assertEqual( - first=sub_plugin.releases.count(), - second=1, - ) - release = sub_plugin.releases.get() - self.assertEqual( - first=release.version, - second=version, - ) - - # Verify cannot create a sub-plugin where the basename already exists - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'name': 'Test SubPlugin', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, - ) - self.assertDictEqual( - d1=response.json(), - d2={'basename': 'SubPlugin already exists. Cannot create.'} - ) - - @override_settings(MEDIA_ROOT=MEDIA_ROOT) - def test_post_with_requirements(self): - plugin = PluginFactory( - basename='test_plugin', - ) - SubPluginPathFactory( - plugin=plugin, - path='sub_plugins', - allow_package_using_basename=True, - ) - base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' - file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' - version = '1.0.0' - api_path = f'{self.base_api_path}{plugin.slug}/' - custom_package_1 = PackageFactory( - basename='custom_package_1', - ) - PackageReleaseFactory( - package=custom_package_1, - version='1.0.0', - ) - custom_package_2 = PackageFactory( - basename='custom_package_2', - ) - PackageReleaseFactory( - package=custom_package_2, - version='1.0.0', - ) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=0, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=0, - ) - self.client.force_login(self.owner.user) - with file_path.open('rb') as open_file: - zip_file = UploadedFile(open_file, content_type='application/zip') - response = self.client.post( - path=api_path, - data={ - 'name': 'Test Package', - 'releases.notes': '', - 'releases.version': version, - 'releases.zip_file': zip_file, - }, - ) - - self.assertEqual( - first=response.status_code, - second=status.HTTP_201_CREATED, - ) - contents = response.json() - sub_plugin = SubPlugin.objects.get(slug=contents['slug'], plugin=plugin) - release = SubPluginRelease.objects.get(sub_plugin=sub_plugin) - self.assertEqual( - first=DownloadRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.download_requirements.count(), - second=2, - ) - self.assertEqual( - first=PyPiRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.pypi_requirements.count(), - second=2, - ) - self.assertEqual( - first=VersionControlRequirement.objects.count(), - second=2, - ) - self.assertEqual( - first=release.vcs_requirements.count(), - second=2, - ) - - def test_patch(self): - # Verify that non logged in user cannot update a path - api_path = f'{self.api_path}{self.sub_plugin.slug}/' - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that regular user cannot update a path - self.client.force_login(self.regular_user.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_403_FORBIDDEN, - ) - - # Verify that contributor can update a path - self.client.force_login(self.contributor.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - # Verify that owner can update a path - self.client.force_login(self.owner.user) - response = self.client.patch( - path=api_path, - data={ - 'synopsis': 'New Test Synopsis', - } - ) - self.assertEqual( - first=response.status_code, - second=status.HTTP_200_OK, - ) - - def test_options(self): - response = self.client.options(path=self.api_path) - self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) - self.assertEqual( - first=response.json()['name'], - second='Sub Plugin List', - ) From 15c8b4fb4db67b8017f447cf45136d0fc29af603 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 18:28:07 -0500 Subject: [PATCH 096/211] Added created_by field to release models to store the user that uploaded the release. Updated admin, serializers, and tests accordingly. --- project_manager/common/admin/inlines.py | 2 ++ .../common/api/serializers/__init__.py | 14 +++++---- .../common/api/serializers/mixins.py | 1 + .../common/api/tests/test_serializers.py | 14 ++++++++- project_manager/common/models.py | 6 ++++ project_manager/common/tests/test_admin.py | 2 ++ project_manager/common/tests/test_models.py | 21 +++++++++++++ .../migrations/0005_release_created_by.py | 30 +++++++++++++++++++ .../packages/api/tests/test_project_views.py | 4 +++ .../packages/api/tests/test_release_views.py | 18 +++++++++++ .../plugins/api/tests/test_project_views.py | 4 +++ .../plugins/api/tests/test_release_views.py | 18 +++++++++++ .../api/tests/test_project_views.py | 4 +++ .../api/tests/test_release_views.py | 18 +++++++++++ test_utils/factories/packages.py | 3 ++ test_utils/factories/plugins.py | 3 ++ test_utils/factories/sub_plugins.py | 3 ++ 17 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 project_manager/migrations/0005_release_created_by.py diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index b47b81fa..f276e21c 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -103,12 +103,14 @@ class ProjectReleaseInline(admin.StackedInline): 'zip_file', 'download_count', 'created', + 'created_by', ) readonly_fields = ( 'zip_file', 'download_count', 'created', + 'created_by', ) def get_queryset(self, request): diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index 17219d4e..9a382b78 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -121,15 +121,13 @@ def create(self, validated_data): validated_data['created'] = validated_data['updated'] = current_time instance = super().create(validated_data) self.requirements = self.release_dict.pop('requirements') - version = self.release_dict['version'] - zip_file = self.release_dict['zip_file'] - notes = self.release_dict['notes'] kwargs = { self.project_type.replace('-', '_'): instance, 'created': current_time, - 'notes': notes, - 'version': version, - 'zip_file': zip_file, + 'notes': self.release_dict['notes'], + 'version': self.release_dict['version'], + 'zip_file': self.release_dict['zip_file'], + 'created_by': self.context['request'].user.forum_user, } release = self.release_model.objects.create(**kwargs) self._create_requirements(release=release) @@ -255,6 +253,9 @@ class ProjectReleaseSerializer( """Base ProjectRelease Serializer for listing.""" created = SerializerMethodField() + created_by = ForumUserContributorSerializer( + read_only=True, + ) download_count = IntegerField(read_only=True) class Meta: @@ -266,6 +267,7 @@ class Meta: 'zip_file', 'version', 'created', + 'created_by', 'download_count', 'download_requirements', 'package_requirements', diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 00aae026..6c1738af 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -126,6 +126,7 @@ def validate(self, attrs): """Validate that the new release can be created.""" version = attrs.get('version', '') zip_file = attrs.get('zip_file') + attrs['created_by'] = self.context['request'].user.forum_user # Validate the version is new for the project kwargs = self.get_project_kwargs() diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/common/api/tests/test_serializers.py index c5be61c5..f1e359b3 100644 --- a/project_manager/common/api/tests/test_serializers.py +++ b/project_manager/common/api/tests/test_serializers.py @@ -460,7 +460,7 @@ def test_declared_fields(self): declared_fields = getattr(ProjectReleaseSerializer, '_declared_fields') self.assertEqual( first=len(declared_fields), - second=2, + second=3, ) self.assertIn( @@ -472,6 +472,17 @@ def test_declared_fields(self): cls=SerializerMethodField, ) + self.assertIn( + member='created_by', + container=declared_fields, + ) + field = declared_fields['created_by'] + self.assertIsInstance( + obj=field, + cls=ForumUserContributorSerializer, + ) + self.assertTrue(expr=field.read_only) + self.assertIn( member='download_count', container=declared_fields, @@ -491,6 +502,7 @@ def test_meta_class(self): 'zip_file', 'version', 'created', + 'created_by', 'download_count', 'download_requirements', 'package_requirements', diff --git a/project_manager/common/models.py b/project_manager/common/models.py index 03e6430d..b3142e4c 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -261,6 +261,12 @@ class ProjectRelease(AbstractUUIDPrimaryKeyModel): created = AutoCreatedField( verbose_name='created', ) + created_by = models.ForeignKey( + to='users.ForumUser', + related_name='%(class)ss', + on_delete=models.SET_NULL, + null=True, + ) class Meta: """Define metaclass attributes.""" diff --git a/project_manager/common/tests/test_admin.py b/project_manager/common/tests/test_admin.py index f3e2b10b..522dccab 100644 --- a/project_manager/common/tests/test_admin.py +++ b/project_manager/common/tests/test_admin.py @@ -210,6 +210,7 @@ def test_fields(self): 'zip_file', 'download_count', 'created', + 'created_by', ), ) @@ -220,6 +221,7 @@ def test_readonly_fields(self): 'zip_file', 'download_count', 'created', + 'created_by', ), ) diff --git a/project_manager/common/tests/test_models.py b/project_manager/common/tests/test_models.py index 5e9e1eba..98fa32c8 100644 --- a/project_manager/common/tests/test_models.py +++ b/project_manager/common/tests/test_models.py @@ -490,6 +490,27 @@ def test_created_field(self): second='created', ) + def test_created_by_field(self): + field = ProjectRelease._meta.get_field('created_by') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second='users.ForumUser', + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.SET_NULL, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='%(class)ss', + ) + self.assertFalse(expr=field.blank) + self.assertTrue(expr=field.null) + def test_project_class_required(self): obj = '' with self.assertRaises(NotImplementedError) as context: diff --git a/project_manager/migrations/0005_release_created_by.py b/project_manager/migrations/0005_release_created_by.py new file mode 100644 index 00000000..dffcb71a --- /dev/null +++ b/project_manager/migrations/0005_release_created_by.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.9 on 2021-11-13 22:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ('project_manager', '0004_auto_20211103_1055'), + ] + + operations = [ + migrations.AddField( + model_name='packagerelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='packagereleases', to='users.forumuser'), + ), + migrations.AddField( + model_name='pluginrelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pluginreleases', to='users.forumuser'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subpluginreleases', to='users.forumuser'), + ), + ] diff --git a/project_manager/packages/api/tests/test_project_views.py b/project_manager/packages/api/tests/test_project_views.py index 835736b8..b404ef27 100644 --- a/project_manager/packages/api/tests/test_project_views.py +++ b/project_manager/packages/api/tests/test_project_views.py @@ -450,6 +450,10 @@ def test_post(self): second=1, ) release = package.releases.get() + self.assertEqual( + first=release.created_by.forum_id, + second=self.regular_user.forum_id, + ) self.assertEqual( first=release.version, second=version, diff --git a/project_manager/packages/api/tests/test_release_views.py b/project_manager/packages/api/tests/test_release_views.py index 4a8870b3..3b15aab9 100644 --- a/project_manager/packages/api/tests/test_release_views.py +++ b/project_manager/packages/api/tests/test_release_views.py @@ -194,6 +194,7 @@ def test_get_list(self): timestamp = self.package_release.created request = response.wsgi_request zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' + created_by = self.package_release.created_by payload = { 'notes': self.package_release.notes, 'zip_file': zip_file, @@ -203,6 +204,10 @@ def test_get_list(self): 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), }, + 'created_by': { + 'forum_id': created_by.forum_id, + 'username': created_by.user.username, + }, 'download_count': self.package_release.download_count, 'download_requirements': [], 'package_requirements': [], @@ -264,6 +269,7 @@ def test_get_details(self): timestamp = self.package_release.created request = response.wsgi_request zip_file = f'{request.scheme}://{request.get_host()}{self.package_release.zip_file.url}' + created_by = self.package_release.created_by payload = { 'notes': self.package_release.notes, 'zip_file': zip_file, @@ -273,6 +279,10 @@ def test_get_details(self): 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), }, + 'created_by': { + 'forum_id': created_by.forum_id, + 'username': created_by.user.username, + }, 'download_count': self.package_release.download_count, 'download_requirements': [], 'package_requirements': [], @@ -413,6 +423,10 @@ def test_post(self): ) content = response.json() release = package.releases.get(id=content['id']) + self.assertEqual( + first=release.created_by.forum_id, + second=self.contributor.forum_id, + ) self.assertEqual( first=release.version, second=version, @@ -441,6 +455,10 @@ def test_post(self): ) content = response.json() release = package.releases.get(id=content['id']) + self.assertEqual( + first=release.created_by.forum_id, + second=self.owner.forum_id, + ) self.assertEqual( first=release.version, second=version, diff --git a/project_manager/plugins/api/tests/test_project_views.py b/project_manager/plugins/api/tests/test_project_views.py index 9e7be739..4bf6f3ee 100644 --- a/project_manager/plugins/api/tests/test_project_views.py +++ b/project_manager/plugins/api/tests/test_project_views.py @@ -451,6 +451,10 @@ def test_post(self): second=1, ) release = plugin.releases.get() + self.assertEqual( + first=release.created_by.forum_id, + second=self.regular_user.forum_id, + ) self.assertEqual( first=release.version, second=version, diff --git a/project_manager/plugins/api/tests/test_release_views.py b/project_manager/plugins/api/tests/test_release_views.py index 2c93ddd2..62e4301c 100644 --- a/project_manager/plugins/api/tests/test_release_views.py +++ b/project_manager/plugins/api/tests/test_release_views.py @@ -195,6 +195,7 @@ def test_get_list(self): timestamp = self.plugin_release.created request = response.wsgi_request zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' + created_by = self.plugin_release.created_by payload = { 'notes': self.plugin_release.notes, 'zip_file': zip_file, @@ -204,6 +205,10 @@ def test_get_list(self): 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), }, + 'created_by': { + 'forum_id': created_by.forum_id, + 'username': created_by.user.username, + }, 'download_count': self.plugin_release.download_count, 'download_requirements': [], 'package_requirements': [], @@ -265,6 +270,7 @@ def test_get_details(self): timestamp = self.plugin_release.created request = response.wsgi_request zip_file = f'{request.scheme}://{request.get_host()}{self.plugin_release.zip_file.url}' + created_by = self.plugin_release.created_by payload = { 'notes': self.plugin_release.notes, 'zip_file': zip_file, @@ -274,6 +280,10 @@ def test_get_details(self): 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), }, + 'created_by': { + 'forum_id': created_by.forum_id, + 'username': created_by.user.username, + }, 'download_count': self.plugin_release.download_count, 'download_requirements': [], 'package_requirements': [], @@ -413,6 +423,10 @@ def test_post(self): ) content = response.json() release = plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.created_by.forum_id, + second=self.contributor.forum_id, + ) self.assertEqual( first=release.version, second=version, @@ -440,6 +454,10 @@ def test_post(self): ) content = response.json() release = plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.created_by.forum_id, + second=self.owner.forum_id, + ) self.assertEqual( first=release.version, second=version, diff --git a/project_manager/sub_plugins/api/tests/test_project_views.py b/project_manager/sub_plugins/api/tests/test_project_views.py index 2d48fa3d..b1c15cb7 100644 --- a/project_manager/sub_plugins/api/tests/test_project_views.py +++ b/project_manager/sub_plugins/api/tests/test_project_views.py @@ -486,6 +486,10 @@ def test_post(self): second=1, ) release = sub_plugin.releases.get() + self.assertEqual( + first=release.created_by.forum_id, + second=self.regular_user.forum_id, + ) self.assertEqual( first=release.version, second=version, diff --git a/project_manager/sub_plugins/api/tests/test_release_views.py b/project_manager/sub_plugins/api/tests/test_release_views.py index 553f2a42..32938306 100644 --- a/project_manager/sub_plugins/api/tests/test_release_views.py +++ b/project_manager/sub_plugins/api/tests/test_release_views.py @@ -234,6 +234,7 @@ def test_get_list(self): timestamp = self.sub_plugin_release.created request = response.wsgi_request zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' + created_by = self.sub_plugin_release.created_by payload = { 'notes': self.sub_plugin_release.notes, 'zip_file': zip_file, @@ -243,6 +244,10 @@ def test_get_list(self): 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), }, + 'created_by': { + 'forum_id': created_by.forum_id, + 'username': created_by.user.username, + }, 'download_count': self.sub_plugin_release.download_count, 'download_requirements': [], 'package_requirements': [], @@ -304,6 +309,7 @@ def test_get_details(self): timestamp = self.sub_plugin_release.created request = response.wsgi_request zip_file = f'{request.scheme}://{request.get_host()}{self.sub_plugin_release.zip_file.url}' + created_by = self.sub_plugin_release.created_by payload = { 'notes': self.sub_plugin_release.notes, 'zip_file': zip_file, @@ -313,6 +319,10 @@ def test_get_details(self): 'locale': formats.date_format(timestamp, 'DATETIME_FORMAT'), 'locale_short': formats.date_format(timestamp, 'SHORT_DATETIME_FORMAT'), }, + 'created_by': { + 'forum_id': created_by.forum_id, + 'username': created_by.user.username, + }, 'download_count': self.sub_plugin_release.download_count, 'download_requirements': [], 'package_requirements': [], @@ -462,6 +472,10 @@ def test_post(self): ) content = response.json() release = sub_plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.created_by.forum_id, + second=self.contributor.forum_id, + ) self.assertEqual( first=release.version, second=version, @@ -490,6 +504,10 @@ def test_post(self): ) content = response.json() release = sub_plugin.releases.get(pk=content['id']) + self.assertEqual( + first=release.created_by.forum_id, + second=self.owner.forum_id, + ) self.assertEqual( first=release.version, second=version, diff --git a/test_utils/factories/packages.py b/test_utils/factories/packages.py index f5c6f0dd..9208f534 100644 --- a/test_utils/factories/packages.py +++ b/test_utils/factories/packages.py @@ -68,6 +68,9 @@ class PackageReleaseFactory(factory.django.DjangoModelFactory): factory='test_utils.factories.packages.PackageFactory', ) version = factory.Sequence(function=lambda n: f'1.0.{n}') + created_by = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) class Meta: """Define metaclass attributes.""" diff --git a/test_utils/factories/plugins.py b/test_utils/factories/plugins.py index b2011e25..385aba1f 100644 --- a/test_utils/factories/plugins.py +++ b/test_utils/factories/plugins.py @@ -70,6 +70,9 @@ class PluginReleaseFactory(factory.django.DjangoModelFactory): factory='test_utils.factories.plugins.PluginFactory', ) version = factory.Sequence(function=lambda n: f'1.0.{n}') + created_by = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) class Meta: """Define metaclass attributes.""" diff --git a/test_utils/factories/sub_plugins.py b/test_utils/factories/sub_plugins.py index 5f21f9b9..a8f456bf 100644 --- a/test_utils/factories/sub_plugins.py +++ b/test_utils/factories/sub_plugins.py @@ -71,6 +71,9 @@ class SubPluginReleaseFactory(factory.django.DjangoModelFactory): factory='test_utils.factories.sub_plugins.SubPluginFactory', ) version = factory.Sequence(function=lambda n: f'1.0.{n}') + created_by = factory.SubFactory( + factory='test_utils.factories.users.ForumUserFactory', + ) class Meta: """Define metaclass attributes.""" From a2c55f2032aad782e86be0d08e089de800354a84 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 18:43:33 -0500 Subject: [PATCH 097/211] Updated statistics view test to avoid missing branch coverage. --- project_manager/tests/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_manager/tests/test_views.py b/project_manager/tests/test_views.py index da90b5e8..9e18b389 100644 --- a/project_manager/tests/test_views.py +++ b/project_manager/tests/test_views.py @@ -109,9 +109,9 @@ def test_get(self): download_count=download_count, ) - if any([ + if n > 1 and any([ + n == 2, choice([True, False]), - (n == plugin_count and not sub_plugin_count), ]): count = randint(1, 2) sub_plugin_count += count From a1eb6368a33eb569042e365a628ab2ef5f195f21 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 13 Nov 2021 21:31:54 -0500 Subject: [PATCH 098/211] Updated requirement. --- pip-requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index dbf72015..be725950 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,6 +1,6 @@ django==3.2.9 django-embed-video==1.4.0 -django-extensions==3.1.3 +django-extensions==3.1.5 django-filter==21.1 django-model-utils==4.2.0 django-precise-bbcode==1.2.13 From a46995b15e7e86fddb325197a72a950fa77f37c8 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Mon, 15 Nov 2021 18:55:55 -0500 Subject: [PATCH 099/211] Fixed a few tests for windows testing. --- project_manager/packages/api/tests/test_related_views.py | 3 +++ project_manager/plugins/api/tests/test_related_views.py | 4 +++- project_manager/sub_plugins/api/tests/test_related_views.py | 4 +++- project_manager/sub_plugins/api/tests/test_serializers.py | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/project_manager/packages/api/tests/test_related_views.py b/project_manager/packages/api/tests/test_related_views.py index 8f2a0759..3c1d9b02 100644 --- a/project_manager/packages/api/tests/test_related_views.py +++ b/project_manager/packages/api/tests/test_related_views.py @@ -4,9 +4,11 @@ # Python import shutil import tempfile +from datetime import timedelta # Django from django.test import override_settings +from django.utils.timezone import now # Third Party Python from path import Path @@ -767,6 +769,7 @@ def setUpTestData(cls): ) cls.package_image_2 = PackageImageFactory( package=cls.package, + created=now() + timedelta(seconds=1) ) cls.regular_user = ForumUserFactory() diff --git a/project_manager/plugins/api/tests/test_related_views.py b/project_manager/plugins/api/tests/test_related_views.py index a59a1bbb..5bfb8359 100644 --- a/project_manager/plugins/api/tests/test_related_views.py +++ b/project_manager/plugins/api/tests/test_related_views.py @@ -4,9 +4,11 @@ # Python import shutil import tempfile +from datetime import timedelta # Django from django.test import override_settings +from django.utils.timezone import now # Third Party Python from path import Path @@ -772,6 +774,7 @@ def setUpTestData(cls): ) cls.plugin_image_2 = PluginImageFactory( plugin=cls.plugin, + created=now() + timedelta(seconds=1) ) cls.regular_user = ForumUserFactory() @@ -841,7 +844,6 @@ def test_get_list(self): content = response.json() self.assertEqual(first=content['count'], second=2) request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.plugin_image_2.image.url}' self.assertDictEqual( d1=content['results'][0], d2={ diff --git a/project_manager/sub_plugins/api/tests/test_related_views.py b/project_manager/sub_plugins/api/tests/test_related_views.py index b365add7..41e08e70 100644 --- a/project_manager/sub_plugins/api/tests/test_related_views.py +++ b/project_manager/sub_plugins/api/tests/test_related_views.py @@ -4,9 +4,11 @@ # Python import shutil import tempfile +from datetime import timedelta # Django from django.test import override_settings +from django.utils.timezone import now # Third Party Python from path import Path @@ -775,6 +777,7 @@ def setUpTestData(cls): ) cls.sub_plugin_image_2 = SubPluginImageFactory( sub_plugin=cls.sub_plugin, + created=now() + timedelta(seconds=1) ) cls.regular_user = ForumUserFactory() @@ -879,7 +882,6 @@ def test_get_list(self): content = response.json() self.assertEqual(first=content['count'], second=2) request = response.wsgi_request - image = f'{request.scheme}://{request.get_host()}{self.sub_plugin_image_2.image.url}' self.assertDictEqual( d1=content['results'][0], d2={ diff --git a/project_manager/sub_plugins/api/tests/test_serializers.py b/project_manager/sub_plugins/api/tests/test_serializers.py index 495ed51e..cd221c4f 100644 --- a/project_manager/sub_plugins/api/tests/test_serializers.py +++ b/project_manager/sub_plugins/api/tests/test_serializers.py @@ -452,8 +452,10 @@ def test_parent_project(self): ) def test_get_download_kwargs(self): + zip_file = settings.MEDIA_ROOT / 'releases' / 'file_name_v1.0.0.zip' + zip_file = zip_file.replace('\\', '/') release = SubPluginReleaseFactory( - zip_file=settings.MEDIA_ROOT / 'releases' / 'file_name_v1.0.0.zip', + zip_file=zip_file, ) obj = release.sub_plugin instance = SubPluginSerializer() From 082465f8eaa532406021477d6fc5865668f2fe1e Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 5 Dec 2021 10:26:28 -0500 Subject: [PATCH 100/211] Fixed requirements. --- pip-requirements/base.txt | 2 +- pip-requirements/local.txt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index be725950..14b57098 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -5,5 +5,5 @@ django-filter==21.1 django-model-utils==4.2.0 django-precise-bbcode==1.2.13 djangorestframework==3.12.4 -Markdown==3.3.4 +Markdown==3.3.6 path==16.2.0 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index ca5dbd61..2875285e 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -2,11 +2,11 @@ django-debug-toolbar==3.2.2 factory-boy==3.2.1 flake8==3.9.2 -prospector==1.5.1 -pycodestyle==2.8.0 +prospector==1.5.3 +pycodestyle==2.7.0 pydocstyle==6.1.1 -pyflakes==2.4.0 +pyflakes==2.3.1 pylint==2.11.1 pytest-cov==3.0.0 -pytest-django==4.4.0 +pytest-django==4.5.1 random-username==1.0.2 From 1ba647a8010462cff6cc9184170f0572cc5f3e35 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 12 Dec 2021 10:01:17 -0500 Subject: [PATCH 101/211] Upgraded a few requirements. --- pip-requirements/base.txt | 2 +- pip-requirements/local.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index 14b57098..9c4217a4 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -3,7 +3,7 @@ django-embed-video==1.4.0 django-extensions==3.1.5 django-filter==21.1 django-model-utils==4.2.0 -django-precise-bbcode==1.2.13 +django-precise-bbcode==1.2.14 djangorestframework==3.12.4 Markdown==3.3.6 path==16.2.0 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index 2875285e..f64d306e 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -2,11 +2,11 @@ django-debug-toolbar==3.2.2 factory-boy==3.2.1 flake8==3.9.2 -prospector==1.5.3 +prospector==1.5.3.1 pycodestyle==2.7.0 pydocstyle==6.1.1 pyflakes==2.3.1 -pylint==2.11.1 +pylint==2.12.2 pytest-cov==3.0.0 -pytest-django==4.5.1 +pytest-django==4.5.2 random-username==1.0.2 From cafb4b2146c8c693f7cc5db1988357ba3621794e Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 12 Dec 2021 10:50:17 -0500 Subject: [PATCH 102/211] Fixed link in base.html. --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index 5bf7f410..f2f8533f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -129,7 +129,7 @@

Popular Tags

  • Forums
  • Wiki
  • Source
  • -
  • Issues
  • +
  • Issues
  • Current Builds
  • From d10fe737c68031d9aa1c4d8e7971e29ed69b363d Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 12 Dec 2021 10:50:56 -0500 Subject: [PATCH 103/211] Fixed coverage for branch that is not really testable. --- project_manager/common/api/views/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index 3bc94616..4942b195 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -101,10 +101,10 @@ def get_queryset(self): def get_view_name(self): """Return the name for the view.""" - if hasattr(self, 'kwargs') and self.related_model_type is not None: + if hasattr(self, 'kwargs'): # pragma: no branch plural = 's' if self.action == 'list' else '' return f'{self.project} - {self.related_model_type}{plural}' - return super().get_view_name() + return super().get_view_name() # pragma: no cover class ProjectThroughModelMixin(ProjectRelatedInfoMixin): From b6a6d4e173ddcf433b865755948af2ca11ba8f72 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 5 Feb 2022 16:57:39 -0500 Subject: [PATCH 104/211] Updated requirements. Created new migration for Django4.0 changes. --- pip-requirements/base.txt | 8 ++-- pip-requirements/local.txt | 4 +- ...lter_packagerelease_created_by_and_more.py | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index 9c4217a4..ec435bad 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,9 +1,9 @@ -django==3.2.9 -django-embed-video==1.4.0 +django==4.0.2 +django-embed-video==1.4.1 django-extensions==3.1.5 django-filter==21.1 django-model-utils==4.2.0 django-precise-bbcode==1.2.14 -djangorestframework==3.12.4 +djangorestframework==3.13.1 Markdown==3.3.6 -path==16.2.0 +path==16.3.0 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index f64d306e..a432620d 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,8 +1,8 @@ -r base.txt -django-debug-toolbar==3.2.2 +django-debug-toolbar==3.2.4 factory-boy==3.2.1 flake8==3.9.2 -prospector==1.5.3.1 +prospector==1.6.0 pycodestyle==2.7.0 pydocstyle==6.1.1 pyflakes==2.3.1 diff --git a/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py b/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py new file mode 100644 index 00000000..1395f02e --- /dev/null +++ b/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 4.0.2 on 2022-02-05 21:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ('project_manager', '0005_release_created_by'), + ] + + operations = [ + migrations.AlterField( + model_name='package', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='users.forumuser'), + ), + migrations.AlterField( + model_name='packagerelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='users.forumuser'), + ), + migrations.AlterField( + model_name='plugin', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='users.forumuser'), + ), + migrations.AlterField( + model_name='pluginrelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='users.forumuser'), + ), + migrations.AlterField( + model_name='subplugin', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='users.forumuser'), + ), + migrations.AlterField( + model_name='subpluginrelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='users.forumuser'), + ), + ] From 6258b7bfb61ad4493b41285ff5f962e49d4fe2f3 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 5 Feb 2022 17:52:06 -0500 Subject: [PATCH 105/211] Updated GameAdmin to allow setting basename on create. Updated/added tests accordingly. --- games/admin.py | 10 +++++++--- games/tests/test_admin.py | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/games/admin.py b/games/admin.py index 029669e9..1f8f76a4 100644 --- a/games/admin.py +++ b/games/admin.py @@ -37,10 +37,14 @@ class GameAdmin(admin.ModelAdmin): 'name', 'icon', ) - readonly_fields = ( - 'basename', - ) search_fields = ( 'name', 'basename', ) + + def get_readonly_fields(self, request, obj=None): + """Allow basename to be created but not edited.""" + if obj: + return self.readonly_fields + ('basename',) + + return self.readonly_fields diff --git a/games/tests/test_admin.py b/games/tests/test_admin.py index 1b9d8e83..7eeda1a5 100644 --- a/games/tests/test_admin.py +++ b/games/tests/test_admin.py @@ -7,6 +7,8 @@ # App from games.admin import GameAdmin +from games.models import Game +from test_utils.factories.games import GameFactory # ============================================================================= @@ -47,7 +49,7 @@ def test_list_editable(self): def test_readonly_fields(self): self.assertTupleEqual( tuple1=GameAdmin.readonly_fields, - tuple2=('basename',), + tuple2=(), ) def test_search_fields(self): @@ -58,3 +60,15 @@ def test_search_fields(self): 'basename', ), ) + + def test_get_readonly_fields(self): + self.assertTupleEqual( + tuple1=GameAdmin(Game, '').get_readonly_fields('', obj=None), + tuple2=(), + ) + + game = GameFactory() + self.assertTupleEqual( + tuple1=GameAdmin(Game, '').get_readonly_fields('', obj=game), + tuple2=('basename',), + ) From a614c70b565b8a8e221c3f4df4b02dbff09cd5c9 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 5 Feb 2022 17:52:53 -0500 Subject: [PATCH 106/211] Fixed warning for settings change in future Django versions. --- SPPM/settings/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index a5c23e35..4e0e7996 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -181,8 +181,6 @@ USE_I18N = True -USE_L10N = True - USE_TZ = True # Static files (CSS, JavaScript, Images) From e4be3f3fcfd80a4eabf8bf300881deda68a9d584 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 19 Mar 2022 15:40:53 -0400 Subject: [PATCH 107/211] Added verbose names for project types. Added field tracker to raise a ValidationError when the version of a release is changed to a value that already exists for the project. --- project_manager/common/models.py | 13 ++++++++++ ...7_alter_packagerelease_options_and_more.py | 25 +++++++++++++++++++ project_manager/packages/models/__init__.py | 11 ++++++++ project_manager/plugins/models/__init__.py | 11 ++++++++ .../sub_plugins/models/__init__.py | 11 ++++++++ 5 files changed, 71 insertions(+) create mode 100644 project_manager/migrations/0007_alter_packagerelease_options_and_more.py diff --git a/project_manager/common/models.py b/project_manager/common/models.py index b3142e4c..d7a3c5c7 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -268,6 +268,8 @@ class ProjectRelease(AbstractUUIDPrimaryKeyModel): null=True, ) + field_tracker = None + class Meta: """Define metaclass attributes.""" @@ -308,6 +310,17 @@ def __str__(self): """Return the project name + release version.""" return f'{self.project} - {self.version}' + def clean(self): + """Raise a proper error when setting version to an existing value.""" + if self.field_tracker.has_changed('version'): + new_version = self.field_tracker.current()['version'] + if self.project.releases.filter(version=new_version).exists(): + raise ValidationError({ + 'version': 'Version already exists.' + }) + + return super().clean() + def save(self, *args, **kwargs): """Update the Project's 'updated' value to the releases 'created'.""" pk = self.pk diff --git a/project_manager/migrations/0007_alter_packagerelease_options_and_more.py b/project_manager/migrations/0007_alter_packagerelease_options_and_more.py new file mode 100644 index 00000000..a83745c4 --- /dev/null +++ b/project_manager/migrations/0007_alter_packagerelease_options_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.2 on 2022-03-19 19:38 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('project_manager', '0006_alter_package_owner_alter_packagerelease_created_by_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='packagerelease', + options={'verbose_name': 'Package Release', 'verbose_name_plural': 'Package Releases'}, + ), + migrations.AlterModelOptions( + name='pluginrelease', + options={'verbose_name': 'Plugin Release', 'verbose_name_plural': 'Plugin Releases'}, + ), + migrations.AlterModelOptions( + name='subpluginrelease', + options={'verbose_name': 'SubPlugin Release', 'verbose_name_plural': 'SubPlugin Releases'}, + ), + ] diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models/__init__.py index 4d71ab09..6a6373eb 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models/__init__.py @@ -7,6 +7,9 @@ from django.urls import reverse from django.db import models +# Third Party Django +from model_utils.tracker import FieldTracker + # App from project_manager.common.constants import ( PROJECT_BASENAME_MAX_LENGTH, @@ -140,6 +143,12 @@ class PackageRelease(ProjectRelease): handle_zip_file_upload = handle_package_zip_upload project_class = Package + field_tracker = FieldTracker( + fields=[ + 'version', + ] + ) + @property def project(self): """Return the Package.""" @@ -149,6 +158,8 @@ class Meta(ProjectRelease.Meta): """Define metaclass attributes.""" unique_together = ('package', 'version') + verbose_name = 'Package Release' + verbose_name_plural = 'Package Releases' def get_absolute_url(https://github.com/self): """Return the URL for the PackageRelease.""" diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index d714371c..0e893d01 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -8,6 +8,9 @@ from django.urls import reverse from django.db import models +# Third Party Django +from model_utils.tracker import FieldTracker + # App from project_manager.common.constants import ( PROJECT_BASENAME_MAX_LENGTH, @@ -144,6 +147,12 @@ class PluginRelease(ProjectRelease): handle_zip_file_upload = handle_plugin_zip_upload project_class = Plugin + field_tracker = FieldTracker( + fields=[ + 'version', + ] + ) + @property def project(self): """Return the Plugin.""" @@ -153,6 +162,8 @@ class Meta(ProjectRelease.Meta): """Define metaclass attributes.""" unique_together = ('plugin', 'version') + verbose_name = 'Plugin Release' + verbose_name_plural = 'Plugin Releases' def get_absolute_url(https://github.com/self): """Return the URL for the PluginRelease.""" diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models/__init__.py index 785e5cf0..0a6716be 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models/__init__.py @@ -7,6 +7,9 @@ from django.urls import reverse from django.db import models +# Third Party Django +from model_utils.tracker import FieldTracker + # App from project_manager.common.constants import ( PROJECT_BASENAME_MAX_LENGTH, @@ -162,6 +165,12 @@ class SubPluginRelease(ProjectRelease): handle_zip_file_upload = handle_sub_plugin_zip_upload project_class = SubPlugin + field_tracker = FieldTracker( + fields=[ + 'version', + ] + ) + @property def project(self): """Return the SubPlugin.""" @@ -171,6 +180,8 @@ class Meta(ProjectRelease.Meta): """Define metaclass attributes.""" unique_together = ('sub_plugin', 'version') + verbose_name = 'SubPlugin Release' + verbose_name_plural = 'SubPlugin Releases' def get_absolute_url(https://github.com/self): """Return the URL for the SubPluginRelease.""" From 0da7e1d87eafd1e8860aa2b243be6e49d97cc3db Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 19 Mar 2022 15:43:45 -0400 Subject: [PATCH 108/211] Moved project release admins from an inline to their own admin classes. Updated existing tests accordingly. --- project_manager/common/admin/__init__.py | 52 +++++++++++++++++++ project_manager/common/admin/inlines.py | 31 ----------- project_manager/common/tests/test_admin.py | 42 --------------- project_manager/packages/admin/__init__.py | 22 ++++++-- project_manager/packages/admin/inlines.py | 8 --- project_manager/packages/tests/test_admin.py | 32 ------------ project_manager/plugins/admin/__init__.py | 22 ++++++-- project_manager/plugins/admin/inlines.py | 8 --- project_manager/plugins/tests/test_admin.py | 25 --------- project_manager/sub_plugins/admin/__init__.py | 22 +++++--- project_manager/sub_plugins/admin/inlines.py | 8 --- .../sub_plugins/tests/test_admin.py | 25 --------- 12 files changed, 104 insertions(+), 193 deletions(-) diff --git a/project_manager/common/admin/__init__.py b/project_manager/common/admin/__init__.py index 33752833..39d47b29 100644 --- a/project_manager/common/admin/__init__.py +++ b/project_manager/common/admin/__init__.py @@ -12,6 +12,7 @@ # ============================================================================= __all__ = ( 'ProjectAdmin', + 'ProjectReleaseAdmin', ) @@ -79,3 +80,54 @@ def has_add_permission(self, request): def has_delete_permission(self, request, obj=None): """Disallow deletion of Project in the Admin.""" return False + + +class ProjectReleaseAdmin(admin.ModelAdmin): + """Base admin class for project releases.""" + + fieldsets = ( + ( + 'Release Info', + { + 'classes': ('wide',), + 'fields': ( + 'version', + 'notes', + 'zip_file', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'created', + 'created_by', + 'download_count', + ), + }, + ) + ) + list_display = ( + 'version', + 'created', + ) + readonly_fields = ( + 'zip_file', + 'download_count', + 'created', + 'created_by', + ) + search_fields = ( + 'version', + ) + view_on_site = False + + def has_add_permission(self, request): + """Disallow creation of a Project in the Admin.""" + return False + + def has_delete_permission(self, request, obj=None): + """Disallow deletion of Project in the Admin.""" + return False diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index f276e21c..62aa6abe 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -15,7 +15,6 @@ 'ProjectContributorInline', 'ProjectGameInline', 'ProjectImageInline', - 'ProjectReleaseInline', 'ProjectTagInline', ) @@ -90,33 +89,3 @@ class ProjectImageInline(admin.TabularInline): def has_add_permission(self, request, obj=None): """Disallow adding new images in the Admin.""" return False - - -class ProjectReleaseInline(admin.StackedInline): - """Base Project Release Inline.""" - - extra = 0 - view_on_site = False - fields = ( - 'version', - 'notes', - 'zip_file', - 'download_count', - 'created', - 'created_by', - ) - - readonly_fields = ( - 'zip_file', - 'download_count', - 'created', - 'created_by', - ) - - def get_queryset(self, request): - """Order the queryset from newest to oldest.""" - return super().get_queryset(request=request).order_by('-created') - - def has_add_permission(self, request, obj=None): - """Disallow adding new images in the Admin.""" - return False diff --git a/project_manager/common/tests/test_admin.py b/project_manager/common/tests/test_admin.py index 522dccab..390f2a7c 100644 --- a/project_manager/common/tests/test_admin.py +++ b/project_manager/common/tests/test_admin.py @@ -11,7 +11,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.common.models import Project @@ -185,47 +184,6 @@ def test_readonly_fields(self): ) -class ProjectReleaseInlineTestCase(TestCase): - - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectReleaseInline, admin.StackedInline), - ) - - def test_extra(self): - self.assertEqual( - first=ProjectReleaseInline.extra, - second=0, - ) - - def test_view_on_site(self): - self.assertFalse(expr=ProjectReleaseInline.view_on_site) - - def test_fields(self): - self.assertTupleEqual( - tuple1=ProjectReleaseInline.fields, - tuple2=( - 'version', - 'notes', - 'zip_file', - 'download_count', - 'created', - 'created_by', - ), - ) - - def test_readonly_fields(self): - self.assertTupleEqual( - tuple1=ProjectReleaseInline.readonly_fields, - tuple2=( - 'zip_file', - 'download_count', - 'created', - 'created_by', - ), - ) - - class ProjectTagInlineTestCase(TestCase): def test_class_inheritance(self): diff --git a/project_manager/packages/admin/__init__.py b/project_manager/packages/admin/__init__.py index 3bffaccf..88c3e9ea 100644 --- a/project_manager/packages/admin/__init__.py +++ b/project_manager/packages/admin/__init__.py @@ -3,19 +3,21 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +from copy import deepcopy + # Django from django.contrib import admin # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.packages.admin.inlines import ( PackageContributorInline, PackageImageInline, PackageGameInline, - PackageReleaseInline, PackageTagInline, ) -from project_manager.packages.models import Package +from project_manager.packages.models import Package, PackageRelease # ============================================================================= @@ -23,6 +25,7 @@ # ============================================================================= __all__ = ( 'PackageAdmin', + 'PackageReleaseAdmin', ) @@ -38,5 +41,16 @@ class PackageAdmin(ProjectAdmin): PackageGameInline, PackageImageInline, PackageTagInline, - PackageReleaseInline, ) + + +@admin.register(PackageRelease) +class PackageReleaseAdmin(ProjectReleaseAdmin): + """PackageRelease admin.""" + + fieldsets = deepcopy(ProjectReleaseAdmin.fieldsets) + fieldsets[0][1]['fields'] += ('package',) + list_display = ProjectReleaseAdmin.list_display + ('package',) + ordering = ('package', '-created',) + readonly_fields = ProjectReleaseAdmin.readonly_fields + ('package',) + search_fields = ProjectReleaseAdmin.search_fields + ('package__name',) diff --git a/project_manager/packages/admin/inlines.py b/project_manager/packages/admin/inlines.py index b584c76a..f9bd1f74 100644 --- a/project_manager/packages/admin/inlines.py +++ b/project_manager/packages/admin/inlines.py @@ -8,7 +8,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.packages.models import ( @@ -27,7 +26,6 @@ 'PackageContributorInline', 'PackageGameInline', 'PackageImageInline', - 'PackageReleaseInline', 'PackageTagInline', ) @@ -57,9 +55,3 @@ class PackageImageInline(ProjectImageInline): """Package Image Inline.""" model = PackageImage - - -class PackageReleaseInline(ProjectReleaseInline): - """Package Release Inline.""" - - model = PackageRelease diff --git a/project_manager/packages/tests/test_admin.py b/project_manager/packages/tests/test_admin.py index d27feec0..01406f7d 100644 --- a/project_manager/packages/tests/test_admin.py +++ b/project_manager/packages/tests/test_admin.py @@ -14,7 +14,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.packages.admin import PackageAdmin @@ -22,7 +21,6 @@ PackageContributorInline, PackageGameInline, PackageImageInline, - PackageReleaseInline, PackageTagInline, ) from project_manager.packages.models import ( @@ -54,7 +52,6 @@ def test_inlines(self): PackageGameInline, PackageImageInline, PackageTagInline, - PackageReleaseInline, ), ) @@ -158,32 +155,3 @@ def test_has_add_permission(self): self.assertFalse( expr=obj.has_add_permission(''), ) - - -class PackageReleaseInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass( - PackageReleaseInline, - ProjectReleaseInline, - ), - ) - - def test_model(self): - self.assertEqual( - first=PackageReleaseInline.model, - second=PackageRelease, - ) - - def test_get_queryset(self): - obj = PackageReleaseInline(PackageRelease, admin.AdminSite()) - self.assertTupleEqual( - tuple1=obj.get_queryset(mock.Mock()).query.order_by, - tuple2=('-created',), - ) - - def test_has_add_permission(self): - obj = PackageReleaseInline(PackageRelease, admin.AdminSite()) - self.assertFalse( - expr=obj.has_add_permission(''), - ) diff --git a/project_manager/plugins/admin/__init__.py b/project_manager/plugins/admin/__init__.py index 9c902a48..e8453048 100644 --- a/project_manager/plugins/admin/__init__.py +++ b/project_manager/plugins/admin/__init__.py @@ -3,20 +3,22 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +from copy import deepcopy + # Django from django.contrib import admin # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.plugins.admin.inlines import ( PluginContributorInline, PluginGameInline, PluginImageInline, - PluginReleaseInline, PluginTagInline, SubPluginPathInline, ) -from project_manager.plugins.models import Plugin +from project_manager.plugins.models import Plugin, PluginRelease # ============================================================================= @@ -24,6 +26,7 @@ # ============================================================================= __all__ = ( 'PluginAdmin', + 'PluginReleaseAdmin', ) @@ -40,5 +43,16 @@ class PluginAdmin(ProjectAdmin): PluginImageInline, PluginTagInline, SubPluginPathInline, - PluginReleaseInline, ) + + +@admin.register(PluginRelease) +class PluginReleaseAdmin(ProjectReleaseAdmin): + """PluginRelease admin.""" + + fieldsets = deepcopy(ProjectReleaseAdmin.fieldsets) + fieldsets[0][1]['fields'] += ('plugin',) + list_display = ProjectReleaseAdmin.list_display + ('plugin',) + ordering = ('plugin', '-created',) + readonly_fields = ProjectReleaseAdmin.readonly_fields + ('plugin',) + search_fields = ProjectReleaseAdmin.search_fields + ('plugin__name',) diff --git a/project_manager/plugins/admin/inlines.py b/project_manager/plugins/admin/inlines.py index f81d80ed..97e69dc3 100644 --- a/project_manager/plugins/admin/inlines.py +++ b/project_manager/plugins/admin/inlines.py @@ -11,7 +11,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.plugins.models import ( @@ -31,7 +30,6 @@ 'PluginContributorInline', 'PluginGameInline', 'PluginImageInline', - 'PluginReleaseInline', 'PluginTagInline', 'SubPluginPathInline', ) @@ -64,12 +62,6 @@ class PluginImageInline(ProjectImageInline): model = PluginImage -class PluginReleaseInline(ProjectReleaseInline): - """Plugin Release Inline.""" - - model = PluginRelease - - class SubPluginPathInline(admin.StackedInline): """SubPluginPath Inline.""" diff --git a/project_manager/plugins/tests/test_admin.py b/project_manager/plugins/tests/test_admin.py index 915dc39c..fca701be 100644 --- a/project_manager/plugins/tests/test_admin.py +++ b/project_manager/plugins/tests/test_admin.py @@ -11,7 +11,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.plugins.admin import PluginAdmin @@ -19,7 +18,6 @@ PluginContributorInline, PluginGameInline, PluginImageInline, - PluginReleaseInline, PluginTagInline, SubPluginPathInline, ) @@ -51,7 +49,6 @@ def test_inlines(self): PluginImageInline, PluginTagInline, SubPluginPathInline, - PluginReleaseInline, ), ) @@ -138,28 +135,6 @@ def test_has_add_permission(self): ) -class PluginReleaseInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass( - PluginReleaseInline, - ProjectReleaseInline, - ), - ) - - def test_model(self): - self.assertEqual( - first=PluginReleaseInline.model, - second=PluginRelease, - ) - - def test_has_add_permission(self): - obj = PluginReleaseInline(PluginRelease, admin.AdminSite()) - self.assertFalse( - expr=obj.has_add_permission(''), - ) - - class SubPluginPathInlineTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( diff --git a/project_manager/sub_plugins/admin/__init__.py b/project_manager/sub_plugins/admin/__init__.py index 79ea3bd9..b8965869 100644 --- a/project_manager/sub_plugins/admin/__init__.py +++ b/project_manager/sub_plugins/admin/__init__.py @@ -4,21 +4,20 @@ # IMPORTS # ============================================================================= # Python -import copy +from copy import deepcopy # Django from django.contrib import admin # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.sub_plugins.admin.inlines import ( SubPluginContributorInline, SubPluginGameInline, SubPluginImageInline, - SubPluginReleaseInline, SubPluginTagInline, ) -from project_manager.sub_plugins.models import SubPlugin +from project_manager.sub_plugins.models import SubPlugin, SubPluginRelease # ============================================================================= @@ -32,7 +31,7 @@ # ============================================================================= # GLOBAL VARIABLES # ============================================================================= -_project_fieldsets = copy.deepcopy(ProjectAdmin.fieldsets) +_project_fieldsets = deepcopy(ProjectAdmin.fieldsets) _fields = _project_fieldsets[0][1]['fields'] _project_fieldsets[0][1]['fields'] = ('plugin',) + _fields @@ -50,7 +49,6 @@ class SubPluginAdmin(ProjectAdmin): SubPluginGameInline, SubPluginImageInline, SubPluginTagInline, - SubPluginReleaseInline, ) list_display = ProjectAdmin.list_display + ( 'plugin', @@ -62,3 +60,15 @@ class SubPluginAdmin(ProjectAdmin): 'plugin__name', 'plugin__basename', ) + + +@admin.register(SubPluginRelease) +class SubPluginReleaseAdmin(ProjectReleaseAdmin): + """SubPluginRelease admin.""" + + fieldsets = deepcopy(ProjectReleaseAdmin.fieldsets) + fieldsets[0][1]['fields'] += ('sub_plugin',) + list_display = ProjectReleaseAdmin.list_display + ('sub_plugin',) + ordering = ('sub_plugin', '-created',) + readonly_fields = ProjectReleaseAdmin.readonly_fields + ('sub_plugin',) + search_fields = ProjectReleaseAdmin.search_fields + ('sub_plugin__name',) diff --git a/project_manager/sub_plugins/admin/inlines.py b/project_manager/sub_plugins/admin/inlines.py index 69291350..6ea3fa80 100644 --- a/project_manager/sub_plugins/admin/inlines.py +++ b/project_manager/sub_plugins/admin/inlines.py @@ -8,7 +8,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.sub_plugins.models import ( @@ -27,7 +26,6 @@ 'SubPluginContributorInline', 'SubPluginGameInline', 'SubPluginImageInline', - 'SubPluginReleaseInline', 'SubPluginTagInline', ) @@ -57,9 +55,3 @@ class SubPluginImageInline(ProjectImageInline): """Plugin Image Inline.""" model = SubPluginImage - - -class SubPluginReleaseInline(ProjectReleaseInline): - """Plugin Release Inline.""" - - model = SubPluginRelease diff --git a/project_manager/sub_plugins/tests/test_admin.py b/project_manager/sub_plugins/tests/test_admin.py index 702b6fd4..2e674da2 100644 --- a/project_manager/sub_plugins/tests/test_admin.py +++ b/project_manager/sub_plugins/tests/test_admin.py @@ -11,7 +11,6 @@ ProjectContributorInline, ProjectGameInline, ProjectImageInline, - ProjectReleaseInline, ProjectTagInline, ) from project_manager.sub_plugins.admin import SubPluginAdmin @@ -19,7 +18,6 @@ SubPluginContributorInline, SubPluginGameInline, SubPluginImageInline, - SubPluginReleaseInline, SubPluginTagInline, ) from project_manager.sub_plugins.models import ( @@ -48,7 +46,6 @@ def test_inlines(self): SubPluginGameInline, SubPluginImageInline, SubPluginTagInline, - SubPluginReleaseInline, ), ) @@ -133,25 +130,3 @@ def test_has_add_permission(self): self.assertFalse( expr=obj.has_add_permission(''), ) - - -class SubPluginReleaseInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass( - SubPluginReleaseInline, - ProjectReleaseInline, - ), - ) - - def test_model(self): - self.assertEqual( - first=SubPluginReleaseInline.model, - second=SubPluginRelease, - ) - - def test_has_add_permission(self): - obj = SubPluginReleaseInline(SubPluginRelease, admin.AdminSite()) - self.assertFalse( - expr=obj.has_add_permission(''), - ) From f2a892cb2b71893874f1de241488c300b31e9fd7 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 19 Mar 2022 17:35:52 -0400 Subject: [PATCH 109/211] Updated all requirements to their newest versions. --- pip-requirements/base.txt | 6 +++--- pip-requirements/local.txt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index ec435bad..3978b6d6 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,9 +1,9 @@ -django==4.0.2 -django-embed-video==1.4.1 +django==4.0.3 +django-embed-video==1.4.2 django-extensions==3.1.5 django-filter==21.1 django-model-utils==4.2.0 django-precise-bbcode==1.2.14 djangorestframework==3.13.1 Markdown==3.3.6 -path==16.3.0 +path==16.4.0 diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index a432620d..f2f7501c 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -1,11 +1,11 @@ -r base.txt django-debug-toolbar==3.2.4 factory-boy==3.2.1 -flake8==3.9.2 -prospector==1.6.0 -pycodestyle==2.7.0 +flake8==4.0.1 +prospector==1.7.7 +pycodestyle==2.8.0 pydocstyle==6.1.1 -pyflakes==2.3.1 +pyflakes==2.4.0 pylint==2.12.2 pytest-cov==3.0.0 pytest-django==4.5.2 From 02f698cdf7d18cd8b4bfcbf6528fc50cf2a0643c Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 19 Mar 2022 17:36:14 -0400 Subject: [PATCH 110/211] Updated prospector configuration. --- prospector.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/prospector.yaml b/prospector.yaml index 9db0e2ab..0e290119 100644 --- a/prospector.yaml +++ b/prospector.yaml @@ -21,12 +21,12 @@ ignore-paths: - .idea - venv -pep8: +pycodestyle: run: true options: max-line-length: 100 -pep257: +pydocstyle: run: true disable: - D203 @@ -38,6 +38,11 @@ mccabe: pylint: run: true disable: + - astroid-error - fixme + - django-not-configured + - ungrouped-imports + - wrong-import-order + - wrong-import-position options: max-line-length: 100 From 3e88df171e20dee3b5dcf2a25d123495ad88b2ec Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 19 Mar 2022 17:40:03 -0400 Subject: [PATCH 111/211] Added tests for new release admins. Added tests for model changes. --- project_manager/common/tests/test_admin.py | 72 ++++++++++++-- project_manager/packages/admin/inlines.py | 1 - project_manager/packages/tests/test_admin.py | 93 +++++++++++++++++- project_manager/packages/tests/test_models.py | 44 +++++++++ project_manager/plugins/admin/inlines.py | 1 - project_manager/plugins/tests/test_admin.py | 93 +++++++++++++++++- project_manager/plugins/tests/test_models.py | 44 +++++++++ project_manager/sub_plugins/admin/__init__.py | 1 + project_manager/sub_plugins/admin/inlines.py | 1 - .../sub_plugins/tests/test_admin.py | 96 ++++++++++++++++++- .../sub_plugins/tests/test_models.py | 44 +++++++++ 11 files changed, 475 insertions(+), 15 deletions(-) diff --git a/project_manager/common/tests/test_admin.py b/project_manager/common/tests/test_admin.py index 390f2a7c..2adab343 100644 --- a/project_manager/common/tests/test_admin.py +++ b/project_manager/common/tests/test_admin.py @@ -6,7 +6,7 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.common.admin.inlines import ( ProjectContributorInline, ProjectGameInline, @@ -20,7 +20,6 @@ # TEST CASES # ============================================================================= class ProjectAdminTestCase(TestCase): - def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectAdmin, admin.ModelAdmin), @@ -112,8 +111,72 @@ def test_has_delete_permission(self): ) -class ProjectContributorInlineTestCase(TestCase): +class ProjectReleaseAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectReleaseAdmin, admin.ModelAdmin)) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.fieldsets, + tuple2=( + ( + 'Release Info', + { + 'classes': ('wide',), + 'fields': ( + 'version', + 'notes', + 'zip_file', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'created', + 'created_by', + 'download_count', + ), + }, + ) + ) + ) + def test_list_display(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.list_display, + tuple2=( + 'version', + 'created', + ) + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.readonly_fields, + tuple2=( + 'zip_file', + 'download_count', + 'created', + 'created_by', + ) + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.search_fields, + tuple2=( + 'version', + ) + ) + + def test_view_on_site(self): + self.assertFalse(expr=ProjectReleaseAdmin.view_on_site) + + +class ProjectContributorInlineTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectContributorInline, admin.TabularInline), @@ -139,7 +202,6 @@ def test_raw_id_fields(self): class ProjectGameInlineTestCase(TestCase): - def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectGameInline, admin.TabularInline), @@ -159,7 +221,6 @@ def test_readonly_fields(self): class ProjectImageInlineTestCase(TestCase): - def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectImageInline, admin.TabularInline), @@ -185,7 +246,6 @@ def test_readonly_fields(self): class ProjectTagInlineTestCase(TestCase): - def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectTagInline, admin.TabularInline), diff --git a/project_manager/packages/admin/inlines.py b/project_manager/packages/admin/inlines.py index f9bd1f74..47fc9491 100644 --- a/project_manager/packages/admin/inlines.py +++ b/project_manager/packages/admin/inlines.py @@ -14,7 +14,6 @@ PackageContributor, PackageGame, PackageImage, - PackageRelease, PackageTag, ) diff --git a/project_manager/packages/tests/test_admin.py b/project_manager/packages/tests/test_admin.py index 01406f7d..47441e36 100644 --- a/project_manager/packages/tests/test_admin.py +++ b/project_manager/packages/tests/test_admin.py @@ -9,14 +9,14 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.common.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, ProjectTagInline, ) -from project_manager.packages.admin import PackageAdmin +from project_manager.packages.admin import PackageAdmin, PackageReleaseAdmin from project_manager.packages.admin.inlines import ( PackageContributorInline, PackageGameInline, @@ -56,6 +56,95 @@ def test_inlines(self): ) +class TestPackageReleaseAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PackageReleaseAdmin, ProjectReleaseAdmin), + ) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=PackageReleaseAdmin.fieldsets, + tuple2=( + ( + 'Release Info', + { + 'classes': ('wide',), + 'fields': ( + 'version', + 'notes', + 'zip_file', + 'package', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'created', + 'created_by', + 'download_count', + ), + }, + ) + ) + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=PackageReleaseAdmin.list_display, + tuple2=( + 'version', + 'created', + 'package', + ) + ) + + def test_ordering(self): + self.assertTupleEqual( + tuple1=PackageReleaseAdmin.ordering, + tuple2=( + 'package', + '-created', + ) + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=PackageReleaseAdmin.readonly_fields, + tuple2=( + 'zip_file', + 'download_count', + 'created', + 'created_by', + 'package', + ) + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=PackageReleaseAdmin.search_fields, + tuple2=( + 'version', + 'package__name', + ) + ) + + def test_has_add_permission(self): + obj = PackageReleaseAdmin(PackageRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + def test_has_delete_permission(self): + obj = PackageReleaseAdmin(PackageRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_delete_permission(''), + ) + + class PackageContributorInlineTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index c7eb11f5..70d9f596 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -13,6 +13,9 @@ from django.urls import reverse from django.utils.timezone import now +# Third Party Django +from model_utils.tracker import FieldTracker + # App from games.models import Game from project_manager.common.constants import ( @@ -466,6 +469,17 @@ def test_vcs_requirements_field(self): second=PackageReleaseVersionControlRequirement, ) + def test_field_tracker(self): + self.assertTrue(expr=hasattr(PackageRelease, 'field_tracker')) + self.assertIsInstance( + obj=PackageRelease.field_tracker, + cls=FieldTracker, + ) + self.assertSetEqual( + set1=PackageRelease.field_tracker.fields, + set2={'version'}, + ) + def test_primary_attributes(self): self.assertEqual( first=PackageRelease.handle_zip_file_upload, @@ -493,6 +507,28 @@ def test__str__(self): second=f'{release.project} - {release.version}', ) + def test_clean(self): + release = PackageReleaseFactory( + version='1.0.0', + ) + PackageReleaseFactory( + package=release.package, + version='1.0.1', + ) + + release.clean() + release.version = '1.0.2' + release.clean() + + release.version = '1.0.1' + with self.assertRaises(ValidationError) as context: + release.clean() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'version': ['Version already exists.']} + ) + def test_save(self): original_updated = now() package = PackageFactory( @@ -530,6 +566,14 @@ def test_meta_class(self): tuple1=PackageRelease._meta.unique_together, tuple2=(('package', 'version'),), ) + self.assertEqual( + first=PackageRelease._meta.verbose_name, + second='Package Release', + ) + self.assertEqual( + first=PackageRelease._meta.verbose_name_plural, + second='Package Releases', + ) class PackageImageTestCase(TestCase): diff --git a/project_manager/plugins/admin/inlines.py b/project_manager/plugins/admin/inlines.py index 97e69dc3..63bffb5f 100644 --- a/project_manager/plugins/admin/inlines.py +++ b/project_manager/plugins/admin/inlines.py @@ -17,7 +17,6 @@ PluginContributor, PluginGame, PluginImage, - PluginRelease, PluginTag, SubPluginPath, ) diff --git a/project_manager/plugins/tests/test_admin.py b/project_manager/plugins/tests/test_admin.py index fca701be..ef17952d 100644 --- a/project_manager/plugins/tests/test_admin.py +++ b/project_manager/plugins/tests/test_admin.py @@ -6,14 +6,14 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.common.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, ProjectTagInline, ) -from project_manager.plugins.admin import PluginAdmin +from project_manager.plugins.admin import PluginAdmin, PluginReleaseAdmin from project_manager.plugins.admin.inlines import ( PluginContributorInline, PluginGameInline, @@ -53,6 +53,95 @@ def test_inlines(self): ) +class TestPluginReleaseAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(PluginReleaseAdmin, ProjectReleaseAdmin), + ) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=PluginReleaseAdmin.fieldsets, + tuple2=( + ( + 'Release Info', + { + 'classes': ('wide',), + 'fields': ( + 'version', + 'notes', + 'zip_file', + 'plugin', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'created', + 'created_by', + 'download_count', + ), + }, + ) + ) + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=PluginReleaseAdmin.list_display, + tuple2=( + 'version', + 'created', + 'plugin', + ) + ) + + def test_ordering(self): + self.assertTupleEqual( + tuple1=PluginReleaseAdmin.ordering, + tuple2=( + 'plugin', + '-created', + ) + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=PluginReleaseAdmin.readonly_fields, + tuple2=( + 'zip_file', + 'download_count', + 'created', + 'created_by', + 'plugin', + ) + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=PluginReleaseAdmin.search_fields, + tuple2=( + 'version', + 'plugin__name', + ) + ) + + def test_has_add_permission(self): + obj = PluginReleaseAdmin(PluginRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + def test_has_delete_permission(self): + obj = PluginReleaseAdmin(PluginRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_delete_permission(''), + ) + + class PluginContributorInlineTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index fa54be11..707cd679 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -13,6 +13,9 @@ from django.urls import reverse from django.utils.timezone import now +# Third Party Django +from model_utils.tracker import FieldTracker + # App from games.models import Game from project_manager.common.constants import ( @@ -472,6 +475,17 @@ def test_vcs_requirements_field(self): second=PluginReleaseVersionControlRequirement, ) + def test_field_tracker(self): + self.assertTrue(expr=hasattr(PluginRelease, 'field_tracker')) + self.assertIsInstance( + obj=PluginRelease.field_tracker, + cls=FieldTracker, + ) + self.assertSetEqual( + set1=PluginRelease.field_tracker.fields, + set2={'version'}, + ) + def test_primary_attributes(self): self.assertEqual( first=PluginRelease.handle_zip_file_upload, @@ -499,6 +513,28 @@ def test__str__(self): second=f'{release.project} - {release.version}', ) + def test_clean(self): + release = PluginReleaseFactory( + version='1.0.0', + ) + PluginReleaseFactory( + plugin=release.plugin, + version='1.0.1', + ) + + release.clean() + release.version = '1.0.2' + release.clean() + + release.version = '1.0.1' + with self.assertRaises(ValidationError) as context: + release.clean() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'version': ['Version already exists.']} + ) + def test_save(self): original_updated = now() plugin = PluginFactory( @@ -536,6 +572,14 @@ def test_meta_class(self): tuple1=PluginRelease._meta.unique_together, tuple2=(('plugin', 'version'),), ) + self.assertEqual( + first=PluginRelease._meta.verbose_name, + second='Plugin Release', + ) + self.assertEqual( + first=PluginRelease._meta.verbose_name_plural, + second='Plugin Releases', + ) class PluginImageTestCase(TestCase): diff --git a/project_manager/sub_plugins/admin/__init__.py b/project_manager/sub_plugins/admin/__init__.py index b8965869..1f002d40 100644 --- a/project_manager/sub_plugins/admin/__init__.py +++ b/project_manager/sub_plugins/admin/__init__.py @@ -25,6 +25,7 @@ # ============================================================================= __all__ = ( 'SubPluginAdmin', + 'SubPluginReleaseAdmin', ) diff --git a/project_manager/sub_plugins/admin/inlines.py b/project_manager/sub_plugins/admin/inlines.py index 6ea3fa80..5d51d81b 100644 --- a/project_manager/sub_plugins/admin/inlines.py +++ b/project_manager/sub_plugins/admin/inlines.py @@ -14,7 +14,6 @@ SubPluginContributor, SubPluginGame, SubPluginImage, - SubPluginRelease, SubPluginTag, ) diff --git a/project_manager/sub_plugins/tests/test_admin.py b/project_manager/sub_plugins/tests/test_admin.py index 2e674da2..e9a41f0a 100644 --- a/project_manager/sub_plugins/tests/test_admin.py +++ b/project_manager/sub_plugins/tests/test_admin.py @@ -6,14 +6,17 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin +from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin from project_manager.common.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, ProjectTagInline, ) -from project_manager.sub_plugins.admin import SubPluginAdmin +from project_manager.sub_plugins.admin import ( + SubPluginAdmin, + SubPluginReleaseAdmin, +) from project_manager.sub_plugins.admin.inlines import ( SubPluginContributorInline, SubPluginGameInline, @@ -50,6 +53,95 @@ def test_inlines(self): ) +class TestSubPluginReleaseAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(SubPluginReleaseAdmin, ProjectReleaseAdmin), + ) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseAdmin.fieldsets, + tuple2=( + ( + 'Release Info', + { + 'classes': ('wide',), + 'fields': ( + 'version', + 'notes', + 'zip_file', + 'sub_plugin', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'created', + 'created_by', + 'download_count', + ), + }, + ) + ) + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseAdmin.list_display, + tuple2=( + 'version', + 'created', + 'sub_plugin', + ) + ) + + def test_ordering(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseAdmin.ordering, + tuple2=( + 'sub_plugin', + '-created', + ) + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseAdmin.readonly_fields, + tuple2=( + 'zip_file', + 'download_count', + 'created', + 'created_by', + 'sub_plugin', + ) + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=SubPluginReleaseAdmin.search_fields, + tuple2=( + 'version', + 'sub_plugin__name', + ) + ) + + def test_has_add_permission(self): + obj = SubPluginReleaseAdmin(SubPluginRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_add_permission(''), + ) + + def test_has_delete_permission(self): + obj = SubPluginReleaseAdmin(SubPluginRelease, admin.AdminSite()) + self.assertFalse( + expr=obj.has_delete_permission(''), + ) + + class SubPluginContributorInlineTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index 8aa22879..b3033773 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -13,6 +13,9 @@ from django.urls import reverse from django.utils.timezone import now +# Third Party Django +from model_utils.tracker import FieldTracker + # App from games.models import Game from project_manager.common.constants import ( @@ -496,6 +499,17 @@ def test_vcs_requirements_field(self): second=SubPluginReleaseVersionControlRequirement, ) + def test_field_tracker(self): + self.assertTrue(expr=hasattr(SubPluginRelease, 'field_tracker')) + self.assertIsInstance( + obj=SubPluginRelease.field_tracker, + cls=FieldTracker, + ) + self.assertSetEqual( + set1=SubPluginRelease.field_tracker.fields, + set2={'version'}, + ) + def test_primary_attributes(self): self.assertEqual( first=SubPluginRelease.handle_zip_file_upload, @@ -523,6 +537,28 @@ def test__str__(self): second=f'{release.project} - {release.version}', ) + def test_clean(self): + release = SubPluginReleaseFactory( + version='1.0.0', + ) + SubPluginReleaseFactory( + sub_plugin=release.sub_plugin, + version='1.0.1', + ) + + release.clean() + release.version = '1.0.2' + release.clean() + + release.version = '1.0.1' + with self.assertRaises(ValidationError) as context: + release.clean() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'version': ['Version already exists.']} + ) + def test_save(self): original_updated = now() sub_plugin = SubPluginFactory( @@ -561,6 +597,14 @@ def test_meta_class(self): tuple1=SubPluginRelease._meta.unique_together, tuple2=(('sub_plugin', 'version'),), ) + self.assertEqual( + first=SubPluginRelease._meta.verbose_name, + second='SubPlugin Release', + ) + self.assertEqual( + first=SubPluginRelease._meta.verbose_name_plural, + second='SubPlugin Releases', + ) class SubPluginImageTestCase(TestCase): From 93d0705bdc13372ff9fb09dec05768bde7a5c98a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 19 Mar 2022 20:56:40 -0400 Subject: [PATCH 112/211] Fixed some extra queries by adding select_related. --- project_manager/common/admin/__init__.py | 7 +++++++ project_manager/common/admin/inlines.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/project_manager/common/admin/__init__.py b/project_manager/common/admin/__init__.py index 39d47b29..e0ebd84f 100644 --- a/project_manager/common/admin/__init__.py +++ b/project_manager/common/admin/__init__.py @@ -73,6 +73,13 @@ class ProjectAdmin(admin.ModelAdmin): 'contributors__user__username', ) + def get_queryset(self, request): + return super().get_queryset( + request=request, + ).select_related( + 'owner__user', + ) + def has_add_permission(self, request): """Disallow creation of a Project in the Admin.""" return False diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index 62aa6abe..0176a4fc 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -53,6 +53,15 @@ class ProjectGameInline(admin.TabularInline): 'game', ) + def get_queryset(self, request): + return super().get_queryset( + request=request, + ).select_related( + 'game', + ).order_by( + 'game__name', + ) + def has_add_permission(self, request, obj=None): """Disallow adding new games in the Admin.""" return False @@ -68,6 +77,15 @@ class ProjectTagInline(admin.TabularInline): 'tag', ) + def get_queryset(self, request): + return super().get_queryset( + request=request, + ).select_related( + 'tag', + ).order_by( + 'tag__name', + ) + def has_add_permission(self, request, obj=None): """Disallow adding new tags in the Admin.""" return False From 8756b1dce0f6e10af36ce716dc511996784d2d9f Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 19 Mar 2022 20:58:05 -0400 Subject: [PATCH 113/211] Added a few more select_related instances. --- project_manager/packages/api/views.py | 1 + project_manager/plugins/api/views.py | 1 + project_manager/sub_plugins/api/views.py | 1 + 3 files changed, 3 insertions(+) diff --git a/project_manager/packages/api/views.py b/project_manager/packages/api/views.py index 706cff0f..702f175d 100644 --- a/project_manager/packages/api/views.py +++ b/project_manager/packages/api/views.py @@ -137,6 +137,7 @@ class PackageReleaseViewSet(ProjectReleaseViewSet): queryset = PackageRelease.objects.select_related( 'package', + 'created_by__user', ).prefetch_related( Prefetch( lookup='packagereleasepackagerequirement_set', diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index 60cf8028..34bc8cee 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -153,6 +153,7 @@ class PluginReleaseViewSet(ProjectReleaseViewSet): queryset = PluginRelease.objects.select_related( 'plugin', + 'created_by__user', ).prefetch_related( Prefetch( lookup='pluginreleasepackagerequirement_set', diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index 485afcac..fc03c9e1 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -195,6 +195,7 @@ class SubPluginReleaseViewSet(ProjectReleaseViewSet): queryset = SubPluginRelease.objects.select_related( 'sub_plugin', + 'created_by__user', ).prefetch_related( Prefetch( lookup='subpluginreleasepackagerequirement_set', From c84308ccdf130afe967f918cc2b42ec150571b1c Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 20 Mar 2022 11:47:12 -0400 Subject: [PATCH 114/211] Added more select_related instances. Added field tracker to SubPluginPath.path to validate the path does not already exist for the plugin on change. Updated/added tests accordingly. --- project_manager/common/admin/__init__.py | 9 ++ project_manager/common/admin/inlines.py | 13 +-- .../packages/api/tests/test_release_views.py | 2 +- project_manager/packages/tests/test_admin.py | 83 ++++++++++++++----- project_manager/plugins/admin/inlines.py | 3 - .../plugins/api/tests/test_release_views.py | 2 +- project_manager/plugins/models/__init__.py | 22 ++++- project_manager/plugins/tests/test_admin.py | 66 ++++++++++++++- project_manager/plugins/tests/test_models.py | 23 +++++ .../api/tests/test_release_views.py | 2 +- .../sub_plugins/tests/test_admin.py | 64 ++++++++++++++ tags/admin.py | 11 +++ tags/tests/test_admin.py | 13 +++ 13 files changed, 271 insertions(+), 42 deletions(-) diff --git a/project_manager/common/admin/__init__.py b/project_manager/common/admin/__init__.py index e0ebd84f..24785d5a 100644 --- a/project_manager/common/admin/__init__.py +++ b/project_manager/common/admin/__init__.py @@ -74,6 +74,7 @@ class ProjectAdmin(admin.ModelAdmin): ) def get_queryset(self, request): + """Cache the 'owner' for the queryset.""" return super().get_queryset( request=request, ).select_related( @@ -131,6 +132,14 @@ class ProjectReleaseAdmin(admin.ModelAdmin): ) view_on_site = False + def get_queryset(self, request): + """Cache 'created_by' for the queryset.""" + return super().get_queryset( + request=request, + ).select_related( + 'created_by__user', + ) + def has_add_permission(self, request): """Disallow creation of a Project in the Admin.""" return False diff --git a/project_manager/common/admin/inlines.py b/project_manager/common/admin/inlines.py index 0176a4fc..682ac047 100644 --- a/project_manager/common/admin/inlines.py +++ b/project_manager/common/admin/inlines.py @@ -5,7 +5,6 @@ # ============================================================================= # Django from django.contrib import admin -from django.db.models import Q # ============================================================================= @@ -33,15 +32,6 @@ class ProjectContributorInline(admin.TabularInline): 'user', ) - def get_formset(self, request, obj=None, **kwargs): - """Disallow the owner to be a contributor.""" - formset = super().get_formset(request=request, obj=obj, **kwargs) - queryset = formset.form.base_fields['user'].queryset - formset.form.base_fields['user'].queryset = queryset.filter( - ~Q(user=obj.owner.user) - ) - return formset - class ProjectGameInline(admin.TabularInline): """Base Project Game Inline.""" @@ -54,6 +44,7 @@ class ProjectGameInline(admin.TabularInline): ) def get_queryset(self, request): + """Cache the 'game' for the queryset.""" return super().get_queryset( request=request, ).select_related( @@ -78,6 +69,7 @@ class ProjectTagInline(admin.TabularInline): ) def get_queryset(self, request): + """Cache the 'tag' for the queryset.""" return super().get_queryset( request=request, ).select_related( @@ -98,7 +90,6 @@ class ProjectImageInline(admin.TabularInline): 'image', 'created', ) - readonly_fields = ( 'image', 'created', diff --git a/project_manager/packages/api/tests/test_release_views.py b/project_manager/packages/api/tests/test_release_views.py index 3b15aab9..a077dfc2 100644 --- a/project_manager/packages/api/tests/test_release_views.py +++ b/project_manager/packages/api/tests/test_release_views.py @@ -99,7 +99,7 @@ def test_base_attributes(self): ) self.assertDictEqual( d1=PackageReleaseViewSet.queryset.query.select_related, - d2={'package': {}}, + d2={'package': {}, 'created_by': {'user': {}}}, ) prefetch_lookups = PackageReleaseViewSet.queryset._prefetch_related_lookups self.assertEqual(first=len(prefetch_lookups), second=4) diff --git a/project_manager/packages/tests/test_admin.py b/project_manager/packages/tests/test_admin.py index 47441e36..e8aa6db0 100644 --- a/project_manager/packages/tests/test_admin.py +++ b/project_manager/packages/tests/test_admin.py @@ -24,15 +24,13 @@ PackageTagInline, ) from project_manager.packages.models import ( + Package, PackageContributor, PackageGame, PackageImage, PackageRelease, PackageTag, ) -from test_utils.factories.packages import PackageFactory -from test_utils.factories.users import ForumUserFactory -from users.models import ForumUser # ============================================================================= @@ -55,6 +53,19 @@ def test_inlines(self): ), ) + def test_get_queryset(self): + request = mock.Mock() + query = PackageAdmin( + Package, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'owner': {'user': {}}} + ) + class TestPackageReleaseAdminTestCase(TestCase): def test_class_inheritance(self): @@ -132,6 +143,19 @@ def test_search_fields(self): ) ) + def test_get_queryset(self): + request = mock.Mock() + query = PackageReleaseAdmin( + PackageRelease, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'created_by': {'user': {}}} + ) + def test_has_add_permission(self): obj = PackageReleaseAdmin(PackageRelease, admin.AdminSite()) self.assertFalse( @@ -154,25 +178,6 @@ def test_class_inheritance(self): ), ) - @mock.patch( - target='django.contrib.admin.options.InlineModelAdmin.get_formset', - ) - def test_get_formset(self, mock_super_get_formset): - user = ForumUserFactory() - package = PackageFactory() - field = mock_super_get_formset.return_value.form.base_fields['user'] - field.queryset = ForumUser.objects.all() - self.assertCountEqual( - first=list(field.queryset), - second=[package.owner, user], - ) - obj = PackageContributorInline(PackageContributor, admin.AdminSite()) - obj.get_formset('', package) - self.assertListEqual( - list1=list(field.queryset), - list2=[user], - ) - def test_model(self): self.assertEqual( first=PackageContributorInline.model, @@ -195,6 +200,23 @@ def test_model(self): second=PackageGame, ) + def test_get_queryset(self): + request = mock.Mock() + query = PackageGameInline( + PackageGame, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'game': {}} + ) + self.assertTupleEqual( + tuple1=query.order_by, + tuple2=('game__name',), + ) + def test_has_add_permission(self): obj = PackageGameInline(PackageGame, admin.AdminSite()) self.assertFalse( @@ -239,6 +261,23 @@ def test_model(self): second=PackageTag, ) + def test_get_queryset(self): + request = mock.Mock() + query = PackageTagInline( + PackageTag, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'tag': {}} + ) + self.assertTupleEqual( + tuple1=query.order_by, + tuple2=('tag__name',), + ) + def test_has_add_permission(self): obj = PackageTagInline(PackageTag, admin.AdminSite()) self.assertFalse( diff --git a/project_manager/plugins/admin/inlines.py b/project_manager/plugins/admin/inlines.py index 63bffb5f..55464f95 100644 --- a/project_manager/plugins/admin/inlines.py +++ b/project_manager/plugins/admin/inlines.py @@ -72,9 +72,6 @@ class SubPluginPathInline(admin.StackedInline): 'allow_package_using_basename', 'allow_package_using_init', ) - readonly_fields = ( - 'path', - ) model = SubPluginPath def has_add_permission(self, request, obj=None): diff --git a/project_manager/plugins/api/tests/test_release_views.py b/project_manager/plugins/api/tests/test_release_views.py index 62e4301c..a159dced 100644 --- a/project_manager/plugins/api/tests/test_release_views.py +++ b/project_manager/plugins/api/tests/test_release_views.py @@ -100,7 +100,7 @@ def test_base_attributes(self): ) self.assertDictEqual( d1=PluginReleaseViewSet.queryset.query.select_related, - d2={'plugin': {}}, + d2={'plugin': {}, 'created_by': {'user': {}}}, ) prefetch_lookups = PluginReleaseViewSet.queryset._prefetch_related_lookups self.assertEqual(first=len(prefetch_lookups), second=4) diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models/__init__.py index 0e893d01..878073c6 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models/__init__.py @@ -237,6 +237,12 @@ class SubPluginPath(AbstractUUIDPrimaryKeyModel): default=False, ) + field_tracker = FieldTracker( + fields=[ + 'path', + ] + ) + class Meta: """Define metaclass attributes.""" @@ -249,18 +255,30 @@ def __str__(self): return str(self.path) def clean(self): - """Validate that at least one of the Allow fields is True.""" + """Validate that at least one of the `allow` fields is True.""" + errors = {} if not any([ self.allow_module, self.allow_package_using_basename, self.allow_package_using_init, ]): message = 'At least one of the "Allow" fields must be True.' - raise ValidationError({ + errors.update({ 'allow_module': message, 'allow_package_using_basename': message, 'allow_package_using_init': message, }) + + if self.field_tracker.has_changed('path'): + new_path = self.field_tracker.current()['path'] + if self.plugin.paths.filter(path=new_path).exists(): + errors.update({ + 'path': 'Path already exists for plugin.', + }) + + if errors: + raise ValidationError(errors) + return super().clean() def get_absolute_url(https://github.com/self): diff --git a/project_manager/plugins/tests/test_admin.py b/project_manager/plugins/tests/test_admin.py index ef17952d..aaafb72d 100644 --- a/project_manager/plugins/tests/test_admin.py +++ b/project_manager/plugins/tests/test_admin.py @@ -1,6 +1,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +from unittest import mock + # Django from django.contrib import admin from django.test import TestCase @@ -22,6 +25,7 @@ SubPluginPathInline, ) from project_manager.plugins.models import ( + Plugin, PluginContributor, PluginGame, PluginImage, @@ -52,6 +56,19 @@ def test_inlines(self): ), ) + def test_get_queryset(self): + request = mock.Mock() + query = PluginAdmin( + Plugin, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'owner': {'user': {}}} + ) + class TestPluginReleaseAdminTestCase(TestCase): def test_class_inheritance(self): @@ -129,6 +146,19 @@ def test_search_fields(self): ) ) + def test_get_queryset(self): + request = mock.Mock() + query = PluginReleaseAdmin( + PluginRelease, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'created_by': {'user': {}}} + ) + def test_has_add_permission(self): obj = PluginReleaseAdmin(PluginRelease, admin.AdminSite()) self.assertFalse( @@ -173,6 +203,23 @@ def test_model(self): second=PluginGame, ) + def test_get_queryset(self): + request = mock.Mock() + query = PluginGameInline( + PluginGame, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'game': {}} + ) + self.assertTupleEqual( + tuple1=query.order_by, + tuple2=('game__name',), + ) + def test_has_add_permission(self): obj = PluginGameInline(PluginGame, admin.AdminSite()) self.assertFalse( @@ -217,6 +264,23 @@ def test_model(self): second=PluginTag, ) + def test_get_queryset(self): + request = mock.Mock() + query = PluginTagInline( + PluginTag, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'tag': {}} + ) + self.assertTupleEqual( + tuple1=query.order_by, + tuple2=('tag__name',), + ) + def test_has_add_permission(self): obj = PluginTagInline(PluginTag, admin.AdminSite()) self.assertFalse( @@ -256,7 +320,7 @@ def test_fields(self): def test_readonly_fields(self): self.assertTupleEqual( tuple1=SubPluginPathInline.readonly_fields, - tuple2=('path',), + tuple2=(), ) def test_model(self): diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 707cd679..83bc2f92 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -956,6 +956,29 @@ def test_clean(self): second='At least one of the "Allow" fields must be True.', ) + plugin = PluginFactory() + path_1 = SubPluginPathFactory( + path='path_1', + plugin=plugin, + allow_module=True, + ) + SubPluginPathFactory( + path='path_2', + plugin=plugin, + ) + + path_1.path = 'path_3' + path_1.clean() + + path_1.path = 'path_2' + with self.assertRaises(ValidationError) as context: + path_1.clean() + + self.assertDictEqual( + d1=context.exception.message_dict, + d2={'path': ['Path already exists for plugin.']} + ) + def test_meta_class(self): self.assertEqual( first=SubPluginPath._meta.verbose_name, diff --git a/project_manager/sub_plugins/api/tests/test_release_views.py b/project_manager/sub_plugins/api/tests/test_release_views.py index 32938306..9056f091 100644 --- a/project_manager/sub_plugins/api/tests/test_release_views.py +++ b/project_manager/sub_plugins/api/tests/test_release_views.py @@ -104,7 +104,7 @@ def test_base_attributes(self): ) self.assertDictEqual( d1=SubPluginReleaseViewSet.queryset.query.select_related, - d2={'sub_plugin': {}}, + d2={'sub_plugin': {}, 'created_by': {'user': {}}}, ) prefetch_lookups = SubPluginReleaseViewSet.queryset._prefetch_related_lookups self.assertEqual(first=len(prefetch_lookups), second=4) diff --git a/project_manager/sub_plugins/tests/test_admin.py b/project_manager/sub_plugins/tests/test_admin.py index e9a41f0a..72058c74 100644 --- a/project_manager/sub_plugins/tests/test_admin.py +++ b/project_manager/sub_plugins/tests/test_admin.py @@ -1,6 +1,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +from unittest import mock + # Django from django.contrib import admin from django.test import TestCase @@ -24,6 +27,7 @@ SubPluginTagInline, ) from project_manager.sub_plugins.models import ( + SubPlugin, SubPluginContributor, SubPluginGame, SubPluginImage, @@ -52,6 +56,19 @@ def test_inlines(self): ), ) + def test_get_queryset(self): + request = mock.Mock() + query = SubPluginAdmin( + SubPlugin, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'owner': {'user': {}}} + ) + class TestSubPluginReleaseAdminTestCase(TestCase): def test_class_inheritance(self): @@ -129,6 +146,19 @@ def test_search_fields(self): ) ) + def test_get_queryset(self): + request = mock.Mock() + query = SubPluginReleaseAdmin( + SubPluginRelease, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'created_by': {'user': {}}} + ) + def test_has_add_permission(self): obj = SubPluginReleaseAdmin(SubPluginRelease, admin.AdminSite()) self.assertFalse( @@ -173,6 +203,23 @@ def test_model(self): second=SubPluginGame, ) + def test_get_queryset(self): + request = mock.Mock() + query = SubPluginGameInline( + SubPluginGame, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'game': {}} + ) + self.assertTupleEqual( + tuple1=query.order_by, + tuple2=('game__name',), + ) + def test_has_add_permission(self): obj = SubPluginGameInline(SubPluginGame, admin.AdminSite()) self.assertFalse( @@ -217,6 +264,23 @@ def test_model(self): second=SubPluginTag, ) + def test_get_queryset(self): + request = mock.Mock() + query = SubPluginTagInline( + SubPluginTag, + admin.AdminSite(), + ).get_queryset( + request=request, + ).query + self.assertDictEqual( + d1=query.select_related, + d2={'tag': {}} + ) + self.assertTupleEqual( + tuple1=query.order_by, + tuple2=('tag__name',), + ) + def test_has_add_permission(self): obj = SubPluginTagInline(SubPluginTag, admin.AdminSite()) self.assertFalse( diff --git a/tags/admin.py b/tags/admin.py index e7edaaae..066522c0 100644 --- a/tags/admin.py +++ b/tags/admin.py @@ -39,10 +39,21 @@ class TagAdmin(admin.ModelAdmin): 'black_listed', 'creator', ) + raw_id_fields = ( + 'creator', + ) readonly_fields = ( 'name', ) + def get_queryset(self, request): + """Cache the 'creator' for the queryset.""" + return super().get_queryset( + request=request, + ).select_related( + 'creator__user', + ) + def has_add_permission(self, request): """Disallow adding of tags in the Admin.""" return False diff --git a/tags/tests/test_admin.py b/tags/tests/test_admin.py index dd04162e..ca33d8fe 100644 --- a/tags/tests/test_admin.py +++ b/tags/tests/test_admin.py @@ -51,12 +51,25 @@ def test_list_editable(self): ), ) + def test_raw_id_fields(self): + self.assertTupleEqual( + tuple1=TagAdmin.raw_id_fields, + tuple2=('creator',), + ) + def test_readonly_fields(self): self.assertTupleEqual( tuple1=TagAdmin.readonly_fields, tuple2=('name',), ) + def test_get_queryset(self): + query = TagAdmin(Tag, '').get_queryset('').query + self.assertDictEqual( + d1=query.select_related, + d2={'creator': {'user': {}}} + ) + def test_has_add_permission(self): self.assertFalse( expr=TagAdmin(Tag, '').has_add_permission(''), From ec8a20fb79b0bad38016b6269067f331e79a69a7 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 20 Mar 2022 20:24:22 -0400 Subject: [PATCH 115/211] Updated API docs. --- project_manager/common/api/views/__init__.py | 94 ++++++++++++++++++++ project_manager/packages/api/views.py | 46 ++-------- project_manager/plugins/api/views.py | 58 ++++-------- project_manager/sub_plugins/api/views.py | 46 ++-------- users/api/views.py | 13 ++- 5 files changed, 138 insertions(+), 119 deletions(-) diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 66753913..19e7a48d 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -74,6 +74,45 @@ def get_view_name(self): class ProjectViewSet(ModelViewSet): """Base ViewSet for creating, updating, and listing Projects.""" + doc_string = """ + + ###Available Filters: + * **game**=*{game}* + * Filters on supported games with exact match to slug. + + ####Example: + `?game=csgo` + + `?game=cstrike` + + * **tag**=*{tag}* + * Filters on tags using exact match. + + ####Example: + `?tag=wcs` + + `?tag=sounds` + + * **user**=*{username}* + * Filters on username using exact match with owner/contributors. + + ####Example: + `?user=satoon101` + + `?user=Ayuto` + + ###Available Ordering: + + * **name** (descending) or **-name** (ascending) + * **basename** (descending) or **-basename** (ascending) + * **created** (descending) or **-created** (ascending) + * **updated** (descending) or **-updated** (ascending) + + ####Example: + `?ordering=basename` + + `?ordering=-updated` + """ authentication_classes = (SessionAuthentication,) filter_backends = (OrderingFilter, DjangoFilterBackend) http_method_names = ('get', 'post', 'patch', 'options') @@ -129,6 +168,17 @@ def get_serializer_class(self): class ProjectImageViewSet(ProjectThroughModelMixin): """Base Image View.""" + doc_string = """ + + ###Available Ordering: + + * **created** (descending) or **-created** (ascending) + + ####Example: + `?ordering=created` + + `?ordering=-created` + """ ordering = ('-created',) ordering_fields = ('created',) related_model_type = 'Image' @@ -137,6 +187,17 @@ class ProjectImageViewSet(ProjectThroughModelMixin): class ProjectReleaseViewSet(ProjectRelatedInfoMixin): """Base Release ViewSet.""" + doc_string = """ + + ###Available Ordering: + + * **created** (descending) or **-created** (ascending) + + ####Example: + `?ordering=created` + + `?ordering=-created` + """ http_method_names = ('get', 'post', 'options') ordering = ('-created',) ordering_fields = ('created',) @@ -161,6 +222,17 @@ def check_permissions(self, request): class ProjectGameViewSet(ProjectThroughModelMixin): """Base Game Support ViewSet.""" + doc_string = """ + + ###Available Ordering: + + * **game** (descending) or **-game** (ascending) + + ####Example: + `?ordering=game` + + `?ordering=-game` + """ ordering = ('-game',) ordering_fields = ('game',) related_model_type = 'Game' @@ -169,6 +241,17 @@ class ProjectGameViewSet(ProjectThroughModelMixin): class ProjectTagViewSet(ProjectThroughModelMixin): """Base Project Tag ViewSet.""" + doc_string = """ + + ###Available Ordering: + + * **tag** (descending) or **-tag** (ascending) + + ####Example: + `?ordering=tag` + + `?ordering=-tag` + """ ordering = ('-tag',) ordering_fields = ('tag',) related_model_type = 'Tag' @@ -177,6 +260,17 @@ class ProjectTagViewSet(ProjectThroughModelMixin): class ProjectContributorViewSet(ProjectThroughModelMixin): """Base Project Contributor ViewSet.""" + doc_string = """ + + ###Available Ordering: + + * **user** (descending) or **-user** (ascending) + + ####Example: + `?ordering=user` + + `?ordering=-user` + """ ordering = ('-user',) ordering_fields = ('user',) related_model_type = 'Contributor' diff --git a/project_manager/packages/api/views.py b/project_manager/packages/api/views.py index 702f175d..e515f9ea 100644 --- a/project_manager/packages/api/views.py +++ b/project_manager/packages/api/views.py @@ -64,46 +64,9 @@ class PackageAPIView(ProjectAPIView): class PackageViewSet(ProjectViewSet): - """ViewSet for creating, updating, and listing Packages. - - ###Available Filters: - * **game**=*{game}* - * Filters on supported games with exact match to slug. - - ####Example: - `?game=csgo` - - `?game=cstrike` - - * **tag**=*{tag}* - * Filters on tags using exact match. - - ####Example: - `?tag=wcs` - - `?tag=sounds` - - * **user**=*{username}* - * Filters on username using exact match with owner/contributors. - - ####Example: - `?user=satoon101` - - `?user=Ayuto` - - ###Available Ordering: - - * **name** (descending) or **-name** (ascending) - * **basename** (descending) or **-basename** (ascending) - * **created** (descending) or **-created** (ascending) - * **updated** (descending) or **-updated** (ascending) - - ####Example: - `?ordering=basename` - - `?ordering=-updated` - """ + """ViewSet for creating, updating, and listing Packages.""" + __doc__ += ProjectViewSet.doc_string filterset_class = PackageFilterSet queryset = Package.objects.prefetch_related( Prefetch( @@ -123,6 +86,7 @@ class PackageViewSet(ProjectViewSet): class PackageImageViewSet(ProjectImageViewSet): """ViewSet for adding, removing, and listing images for Packages.""" + __doc__ += ProjectImageViewSet.doc_string queryset = PackageImage.objects.select_related( 'package', ) @@ -135,6 +99,7 @@ class PackageImageViewSet(ProjectImageViewSet): class PackageReleaseViewSet(ProjectReleaseViewSet): """ViewSet for retrieving releases for Packages.""" + __doc__ += ProjectReleaseViewSet.doc_string queryset = PackageRelease.objects.select_related( 'package', 'created_by__user', @@ -181,6 +146,7 @@ class PackageReleaseViewSet(ProjectReleaseViewSet): class PackageGameViewSet(ProjectGameViewSet): """Supported Games listing for Packages.""" + __doc__ += ProjectGameViewSet.doc_string queryset = PackageGame.objects.select_related( 'game', 'package', @@ -194,6 +160,7 @@ class PackageGameViewSet(ProjectGameViewSet): class PackageTagViewSet(ProjectTagViewSet): """Tags listing for Packages.""" + __doc__ += ProjectTagViewSet.doc_string queryset = PackageTag.objects.select_related( 'tag', 'package', @@ -207,6 +174,7 @@ class PackageTagViewSet(ProjectTagViewSet): class PackageContributorViewSet(ProjectContributorViewSet): """Contributors listing for Packages.""" + __doc__ += ProjectContributorViewSet.doc_string queryset = PackageContributor.objects.select_related( 'user__user', 'package', diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index 34bc8cee..82a27540 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -80,46 +80,9 @@ def get(self, request): class PluginViewSet(ProjectViewSet): - """ViewSet for creating, updating, and listing Plugins. - - ###Available Filters: - * **game**=*{game}* - * Filters on supported games with exact match to slug. - - ####Example: - `?game=csgo` - - `?game=cstrike` - - * **tag**=*{tag}* - * Filters on tags using exact match. - - ####Example: - `?tag=wcs` - - `?tag=sounds` - - * **user**=*{username}* - * Filters on username using exact match with owner/contributors. - - ####Example: - `?user=satoon101` - - `?user=Ayuto` - - ###Available Ordering: - - * **name** (descending) or **-name** (ascending) - * **basename** (descending) or **-basename** (ascending) - * **created** (descending) or **-created** (ascending) - * **updated** (descending) or **-updated** (ascending) - - ####Example: - `?ordering=basename` - - `?ordering=-updated` - """ + """ViewSet for creating, updating, and listing Plugins.""" + __doc__ += ProjectViewSet.doc_string filterset_class = PluginFilterSet queryset = Plugin.objects.prefetch_related( Prefetch( @@ -139,6 +102,7 @@ class PluginViewSet(ProjectViewSet): class PluginImageViewSet(ProjectImageViewSet): """ViewSet for adding, removing, and listing images for Plugins.""" + __doc__ += ProjectImageViewSet.doc_string queryset = PluginImage.objects.select_related( 'plugin', ) @@ -151,6 +115,7 @@ class PluginImageViewSet(ProjectImageViewSet): class PluginReleaseViewSet(ProjectReleaseViewSet): """ViewSet for retrieving releases for Plugins.""" + __doc__ += ProjectReleaseViewSet.doc_string queryset = PluginRelease.objects.select_related( 'plugin', 'created_by__user', @@ -197,6 +162,7 @@ class PluginReleaseViewSet(ProjectReleaseViewSet): class PluginGameViewSet(ProjectGameViewSet): """Supported Games listing for Plugins.""" + __doc__ += ProjectGameViewSet.doc_string queryset = PluginGame.objects.select_related( 'game', 'plugin', @@ -210,6 +176,7 @@ class PluginGameViewSet(ProjectGameViewSet): class PluginTagViewSet(ProjectTagViewSet): """Tags listing for Plugins.""" + __doc__ += ProjectTagViewSet.doc_string queryset = PluginTag.objects.select_related( 'tag', 'plugin', @@ -223,6 +190,7 @@ class PluginTagViewSet(ProjectTagViewSet): class PluginContributorViewSet(ProjectContributorViewSet): """Contributors listing for Plugins.""" + __doc__ += ProjectContributorViewSet.doc_string queryset = PluginContributor.objects.select_related( 'user__user', 'plugin', @@ -234,7 +202,17 @@ class PluginContributorViewSet(ProjectContributorViewSet): class SubPluginPathViewSet(ProjectThroughModelMixin): - """Sub-Plugin Paths listing.""" + """Sub-Plugin Paths listing. + + ###Available Ordering: + + * **path** (descending) or **-path** (ascending) + + ####Example: + `?ordering=path` + + `?ordering=-path` + """ http_method_names = ('get', 'post', 'patch', 'delete', 'options') ordering = ('path',) diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index fc03c9e1..97d08400 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -87,46 +87,9 @@ def get(self, request): class SubPluginViewSet(ProjectViewSet): - """ViewSet for creating, updating, and listing SubPlugins. - - ###Available Filters: - * **game**=*{game}* - * Filters on supported games with exact match to slug. - - ####Example: - `?game=csgo` - - `?game=cstrike` - - * **tag**=*{tag}* - * Filters on tags using exact match. - - ####Example: - `?tag=wcs` - - `?tag=sounds` - - * **user**=*{username}* - * Filters on username using exact match with owner/contributors. - - ####Example: - `?user=satoon101` - - `?user=Ayuto` - - ###Available Ordering: - - * **name** (descending) or **-name** (ascending) - * **basename** (descending) or **-basename** (ascending) - * **created** (descending) or **-created** (ascending) - * **updated** (descending) or **-updated** (ascending) - - ####Example: - `?ordering=basename` - - `?ordering=-updated` - """ + """ViewSet for creating, updating, and listing SubPlugins.""" + __doc__ += ProjectViewSet.doc_string filterset_class = SubPluginFilterSet queryset = SubPlugin.objects.prefetch_related( Prefetch( @@ -161,6 +124,7 @@ def get_queryset(self): class SubPluginImageViewSet(ProjectImageViewSet): """ViewSet for adding, removing, and listing images for SubPlugins.""" + __doc__ += ProjectImageViewSet.doc_string queryset = SubPluginImage.objects.select_related( 'sub_plugin', ) @@ -193,6 +157,7 @@ def get_project_kwargs(self): class SubPluginReleaseViewSet(ProjectReleaseViewSet): """ViewSet for retrieving releases for SubPlugins.""" + __doc__ += ProjectReleaseViewSet.doc_string queryset = SubPluginRelease.objects.select_related( 'sub_plugin', 'created_by__user', @@ -261,6 +226,7 @@ def get_project_kwargs(self): class SubPluginGameViewSet(ProjectGameViewSet): """Supported Games listing for SubPlugins.""" + __doc__ += ProjectGameViewSet.doc_string queryset = SubPluginGame.objects.select_related( 'game', 'sub_plugin', @@ -274,6 +240,7 @@ class SubPluginGameViewSet(ProjectGameViewSet): class SubPluginTagViewSet(ProjectTagViewSet): """Tags listing for SubPlugins.""" + __doc__ += ProjectTagViewSet.doc_string queryset = SubPluginTag.objects.select_related( 'tag', 'sub_plugin', @@ -287,6 +254,7 @@ class SubPluginTagViewSet(ProjectTagViewSet): class SubPluginContributorViewSet(ProjectContributorViewSet): """Contributors listing for SubPlugins.""" + __doc__ += ProjectContributorViewSet.doc_string queryset = SubPluginContributor.objects.select_related( 'user__user', 'sub_plugin', diff --git a/users/api/views.py b/users/api/views.py index f5137ad5..78a6b905 100644 --- a/users/api/views.py +++ b/users/api/views.py @@ -32,7 +32,18 @@ # VIEWS # ============================================================================= class ForumUserViewSet(ModelViewSet): - """ForumUser API view.""" + """ForumUser API view. + + ###Available Ordering: + + * **forum_id** (descending) or **-forum_id** (ascending) + * **user__username** (descending) or **-user__username** (ascending) + + ####Example: + `?ordering=forum_id` + + `?ordering=-user__username` + """ filter_backends = (OrderingFilter, DjangoFilterBackend) filterset_class = ForumUserFilterSet From d3dc4cb248e9b1bd99ef9b4c94af853f17198b14 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 20 Mar 2022 20:25:58 -0400 Subject: [PATCH 116/211] Improved sub-plugin release to only get the SubPluginPaths once instead of in a nested loop. --- project_manager/sub_plugins/helpers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/project_manager/sub_plugins/helpers.py b/project_manager/sub_plugins/helpers.py index 0a222389..4b9782da 100644 --- a/project_manager/sub_plugins/helpers.py +++ b/project_manager/sub_plugins/helpers.py @@ -52,6 +52,12 @@ class SubPluginZipFile(ProjectZipFile): def __init__(self, zip_file, plugin): """Store the base attributes and the plugin.""" self.plugin = plugin + self.sub_plugin_paths = list( + self.plugin.paths.values_list( + 'path', + flat=True, + ) + ) super().__init__(zip_file) def _validate_path(self, path): @@ -65,10 +71,7 @@ def _validate_path(self, path): return True for base_path, allowed_extensions in self.file_types.items(): - for sub_plugin_path in self.plugin.paths.values_list( - 'path', - flat=True, - ): + for sub_plugin_path in self.sub_plugin_paths: if not path.startswith( base_path.format( self=self, From 2fa8fb5103ca9e8e8d51fa62a748ad74ff6ea1ff Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Wed, 23 Mar 2022 18:08:59 -0400 Subject: [PATCH 117/211] Fixed bug in ProjectAPI ordering that caused duplicate of the same Project to show in the results. --- project_manager/common/api/tests/test_views.py | 2 +- project_manager/common/api/views/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project_manager/common/api/tests/test_views.py b/project_manager/common/api/tests/test_views.py index 21f09f43..391ccaea 100644 --- a/project_manager/common/api/tests/test_views.py +++ b/project_manager/common/api/tests/test_views.py @@ -118,7 +118,7 @@ def test_base_attributes(self): ) self.assertTupleEqual( tuple1=ProjectViewSet.ordering, - tuple2=('-releases__created',), + tuple2=('-updated',), ) self.assertTupleEqual( tuple1=ProjectViewSet.ordering_fields, diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 19e7a48d..7b2b30f5 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -116,7 +116,7 @@ class ProjectViewSet(ModelViewSet): authentication_classes = (SessionAuthentication,) filter_backends = (OrderingFilter, DjangoFilterBackend) http_method_names = ('get', 'post', 'patch', 'options') - ordering = ('-releases__created',) + ordering = ('-updated',) ordering_fields = ('name', 'basename', 'updated', 'created') permission_classes = (IsAuthenticatedOrReadOnly,) From 96908446a267d9c140e3d292a4fbf963369a88b0 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Wed, 23 Mar 2022 18:09:56 -0400 Subject: [PATCH 118/211] Changed Tag.creator to a readonly field in the Admin. --- tags/admin.py | 5 +---- tags/tests/test_admin.py | 9 +++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/tags/admin.py b/tags/admin.py index 066522c0..e0f09df1 100644 --- a/tags/admin.py +++ b/tags/admin.py @@ -37,12 +37,9 @@ class TagAdmin(admin.ModelAdmin): ) list_editable = ( 'black_listed', - 'creator', - ) - raw_id_fields = ( - 'creator', ) readonly_fields = ( + 'creator', 'name', ) diff --git a/tags/tests/test_admin.py b/tags/tests/test_admin.py index ca33d8fe..9c9aa227 100644 --- a/tags/tests/test_admin.py +++ b/tags/tests/test_admin.py @@ -45,22 +45,19 @@ def test_list_filter(self): def test_list_editable(self): self.assertTupleEqual( tuple1=TagAdmin.list_editable, - tuple2=( - 'black_listed', - 'creator', - ), + tuple2=('black_listed',), ) def test_raw_id_fields(self): self.assertTupleEqual( tuple1=TagAdmin.raw_id_fields, - tuple2=('creator',), + tuple2=(), ) def test_readonly_fields(self): self.assertTupleEqual( tuple1=TagAdmin.readonly_fields, - tuple2=('name',), + tuple2=('creator', 'name'), ) def test_get_queryset(self): From fa6f043600354853c6656c8d216b4b6d876765dc Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Wed, 23 Mar 2022 18:24:15 -0400 Subject: [PATCH 119/211] Added some select_related calls to Admins to lower query counts. --- project_manager/packages/admin/__init__.py | 8 ++++++++ project_manager/packages/tests/test_admin.py | 2 +- project_manager/plugins/admin/__init__.py | 8 ++++++++ project_manager/plugins/api/views.py | 8 ++++---- project_manager/plugins/tests/test_admin.py | 2 +- project_manager/sub_plugins/admin/__init__.py | 16 ++++++++++++++++ project_manager/sub_plugins/tests/test_admin.py | 4 ++-- 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/project_manager/packages/admin/__init__.py b/project_manager/packages/admin/__init__.py index 88c3e9ea..a8a8c2e7 100644 --- a/project_manager/packages/admin/__init__.py +++ b/project_manager/packages/admin/__init__.py @@ -54,3 +54,11 @@ class PackageReleaseAdmin(ProjectReleaseAdmin): ordering = ('package', '-created',) readonly_fields = ProjectReleaseAdmin.readonly_fields + ('package',) search_fields = ProjectReleaseAdmin.search_fields + ('package__name',) + + def get_queryset(self, request): + """Cache 'package' for the queryset.""" + return super().get_queryset( + request=request, + ).select_related( + 'package', + ) diff --git a/project_manager/packages/tests/test_admin.py b/project_manager/packages/tests/test_admin.py index e8aa6db0..7cb820d1 100644 --- a/project_manager/packages/tests/test_admin.py +++ b/project_manager/packages/tests/test_admin.py @@ -153,7 +153,7 @@ def test_get_queryset(self): ).query self.assertDictEqual( d1=query.select_related, - d2={'created_by': {'user': {}}} + d2={'created_by': {'user': {}}, 'package': {}}, ) def test_has_add_permission(self): diff --git a/project_manager/plugins/admin/__init__.py b/project_manager/plugins/admin/__init__.py index e8453048..9be8f4f8 100644 --- a/project_manager/plugins/admin/__init__.py +++ b/project_manager/plugins/admin/__init__.py @@ -56,3 +56,11 @@ class PluginReleaseAdmin(ProjectReleaseAdmin): ordering = ('plugin', '-created',) readonly_fields = ProjectReleaseAdmin.readonly_fields + ('plugin',) search_fields = ProjectReleaseAdmin.search_fields + ('plugin__name',) + + def get_queryset(self, request): + """Cache 'plugin' for the queryset.""" + return super().get_queryset( + request=request, + ).select_related( + 'plugin', + ) diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index 82a27540..a53f55e3 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -70,7 +70,7 @@ class PluginAPIView(ProjectAPIView): project_type = 'plugin' def get(self, request): - """Add the 'paths' route and return all of the routes.""" + """Add the 'paths' route and return all the routes.""" response = super().get(request=request) response.data['paths'] = reverse( viewname=f'api:{self.project_type}s:endpoints', @@ -84,15 +84,15 @@ class PluginViewSet(ProjectViewSet): __doc__ += ProjectViewSet.doc_string filterset_class = PluginFilterSet - queryset = Plugin.objects.prefetch_related( + queryset = Plugin.objects.select_related( + 'owner__user', + ).prefetch_related( Prefetch( lookup='releases', queryset=PluginRelease.objects.order_by( '-created', ), ), - ).select_related( - 'owner__user', ) serializer_class = PluginSerializer diff --git a/project_manager/plugins/tests/test_admin.py b/project_manager/plugins/tests/test_admin.py index aaafb72d..4ca8a2d4 100644 --- a/project_manager/plugins/tests/test_admin.py +++ b/project_manager/plugins/tests/test_admin.py @@ -156,7 +156,7 @@ def test_get_queryset(self): ).query self.assertDictEqual( d1=query.select_related, - d2={'created_by': {'user': {}}} + d2={'created_by': {'user': {}}, 'plugin': {}}, ) def test_has_add_permission(self): diff --git a/project_manager/sub_plugins/admin/__init__.py b/project_manager/sub_plugins/admin/__init__.py index 1f002d40..507d6883 100644 --- a/project_manager/sub_plugins/admin/__init__.py +++ b/project_manager/sub_plugins/admin/__init__.py @@ -62,6 +62,14 @@ class SubPluginAdmin(ProjectAdmin): 'plugin__basename', ) + def get_queryset(self, request): + """Cache 'plugin' for the queryset.""" + return super().get_queryset( + request=request, + ).select_related( + 'plugin', + ) + @admin.register(SubPluginRelease) class SubPluginReleaseAdmin(ProjectReleaseAdmin): @@ -73,3 +81,11 @@ class SubPluginReleaseAdmin(ProjectReleaseAdmin): ordering = ('sub_plugin', '-created',) readonly_fields = ProjectReleaseAdmin.readonly_fields + ('sub_plugin',) search_fields = ProjectReleaseAdmin.search_fields + ('sub_plugin__name',) + + def get_queryset(self, request): + """Cache 'plugin' for the queryset.""" + return super().get_queryset( + request=request, + ).select_related( + 'sub_plugin__plugin', + ) diff --git a/project_manager/sub_plugins/tests/test_admin.py b/project_manager/sub_plugins/tests/test_admin.py index 72058c74..e6153f82 100644 --- a/project_manager/sub_plugins/tests/test_admin.py +++ b/project_manager/sub_plugins/tests/test_admin.py @@ -66,7 +66,7 @@ def test_get_queryset(self): ).query self.assertDictEqual( d1=query.select_related, - d2={'owner': {'user': {}}} + d2={'owner': {'user': {}}, 'plugin': {}}, ) @@ -156,7 +156,7 @@ def test_get_queryset(self): ).query self.assertDictEqual( d1=query.select_related, - d2={'created_by': {'user': {}}} + d2={'created_by': {'user': {}}, 'sub_plugin': {'plugin': {}}}, ) def test_has_add_permission(self): From e10f7e902872af03601a806b9de39ab168f98d39 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 10:06:06 -0400 Subject: [PATCH 120/211] Started rewrite of the Tags APIs. --- tags/api/filtersets.py | 33 -------- tags/api/serializers.py | 17 +++- tags/api/tests/test_filtersets.py | 33 -------- tags/api/tests/test_serializers.py | 35 ++++++++- tags/api/tests/test_views.py | 122 ++++++++++++++++++++++++++--- tags/api/views.py | 42 ++++++---- 6 files changed, 185 insertions(+), 97 deletions(-) delete mode 100644 tags/api/filtersets.py delete mode 100644 tags/api/tests/test_filtersets.py diff --git a/tags/api/filtersets.py b/tags/api/filtersets.py deleted file mode 100644 index 05ac42b8..00000000 --- a/tags/api/filtersets.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Tag API filters.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Third Party Django -from django_filters.filterset import FilterSet - -# App -from tags.models import Tag - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'TagFilterSet', -) - - -# ============================================================================= -# FILTERS -# ============================================================================= -class TagFilterSet(FilterSet): - """Filters for Tags.""" - - class Meta: - """Define metaclass attributes.""" - - fields = ( - 'black_listed', - ) - model = Tag diff --git a/tags/api/serializers.py b/tags/api/serializers.py index 5d96f933..4ddc92c3 100644 --- a/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -4,6 +4,7 @@ # IMPORTS # ============================================================================= # Third Party Django +from rest_framework.relations import RelatedField from rest_framework.serializers import ModelSerializer # App @@ -15,6 +16,7 @@ # ALL DECLARATION # ============================================================================= __all__ = ( + 'RelatedTagSerializer', 'TagSerializer', ) @@ -22,12 +24,23 @@ # ============================================================================= # SERIALIZERS # ============================================================================= +class RelatedTagSerializer(RelatedField): + """Serializer for project tag fields.""" + + def to_representation(self, value): + """Return the name of the project.""" + return value.name + + class TagSerializer(ModelSerializer): """Serializer for project Tags.""" creator = ForumUserContributorSerializer( read_only=True, ) + packages = RelatedTagSerializer(many=True, read_only=True) + plugins = RelatedTagSerializer(many=True, read_only=True) + subplugins = RelatedTagSerializer(many=True, read_only=True) class Meta: """Define metaclass attributes.""" @@ -35,6 +48,8 @@ class Meta: model = Tag fields = ( 'name', - 'black_listed', + 'packages', + 'plugins', + 'subplugins', 'creator', ) diff --git a/tags/api/tests/test_filtersets.py b/tags/api/tests/test_filtersets.py deleted file mode 100644 index 3e44d35b..00000000 --- a/tags/api/tests/test_filtersets.py +++ /dev/null @@ -1,33 +0,0 @@ -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.test import TestCase - -# Third Party Django -from django_filters.filterset import FilterSet - -# App -from tags.api.filtersets import TagFilterSet -from tags.models import Tag - - -# ============================================================================= -# TEST CASES -# ============================================================================= -class TagFilterSetTestCase(TestCase): - - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(TagFilterSet, FilterSet), - ) - - def test_meta_class(self): - self.assertEqual( - first=TagFilterSet.Meta.model, - second=Tag, - ) - self.assertTupleEqual( - tuple1=TagFilterSet.Meta.fields, - tuple2=('black_listed',), - ) diff --git a/tags/api/tests/test_serializers.py b/tags/api/tests/test_serializers.py index aaad1fdb..6870e421 100644 --- a/tags/api/tests/test_serializers.py +++ b/tags/api/tests/test_serializers.py @@ -5,10 +5,11 @@ from django.test import TestCase # Third Party Django +from rest_framework.relations import ManyRelatedField, RelatedField from rest_framework.serializers import ModelSerializer # App -from tags.api.serializers import TagSerializer +from tags.api.serializers import TagSerializer, RelatedTagSerializer from tags.models import Tag from users.api.serializers.common import ForumUserContributorSerializer @@ -16,8 +17,12 @@ # ============================================================================= # TEST CASES # ============================================================================= -class TagSerializerTestCase(TestCase): +class RelatedTagSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(RelatedTagSerializer, RelatedField)) + +class TagSerializerTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( expr=issubclass(TagSerializer, ModelSerializer), @@ -27,7 +32,7 @@ def test_declared_fields(self): declared_fields = getattr(TagSerializer, '_declared_fields') self.assertEqual( first=len(declared_fields), - second=1, + second=4, ) self.assertIn( @@ -40,6 +45,26 @@ def test_declared_fields(self): ) self.assertTrue(expr=declared_fields['creator'].read_only) + for field in ( + 'packages', + 'plugins', + 'subplugins', + ): + self.assertIn( + member=field, + container=declared_fields, + ) + obj = declared_fields[field] + self.assertIsInstance( + obj=obj, + cls=ManyRelatedField, + ) + self.assertIsInstance( + obj=obj.child_relation, + cls=RelatedTagSerializer, + ) + self.assertTrue(expr=obj.child_relation.read_only) + def test_meta_class(self): self.assertEqual( first=TagSerializer.Meta.model, @@ -49,7 +74,9 @@ def test_meta_class(self): tuple1=TagSerializer.Meta.fields, tuple2=( 'name', - 'black_listed', + 'packages', + 'plugins', + 'subplugins', 'creator', ), ) diff --git a/tags/api/tests/test_views.py b/tags/api/tests/test_views.py index 1aa9c0a5..0c390976 100644 --- a/tags/api/tests/test_views.py +++ b/tags/api/tests/test_views.py @@ -8,9 +8,14 @@ from rest_framework.test import APITestCase # App -from tags.api.filtersets import TagFilterSet from tags.api.serializers import TagSerializer from tags.api.views import TagViewSet +from test_utils.factories.packages import PackageFactory, PackageTagFactory +from test_utils.factories.plugins import PluginFactory, PluginTagFactory +from test_utils.factories.sub_plugins import ( + SubPluginFactory, + SubPluginTagFactory, +) from test_utils.factories.tags import TagFactory @@ -27,12 +32,6 @@ def test_filter_backends(self): tuple2=(OrderingFilter, DjangoFilterBackend) ) - def test_filterset_class(self): - self.assertEqual( - first=TagViewSet.filterset_class, - second=TagFilterSet, - ) - def test_serializer_class(self): self.assertEqual( first=TagViewSet.serializer_class, @@ -57,6 +56,33 @@ def test_http_method_names(self): tuple2=('get', 'options'), ) + def test_get_queryset(self): + queryset = TagViewSet().get_queryset().filter() + prefetch_lookups = queryset._prefetch_related_lookups + self.assertEqual( + first=len(prefetch_lookups), + second=3, + ) + for n, lookup_name in enumerate([ + 'packages', + 'plugins', + 'subplugins', + ]): + lookup = prefetch_lookups[n] + self.assertEqual( + first=lookup.prefetch_to, + second=lookup_name, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('name',), + ) + + self.assertDictEqual( + d1=queryset.query.select_related, + d2={'creator': {'user': {}}}, + ) + def test_get(self): response = self.client.get(path=self.api_path) self.assertEqual( @@ -68,7 +94,30 @@ def test_get(self): second=0, ) - tag = TagFactory() + tag_1 = TagFactory() + package = PackageFactory() + PackageTagFactory( + package=package, + tag=tag_1, + ) + tag_2 = TagFactory() + plugin = PluginFactory() + PluginTagFactory( + plugin=plugin, + tag=tag_2, + ) + tag_3 = TagFactory() + sub_plugin = SubPluginFactory( + plugin=plugin, + ) + SubPluginTagFactory( + sub_plugin=sub_plugin, + tag=tag_3, + ) + tag_4 = TagFactory() + black_listed_tag = TagFactory( + black_listed=True, + ) response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, @@ -77,11 +126,60 @@ def test_get(self): content = response.json() self.assertEqual( first=content['count'], - second=1, + second=4, ) - self.assertEqual( - first=content['results'][0]['name'], - second=tag.name, + results = content['results'] + self.assertDictEqual( + d1=results[0], + d2={ + 'name': tag_1.name, + 'packages': [package.name], + 'plugins': [], + 'subplugins': [], + 'creator': { + 'forum_id': tag_1.creator.forum_id, + 'username': tag_1.creator.user.username, + } + } + ) + self.assertDictEqual( + d1=results[1], + d2={ + 'name': tag_2.name, + 'packages': [], + 'plugins': [plugin.name], + 'subplugins': [], + 'creator': { + 'forum_id': tag_2.creator.forum_id, + 'username': tag_2.creator.user.username, + } + } + ) + self.assertDictEqual( + d1=results[2], + d2={ + 'name': tag_3.name, + 'packages': [], + 'plugins': [], + 'subplugins': [sub_plugin.name], + 'creator': { + 'forum_id': tag_3.creator.forum_id, + 'username': tag_3.creator.user.username, + } + } + ) + self.assertDictEqual( + d1=results[3], + d2={ + 'name': tag_4.name, + 'packages': [], + 'plugins': [], + 'subplugins': [], + 'creator': { + 'forum_id': tag_4.creator.forum_id, + 'username': tag_4.creator.user.username, + } + } ) def test_options(self): diff --git a/tags/api/views.py b/tags/api/views.py index 93ada6de..1860f502 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -3,6 +3,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Django +from django.db.models import Prefetch + # Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter @@ -10,7 +13,9 @@ from rest_framework.viewsets import GenericViewSet # App -from tags.api.filtersets import TagFilterSet +from project_manager.packages.models import Package +from project_manager.plugins.models import Plugin +from project_manager.sub_plugins.models import SubPlugin from tags.api.serializers import TagSerializer from tags.models import Tag @@ -29,15 +34,6 @@ class TagViewSet(ListModelMixin, GenericViewSet): """ViewSet for listing Supported Games. - ###Available Filters: - * **black_listed**=*{boolean}* - * Filters on blacklisted or not blacklisted. - - ####Example: - `?black_listed=true` - - `?black_listed=false` - ###Available Ordering: * **name** (descending) or **-name** (ascending) @@ -49,11 +45,29 @@ class TagViewSet(ListModelMixin, GenericViewSet): """ filter_backends = (OrderingFilter, DjangoFilterBackend) - filterset_class = TagFilterSet serializer_class = TagSerializer - queryset = Tag.objects.select_related( - 'creator__user', - ) + queryset = Tag.objects.all() ordering = ('name',) ordering_fields = ('name',) http_method_names = ('get', 'options') + + def get_queryset(self): + """Filter the queryset to not return black-listed tags.""" + return super().get_queryset().filter( + black_listed=False, + ).select_related( + 'creator__user', + ).prefetch_related( + Prefetch( + lookup='packages', + queryset=Package.objects.order_by('name'), + ), + Prefetch( + lookup='plugins', + queryset=Plugin.objects.order_by('name'), + ), + Prefetch( + lookup='subplugins', + queryset=SubPlugin.objects.order_by('name'), + ), + ) From e6c18a1767a4bb557728b6eea5d534d28c5be039 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 10:10:10 -0400 Subject: [PATCH 121/211] Updated ProjectRelatedInfoMixin.project to raise a NotFound error instead of ParseError. Updated tests accordingly. --- project_manager/common/api/views/mixins.py | 7 +- .../packages/api/tests/test_related_views.py | 32 +-- .../packages/api/tests/test_release_views.py | 14 +- .../plugins/api/tests/test_related_views.py | 40 ++-- .../plugins/api/tests/test_release_views.py | 14 +- .../api/tests/test_project_views.py | 40 ++-- .../api/tests/test_related_views.py | 220 ++++++++++-------- .../api/tests/test_release_views.py | 27 +-- 8 files changed, 207 insertions(+), 187 deletions(-) diff --git a/project_manager/common/api/views/mixins.py b/project_manager/common/api/views/mixins.py index 4942b195..ea16baac 100644 --- a/project_manager/common/api/views/mixins.py +++ b/project_manager/common/api/views/mixins.py @@ -6,9 +6,8 @@ # Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.authentication import SessionAuthentication -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import NotFound, PermissionDenied from rest_framework.filters import OrderingFilter -from rest_framework.parsers import ParseError from rest_framework.permissions import IsAuthenticatedOrReadOnly, SAFE_METHODS from rest_framework.viewsets import ModelViewSet @@ -63,8 +62,8 @@ def project(self): 'owner__user' ).get(**kwargs) except self.project_model.DoesNotExist as exception: - raise ParseError( - f"Invalid {self.project_type.replace('-', '_')}_slug." + raise NotFound( + detail=f"Invalid {self.project_type.replace('-', '_')}_slug.", ) from exception return self._project diff --git a/project_manager/packages/api/tests/test_related_views.py b/project_manager/packages/api/tests/test_related_views.py index 3c1d9b02..2935272b 100644 --- a/project_manager/packages/api/tests/test_related_views.py +++ b/project_manager/packages/api/tests/test_related_views.py @@ -69,8 +69,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/contributors/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.base_api_path = f'/api/packages/contributors' + cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' cls.contributor = ForumUserFactory() cls.package_contributor = PackageContributorFactory( package=cls.package, @@ -238,11 +238,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -398,8 +398,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/games/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.base_api_path = f'/api/packages/games' + cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -610,11 +610,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -757,8 +757,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/images/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.base_api_path = f'/api/packages/images' + cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -929,11 +929,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -1037,8 +1037,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/tags/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.base_api_path = f'/api/packages/tags' + cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -1197,11 +1197,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), diff --git a/project_manager/packages/api/tests/test_release_views.py b/project_manager/packages/api/tests/test_release_views.py index a077dfc2..e35e62cb 100644 --- a/project_manager/packages/api/tests/test_release_views.py +++ b/project_manager/packages/api/tests/test_release_views.py @@ -57,8 +57,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/releases/' - cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' + cls.base_api_path = f'/api/packages/releases' + cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -336,11 +336,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -361,7 +361,7 @@ def test_post(self): package=package, user=self.contributor, ) - api_path = f'{self.base_api_path}{package.slug}/' + api_path = f'{self.base_api_path}/{package.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' @@ -494,7 +494,7 @@ def test_post(self): package=package, version='1.0.0', ) - api_path = f'{self.base_api_path}{package.slug}/' + api_path = f'{self.base_api_path}/{package.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -559,7 +559,7 @@ def test_post_with_requirements(self): package=package, version='1.0.0', ) - api_path = f'{self.base_api_path}{package.slug}/' + api_path = f'{self.base_api_path}/{package.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( diff --git a/project_manager/plugins/api/tests/test_related_views.py b/project_manager/plugins/api/tests/test_related_views.py index 5bfb8359..08d7dc63 100644 --- a/project_manager/plugins/api/tests/test_related_views.py +++ b/project_manager/plugins/api/tests/test_related_views.py @@ -74,8 +74,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/contributors/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/contributors' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' cls.contributor = ForumUserFactory() cls.plugin_contributor = PluginContributorFactory( plugin=cls.plugin, @@ -243,11 +243,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -403,8 +403,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/games/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/games' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -615,11 +615,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -762,8 +762,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/images/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/images' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -935,11 +935,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -1043,8 +1043,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/tags/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/tags' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -1203,11 +1203,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -1351,8 +1351,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/paths/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/paths' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -1541,11 +1541,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), diff --git a/project_manager/plugins/api/tests/test_release_views.py b/project_manager/plugins/api/tests/test_release_views.py index a159dced..e2e2d866 100644 --- a/project_manager/plugins/api/tests/test_release_views.py +++ b/project_manager/plugins/api/tests/test_release_views.py @@ -58,8 +58,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/releases/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/releases' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -337,11 +337,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -362,7 +362,7 @@ def test_post(self): plugin=plugin, user=self.contributor, ) - api_path = f'{self.base_api_path}{plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' @@ -493,7 +493,7 @@ def test_post(self): plugin=plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}{plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -558,7 +558,7 @@ def test_post_with_requirements(self): plugin=plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}{plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( diff --git a/project_manager/sub_plugins/api/tests/test_project_views.py b/project_manager/sub_plugins/api/tests/test_project_views.py index b1c15cb7..2bc773c6 100644 --- a/project_manager/sub_plugins/api/tests/test_project_views.py +++ b/project_manager/sub_plugins/api/tests/test_project_views.py @@ -71,8 +71,8 @@ def setUpTestData(cls): sub_plugin=cls.sub_plugin, zip_file='/media/release_v1.0.0.zip', ) - cls.base_api_path = f'/api/sub-plugins/projects/' - cls.api_path = f'{cls.base_api_path}{plugin.slug}/' + cls.base_api_path = f'/api/sub-plugins/projects' + cls.api_path = f'{cls.base_api_path}/{plugin.slug}' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -144,7 +144,8 @@ def test_http_method_names(self): def test_get_list(self): # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) + api_path = f'{self.api_path}/' + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -193,7 +194,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -207,7 +208,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -221,7 +222,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -234,7 +235,8 @@ def test_get_list(self): ) def test_get_list_filters(self): - response = self.client.get(path=self.api_path) + api_path = f'{self.api_path}/' + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -245,7 +247,7 @@ def test_get_list_filters(self): ) # Validate tag filtering - response = self.client.get(path=f'{self.api_path}?tag=test_tag') + response = self.client.get(path=f'{api_path}?tag=test_tag') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -259,7 +261,7 @@ def test_get_list_filters(self): sub_plugin=self.sub_plugin, tag=tag, ) - response = self.client.get(path=f'{self.api_path}?tag=test_tag') + response = self.client.get(path=f'{api_path}?tag=test_tag') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -270,7 +272,7 @@ def test_get_list_filters(self): ) # Validate game filtering - response = self.client.get(path=f'{self.api_path}?game=game1') + response = self.client.get(path=f'{api_path}?game=game1') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -288,7 +290,7 @@ def test_get_list_filters(self): sub_plugin=self.sub_plugin, game=game, ) - response = self.client.get(path=f'{self.api_path}?game=game1') + response = self.client.get(path=f'{api_path}?game=game1') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -300,7 +302,7 @@ def test_get_list_filters(self): # Validate game filtering response = self.client.get( - path=f'{self.api_path}?user={self.regular_user.user.username}', + path=f'{api_path}?user={self.regular_user.user.username}', ) self.assertEqual( first=response.status_code, @@ -311,7 +313,7 @@ def test_get_list_filters(self): second=0, ) response = self.client.get( - path=f'{self.api_path}?user={self.contributor.user.username}', + path=f'{api_path}?user={self.contributor.user.username}', ) self.assertEqual( first=response.status_code, @@ -322,7 +324,7 @@ def test_get_list_filters(self): second=1, ) response = self.client.get( - path=f'{self.api_path}?user={self.owner.user.username}', + path=f'{api_path}?user={self.owner.user.username}', ) self.assertEqual( first=response.status_code, @@ -335,7 +337,7 @@ def test_get_list_filters(self): def test_get_details(self): # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.sub_plugin.slug}/' + api_path = f'{self.api_path}/{self.sub_plugin.slug}/' response = self.client.get(path=api_path) request = response.wsgi_request domain = f'{request.scheme}://{request.get_host()}' @@ -435,7 +437,7 @@ def test_post(self): base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' version = '1.0.0' - api_path = f'{self.base_api_path}{plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -530,7 +532,7 @@ def test_post_with_requirements(self): base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' version = '1.0.0' - api_path = f'{self.base_api_path}{plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/' custom_package_1 = PackageFactory( basename='custom_package_1', ) @@ -604,7 +606,7 @@ def test_post_with_requirements(self): def test_patch(self): # Verify that non logged in user cannot update a path - api_path = f'{self.api_path}{self.sub_plugin.slug}/' + api_path = f'{self.api_path}/{self.sub_plugin.slug}/' response = self.client.patch( path=api_path, data={ @@ -656,7 +658,7 @@ def test_patch(self): ) def test_options(self): - response = self.client.options(path=self.api_path) + response = self.client.options(path=f'{self.api_path}/') self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], diff --git a/project_manager/sub_plugins/api/tests/test_related_views.py b/project_manager/sub_plugins/api/tests/test_related_views.py index 41e08e70..a33ae484 100644 --- a/project_manager/sub_plugins/api/tests/test_related_views.py +++ b/project_manager/sub_plugins/api/tests/test_related_views.py @@ -73,8 +73,8 @@ def setUpTestData(cls): plugin=cls.plugin, owner=cls.owner, ) - cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}' + cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' cls.contributor = ForumUserFactory() cls.sub_plugin_contributor = SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -121,7 +121,8 @@ def test_http_method_names(self): def test_get_list(self): # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) + api_path = f'{self.api_path}/' + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -141,7 +142,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -161,7 +162,7 @@ def test_get_list(self): # Verify that contributors can see results but not 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -180,7 +181,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -200,7 +201,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_contributor.id}/' + api_path = f'{self.api_path}/{self.sub_plugin_contributor.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -242,11 +243,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -255,8 +256,9 @@ def test_get_details_failure(self): def test_post(self): # Verify that non logged in user cannot add a contributor + api_path = f'{self.api_path}/' response = self.client.post( - path=self.api_path, + path=api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -267,7 +269,7 @@ def test_post(self): # Verify that regular user cannot add a contributor self.client.force_login(self.regular_user.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -278,7 +280,7 @@ def test_post(self): # Verify that contributor cannot add a contributor self.client.force_login(self.contributor.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -289,7 +291,7 @@ def test_post(self): # Verify that owner can add a contributor self.client.force_login(self.owner.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -298,11 +300,12 @@ def test_post(self): ) def test_post_failure(self): + api_path = f'{self.api_path}/' self.client.force_login(self.owner.user) # Verify existing contributor cannot be added response = self.client.post( - path=self.api_path, + path=api_path, data={'username': self.contributor.user.username}, ) self.assertEqual( @@ -316,7 +319,7 @@ def test_post_failure(self): # Verify owner cannot be added response = self.client.post( - path=self.api_path, + path=api_path, data={'username': self.owner.user.username}, ) self.assertEqual( @@ -331,7 +334,7 @@ def test_post_failure(self): # Verify unknown username cannot be added invalid_username = 'invalid' response = self.client.post( - path=self.api_path, + path=api_path, data={'username': invalid_username}, ) self.assertEqual( @@ -345,9 +348,8 @@ def test_post_failure(self): def test_delete(self): # Verify that non logged in user cannot delete a contributor - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) + api_path = f'{self.api_path}/{self.sub_plugin_contributor.id}/' + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -355,9 +357,7 @@ def test_delete(self): # Verify that regular user cannot delete a contributor self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -365,9 +365,7 @@ def test_delete(self): # Verify that contributor cannot delete a contributor self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -375,16 +373,14 @@ def test_delete(self): # Verify that owner can delete a contributor self.client.force_login(self.owner.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_contributor.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_204_NO_CONTENT, ) def test_options(self): - response = self.client.options(path=self.api_path) + response = self.client.options(path=f'{self.api_path}/') self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], @@ -404,8 +400,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}' + cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' cls.contributor = ForumUserFactory() cls.package_contributor = SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -475,8 +471,10 @@ def test_http_method_names(self): ) def test_get_list(self): + api_path = f'{self.api_path}/' + # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -498,7 +496,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -518,7 +516,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -539,7 +537,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -560,7 +558,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_game_1.id}/' + api_path = f'{self.api_path}/{self.sub_plugin_game_1.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -616,11 +614,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -628,9 +626,11 @@ def test_get_details_failure(self): ) def test_post(self): + api_path = f'{self.api_path}/' + # Verify that non logged in user cannot add a game response = self.client.post( - path=self.api_path, + path=api_path, data={'game_slug': self.game_3.slug}, ) self.assertEqual( @@ -641,7 +641,7 @@ def test_post(self): # Verify that regular user cannot add a game self.client.force_login(self.regular_user.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'game_slug': self.game_3.slug}, ) self.assertEqual( @@ -652,7 +652,7 @@ def test_post(self): # Verify that contributor can add a game self.client.force_login(self.contributor.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'game_slug': self.game_3.slug}, ) self.assertEqual( @@ -663,7 +663,7 @@ def test_post(self): # Verify that owner can add a game self.client.force_login(self.owner.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'game_slug': self.game_4.slug}, ) self.assertEqual( @@ -672,11 +672,12 @@ def test_post(self): ) def test_post_failure(self): + api_path = f'{self.api_path}/' self.client.force_login(self.owner.user) # Verify existing affiliated game cannot be added response = self.client.post( - path=self.api_path, + path=api_path, data={'game_slug': self.game_1.slug}, ) self.assertEqual( @@ -691,7 +692,7 @@ def test_post_failure(self): # Verify non-existing game cannot be added invalid_slug = 'invalid' response = self.client.post( - path=self.api_path, + path=api_path, data={'game_slug': invalid_slug}, ) self.assertEqual( @@ -705,9 +706,8 @@ def test_post_failure(self): def test_delete(self): # Verify that non logged in user cannot delete a game - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_1.id}/', - ) + api_path = f'{self.api_path}/{self.sub_plugin_game_1.id}/' + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -715,9 +715,7 @@ def test_delete(self): # Verify that regular user cannot delete a game self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_1.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -725,9 +723,7 @@ def test_delete(self): # Verify that contributor can delete a game self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_1.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_204_NO_CONTENT, @@ -736,7 +732,7 @@ def test_delete(self): # Verify that owner can delete a game self.client.force_login(self.owner.user) response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_game_2.id}/', + path=f'{self.api_path}/{self.sub_plugin_game_2.id}/', ) self.assertEqual( first=response.status_code, @@ -744,7 +740,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=self.api_path) + response = self.client.options(path=f'{self.api_path}/') self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], @@ -765,8 +761,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}' + cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -855,8 +851,10 @@ def test_http_method_names(self): ) def test_get_list(self): + api_path = f'{self.api_path}/' + # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -874,7 +872,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -891,7 +889,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -908,7 +906,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -925,7 +923,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_image_1.id}/' + api_path = f'{self.api_path}/{self.sub_plugin_image_1.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -973,11 +971,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -986,6 +984,22 @@ def test_get_details_failure(self): @override_settings(MEDIA_ROOT=MEDIA_ROOT) def test_post(self): + api_path = f'{self.api_path}/' + + # Verify that non logged in user cannot add a game + image = Image.new('RGB', (100, 100)) + tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') + image.save(tmp_file) + tmp_file.seek(0) + response = self.client.post( + path=api_path, + data={'image': tmp_file}, + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + # Verify that regular user cannot add an image self.client.force_login(self.regular_user.user) image = Image.new('RGB', (100, 100)) @@ -993,7 +1007,7 @@ def test_post(self): image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=self.api_path, + path=api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1008,7 +1022,7 @@ def test_post(self): image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=self.api_path, + path=api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1023,7 +1037,7 @@ def test_post(self): image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=self.api_path, + path=api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1032,11 +1046,17 @@ def test_post(self): ) def test_delete(self): + # Verify that non logged in user cannot delete a game + api_path = f'{self.api_path}/{self.sub_plugin_image_1.id}/' + response = self.client.delete(path=api_path) + self.assertEqual( + first=response.status_code, + second=status.HTTP_403_FORBIDDEN, + ) + # Verify that regular user cannot delete an image self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_image_1.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -1044,9 +1064,7 @@ def test_delete(self): # Verify that contributor can delete an image self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_image_1.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_204_NO_CONTENT, @@ -1055,7 +1073,7 @@ def test_delete(self): # Verify that owner can delete an image self.client.force_login(self.owner.user) response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_image_2.id}/', + path=f'{self.api_path}/{self.sub_plugin_image_2.id}/', ) self.assertEqual( first=response.status_code, @@ -1063,7 +1081,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=self.api_path) + response = self.client.options(path=f'{self.api_path}/') self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], @@ -1083,8 +1101,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}/' - cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' + cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}' + cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -1130,8 +1148,10 @@ def test_http_method_names(self): ) def test_get_list(self): + api_path = f'{self.api_path}/' + # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1147,7 +1167,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1163,7 +1183,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1180,7 +1200,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1197,7 +1217,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}{self.sub_plugin_tag_1.id}/' + api_path = f'{self.api_path}/{self.sub_plugin_tag_1.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -1243,11 +1263,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}invalid/' + api_path = f'{self.base_api_path}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -1255,9 +1275,11 @@ def test_get_details_failure(self): ) def test_post(self): + api_path = f'{self.api_path}/' + # Verify that non logged in user cannot add a tag response = self.client.post( - path=self.api_path, + path=api_path, data={'tag': 'new-tag-1'}, ) self.assertEqual( @@ -1268,7 +1290,7 @@ def test_post(self): # Verify that regular user cannot add a tag self.client.force_login(self.regular_user.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'tag': 'new-tag-1'}, ) self.assertEqual( @@ -1279,7 +1301,7 @@ def test_post(self): # Verify that contributor can add a tag self.client.force_login(self.contributor.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'tag': 'new-tag-1'}, ) self.assertEqual( @@ -1290,7 +1312,7 @@ def test_post(self): # Verify that owner can add a tag self.client.force_login(self.owner.user) response = self.client.post( - path=self.api_path, + path=api_path, data={'tag': 'new-tag-2'}, ) self.assertEqual( @@ -1299,11 +1321,12 @@ def test_post(self): ) def test_post_failure(self): + api_path = f'{self.api_path}/' self.client.force_login(self.owner.user) # Verify existing affiliated tag cannot be added response = self.client.post( - path=self.api_path, + path=api_path, data={'tag': self.sub_plugin_tag_1.tag}, ) self.assertEqual( @@ -1320,7 +1343,7 @@ def test_post_failure(self): black_listed=True, ) response = self.client.post( - path=self.api_path, + path=api_path, data={'tag': tag.name}, ) self.assertEqual( @@ -1334,9 +1357,8 @@ def test_post_failure(self): def test_delete(self): # Verify that non logged in user cannot delete a tag - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_1.id}/', - ) + api_path = f'{self.api_path}/{self.sub_plugin_tag_1.id}/' + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -1344,9 +1366,7 @@ def test_delete(self): # Verify that regular user cannot delete a tag self.client.force_login(self.regular_user.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_1.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_403_FORBIDDEN, @@ -1354,9 +1374,7 @@ def test_delete(self): # Verify that contributor can delete a tag self.client.force_login(self.contributor.user) - response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_1.id}/', - ) + response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_204_NO_CONTENT, @@ -1365,7 +1383,7 @@ def test_delete(self): # Verify that owner can delete a tag self.client.force_login(self.owner.user) response = self.client.delete( - path=self.api_path + f'{self.sub_plugin_tag_2.id}/', + path=f'{self.api_path}/{self.sub_plugin_tag_2.id}/', ) self.assertEqual( first=response.status_code, @@ -1373,7 +1391,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=self.api_path) + response = self.client.options(path=f'{self.api_path}/') self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], diff --git a/project_manager/sub_plugins/api/tests/test_release_views.py b/project_manager/sub_plugins/api/tests/test_release_views.py index 9056f091..bfc34757 100644 --- a/project_manager/sub_plugins/api/tests/test_release_views.py +++ b/project_manager/sub_plugins/api/tests/test_release_views.py @@ -62,8 +62,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/releases/' - cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/{cls.sub_plugin.slug}/' + cls.base_api_path = f'/api/sub-plugins/releases' + cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/{cls.sub_plugin.slug}' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -224,7 +224,8 @@ def test_http_method_names(self): def test_get_list(self): # Verify that a non logged in user can see results - response = self.client.get(path=self.api_path) + api_path = f'{self.api_path}/' + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -262,7 +263,7 @@ def test_get_list(self): # Verify that regular user can see results self.client.force_login(self.regular_user.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -276,7 +277,7 @@ def test_get_list(self): # Verify that contributors can see results self.client.force_login(self.contributor.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -290,7 +291,7 @@ def test_get_list(self): # Verify that the owner can see results self.client.force_login(self.owner.user) - response = self.client.get(path=self.api_path) + response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -304,7 +305,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user can see details - api_path = f'{self.api_path}{self.sub_plugin_release.version}/' + api_path = f'{self.api_path}/{self.sub_plugin_release.version}/' response = self.client.get(path=api_path) timestamp = self.sub_plugin_release.created request = response.wsgi_request @@ -376,11 +377,11 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}{self.plugin.slug}/invalid/' + api_path = f'{self.base_api_path}/{self.plugin.slug}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, - second=status.HTTP_400_BAD_REQUEST, + second=status.HTTP_404_NOT_FOUND, ) self.assertDictEqual( d1=response.json(), @@ -410,7 +411,7 @@ def test_post(self): sub_plugin=sub_plugin, user=self.contributor, ) - api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/{sub_plugin.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' @@ -544,7 +545,7 @@ def test_post(self): sub_plugin=sub_plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/{sub_plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -588,7 +589,7 @@ def test_post_with_requirements(self): sub_plugin=sub_plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' + api_path = f'{self.base_api_path}/{plugin.slug}/{sub_plugin.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' version = '1.0.1' @@ -668,7 +669,7 @@ def test_post_with_requirements(self): ) def test_options(self): - response = self.client.options(path=self.api_path) + response = self.client.options(path=f'{self.api_path}/') self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], From da5352feb483042257bf06637073dfd5cecd22ef Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 13:06:07 -0400 Subject: [PATCH 122/211] Reverted some test changes. --- .../packages/api/tests/test_related_views.py | 24 +-- .../packages/api/tests/test_release_views.py | 12 +- .../plugins/api/tests/test_related_views.py | 30 ++-- .../plugins/api/tests/test_release_views.py | 12 +- .../api/tests/test_project_views.py | 40 +++-- .../api/tests/test_related_views.py | 149 ++++++++---------- .../api/tests/test_release_views.py | 25 ++- 7 files changed, 136 insertions(+), 156 deletions(-) diff --git a/project_manager/packages/api/tests/test_related_views.py b/project_manager/packages/api/tests/test_related_views.py index 2935272b..d40b7f53 100644 --- a/project_manager/packages/api/tests/test_related_views.py +++ b/project_manager/packages/api/tests/test_related_views.py @@ -69,8 +69,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/contributors' - cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' + cls.base_api_path = f'/api/packages/contributors/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' cls.contributor = ForumUserFactory() cls.package_contributor = PackageContributorFactory( package=cls.package, @@ -238,7 +238,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -398,8 +398,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/games' - cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' + cls.base_api_path = f'/api/packages/games/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -610,7 +610,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -757,8 +757,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/images' - cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' + cls.base_api_path = f'/api/packages/images/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -929,7 +929,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -1037,8 +1037,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/tags' - cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' + cls.base_api_path = f'/api/packages/tags/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -1197,7 +1197,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, diff --git a/project_manager/packages/api/tests/test_release_views.py b/project_manager/packages/api/tests/test_release_views.py index e35e62cb..59e8a246 100644 --- a/project_manager/packages/api/tests/test_release_views.py +++ b/project_manager/packages/api/tests/test_release_views.py @@ -57,8 +57,8 @@ def setUpTestData(cls): cls.package = PackageFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/packages/releases' - cls.api_path = f'{cls.base_api_path}/{cls.package.slug}/' + cls.base_api_path = f'/api/packages/releases/' + cls.api_path = f'{cls.base_api_path}{cls.package.slug}/' cls.contributor = ForumUserFactory() PackageContributorFactory( package=cls.package, @@ -336,7 +336,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -361,7 +361,7 @@ def test_post(self): package=package, user=self.contributor, ) - api_path = f'{self.base_api_path}/{package.slug}/' + api_path = f'{self.base_api_path}{package.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' file_path = base_path / 'test-package' / 'test-package-v1.0.0.zip' @@ -494,7 +494,7 @@ def test_post(self): package=package, version='1.0.0', ) - api_path = f'{self.base_api_path}/{package.slug}/' + api_path = f'{self.base_api_path}{package.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -559,7 +559,7 @@ def test_post_with_requirements(self): package=package, version='1.0.0', ) - api_path = f'{self.base_api_path}/{package.slug}/' + api_path = f'{self.base_api_path}{package.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( diff --git a/project_manager/plugins/api/tests/test_related_views.py b/project_manager/plugins/api/tests/test_related_views.py index 08d7dc63..608b7a58 100644 --- a/project_manager/plugins/api/tests/test_related_views.py +++ b/project_manager/plugins/api/tests/test_related_views.py @@ -74,8 +74,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/contributors' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/contributors/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' cls.contributor = ForumUserFactory() cls.plugin_contributor = PluginContributorFactory( plugin=cls.plugin, @@ -243,7 +243,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -403,8 +403,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/games' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/games/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -615,7 +615,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -762,8 +762,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/images' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/images/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -935,7 +935,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -1043,8 +1043,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/tags' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/tags/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -1203,7 +1203,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -1351,8 +1351,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/paths' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/paths/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -1541,7 +1541,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, diff --git a/project_manager/plugins/api/tests/test_release_views.py b/project_manager/plugins/api/tests/test_release_views.py index e2e2d866..00254c7e 100644 --- a/project_manager/plugins/api/tests/test_release_views.py +++ b/project_manager/plugins/api/tests/test_release_views.py @@ -58,8 +58,8 @@ def setUpTestData(cls): cls.plugin = PluginFactory( owner=cls.owner, ) - cls.base_api_path = f'/api/plugins/releases' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/' + cls.base_api_path = f'/api/plugins/releases/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/' cls.contributor = ForumUserFactory() PluginContributorFactory( plugin=cls.plugin, @@ -337,7 +337,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -362,7 +362,7 @@ def test_post(self): plugin=plugin, user=self.contributor, ) - api_path = f'{self.base_api_path}/{plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' file_path = base_path / 'test-plugin' / 'test-plugin-v1.0.0.zip' @@ -493,7 +493,7 @@ def test_post(self): plugin=plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}/{plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -558,7 +558,7 @@ def test_post_with_requirements(self): plugin=plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}/{plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( diff --git a/project_manager/sub_plugins/api/tests/test_project_views.py b/project_manager/sub_plugins/api/tests/test_project_views.py index 2bc773c6..b1c15cb7 100644 --- a/project_manager/sub_plugins/api/tests/test_project_views.py +++ b/project_manager/sub_plugins/api/tests/test_project_views.py @@ -71,8 +71,8 @@ def setUpTestData(cls): sub_plugin=cls.sub_plugin, zip_file='/media/release_v1.0.0.zip', ) - cls.base_api_path = f'/api/sub-plugins/projects' - cls.api_path = f'{cls.base_api_path}/{plugin.slug}' + cls.base_api_path = f'/api/sub-plugins/projects/' + cls.api_path = f'{cls.base_api_path}{plugin.slug}/' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -144,8 +144,7 @@ def test_http_method_names(self): def test_get_list(self): # Verify that non logged in user can see results but not 'id' - api_path = f'{self.api_path}/' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -194,7 +193,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -208,7 +207,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -222,7 +221,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -235,8 +234,7 @@ def test_get_list(self): ) def test_get_list_filters(self): - api_path = f'{self.api_path}/' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -247,7 +245,7 @@ def test_get_list_filters(self): ) # Validate tag filtering - response = self.client.get(path=f'{api_path}?tag=test_tag') + response = self.client.get(path=f'{self.api_path}?tag=test_tag') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -261,7 +259,7 @@ def test_get_list_filters(self): sub_plugin=self.sub_plugin, tag=tag, ) - response = self.client.get(path=f'{api_path}?tag=test_tag') + response = self.client.get(path=f'{self.api_path}?tag=test_tag') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -272,7 +270,7 @@ def test_get_list_filters(self): ) # Validate game filtering - response = self.client.get(path=f'{api_path}?game=game1') + response = self.client.get(path=f'{self.api_path}?game=game1') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -290,7 +288,7 @@ def test_get_list_filters(self): sub_plugin=self.sub_plugin, game=game, ) - response = self.client.get(path=f'{api_path}?game=game1') + response = self.client.get(path=f'{self.api_path}?game=game1') self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -302,7 +300,7 @@ def test_get_list_filters(self): # Validate game filtering response = self.client.get( - path=f'{api_path}?user={self.regular_user.user.username}', + path=f'{self.api_path}?user={self.regular_user.user.username}', ) self.assertEqual( first=response.status_code, @@ -313,7 +311,7 @@ def test_get_list_filters(self): second=0, ) response = self.client.get( - path=f'{api_path}?user={self.contributor.user.username}', + path=f'{self.api_path}?user={self.contributor.user.username}', ) self.assertEqual( first=response.status_code, @@ -324,7 +322,7 @@ def test_get_list_filters(self): second=1, ) response = self.client.get( - path=f'{api_path}?user={self.owner.user.username}', + path=f'{self.api_path}?user={self.owner.user.username}', ) self.assertEqual( first=response.status_code, @@ -337,7 +335,7 @@ def test_get_list_filters(self): def test_get_details(self): # Verify that non logged in user can see details - api_path = f'{self.api_path}/{self.sub_plugin.slug}/' + api_path = f'{self.api_path}{self.sub_plugin.slug}/' response = self.client.get(path=api_path) request = response.wsgi_request domain = f'{request.scheme}://{request.get_host()}' @@ -437,7 +435,7 @@ def test_post(self): base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' version = '1.0.0' - api_path = f'{self.base_api_path}/{plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -532,7 +530,7 @@ def test_post_with_requirements(self): base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' version = '1.0.0' - api_path = f'{self.base_api_path}/{plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/' custom_package_1 = PackageFactory( basename='custom_package_1', ) @@ -606,7 +604,7 @@ def test_post_with_requirements(self): def test_patch(self): # Verify that non logged in user cannot update a path - api_path = f'{self.api_path}/{self.sub_plugin.slug}/' + api_path = f'{self.api_path}{self.sub_plugin.slug}/' response = self.client.patch( path=api_path, data={ @@ -658,7 +656,7 @@ def test_patch(self): ) def test_options(self): - response = self.client.options(path=f'{self.api_path}/') + response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], diff --git a/project_manager/sub_plugins/api/tests/test_related_views.py b/project_manager/sub_plugins/api/tests/test_related_views.py index a33ae484..be6d9ac6 100644 --- a/project_manager/sub_plugins/api/tests/test_related_views.py +++ b/project_manager/sub_plugins/api/tests/test_related_views.py @@ -73,8 +73,8 @@ def setUpTestData(cls): plugin=cls.plugin, owner=cls.owner, ) - cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}' - cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' + cls.base_api_path = f'/api/sub-plugins/contributors/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' cls.contributor = ForumUserFactory() cls.sub_plugin_contributor = SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -121,8 +121,7 @@ def test_http_method_names(self): def test_get_list(self): # Verify that non logged in user can see results but not 'id' - api_path = f'{self.api_path}/' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -142,7 +141,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -162,7 +161,7 @@ def test_get_list(self): # Verify that contributors can see results but not 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -181,7 +180,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -201,7 +200,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}/{self.sub_plugin_contributor.id}/' + api_path = f'{self.api_path}{self.sub_plugin_contributor.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -243,7 +242,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -256,9 +255,8 @@ def test_get_details_failure(self): def test_post(self): # Verify that non logged in user cannot add a contributor - api_path = f'{self.api_path}/' response = self.client.post( - path=api_path, + path=self.api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -269,7 +267,7 @@ def test_post(self): # Verify that regular user cannot add a contributor self.client.force_login(self.regular_user.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -280,7 +278,7 @@ def test_post(self): # Verify that contributor cannot add a contributor self.client.force_login(self.contributor.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -291,7 +289,7 @@ def test_post(self): # Verify that owner can add a contributor self.client.force_login(self.owner.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'username': self.new_contributor.user.username}, ) self.assertEqual( @@ -300,12 +298,11 @@ def test_post(self): ) def test_post_failure(self): - api_path = f'{self.api_path}/' self.client.force_login(self.owner.user) # Verify existing contributor cannot be added response = self.client.post( - path=api_path, + path=self.api_path, data={'username': self.contributor.user.username}, ) self.assertEqual( @@ -319,7 +316,7 @@ def test_post_failure(self): # Verify owner cannot be added response = self.client.post( - path=api_path, + path=self.api_path, data={'username': self.owner.user.username}, ) self.assertEqual( @@ -334,7 +331,7 @@ def test_post_failure(self): # Verify unknown username cannot be added invalid_username = 'invalid' response = self.client.post( - path=api_path, + path=self.api_path, data={'username': invalid_username}, ) self.assertEqual( @@ -348,7 +345,7 @@ def test_post_failure(self): def test_delete(self): # Verify that non logged in user cannot delete a contributor - api_path = f'{self.api_path}/{self.sub_plugin_contributor.id}/' + api_path = f'{self.api_path}{self.sub_plugin_contributor.id}/' response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, @@ -380,7 +377,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=f'{self.api_path}/') + response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], @@ -400,8 +397,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}' - cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' + cls.base_api_path = f'/api/sub-plugins/games/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' cls.contributor = ForumUserFactory() cls.package_contributor = SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -471,10 +468,8 @@ def test_http_method_names(self): ) def test_get_list(self): - api_path = f'{self.api_path}/' - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -496,7 +491,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -516,7 +511,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -537,7 +532,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -558,7 +553,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}/{self.sub_plugin_game_1.id}/' + api_path = f'{self.api_path}{self.sub_plugin_game_1.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -614,7 +609,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -626,11 +621,9 @@ def test_get_details_failure(self): ) def test_post(self): - api_path = f'{self.api_path}/' - # Verify that non logged in user cannot add a game response = self.client.post( - path=api_path, + path=self.api_path, data={'game_slug': self.game_3.slug}, ) self.assertEqual( @@ -641,7 +634,7 @@ def test_post(self): # Verify that regular user cannot add a game self.client.force_login(self.regular_user.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'game_slug': self.game_3.slug}, ) self.assertEqual( @@ -652,7 +645,7 @@ def test_post(self): # Verify that contributor can add a game self.client.force_login(self.contributor.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'game_slug': self.game_3.slug}, ) self.assertEqual( @@ -663,7 +656,7 @@ def test_post(self): # Verify that owner can add a game self.client.force_login(self.owner.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'game_slug': self.game_4.slug}, ) self.assertEqual( @@ -672,12 +665,11 @@ def test_post(self): ) def test_post_failure(self): - api_path = f'{self.api_path}/' self.client.force_login(self.owner.user) # Verify existing affiliated game cannot be added response = self.client.post( - path=api_path, + path=self.api_path, data={'game_slug': self.game_1.slug}, ) self.assertEqual( @@ -692,7 +684,7 @@ def test_post_failure(self): # Verify non-existing game cannot be added invalid_slug = 'invalid' response = self.client.post( - path=api_path, + path=self.api_path, data={'game_slug': invalid_slug}, ) self.assertEqual( @@ -706,7 +698,7 @@ def test_post_failure(self): def test_delete(self): # Verify that non logged in user cannot delete a game - api_path = f'{self.api_path}/{self.sub_plugin_game_1.id}/' + api_path = f'{self.api_path}{self.sub_plugin_game_1.id}/' response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, @@ -732,7 +724,7 @@ def test_delete(self): # Verify that owner can delete a game self.client.force_login(self.owner.user) response = self.client.delete( - path=f'{self.api_path}/{self.sub_plugin_game_2.id}/', + path=f'{self.api_path}{self.sub_plugin_game_2.id}/', ) self.assertEqual( first=response.status_code, @@ -740,7 +732,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=f'{self.api_path}/') + response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], @@ -761,8 +753,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}' - cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' + cls.base_api_path = f'/api/sub-plugins/images/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -851,10 +843,8 @@ def test_http_method_names(self): ) def test_get_list(self): - api_path = f'{self.api_path}/' - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -872,7 +862,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -889,7 +879,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -906,7 +896,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -923,7 +913,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}/{self.sub_plugin_image_1.id}/' + api_path = f'{self.api_path}{self.sub_plugin_image_1.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -971,7 +961,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -984,15 +974,13 @@ def test_get_details_failure(self): @override_settings(MEDIA_ROOT=MEDIA_ROOT) def test_post(self): - api_path = f'{self.api_path}/' - # Verify that non logged in user cannot add a game image = Image.new('RGB', (100, 100)) tmp_file = tempfile.NamedTemporaryFile(suffix='.jpg') image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=api_path, + path=self.api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1007,7 +995,7 @@ def test_post(self): image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=api_path, + path=self.api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1022,7 +1010,7 @@ def test_post(self): image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=api_path, + path=self.api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1037,7 +1025,7 @@ def test_post(self): image.save(tmp_file) tmp_file.seek(0) response = self.client.post( - path=api_path, + path=self.api_path, data={'image': tmp_file}, ) self.assertEqual( @@ -1047,7 +1035,7 @@ def test_post(self): def test_delete(self): # Verify that non logged in user cannot delete a game - api_path = f'{self.api_path}/{self.sub_plugin_image_1.id}/' + api_path = f'{self.api_path}{self.sub_plugin_image_1.id}/' response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, @@ -1073,7 +1061,7 @@ def test_delete(self): # Verify that owner can delete an image self.client.force_login(self.owner.user) response = self.client.delete( - path=f'{self.api_path}/{self.sub_plugin_image_2.id}/', + path=f'{self.api_path}{self.sub_plugin_image_2.id}/', ) self.assertEqual( first=response.status_code, @@ -1081,7 +1069,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=f'{self.api_path}/') + response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], @@ -1101,8 +1089,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}' - cls.api_path = f'{cls.base_api_path}/{cls.sub_plugin.slug}' + cls.base_api_path = f'/api/sub-plugins/tags/{cls.plugin.slug}/' + cls.api_path = f'{cls.base_api_path}{cls.sub_plugin.slug}/' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -1148,10 +1136,8 @@ def test_http_method_names(self): ) def test_get_list(self): - api_path = f'{self.api_path}/' - # Verify that non logged in user can see results but not 'id' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1167,7 +1153,7 @@ def test_get_list(self): # Verify that regular user can see results but not 'id' self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1183,7 +1169,7 @@ def test_get_list(self): # Verify that contributors can see results AND 'id' self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1200,7 +1186,7 @@ def test_get_list(self): # Verify that the owner can see results AND 'id' self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -1217,7 +1203,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user cannot see details - api_path = f'{self.api_path}/{self.sub_plugin_tag_1.id}/' + api_path = f'{self.api_path}{self.sub_plugin_tag_1.id}/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -1263,7 +1249,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/invalid/' + api_path = f'{self.base_api_path}invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -1275,11 +1261,9 @@ def test_get_details_failure(self): ) def test_post(self): - api_path = f'{self.api_path}/' - # Verify that non logged in user cannot add a tag response = self.client.post( - path=api_path, + path=self.api_path, data={'tag': 'new-tag-1'}, ) self.assertEqual( @@ -1290,7 +1274,7 @@ def test_post(self): # Verify that regular user cannot add a tag self.client.force_login(self.regular_user.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'tag': 'new-tag-1'}, ) self.assertEqual( @@ -1301,7 +1285,7 @@ def test_post(self): # Verify that contributor can add a tag self.client.force_login(self.contributor.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'tag': 'new-tag-1'}, ) self.assertEqual( @@ -1312,7 +1296,7 @@ def test_post(self): # Verify that owner can add a tag self.client.force_login(self.owner.user) response = self.client.post( - path=api_path, + path=self.api_path, data={'tag': 'new-tag-2'}, ) self.assertEqual( @@ -1321,12 +1305,11 @@ def test_post(self): ) def test_post_failure(self): - api_path = f'{self.api_path}/' self.client.force_login(self.owner.user) # Verify existing affiliated tag cannot be added response = self.client.post( - path=api_path, + path=self.api_path, data={'tag': self.sub_plugin_tag_1.tag}, ) self.assertEqual( @@ -1343,7 +1326,7 @@ def test_post_failure(self): black_listed=True, ) response = self.client.post( - path=api_path, + path=self.api_path, data={'tag': tag.name}, ) self.assertEqual( @@ -1357,7 +1340,7 @@ def test_post_failure(self): def test_delete(self): # Verify that non logged in user cannot delete a tag - api_path = f'{self.api_path}/{self.sub_plugin_tag_1.id}/' + api_path = f'{self.api_path}{self.sub_plugin_tag_1.id}/' response = self.client.delete(path=api_path) self.assertEqual( first=response.status_code, @@ -1383,7 +1366,7 @@ def test_delete(self): # Verify that owner can delete a tag self.client.force_login(self.owner.user) response = self.client.delete( - path=f'{self.api_path}/{self.sub_plugin_tag_2.id}/', + path=f'{self.api_path}{self.sub_plugin_tag_2.id}/', ) self.assertEqual( first=response.status_code, @@ -1391,7 +1374,7 @@ def test_delete(self): ) def test_options(self): - response = self.client.options(path=f'{self.api_path}/') + response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], diff --git a/project_manager/sub_plugins/api/tests/test_release_views.py b/project_manager/sub_plugins/api/tests/test_release_views.py index bfc34757..e2e921d1 100644 --- a/project_manager/sub_plugins/api/tests/test_release_views.py +++ b/project_manager/sub_plugins/api/tests/test_release_views.py @@ -62,8 +62,8 @@ def setUpTestData(cls): owner=cls.owner, plugin=cls.plugin, ) - cls.base_api_path = f'/api/sub-plugins/releases' - cls.api_path = f'{cls.base_api_path}/{cls.plugin.slug}/{cls.sub_plugin.slug}' + cls.base_api_path = f'/api/sub-plugins/releases/' + cls.api_path = f'{cls.base_api_path}{cls.plugin.slug}/{cls.sub_plugin.slug}/' cls.contributor = ForumUserFactory() SubPluginContributorFactory( sub_plugin=cls.sub_plugin, @@ -224,8 +224,7 @@ def test_http_method_names(self): def test_get_list(self): # Verify that a non logged in user can see results - api_path = f'{self.api_path}/' - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -263,7 +262,7 @@ def test_get_list(self): # Verify that regular user can see results self.client.force_login(self.regular_user.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -277,7 +276,7 @@ def test_get_list(self): # Verify that contributors can see results self.client.force_login(self.contributor.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -291,7 +290,7 @@ def test_get_list(self): # Verify that the owner can see results self.client.force_login(self.owner.user) - response = self.client.get(path=api_path) + response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -305,7 +304,7 @@ def test_get_list(self): def test_get_details(self): # Verify that non logged in user can see details - api_path = f'{self.api_path}/{self.sub_plugin_release.version}/' + api_path = f'{self.api_path}{self.sub_plugin_release.version}/' response = self.client.get(path=api_path) timestamp = self.sub_plugin_release.created request = response.wsgi_request @@ -377,7 +376,7 @@ def test_get_details(self): ) def test_get_details_failure(self): - api_path = f'{self.base_api_path}/{self.plugin.slug}/invalid/' + api_path = f'{self.base_api_path}{self.plugin.slug}/invalid/' response = self.client.get(path=api_path) self.assertEqual( first=response.status_code, @@ -411,7 +410,7 @@ def test_post(self): sub_plugin=sub_plugin, user=self.contributor, ) - api_path = f'{self.base_api_path}/{plugin.slug}/{sub_plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-v1.0.0.zip' @@ -545,7 +544,7 @@ def test_post(self): sub_plugin=sub_plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}/{plugin.slug}/{sub_plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' with file_path.open('rb') as open_file: zip_file = UploadedFile(open_file, content_type='application/zip') response = self.client.post( @@ -589,7 +588,7 @@ def test_post_with_requirements(self): sub_plugin=sub_plugin, version='1.0.0', ) - api_path = f'{self.base_api_path}/{plugin.slug}/{sub_plugin.slug}/' + api_path = f'{self.base_api_path}{plugin.slug}/{sub_plugin.slug}/' base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'sub-plugins' file_path = base_path / 'test-plugin' / 'test-sub-plugin' / 'test-sub-plugin-requirements-v1.0.0.zip' version = '1.0.1' @@ -669,7 +668,7 @@ def test_post_with_requirements(self): ) def test_options(self): - response = self.client.options(path=f'{self.api_path}/') + response = self.client.options(path=self.api_path) self.assertEqual(first=response.status_code, second=status.HTTP_200_OK) self.assertEqual( first=response.json()['name'], From d1f85ff4520878a8a65b5919e16a8478483caa8b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 14:02:49 -0400 Subject: [PATCH 123/211] Updated Tags API. --- tags/api/serializers.py | 38 ++- tags/api/tests/test_serializers.py | 77 ++++-- tags/api/tests/test_views.py | 404 ++++++++++++++++++++++++----- tags/api/views.py | 74 ++++-- 4 files changed, 476 insertions(+), 117 deletions(-) diff --git a/tags/api/serializers.py b/tags/api/serializers.py index 4ddc92c3..f0ca0aa6 100644 --- a/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -4,12 +4,12 @@ # IMPORTS # ============================================================================= # Third Party Django +from rest_framework.fields import IntegerField from rest_framework.relations import RelatedField from rest_framework.serializers import ModelSerializer # App from tags.models import Tag -from users.api.serializers.common import ForumUserContributorSerializer # ============================================================================= @@ -17,7 +17,8 @@ # ============================================================================= __all__ = ( 'RelatedTagSerializer', - 'TagSerializer', + 'TagListSerializer', + 'TagRetrieveSerializer', ) @@ -29,15 +30,14 @@ class RelatedTagSerializer(RelatedField): def to_representation(self, value): """Return the name of the project.""" - return value.name + # TODO: return the url once the paths exist + # return {'name': value.name, 'id': value.pk, 'url': value.get_absolute_url()} + return {'name': value.name, 'id': value.pk} -class TagSerializer(ModelSerializer): - """Serializer for project Tags.""" +class TagRetrieveSerializer(ModelSerializer): + """Serializer for project Tags on retrieve.""" - creator = ForumUserContributorSerializer( - read_only=True, - ) packages = RelatedTagSerializer(many=True, read_only=True) plugins = RelatedTagSerializer(many=True, read_only=True) subplugins = RelatedTagSerializer(many=True, read_only=True) @@ -51,5 +51,25 @@ class Meta: 'packages', 'plugins', 'subplugins', - 'creator', + ) + + +class TagListSerializer(ModelSerializer): + """Serializer for project Tags on list.""" + + package_count = IntegerField() + plugin_count = IntegerField() + subplugin_count = IntegerField() + project_count = IntegerField() + + class Meta: + """Define metaclass attributes.""" + + model = Tag + fields = ( + 'name', + 'package_count', + 'plugin_count', + 'subplugin_count', + 'project_count', ) diff --git a/tags/api/tests/test_serializers.py b/tags/api/tests/test_serializers.py index 6870e421..c2d6a959 100644 --- a/tags/api/tests/test_serializers.py +++ b/tags/api/tests/test_serializers.py @@ -5,13 +5,17 @@ from django.test import TestCase # Third Party Django +from rest_framework.fields import IntegerField from rest_framework.relations import ManyRelatedField, RelatedField from rest_framework.serializers import ModelSerializer # App -from tags.api.serializers import TagSerializer, RelatedTagSerializer +from tags.api.serializers import ( + RelatedTagSerializer, + TagListSerializer, + TagRetrieveSerializer, +) from tags.models import Tag -from users.api.serializers.common import ForumUserContributorSerializer # ============================================================================= @@ -22,29 +26,19 @@ def test_class_inheritance(self): self.assertTrue(expr=issubclass(RelatedTagSerializer, RelatedField)) -class TagSerializerTestCase(TestCase): +class TagRetrieveSerializerTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( - expr=issubclass(TagSerializer, ModelSerializer), + expr=issubclass(TagRetrieveSerializer, ModelSerializer), ) def test_declared_fields(self): - declared_fields = getattr(TagSerializer, '_declared_fields') + declared_fields = getattr(TagRetrieveSerializer, '_declared_fields') self.assertEqual( first=len(declared_fields), - second=4, + second=3, ) - self.assertIn( - member='creator', - container=declared_fields, - ) - self.assertIsInstance( - obj=declared_fields['creator'], - cls=ForumUserContributorSerializer, - ) - self.assertTrue(expr=declared_fields['creator'].read_only) - for field in ( 'packages', 'plugins', @@ -67,16 +61,61 @@ def test_declared_fields(self): def test_meta_class(self): self.assertEqual( - first=TagSerializer.Meta.model, + first=TagRetrieveSerializer.Meta.model, second=Tag, ) self.assertTupleEqual( - tuple1=TagSerializer.Meta.fields, + tuple1=TagRetrieveSerializer.Meta.fields, tuple2=( 'name', 'packages', 'plugins', 'subplugins', - 'creator', + ), + ) + + +class TagListSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(TagListSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(TagListSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=4, + ) + + for field in ( + 'package_count', + 'plugin_count', + 'subplugin_count', + 'project_count', + ): + self.assertIn( + member=field, + container=declared_fields, + ) + obj = declared_fields[field] + self.assertIsInstance( + obj=obj, + cls=IntegerField, + ) + + def test_meta_class(self): + self.assertEqual( + first=TagListSerializer.Meta.model, + second=Tag, + ) + self.assertTupleEqual( + tuple1=TagListSerializer.Meta.fields, + tuple2=( + 'name', + 'package_count', + 'plugin_count', + 'subplugin_count', + 'project_count', ), ) diff --git a/tags/api/tests/test_views.py b/tags/api/tests/test_views.py index 0c390976..1ec851a0 100644 --- a/tags/api/tests/test_views.py +++ b/tags/api/tests/test_views.py @@ -1,6 +1,13 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Python +from collections import defaultdict +from random import randint + +# Django +from django.db.models.expressions import CombinedExpression + # Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status @@ -8,7 +15,9 @@ from rest_framework.test import APITestCase # App -from tags.api.serializers import TagSerializer +from project_manager.packages.models import PackageTag +from project_manager.plugins.models import PluginTag +from project_manager.sub_plugins.models import SubPluginTag from tags.api.views import TagViewSet from test_utils.factories.packages import PackageFactory, PackageTagFactory from test_utils.factories.plugins import PluginFactory, PluginTagFactory @@ -24,20 +33,81 @@ # ============================================================================= class TagViewSetTestCase(APITestCase): + tag_1 = tag_2 = tag_3 = None + package_1 = package_2 = None + plugin_1 = plugin_2 = None + sub_plugin_1 = sub_plugin_2 = None api_path = '/api/tags/' + @classmethod + def setUpTestData(cls): + cls.tag_1 = TagFactory() + cls.tag_2 = TagFactory() + cls.tag_3 = TagFactory() + cls.tag_4 = TagFactory() + cls.black_listed_tag = TagFactory( + black_listed=True, + ) + + cls.package_1 = PackageFactory() + cls.package_2 = PackageFactory() + cls.plugin_1 = PluginFactory() + cls.plugin_2 = PluginFactory() + cls.sub_plugin_1 = SubPluginFactory( + plugin=cls.plugin_1, + ) + cls.sub_plugin_2 = SubPluginFactory( + plugin=cls.plugin_1, + ) + + # tag_1 associations + PackageTagFactory( + package=cls.package_1, + tag=cls.tag_1, + ) + PluginTagFactory( + plugin=cls.plugin_1, + tag=cls.tag_1, + ) + PluginTagFactory( + plugin=cls.plugin_2, + tag=cls.tag_1, + ) + + # tag_2 associations + PackageTagFactory( + package=cls.package_1, + tag=cls.tag_2, + ) + PackageTagFactory( + package=cls.package_2, + tag=cls.tag_2, + ) + PluginTagFactory( + plugin=cls.plugin_1, + tag=cls.tag_2, + ) + SubPluginTagFactory( + sub_plugin=cls.sub_plugin_1, + tag=cls.tag_2, + ) + SubPluginTagFactory( + sub_plugin=cls.sub_plugin_2, + tag=cls.tag_2, + ) + + # tag_3 associations + PluginTagFactory( + plugin=cls.plugin_2, + tag=cls.tag_3, + ) + def test_filter_backends(self): self.assertTupleEqual( tuple1=TagViewSet.filter_backends, tuple2=(OrderingFilter, DjangoFilterBackend) ) - def test_serializer_class(self): - self.assertEqual( - first=TagViewSet.serializer_class, - second=TagSerializer, - ) - def test_ordering(self): self.assertTupleEqual( tuple1=TagViewSet.ordering, @@ -47,7 +117,7 @@ def test_ordering(self): def test_ordering_fields(self): self.assertTupleEqual( tuple1=TagViewSet.ordering_fields, - tuple2=('name',), + tuple2=('name', 'project_count'), ) def test_http_method_names(self): @@ -57,7 +127,8 @@ def test_http_method_names(self): ) def test_get_queryset(self): - queryset = TagViewSet().get_queryset().filter() + queryset = TagViewSet(action='https://github.com/retrieve').get_queryset().filter() + self.assertFalse(expr=queryset.query.select_related) prefetch_lookups = queryset._prefetch_related_lookups self.assertEqual( first=len(prefetch_lookups), @@ -78,47 +149,149 @@ def test_get_queryset(self): second=('name',), ) - self.assertDictEqual( - d1=queryset.query.select_related, - d2={'creator': {'user': {}}}, + queryset = TagViewSet(action='https://github.com/list').get_queryset().filter() + self.assertFalse(expr=queryset.query.select_related) + self.assertTupleEqual( + tuple1=queryset._prefetch_related_lookups, + tuple2=(), + ) + annotations = queryset.query.annotations + self.assertIn( + member='package_count', + container=annotations, + ) + package_count = annotations['package_count'] + self.assertTrue(expr=package_count.distinct) + self.assertEqual( + first=len(package_count.source_expressions), + second=1, + ) + self.assertIs( + expr1=package_count.source_expressions[0].target, + expr2=PackageTag.package.field, ) - def test_get(self): + self.assertIn( + member='plugin_count', + container=annotations, + ) + plugin_count = annotations['plugin_count'] + self.assertTrue(expr=plugin_count.distinct) + self.assertEqual( + first=len(plugin_count.source_expressions), + second=1, + ) + self.assertIs( + expr1=plugin_count.source_expressions[0].target, + expr2=PluginTag.plugin.field, + ) + + self.assertIn( + member='subplugin_count', + container=annotations, + ) + subplugin_count = annotations['subplugin_count'] + self.assertTrue(expr=subplugin_count.distinct) + self.assertEqual( + first=len(subplugin_count.source_expressions), + second=1, + ) + self.assertIs( + expr1=subplugin_count.source_expressions[0].target, + expr2=getattr(SubPluginTag.sub_plugin, 'field'), + ) + + self.assertIn( + member='project_count', + container=annotations, + ) + project_count = annotations['project_count'] + self.assertIsInstance( + obj=project_count, + cls=CombinedExpression, + ) + self.assertEqual( + first=project_count.connector, + second='+', + ) + self.assertEqual( + first=project_count.rhs, + second=subplugin_count, + ) + lhs = project_count.lhs + self.assertIsInstance( + obj=lhs, + cls=CombinedExpression, + ) + self.assertEqual( + first=lhs.lhs, + second=package_count, + ) + self.assertEqual( + first=lhs.connector, + second='+', + ) + self.assertEqual( + first=lhs.rhs, + second=plugin_count, + ) + + def test_list(self): response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) + content = response.json() self.assertEqual( - first=response.json()['count'], - second=0, + first=content['count'], + second=4, ) - - tag_1 = TagFactory() - package = PackageFactory() - PackageTagFactory( - package=package, - tag=tag_1, + results = content['results'] + self.assertDictEqual( + d1=results[0], + d2={ + 'name': self.tag_1.name, + 'package_count': 1, + 'plugin_count': 2, + 'subplugin_count': 0, + 'project_count': 3, + }, ) - tag_2 = TagFactory() - plugin = PluginFactory() - PluginTagFactory( - plugin=plugin, - tag=tag_2, + self.assertDictEqual( + d1=results[1], + d2={ + 'name': self.tag_2.name, + 'package_count': 2, + 'plugin_count': 1, + 'subplugin_count': 2, + 'project_count': 5, + }, ) - tag_3 = TagFactory() - sub_plugin = SubPluginFactory( - plugin=plugin, + self.assertDictEqual( + d1=results[2], + d2={ + 'name': self.tag_3.name, + 'package_count': 0, + 'plugin_count': 1, + 'subplugin_count': 0, + 'project_count': 1, + }, ) - SubPluginTagFactory( - sub_plugin=sub_plugin, - tag=tag_3, + self.assertDictEqual( + d1=results[3], + d2={ + 'name': self.tag_4.name, + 'package_count': 0, + 'plugin_count': 0, + 'subplugin_count': 0, + 'project_count': 0, + }, ) - tag_4 = TagFactory() - black_listed_tag = TagFactory( - black_listed=True, + + response = self.client.get( + path=f'{self.api_path}?ordering=-project_count', ) - response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, @@ -132,56 +305,155 @@ def test_get(self): self.assertDictEqual( d1=results[0], d2={ - 'name': tag_1.name, - 'packages': [package.name], - 'plugins': [], - 'subplugins': [], - 'creator': { - 'forum_id': tag_1.creator.forum_id, - 'username': tag_1.creator.user.username, - } - } + 'name': self.tag_2.name, + 'package_count': 2, + 'plugin_count': 1, + 'subplugin_count': 2, + 'project_count': 5, + }, ) self.assertDictEqual( d1=results[1], d2={ - 'name': tag_2.name, - 'packages': [], - 'plugins': [plugin.name], + 'name': self.tag_1.name, + 'package_count': 1, + 'plugin_count': 2, + 'subplugin_count': 0, + 'project_count': 3, + }, + ) + self.assertDictEqual( + d1=results[2], + d2={ + 'name': self.tag_3.name, + 'package_count': 0, + 'plugin_count': 1, + 'subplugin_count': 0, + 'project_count': 1, + }, + ) + self.assertDictEqual( + d1=results[3], + d2={ + 'name': self.tag_4.name, + 'package_count': 0, + 'plugin_count': 0, + 'subplugin_count': 0, + 'project_count': 0, + }, + ) + + def test_retrieve(self): + response = self.client.get(path=f'{self.api_path}{self.tag_1.name}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'name': self.tag_1.name, + 'packages': [ + { + 'name': self.package_1.name, + 'id': self.package_1.pk, + }, + ], + 'plugins': [ + { + 'name': self.plugin_1.name, + 'id': self.plugin_1.pk, + }, + { + 'name': self.plugin_2.name, + 'id': self.plugin_2.pk, + }, + ], 'subplugins': [], - 'creator': { - 'forum_id': tag_2.creator.forum_id, - 'username': tag_2.creator.user.username, - } } ) + + response = self.client.get(path=f'{self.api_path}{self.tag_2.name}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) self.assertDictEqual( - d1=results[2], + d1=response.json(), d2={ - 'name': tag_3.name, + 'name': self.tag_2.name, + 'packages': [ + { + 'name': self.package_1.name, + 'id': self.package_1.pk, + }, + { + 'name': self.package_2.name, + 'id': self.package_2.pk, + }, + ], + 'plugins': [ + { + 'name': self.plugin_1.name, + 'id': self.plugin_1.pk, + }, + ], + 'subplugins': [ + { + 'name': self.sub_plugin_1.name, + 'id': self.sub_plugin_1.pk, + }, + { + 'name': self.sub_plugin_2.name, + 'id': self.sub_plugin_2.pk, + }, + ], + } + ) + + response = self.client.get(path=f'{self.api_path}{self.tag_3.name}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'name': self.tag_3.name, 'packages': [], - 'plugins': [], - 'subplugins': [sub_plugin.name], - 'creator': { - 'forum_id': tag_3.creator.forum_id, - 'username': tag_3.creator.user.username, - } + 'plugins': [ + { + 'name': self.plugin_2.name, + 'id': self.plugin_2.pk, + }, + ], + 'subplugins': [], } ) + + response = self.client.get(path=f'{self.api_path}{self.tag_4.name}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) self.assertDictEqual( - d1=results[3], + d1=response.json(), d2={ - 'name': tag_4.name, + 'name': self.tag_4.name, 'packages': [], 'plugins': [], 'subplugins': [], - 'creator': { - 'forum_id': tag_4.creator.forum_id, - 'username': tag_4.creator.user.username, - } } ) + response = self.client.get( + path=f'{self.api_path}{self.black_listed_tag.name}/', + ) + self.assertEqual( + first=response.status_code, + second=status.HTTP_404_NOT_FOUND, + ) + def test_options(self): response = self.client.options(path=self.api_path) self.assertEqual( diff --git a/tags/api/views.py b/tags/api/views.py index 1860f502..3c7c979c 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -4,19 +4,19 @@ # IMPORTS # ============================================================================= # Django -from django.db.models import Prefetch +from django.db.models import Count, F, Prefetch # Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter -from rest_framework.mixins import ListModelMixin +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.viewsets import GenericViewSet # App from project_manager.packages.models import Package from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin -from tags.api.serializers import TagSerializer +from tags.api.serializers import TagListSerializer, TagRetrieveSerializer from tags.models import Tag @@ -31,43 +31,71 @@ # ============================================================================= # VIEWS # ============================================================================= -class TagViewSet(ListModelMixin, GenericViewSet): +class TagViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): """ViewSet for listing Supported Games. ###Available Ordering: * **name** (descending) or **-name** (ascending) + * **project_count** (descending) or **-project_count** (ascending) ####Example: `?ordering=name` - `?ordering=-name` + `?ordering=-project_count` """ filter_backends = (OrderingFilter, DjangoFilterBackend) - serializer_class = TagSerializer queryset = Tag.objects.all() ordering = ('name',) - ordering_fields = ('name',) + ordering_fields = ('name', 'project_count') http_method_names = ('get', 'options') + def retrieve(self, request, *args, **kwargs): + """Overwrite the ordering fields on retrieve to exclude project_count. + + This helps avoid a FieldError since project_count is an annotation + that only occurs during the list view. + """ + self.ordering_fields = ('name',) + return super().retrieve(request=request, *args, **kwargs) + + def get_serializer_class(self): + """Return the correct serializer based on the action.""" + if self.action == 'retrieve': + return TagRetrieveSerializer + + return TagListSerializer + def get_queryset(self): """Filter the queryset to not return black-listed tags.""" - return super().get_queryset().filter( + queryset = super().get_queryset().filter( black_listed=False, - ).select_related( - 'creator__user', - ).prefetch_related( - Prefetch( - lookup='packages', - queryset=Package.objects.order_by('name'), - ), - Prefetch( - lookup='plugins', - queryset=Plugin.objects.order_by('name'), - ), - Prefetch( - lookup='subplugins', - queryset=SubPlugin.objects.order_by('name'), - ), + ) + if self.action == 'retrieve': + # self.ordering_fields = ('name',) + return queryset.prefetch_related( + Prefetch( + lookup='packages', + queryset=Package.objects.order_by('name'), + ), + Prefetch( + lookup='plugins', + queryset=Plugin.objects.order_by('name'), + ), + Prefetch( + lookup='subplugins', + queryset=SubPlugin.objects.order_by('name'), + ), + ) + + package_count = Count('packages', distinct=True) + plugin_count = Count('plugins', distinct=True) + subplugin_count = Count('subplugins', distinct=True) + return queryset.annotate( + package_count=package_count, + plugin_count=plugin_count, + subplugin_count=subplugin_count, + ).annotate( + project_count=F('package_count') + F('plugin_count') + F('subplugin_count'), ) From e06701c8ae0be93b82b677e50d2178574a55726a Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 22:05:20 -0400 Subject: [PATCH 124/211] Updated the Games and Tags ViewSets to be more useful. --- games/api/serializers.py | 39 +- games/api/tests/test_serializers.py | 116 ++++- games/api/tests/test_views.py | 440 +++++++++++++++++- games/api/views.py | 68 ++- .../common.py => common/serializers.py} | 17 + tags/api/serializers.py | 21 +- tags/api/tests/test_serializers.py | 33 +- tags/api/tests/test_views.py | 36 +- tags/api/views.py | 7 +- test_utils/factories/games.py | 4 + .../common.py => common/serializers.py} | 0 .../__init__.py => serializers.py} | 0 12 files changed, 700 insertions(+), 81 deletions(-) rename project_manager/packages/api/{serializers/common.py => common/serializers.py} (77%) rename users/api/{serializers/common.py => common/serializers.py} (100%) rename users/api/{serializers/__init__.py => serializers.py} (100%) diff --git a/games/api/serializers.py b/games/api/serializers.py index c78da28f..f6d9cd50 100644 --- a/games/api/serializers.py +++ b/games/api/serializers.py @@ -4,26 +4,57 @@ # IMPORTS # ============================================================================= # Third Party Django +from rest_framework.fields import IntegerField from rest_framework.serializers import ModelSerializer # App from games.models import Game +from project_manager.packages.api.common.serializers import MinimalPackageSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer # ============================================================================= # ALL DECLARATION # ============================================================================= __all__ = ( - 'GameSerializer', + 'GameListSerializer', + 'GameRetrieveSerializer', ) # ============================================================================= # SERIALIZERS # ============================================================================= -class GameSerializer(ModelSerializer): +class GameRetrieveSerializer(ModelSerializer): """Serializer for supported games for projects.""" + packages = MinimalPackageSerializer(many=True, read_only=True) + plugins = MinimalPluginSerializer(many=True, read_only=True) + subplugins = MinimalSubPluginSerializer(many=True, read_only=True) + + class Meta: + """Define metaclass attributes.""" + + model = Game + fields = ( + 'name', + 'slug', + 'icon', + 'packages', + 'plugins', + 'subplugins', + ) + + +class GameListSerializer(ModelSerializer): + """Serializer for project Tags on list.""" + + package_count = IntegerField() + plugin_count = IntegerField() + subplugin_count = IntegerField() + project_count = IntegerField() + class Meta: """Define metaclass attributes.""" @@ -32,4 +63,8 @@ class Meta: 'name', 'slug', 'icon', + 'package_count', + 'plugin_count', + 'subplugin_count', + 'project_count', ) diff --git a/games/api/tests/test_serializers.py b/games/api/tests/test_serializers.py index 0c39265d..3f19b570 100644 --- a/games/api/tests/test_serializers.py +++ b/games/api/tests/test_serializers.py @@ -5,33 +5,137 @@ from django.test import TestCase # Third Party Django -from rest_framework.serializers import ModelSerializer +from rest_framework.fields import IntegerField +from rest_framework.serializers import ListSerializer, ModelSerializer # App -from games.api.serializers import GameSerializer +from games.api.common.serializers import MinimalGameSerializer +from games.api.serializers import GameListSerializer, GameRetrieveSerializer from games.models import Game +from project_manager.packages.api.common.serializers import MinimalPackageSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer # ============================================================================= # TEST CASES # ============================================================================= -class GameSerializerTestCase(TestCase): +class MinimalGameSerializerTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( - expr=issubclass(GameSerializer, ModelSerializer), + expr=issubclass(MinimalGameSerializer, ModelSerializer), ) def test_meta_class(self): self.assertEqual( - first=GameSerializer.Meta.model, + first=MinimalGameSerializer.Meta.model, second=Game, ) self.assertTupleEqual( - tuple1=GameSerializer.Meta.fields, + tuple1=MinimalGameSerializer.Meta.fields, tuple2=( 'name', 'slug', 'icon', ), ) + + +class GameRetrieveSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(GameRetrieveSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(GameRetrieveSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=3, + ) + + for field, cls in ( + ('packages', MinimalPackageSerializer), + ('plugins', MinimalPluginSerializer), + ('subplugins', MinimalSubPluginSerializer), + ): + self.assertIn( + member=field, + container=declared_fields, + ) + obj = declared_fields[field] + self.assertIsInstance( + obj=obj, + cls=ListSerializer, + ) + self.assertTrue(expr=obj.read_only) + self.assertIsInstance( + obj=obj.child, + cls=cls, + ) + + def test_meta_class(self): + self.assertEqual( + first=GameRetrieveSerializer.Meta.model, + second=Game, + ) + self.assertTupleEqual( + tuple1=GameRetrieveSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'icon', + 'packages', + 'plugins', + 'subplugins', + ), + ) + + +class GameListSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(GameListSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(GameListSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=4, + ) + + for field in ( + 'package_count', + 'plugin_count', + 'subplugin_count', + 'project_count', + ): + self.assertIn( + member=field, + container=declared_fields, + ) + obj = declared_fields[field] + self.assertIsInstance( + obj=obj, + cls=IntegerField, + ) + + def test_meta_class(self): + self.assertEqual( + first=GameListSerializer.Meta.model, + second=Game, + ) + self.assertTupleEqual( + tuple1=GameListSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'icon', + 'package_count', + 'plugin_count', + 'subplugin_count', + 'project_count', + ), + ) diff --git a/games/api/tests/test_views.py b/games/api/tests/test_views.py index b415e448..27f709d4 100644 --- a/games/api/tests/test_views.py +++ b/games/api/tests/test_views.py @@ -2,7 +2,7 @@ # IMPORTS # ============================================================================= # Django -from django.core.management import call_command +from django.db.models.expressions import CombinedExpression # Third Party Django from rest_framework import status @@ -10,9 +10,17 @@ from rest_framework.test import APITestCase # App -from games.api.serializers import GameSerializer +from project_manager.packages.models import PackageGame +from project_manager.plugins.models import PluginGame +from project_manager.sub_plugins.models import SubPluginGame from games.api.views import GameViewSet -from games.management.commands.create_game_instances import GAMES +from test_utils.factories.packages import PackageFactory, PackageGameFactory +from test_utils.factories.plugins import PluginFactory, PluginGameFactory +from test_utils.factories.sub_plugins import ( + SubPluginFactory, + SubPluginGameFactory, +) +from test_utils.factories.games import GameFactory # ============================================================================= @@ -20,20 +28,78 @@ # ============================================================================= class GameViewSetTestCase(APITestCase): + game_1 = game_2 = game_3 = None + package_1 = package_2 = None + plugin_1 = plugin_2 = None + sub_plugin_1 = sub_plugin_2 = None api_path = '/api/games/' + @classmethod + def setUpTestData(cls): + cls.game_1 = GameFactory() + cls.game_2 = GameFactory() + cls.game_3 = GameFactory() + cls.game_4 = GameFactory() + + cls.package_1 = PackageFactory() + cls.package_2 = PackageFactory() + cls.plugin_1 = PluginFactory() + cls.plugin_2 = PluginFactory() + cls.sub_plugin_1 = SubPluginFactory( + plugin=cls.plugin_1, + ) + cls.sub_plugin_2 = SubPluginFactory( + plugin=cls.plugin_1, + ) + + # game_1 associations + PackageGameFactory( + package=cls.package_1, + game=cls.game_1, + ) + PluginGameFactory( + plugin=cls.plugin_1, + game=cls.game_1, + ) + PluginGameFactory( + plugin=cls.plugin_2, + game=cls.game_1, + ) + + # game_2 associations + PackageGameFactory( + package=cls.package_1, + game=cls.game_2, + ) + PackageGameFactory( + package=cls.package_2, + game=cls.game_2, + ) + PluginGameFactory( + plugin=cls.plugin_1, + game=cls.game_2, + ) + SubPluginGameFactory( + sub_plugin=cls.sub_plugin_1, + game=cls.game_2, + ) + SubPluginGameFactory( + sub_plugin=cls.sub_plugin_2, + game=cls.game_2, + ) + + # game_3 associations + PluginGameFactory( + plugin=cls.plugin_2, + game=cls.game_3, + ) + def test_filter_backends(self): self.assertTupleEqual( tuple1=GameViewSet.filter_backends, tuple2=(OrderingFilter,) ) - def test_serializer_class(self): - self.assertEqual( - first=GameViewSet.serializer_class, - second=GameSerializer, - ) - def test_ordering(self): self.assertTupleEqual( tuple1=GameViewSet.ordering, @@ -43,7 +109,7 @@ def test_ordering(self): def test_ordering_fields(self): self.assertTupleEqual( tuple1=GameViewSet.ordering_fields, - tuple2=('basename', 'name',), + tuple2=('basename', 'name', 'project_count'), ) def test_http_method_names(self): @@ -52,26 +118,366 @@ def test_http_method_names(self): tuple2=('get', 'options'), ) - def test_get(self): + def test_get_queryset(self): + queryset = GameViewSet(action='https://github.com/retrieve').get_queryset().filter() + self.assertFalse(expr=queryset.query.select_related) + prefetch_lookups = queryset._prefetch_related_lookups + self.assertEqual( + first=len(prefetch_lookups), + second=3, + ) + for n, lookup_name in enumerate([ + 'packages', + 'plugins', + 'subplugins', + ]): + lookup = prefetch_lookups[n] + self.assertEqual( + first=lookup.prefetch_to, + second=lookup_name, + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('name',), + ) + + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'plugin': {}} + ) + + queryset = GameViewSet(action='https://github.com/list').get_queryset().filter() + self.assertFalse(expr=queryset.query.select_related) + self.assertTupleEqual( + tuple1=queryset._prefetch_related_lookups, + tuple2=(), + ) + annotations = queryset.query.annotations + self.assertIn( + member='package_count', + container=annotations, + ) + package_count = annotations['package_count'] + self.assertTrue(expr=package_count.distinct) + self.assertEqual( + first=len(package_count.source_expressions), + second=1, + ) + self.assertIs( + expr1=package_count.source_expressions[0].target, + expr2=PackageGame.package.field, + ) + + self.assertIn( + member='plugin_count', + container=annotations, + ) + plugin_count = annotations['plugin_count'] + self.assertTrue(expr=plugin_count.distinct) + self.assertEqual( + first=len(plugin_count.source_expressions), + second=1, + ) + self.assertIs( + expr1=plugin_count.source_expressions[0].target, + expr2=PluginGame.plugin.field, + ) + + self.assertIn( + member='subplugin_count', + container=annotations, + ) + subplugin_count = annotations['subplugin_count'] + self.assertTrue(expr=subplugin_count.distinct) + self.assertEqual( + first=len(subplugin_count.source_expressions), + second=1, + ) + self.assertIs( + expr1=subplugin_count.source_expressions[0].target, + expr2=getattr(SubPluginGame.sub_plugin, 'field'), + ) + + self.assertIn( + member='project_count', + container=annotations, + ) + project_count = annotations['project_count'] + self.assertIsInstance( + obj=project_count, + cls=CombinedExpression, + ) + self.assertEqual( + first=project_count.connector, + second='+', + ) + self.assertEqual( + first=project_count.rhs, + second=subplugin_count, + ) + lhs = project_count.lhs + self.assertIsInstance( + obj=lhs, + cls=CombinedExpression, + ) + self.assertEqual( + first=lhs.lhs, + second=package_count, + ) + self.assertEqual( + first=lhs.connector, + second='+', + ) + self.assertEqual( + first=lhs.rhs, + second=plugin_count, + ) + + def test_list(self): response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) + content = response.json() self.assertEqual( - first=response.json()['count'], - second=0, + first=content['count'], + second=4, + ) + results = content['results'] + request = response.wsgi_request + icon_base_url = f'{request.scheme}://{request.get_host()}' + self.assertDictEqual( + d1=results[0], + d2={ + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': f'{icon_base_url}{self.game_1.icon.url}', + 'package_count': 1, + 'plugin_count': 2, + 'subplugin_count': 0, + 'project_count': 3, + }, + ) + self.assertDictEqual( + d1=results[1], + d2={ + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': f'{icon_base_url}{self.game_2.icon.url}', + 'package_count': 2, + 'plugin_count': 1, + 'subplugin_count': 2, + 'project_count': 5, + }, + ) + self.assertDictEqual( + d1=results[2], + d2={ + 'name': self.game_3.name, + 'slug': self.game_3.slug, + 'icon': f'{icon_base_url}{self.game_3.icon.url}', + 'package_count': 0, + 'plugin_count': 1, + 'subplugin_count': 0, + 'project_count': 1, + }, + ) + self.assertDictEqual( + d1=results[3], + d2={ + 'name': self.game_4.name, + 'slug': self.game_4.slug, + 'icon': f'{icon_base_url}{self.game_4.icon.url}', + 'package_count': 0, + 'plugin_count': 0, + 'subplugin_count': 0, + 'project_count': 0, + }, ) - call_command('create_game_instances') - response = self.client.get(path=self.api_path) + response = self.client.get( + path=f'{self.api_path}?ordering=-project_count', + ) self.assertEqual( first=response.status_code, second=status.HTTP_200_OK, ) + content = response.json() self.assertEqual( - first=response.json()['count'], - second=len(GAMES), + first=content['count'], + second=4, + ) + results = content['results'] + self.assertDictEqual( + d1=results[0], + d2={ + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': f'{icon_base_url}{self.game_2.icon.url}', + 'package_count': 2, + 'plugin_count': 1, + 'subplugin_count': 2, + 'project_count': 5, + }, + ) + self.assertDictEqual( + d1=results[1], + d2={ + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': f'{icon_base_url}{self.game_1.icon.url}', + 'package_count': 1, + 'plugin_count': 2, + 'subplugin_count': 0, + 'project_count': 3, + }, + ) + self.assertDictEqual( + d1=results[2], + d2={ + 'name': self.game_3.name, + 'slug': self.game_3.slug, + 'icon': f'{icon_base_url}{self.game_3.icon.url}', + 'package_count': 0, + 'plugin_count': 1, + 'subplugin_count': 0, + 'project_count': 1, + }, + ) + self.assertDictEqual( + d1=results[3], + d2={ + 'name': self.game_4.name, + 'slug': self.game_4.slug, + 'icon': f'{icon_base_url}{self.game_4.icon.url}', + 'package_count': 0, + 'plugin_count': 0, + 'subplugin_count': 0, + 'project_count': 0, + }, + ) + + def test_retrieve(self): + response = self.client.get(path=f'{self.api_path}{self.game_1.slug}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + request = response.wsgi_request + icon_base_url = f'{request.scheme}://{request.get_host()}' + self.assertDictEqual( + d1=response.json(), + d2={ + 'name': self.game_1.name, + 'slug': self.game_1.slug, + 'icon': f'{icon_base_url}{self.game_1.icon.url}', + 'packages': [ + { + 'name': self.package_1.name, + 'slug': self.package_1.slug, + }, + ], + 'plugins': [ + { + 'name': self.plugin_1.name, + 'slug': self.plugin_1.slug, + }, + { + 'name': self.plugin_2.name, + 'slug': self.plugin_2.slug, + }, + ], + 'subplugins': [], + } + ) + + response = self.client.get(path=f'{self.api_path}{self.game_2.slug}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'name': self.game_2.name, + 'slug': self.game_2.slug, + 'icon': f'{icon_base_url}{self.game_2.icon.url}', + 'packages': [ + { + 'name': self.package_1.name, + 'slug': self.package_1.slug, + }, + { + 'name': self.package_2.name, + 'slug': self.package_2.slug, + }, + ], + 'plugins': [ + { + 'name': self.plugin_1.name, + 'slug': self.plugin_1.slug, + }, + ], + 'subplugins': [ + { + 'name': self.sub_plugin_1.name, + 'slug': self.sub_plugin_1.slug, + 'plugin': { + 'name': self.plugin_1.name, + 'slug': self.plugin_1.slug, + } + }, + { + 'name': self.sub_plugin_2.name, + 'slug': self.sub_plugin_2.slug, + 'plugin': { + 'name': self.plugin_1.name, + 'slug': self.plugin_1.slug, + } + }, + ], + } + ) + + response = self.client.get(path=f'{self.api_path}{self.game_3.slug}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'name': self.game_3.name, + 'slug': self.game_3.slug, + 'icon': f'{icon_base_url}{self.game_3.icon.url}', + 'packages': [], + 'plugins': [ + { + 'name': self.plugin_2.name, + 'slug': self.plugin_2.slug, + }, + ], + 'subplugins': [], + } + ) + + response = self.client.get(path=f'{self.api_path}{self.game_4.slug}/') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + self.assertDictEqual( + d1=response.json(), + d2={ + 'name': self.game_4.name, + 'slug': self.game_4.slug, + 'icon': f'{icon_base_url}{self.game_4.icon.url}', + 'packages': [], + 'plugins': [], + 'subplugins': [], + } ) def test_options(self): diff --git a/games/api/views.py b/games/api/views.py index 3993f67d..7dcc493f 100644 --- a/games/api/views.py +++ b/games/api/views.py @@ -3,14 +3,20 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Django +from django.db.models import Count, F, Prefetch + # Third Party Django from rest_framework.filters import OrderingFilter -from rest_framework.mixins import ListModelMixin +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.viewsets import GenericViewSet # App -from games.api.serializers import GameSerializer +from games.api.serializers import GameListSerializer, GameRetrieveSerializer from games.models import Game +from project_manager.packages.models import Package +from project_manager.plugins.models import Plugin +from project_manager.sub_plugins.models import SubPlugin # ============================================================================= @@ -24,23 +30,73 @@ # ============================================================================= # VIEWS # ============================================================================= -class GameViewSet(ListModelMixin, GenericViewSet): +class GameViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): """ViewSet for listing Supported Games. ###Available Ordering: * **name** (descending) or **-name** (ascending) * **basename** (descending) or **-basename** (ascending) + * **project_count** (descending) or **-project_count** (ascending) ####Example: `?ordering=name` - `?ordering=-basename` + `?ordering=-project_count` """ filter_backends = (OrderingFilter,) - serializer_class = GameSerializer queryset = Game.objects.all() ordering = ('name',) - ordering_fields = ('basename', 'name') + ordering_fields = ('basename', 'name', 'project_count') http_method_names = ('get', 'options') + + def retrieve(self, request, *args, **kwargs): + """Overwrite the ordering fields on retrieve to exclude project_count. + + This helps avoid a FieldError since project_count is an annotation + that only occurs during the list view. + """ + self.ordering_fields = ('basename', 'name') + return super().retrieve(request=request, *args, **kwargs) + + def get_serializer_class(self): + """Return the correct serializer based on the action.""" + if self.action == 'retrieve': + return GameRetrieveSerializer + + return GameListSerializer + + def get_queryset(self): + """Filter the queryset to not return black-listed tags.""" + queryset = super().get_queryset() + if self.action == 'retrieve': + return queryset.prefetch_related( + Prefetch( + lookup='packages', + queryset=Package.objects.order_by('name'), + ), + Prefetch( + lookup='plugins', + queryset=Plugin.objects.order_by('name'), + ), + Prefetch( + lookup='subplugins', + queryset=SubPlugin.objects.select_related( + 'plugin', + ).order_by( + 'name', + ), + ), + ) + + package_count = Count('packages', distinct=True) + plugin_count = Count('plugins', distinct=True) + subplugin_count = Count('subplugins', distinct=True) + return queryset.annotate( + package_count=package_count, + plugin_count=plugin_count, + subplugin_count=subplugin_count, + ).annotate( + project_count=F('package_count') + F('plugin_count') + F('subplugin_count'), + ) diff --git a/project_manager/packages/api/serializers/common.py b/project_manager/packages/api/common/serializers.py similarity index 77% rename from project_manager/packages/api/serializers/common.py rename to project_manager/packages/api/common/serializers.py index f2991c6c..2bf61f52 100644 --- a/project_manager/packages/api/serializers/common.py +++ b/project_manager/packages/api/common/serializers.py @@ -7,11 +7,15 @@ from rest_framework.fields import ReadOnlyField from rest_framework.serializers import ModelSerializer +# App +from project_manager.packages.models import Package + # ============================================================================= # ALL DECLARATION # ============================================================================= __all__ = ( + 'MinimalPackageSerializer', 'ReleasePackageRequirementSerializer', ) @@ -35,3 +39,16 @@ class Meta: 'version', 'optional', ) + + +class MinimalPackageSerializer(ModelSerializer): + """Serializer for Package Contributions.""" + + class Meta: + """Define metaclass attributes.""" + + model = Package + fields = ( + 'name', + 'slug', + ) diff --git a/tags/api/serializers.py b/tags/api/serializers.py index f0ca0aa6..844b4043 100644 --- a/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -5,10 +5,12 @@ # ============================================================================= # Third Party Django from rest_framework.fields import IntegerField -from rest_framework.relations import RelatedField from rest_framework.serializers import ModelSerializer # App +from project_manager.packages.api.common.serializers import MinimalPackageSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer from tags.models import Tag @@ -16,7 +18,6 @@ # ALL DECLARATION # ============================================================================= __all__ = ( - 'RelatedTagSerializer', 'TagListSerializer', 'TagRetrieveSerializer', ) @@ -25,22 +26,12 @@ # ============================================================================= # SERIALIZERS # ============================================================================= -class RelatedTagSerializer(RelatedField): - """Serializer for project tag fields.""" - - def to_representation(self, value): - """Return the name of the project.""" - # TODO: return the url once the paths exist - # return {'name': value.name, 'id': value.pk, 'url': value.get_absolute_url()} - return {'name': value.name, 'id': value.pk} - - class TagRetrieveSerializer(ModelSerializer): """Serializer for project Tags on retrieve.""" - packages = RelatedTagSerializer(many=True, read_only=True) - plugins = RelatedTagSerializer(many=True, read_only=True) - subplugins = RelatedTagSerializer(many=True, read_only=True) + packages = MinimalPackageSerializer(many=True, read_only=True) + plugins = MinimalPluginSerializer(many=True, read_only=True) + subplugins = MinimalSubPluginSerializer(many=True, read_only=True) class Meta: """Define metaclass attributes.""" diff --git a/tags/api/tests/test_serializers.py b/tags/api/tests/test_serializers.py index c2d6a959..e3c97745 100644 --- a/tags/api/tests/test_serializers.py +++ b/tags/api/tests/test_serializers.py @@ -6,26 +6,19 @@ # Third Party Django from rest_framework.fields import IntegerField -from rest_framework.relations import ManyRelatedField, RelatedField -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import ListSerializer, ModelSerializer # App -from tags.api.serializers import ( - RelatedTagSerializer, - TagListSerializer, - TagRetrieveSerializer, -) +from project_manager.packages.api.common.serializers import MinimalPackageSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer +from tags.api.serializers import TagListSerializer, TagRetrieveSerializer from tags.models import Tag # ============================================================================= # TEST CASES # ============================================================================= -class RelatedTagSerializerTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue(expr=issubclass(RelatedTagSerializer, RelatedField)) - - class TagRetrieveSerializerTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( @@ -39,10 +32,10 @@ def test_declared_fields(self): second=3, ) - for field in ( - 'packages', - 'plugins', - 'subplugins', + for field, cls in ( + ('packages', MinimalPackageSerializer), + ('plugins', MinimalPluginSerializer), + ('subplugins', MinimalSubPluginSerializer), ): self.assertIn( member=field, @@ -51,13 +44,13 @@ def test_declared_fields(self): obj = declared_fields[field] self.assertIsInstance( obj=obj, - cls=ManyRelatedField, + cls=ListSerializer, ) + self.assertTrue(expr=obj.read_only) self.assertIsInstance( - obj=obj.child_relation, - cls=RelatedTagSerializer, + obj=obj.child, + cls=cls, ) - self.assertTrue(expr=obj.child_relation.read_only) def test_meta_class(self): self.assertEqual( diff --git a/tags/api/tests/test_views.py b/tags/api/tests/test_views.py index 1ec851a0..d04ab132 100644 --- a/tags/api/tests/test_views.py +++ b/tags/api/tests/test_views.py @@ -1,10 +1,6 @@ # ============================================================================= # IMPORTS # ============================================================================= -# Python -from collections import defaultdict -from random import randint - # Django from django.db.models.expressions import CombinedExpression @@ -149,6 +145,12 @@ def test_get_queryset(self): second=('name',), ) + lookup = prefetch_lookups[2] + self.assertEqual( + first=lookup.queryset.query.select_related, + second={'plugin': {}} + ) + queryset = TagViewSet(action='https://github.com/list').get_queryset().filter() self.assertFalse(expr=queryset.query.select_related) self.assertTupleEqual( @@ -356,17 +358,17 @@ def test_retrieve(self): 'packages': [ { 'name': self.package_1.name, - 'id': self.package_1.pk, + 'slug': self.package_1.slug, }, ], 'plugins': [ { 'name': self.plugin_1.name, - 'id': self.plugin_1.pk, + 'slug': self.plugin_1.slug, }, { 'name': self.plugin_2.name, - 'id': self.plugin_2.pk, + 'slug': self.plugin_2.slug, }, ], 'subplugins': [], @@ -385,27 +387,35 @@ def test_retrieve(self): 'packages': [ { 'name': self.package_1.name, - 'id': self.package_1.pk, + 'slug': self.package_1.slug, }, { 'name': self.package_2.name, - 'id': self.package_2.pk, + 'slug': self.package_2.slug, }, ], 'plugins': [ { 'name': self.plugin_1.name, - 'id': self.plugin_1.pk, + 'slug': self.plugin_1.slug, }, ], 'subplugins': [ { 'name': self.sub_plugin_1.name, - 'id': self.sub_plugin_1.pk, + 'slug': self.sub_plugin_1.slug, + 'plugin': { + 'name': self.plugin_1.name, + 'slug': self.plugin_1.slug, + } }, { 'name': self.sub_plugin_2.name, - 'id': self.sub_plugin_2.pk, + 'slug': self.sub_plugin_2.slug, + 'plugin': { + 'name': self.plugin_1.name, + 'slug': self.plugin_1.slug, + } }, ], } @@ -424,7 +434,7 @@ def test_retrieve(self): 'plugins': [ { 'name': self.plugin_2.name, - 'id': self.plugin_2.pk, + 'slug': self.plugin_2.slug, }, ], 'subplugins': [], diff --git a/tags/api/views.py b/tags/api/views.py index 3c7c979c..5b4ac4ad 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -73,7 +73,6 @@ def get_queryset(self): black_listed=False, ) if self.action == 'retrieve': - # self.ordering_fields = ('name',) return queryset.prefetch_related( Prefetch( lookup='packages', @@ -85,7 +84,11 @@ def get_queryset(self): ), Prefetch( lookup='subplugins', - queryset=SubPlugin.objects.order_by('name'), + queryset=SubPlugin.objects.select_related( + 'plugin', + ).order_by( + 'name', + ), ), ) diff --git a/test_utils/factories/games.py b/test_utils/factories/games.py index 6bbd3db4..830baa6b 100644 --- a/test_utils/factories/games.py +++ b/test_utils/factories/games.py @@ -24,6 +24,10 @@ class GameFactory(factory.django.DjangoModelFactory): """Model factory to use when testing with Game objects.""" + name = factory.Sequence(function=lambda n: f'Game {n}') + basename = factory.Sequence(function=lambda n: f'game_{n}') + icon = factory.Sequence(function=lambda n: f'game_{n}.png') + class Meta: """Define metaclass attributes.""" diff --git a/users/api/serializers/common.py b/users/api/common/serializers.py similarity index 100% rename from users/api/serializers/common.py rename to users/api/common/serializers.py diff --git a/users/api/serializers/__init__.py b/users/api/serializers.py similarity index 100% rename from users/api/serializers/__init__.py rename to users/api/serializers.py From e421b432bdd2705347449702792520b6995659f1 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 22:07:29 -0400 Subject: [PATCH 125/211] Rearranged some packages to avoid a recursive import issue. --- games/api/common/__init__.py | 1 + games/api/common/serializers.py | 35 ++++ .../packages/api/common/__init__.py | 1 + .../packages/api/serializers/__init__.py | 4 +- .../packages/api/tests/test_project_views.py | 8 +- .../packages/api/tests/test_serializers.py | 30 +++- .../plugins/api/common/__init__.py | 1 + .../plugins/api/common/serializers.py | 34 ++++ .../plugins/api/serializers/__init__.py | 13 +- .../plugins/api/tests/test_serializers.py | 42 +++-- .../sub_plugins/api/common/__init__.py | 1 + .../sub_plugins/api/common/serializers.py | 38 +++++ .../sub_plugins/api/serializers/__init__.py | 8 +- .../sub_plugins/api/tests/test_serializers.py | 44 ++++- users/api/common/__init__.py | 1 + users/api/serializers.py | 68 +------- users/api/tests/test_serializers.py | 154 ++---------------- 17 files changed, 244 insertions(+), 239 deletions(-) create mode 100644 games/api/common/__init__.py create mode 100644 games/api/common/serializers.py create mode 100644 project_manager/packages/api/common/__init__.py create mode 100644 project_manager/plugins/api/common/__init__.py create mode 100644 project_manager/plugins/api/common/serializers.py create mode 100644 project_manager/sub_plugins/api/common/__init__.py create mode 100644 project_manager/sub_plugins/api/common/serializers.py create mode 100644 users/api/common/__init__.py diff --git a/games/api/common/__init__.py b/games/api/common/__init__.py new file mode 100644 index 00000000..1cfd324c --- /dev/null +++ b/games/api/common/__init__.py @@ -0,0 +1 @@ +"""Common Game functionality used by other apps.""" diff --git a/games/api/common/serializers.py b/games/api/common/serializers.py new file mode 100644 index 00000000..f1c55ac0 --- /dev/null +++ b/games/api/common/serializers.py @@ -0,0 +1,35 @@ +"""Game serializers for APIs in other apps.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +from rest_framework.serializers import ModelSerializer + +# App +from games.models import Game + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'MinimalGameSerializer', +) + + +# ============================================================================= +# SERIALIZERS +# ============================================================================= +class MinimalGameSerializer(ModelSerializer): + """Serializer for Package Contributions.""" + + class Meta: + """Define metaclass attributes.""" + + model = Game + fields = ( + 'name', + 'slug', + 'icon', + ) diff --git a/project_manager/packages/api/common/__init__.py b/project_manager/packages/api/common/__init__.py new file mode 100644 index 00000000..82e77451 --- /dev/null +++ b/project_manager/packages/api/common/__init__.py @@ -0,0 +1 @@ +"""Common Package functionality used by other apps.""" diff --git a/project_manager/packages/api/serializers/__init__.py b/project_manager/packages/api/serializers/__init__.py index bf762ee5..e692910b 100644 --- a/project_manager/packages/api/serializers/__init__.py +++ b/project_manager/packages/api/serializers/__init__.py @@ -13,9 +13,7 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.packages.api.serializers.common import ( - ReleasePackageRequirementSerializer, -) +from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer from project_manager.packages.api.serializers.mixins import PackageReleaseBase from project_manager.packages.models import ( Package, diff --git a/project_manager/packages/api/tests/test_project_views.py b/project_manager/packages/api/tests/test_project_views.py index b404ef27..2b740313 100644 --- a/project_manager/packages/api/tests/test_project_views.py +++ b/project_manager/packages/api/tests/test_project_views.py @@ -558,7 +558,7 @@ def test_post_with_requirements(self): ) def test_patch(self): - # Verify that non logged in user cannot update a path + # Verify that non logged in user cannot update the package api_path = f'{self.api_path}{self.package.slug}/' response = self.client.patch( path=api_path, @@ -571,7 +571,7 @@ def test_patch(self): second=status.HTTP_403_FORBIDDEN, ) - # Verify that regular user cannot update a path + # Verify that regular user cannot update the package self.client.force_login(self.regular_user.user) response = self.client.patch( path=api_path, @@ -584,7 +584,7 @@ def test_patch(self): second=status.HTTP_403_FORBIDDEN, ) - # Verify that contributor can update a path + # Verify that contributor can update the package self.client.force_login(self.contributor.user) response = self.client.patch( path=api_path, @@ -597,7 +597,7 @@ def test_patch(self): second=status.HTTP_200_OK, ) - # Verify that owner can update a path + # Verify that owner can update the package self.client.force_login(self.owner.user) response = self.client.patch( path=api_path, diff --git a/project_manager/packages/api/tests/test_serializers.py b/project_manager/packages/api/tests/test_serializers.py index 40f42774..db029305 100644 --- a/project_manager/packages/api/tests/test_serializers.py +++ b/project_manager/packages/api/tests/test_serializers.py @@ -35,7 +35,8 @@ PackageSerializer, PackageTagSerializer, ) -from project_manager.packages.api.serializers.common import ( +from project_manager.packages.api.common.serializers import ( + MinimalPackageSerializer, ReleasePackageRequirementSerializer, ) from project_manager.packages.api.serializers.mixins import PackageReleaseBase @@ -458,6 +459,33 @@ def test_meta_class(self): ) +class MinimalPackageSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(MinimalPackageSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(MinimalPackageSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=0, + ) + + def test_meta_class(self): + self.assertEqual( + first=MinimalPackageSerializer.Meta.model, + second=Package, + ) + self.assertTupleEqual( + tuple1=MinimalPackageSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + ), + ) + + class PackageReleaseBaseTestCase(TestCase): def test_base_attributes(self): self.assertEqual( diff --git a/project_manager/plugins/api/common/__init__.py b/project_manager/plugins/api/common/__init__.py new file mode 100644 index 00000000..d087de24 --- /dev/null +++ b/project_manager/plugins/api/common/__init__.py @@ -0,0 +1 @@ +"""Common Plugin functionality used by other apps.""" diff --git a/project_manager/plugins/api/common/serializers.py b/project_manager/plugins/api/common/serializers.py new file mode 100644 index 00000000..73c169c2 --- /dev/null +++ b/project_manager/plugins/api/common/serializers.py @@ -0,0 +1,34 @@ +"""Plugin serializers for APIs in other apps.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +from rest_framework.serializers import ModelSerializer + +# App +from project_manager.plugins.models import Plugin + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'MinimalPluginSerializer', +) + + +# ============================================================================= +# SERIALIZERS +# ============================================================================= +class MinimalPluginSerializer(ModelSerializer): + """Serializer for Package Contributions.""" + + class Meta: + """Define metaclass attributes.""" + + model = Plugin + fields = ( + 'name', + 'slug', + ) diff --git a/project_manager/plugins/api/serializers/__init__.py b/project_manager/plugins/api/serializers/__init__.py index 14544669..4d70b120 100644 --- a/project_manager/plugins/api/serializers/__init__.py +++ b/project_manager/plugins/api/serializers/__init__.py @@ -16,13 +16,8 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.common.api.serializers.mixins import ( - AddProjectToViewMixin, - ProjectThroughMixin, -) -from project_manager.packages.api.serializers.common import ( - ReleasePackageRequirementSerializer, -) +from project_manager.common.api.serializers.mixins import ProjectThroughMixin +from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer from project_manager.plugins.api.serializers.mixins import PluginReleaseBase from project_manager.plugins.models import ( Plugin, @@ -213,7 +208,7 @@ class Meta(ProjectContributorSerializer.Meta): model = PluginContributor -class SubPluginPathSerializer(ProjectThroughMixin, AddProjectToViewMixin): +class SubPluginPathSerializer(ProjectThroughMixin): """Sub-Plugin Paths Serializer.""" class Meta: @@ -237,6 +232,7 @@ def get_field_names(self, declared_fields, info): field_names = list(field_names) field_names.remove('path') field_names = tuple(field_names) + return field_names def validate(self, attrs): @@ -261,4 +257,5 @@ def validate(self, attrs): 'allow_package_using_basename': message, 'allow_package_using_init': message, }) + return super().validate(attrs=attrs) diff --git a/project_manager/plugins/api/tests/test_serializers.py b/project_manager/plugins/api/tests/test_serializers.py index 28da8b77..7f590a0f 100644 --- a/project_manager/plugins/api/tests/test_serializers.py +++ b/project_manager/plugins/api/tests/test_serializers.py @@ -10,7 +10,7 @@ # Third Party Django from rest_framework.exceptions import ValidationError from rest_framework.fields import ReadOnlyField -from rest_framework.serializers import ListSerializer +from rest_framework.serializers import ListSerializer, ModelSerializer # App from project_manager.common.api.serializers import ( @@ -22,13 +22,8 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.common.api.serializers.mixins import ( - ProjectThroughMixin, - AddProjectToViewMixin, -) -from project_manager.packages.api.serializers.common import ( - ReleasePackageRequirementSerializer, -) +from project_manager.common.api.serializers.mixins import ProjectThroughMixin +from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer from project_manager.plugins.api.serializers import ( PluginContributorSerializer, PluginCreateReleaseSerializer, @@ -44,6 +39,7 @@ PluginTagSerializer, SubPluginPathSerializer, ) +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer from project_manager.plugins.api.serializers.mixins import PluginReleaseBase from project_manager.plugins.helpers import PluginZipFile from project_manager.plugins.models import ( @@ -450,9 +446,6 @@ def test_class_inheritance(self): self.assertTrue( expr=issubclass(SubPluginPathSerializer, ProjectThroughMixin), ) - self.assertTrue( - expr=issubclass(SubPluginPathSerializer, AddProjectToViewMixin), - ) def test_get_field_names(self): obj = SubPluginPathSerializer( @@ -567,6 +560,33 @@ def test_meta_class(self): ) +class MinimalPluginSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(MinimalPluginSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(MinimalPluginSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=0, + ) + + def test_meta_class(self): + self.assertEqual( + first=MinimalPluginSerializer.Meta.model, + second=Plugin, + ) + self.assertTupleEqual( + tuple1=MinimalPluginSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + ), + ) + + class PluginReleaseBaseTestCase(TestCase): def test_base_attributes(self): self.assertEqual( diff --git a/project_manager/sub_plugins/api/common/__init__.py b/project_manager/sub_plugins/api/common/__init__.py new file mode 100644 index 00000000..0c8cbece --- /dev/null +++ b/project_manager/sub_plugins/api/common/__init__.py @@ -0,0 +1 @@ +"""Common SubPlugin functionality used by other apps.""" diff --git a/project_manager/sub_plugins/api/common/serializers.py b/project_manager/sub_plugins/api/common/serializers.py new file mode 100644 index 00000000..c5363ac5 --- /dev/null +++ b/project_manager/sub_plugins/api/common/serializers.py @@ -0,0 +1,38 @@ +"""SubPlugin serializers for APIs in other apps.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +from rest_framework.serializers import ModelSerializer + +# App +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.models import SubPlugin + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'MinimalSubPluginSerializer', +) + + +# ============================================================================= +# SERIALIZERS +# ============================================================================= +class MinimalSubPluginSerializer(ModelSerializer): + """Serializer for SubPlugin Contributions.""" + + plugin = MinimalPluginSerializer() + + class Meta: + """Define metaclass attributes.""" + + model = SubPlugin + fields = ( + 'name', + 'slug', + 'plugin', + ) diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index cd096896..a3ecd59c 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -16,13 +16,9 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.packages.api.serializers.common import ( - ReleasePackageRequirementSerializer, -) +from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer from project_manager.plugins.models import Plugin -from project_manager.sub_plugins.api.serializers.mixins import ( - SubPluginReleaseBase, -) +from project_manager.sub_plugins.api.serializers.mixins import SubPluginReleaseBase from project_manager.sub_plugins.models import ( SubPlugin, SubPluginContributor, diff --git a/project_manager/sub_plugins/api/tests/test_serializers.py b/project_manager/sub_plugins/api/tests/test_serializers.py index cd221c4f..31528015 100644 --- a/project_manager/sub_plugins/api/tests/test_serializers.py +++ b/project_manager/sub_plugins/api/tests/test_serializers.py @@ -11,7 +11,7 @@ # Third Party Django from rest_framework.exceptions import ValidationError from rest_framework.fields import ReadOnlyField -from rest_framework.serializers import ListSerializer +from rest_framework.serializers import ListSerializer, ModelSerializer # App from project_manager.common.api.serializers import ( @@ -23,9 +23,8 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.packages.api.serializers.common import ( - ReleasePackageRequirementSerializer, -) +from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer from project_manager.sub_plugins.api.serializers import ( SubPluginContributorSerializer, SubPluginCreateReleaseSerializer, @@ -40,6 +39,7 @@ SubPluginSerializer, SubPluginTagSerializer, ) +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer from project_manager.sub_plugins.api.serializers.mixins import SubPluginReleaseBase from project_manager.sub_plugins.helpers import SubPluginZipFile from project_manager.sub_plugins.models import ( @@ -532,6 +532,42 @@ def test_meta_class(self): ) +class MinimalSubPluginSerializerTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(MinimalSubPluginSerializer, ModelSerializer), + ) + + def test_declared_fields(self): + declared_fields = getattr(MinimalSubPluginSerializer, '_declared_fields') + self.assertEqual( + first=len(declared_fields), + second=1, + ) + self.assertIn( + member='plugin', + container=declared_fields, + ) + self.assertIsInstance( + obj=declared_fields['plugin'], + cls=MinimalPluginSerializer, + ) + + def test_meta_class(self): + self.assertEqual( + first=MinimalSubPluginSerializer.Meta.model, + second=SubPlugin, + ) + self.assertTupleEqual( + tuple1=MinimalSubPluginSerializer.Meta.fields, + tuple2=( + 'name', + 'slug', + 'plugin', + ), + ) + + class SubPluginReleaseBaseTestCase(TestCase): def test_base_attributes(self): self.assertEqual( diff --git a/users/api/common/__init__.py b/users/api/common/__init__.py new file mode 100644 index 00000000..881b5ebc --- /dev/null +++ b/users/api/common/__init__.py @@ -0,0 +1 @@ +"""Common User functionality used by other apps.""" diff --git a/users/api/serializers.py b/users/api/serializers.py index b026b331..e6590eec 100644 --- a/users/api/serializers.py +++ b/users/api/serializers.py @@ -8,9 +8,9 @@ from rest_framework.serializers import ModelSerializer # App -from project_manager.packages.models import Package -from project_manager.plugins.models import Plugin -from project_manager.sub_plugins.models import SubPlugin +from project_manager.packages.api.common.serializers import MinimalPackageSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer from users.models import ForumUser @@ -19,87 +19,37 @@ # ============================================================================= __all__ = ( 'ForumUserSerializer', - 'PackageContributionSerializer', - 'PluginContributionSerializer', - 'ProjectContributionSerializer', - 'SubPluginContributionSerializer', ) # ============================================================================= # SERIALIZERS # ============================================================================= -class ProjectContributionSerializer(ModelSerializer): - """Base class for Project contributions.""" - - class Meta: - """Define metaclass attributes.""" - - fields = ( - 'name', - 'slug', - ) - - -class PackageContributionSerializer(ProjectContributionSerializer): - """Serializer for Package Contributions.""" - - class Meta(ProjectContributionSerializer.Meta): - """Define metaclass attributes.""" - - model = Package - - -class PluginContributionSerializer(ProjectContributionSerializer): - """Serializer for Plugin Contributions.""" - - class Meta(ProjectContributionSerializer.Meta): - """Define metaclass attributes.""" - - model = Plugin - - -class SubPluginContributionSerializer(ModelSerializer): - """Serializer for SubPlugin Contributions.""" - - plugin = PluginContributionSerializer() - - class Meta: - """Define metaclass attributes.""" - - model = SubPlugin - fields = ( - 'name', - 'slug', - 'plugin', - ) - - class ForumUserSerializer(ModelSerializer): """Serializer for User Contributions.""" username = SerializerMethodField() - packages = PackageContributionSerializer( + packages = MinimalPackageSerializer( many=True, read_only=True, ) - package_contributions = PackageContributionSerializer( + package_contributions = MinimalPackageSerializer( many=True, read_only=True, ) - plugins = PluginContributionSerializer( + plugins = MinimalPluginSerializer( many=True, read_only=True, ) - plugin_contributions = PluginContributionSerializer( + plugin_contributions = MinimalPluginSerializer( many=True, read_only=True, ) - subplugins = SubPluginContributionSerializer( + subplugins = MinimalSubPluginSerializer( many=True, read_only=True, ) - subplugin_contributions = SubPluginContributionSerializer( + subplugin_contributions = MinimalSubPluginSerializer( many=True, read_only=True, ) diff --git a/users/api/tests/test_serializers.py b/users/api/tests/test_serializers.py index af9a5809..71afca28 100644 --- a/users/api/tests/test_serializers.py +++ b/users/api/tests/test_serializers.py @@ -9,17 +9,11 @@ from rest_framework.serializers import ListSerializer, ModelSerializer # App -from project_manager.packages.models import Package -from project_manager.plugins.models import Plugin -from project_manager.sub_plugins.models import SubPlugin -from users.api.serializers import ( - ForumUserSerializer, - PackageContributionSerializer, - PluginContributionSerializer, - ProjectContributionSerializer, - SubPluginContributionSerializer, -) -from users.api.serializers.common import ForumUserContributorSerializer +from project_manager.packages.api.common.serializers import MinimalPackageSerializer +from project_manager.plugins.api.common.serializers import MinimalPluginSerializer +from project_manager.sub_plugins.api.common.serializers import MinimalSubPluginSerializer +from users.api.serializers import ForumUserSerializer +from users.api.common.serializers import ForumUserContributorSerializer from users.models import ForumUser @@ -27,7 +21,6 @@ # TEST CASES # ============================================================================= class ForumUserSerializerTestCase(TestCase): - def test_class_inheritance(self): self.assertTrue( expr=issubclass(ForumUserSerializer, ModelSerializer), @@ -61,7 +54,7 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['packages'].read_only) self.assertIsInstance( obj=declared_fields['packages'].child, - cls=PackageContributionSerializer, + cls=MinimalPackageSerializer, ) self.assertTrue(expr=declared_fields['packages'].child.read_only) @@ -77,7 +70,7 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['package_contributions'].read_only) self.assertIsInstance( obj=declared_fields['package_contributions'].child, - cls=PackageContributionSerializer, + cls=MinimalPackageSerializer, ) self.assertTrue(expr=declared_fields['package_contributions'].child.read_only) @@ -93,7 +86,7 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['plugins'].read_only) self.assertIsInstance( obj=declared_fields['plugins'].child, - cls=PluginContributionSerializer, + cls=MinimalPluginSerializer, ) self.assertTrue(expr=declared_fields['plugins'].child.read_only) @@ -109,7 +102,7 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['plugin_contributions'].read_only) self.assertIsInstance( obj=declared_fields['plugin_contributions'].child, - cls=PluginContributionSerializer, + cls=MinimalPluginSerializer, ) self.assertTrue(expr=declared_fields['plugin_contributions'].child.read_only) @@ -125,7 +118,7 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['subplugins'].read_only) self.assertIsInstance( obj=declared_fields['subplugins'].child, - cls=SubPluginContributionSerializer, + cls=MinimalSubPluginSerializer, ) self.assertTrue(expr=declared_fields['subplugins'].child.read_only) @@ -141,7 +134,7 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['subplugin_contributions'].read_only) self.assertIsInstance( obj=declared_fields['subplugin_contributions'].child, - cls=SubPluginContributionSerializer, + cls=MinimalSubPluginSerializer, ) self.assertTrue(expr=declared_fields['subplugin_contributions'].child.read_only) @@ -165,132 +158,7 @@ def test_meta_class(self): ) -class PackageContributionSerializerTestCase(TestCase): - - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(PackageContributionSerializer, ProjectContributionSerializer), - ) - - def test_declared_fields(self): - declared_fields = getattr(PackageContributionSerializer, '_declared_fields') - self.assertEqual( - first=len(declared_fields), - second=0, - ) - - def test_meta_class(self): - self.assertEqual( - first=PackageContributionSerializer.Meta.model, - second=Package, - ) - self.assertTrue( - expr=issubclass( - PackageContributionSerializer.Meta, - ProjectContributionSerializer.Meta, - ), - ) - self.assertTupleEqual( - tuple1=PackageContributionSerializer.Meta.fields, - tuple2=ProjectContributionSerializer.Meta.fields, - ) - - -class PluginContributionSerializerTestCase(TestCase): - - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(PluginContributionSerializer, ProjectContributionSerializer), - ) - - def test_declared_fields(self): - declared_fields = getattr(PluginContributionSerializer, '_declared_fields') - self.assertEqual( - first=len(declared_fields), - second=0, - ) - - def test_meta_class(self): - self.assertEqual( - first=PluginContributionSerializer.Meta.model, - second=Plugin, - ) - self.assertTrue( - expr=issubclass( - PluginContributionSerializer.Meta, - ProjectContributionSerializer.Meta, - ), - ) - self.assertTupleEqual( - tuple1=PluginContributionSerializer.Meta.fields, - tuple2=ProjectContributionSerializer.Meta.fields, - ) - - -class ProjectContributionSerializerTestCase(TestCase): - - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectContributionSerializer, ModelSerializer), - ) - - def test_declared_fields(self): - declared_fields = getattr(ProjectContributionSerializer, '_declared_fields') - self.assertEqual( - first=len(declared_fields), - second=0, - ) - - def test_meta_class(self): - self.assertTupleEqual( - tuple1=ProjectContributionSerializer.Meta.fields, - tuple2=( - 'name', - 'slug', - ), - ) - - -class SubPluginContributionSerializerTestCase(TestCase): - - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginContributionSerializer, ModelSerializer), - ) - - def test_declared_fields(self): - declared_fields = getattr(SubPluginContributionSerializer, '_declared_fields') - self.assertEqual( - first=len(declared_fields), - second=1, - ) - - self.assertIn( - member='plugin', - container=declared_fields, - ) - self.assertIsInstance( - obj=declared_fields['plugin'], - cls=PluginContributionSerializer, - ) - - def test_meta_class(self): - self.assertEqual( - first=SubPluginContributionSerializer.Meta.model, - second=SubPlugin, - ) - self.assertTupleEqual( - tuple1=SubPluginContributionSerializer.Meta.fields, - tuple2=( - 'name', - 'slug', - 'plugin', - ), - ) - - class ForumUserContributorSerializerTestCase(TestCase): - def test_class_inheritance(self): self.assertTrue( expr=issubclass(ForumUserContributorSerializer, ModelSerializer), From 832a2ff2570a99edfcb947faf13f13555b21fe03 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 22:07:59 -0400 Subject: [PATCH 126/211] Minor fixes. --- project_manager/packages/constants.py | 5 ++++- project_manager/sub_plugins/constants.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/project_manager/packages/constants.py b/project_manager/packages/constants.py index dc607c71..a70e77d7 100644 --- a/project_manager/packages/constants.py +++ b/project_manager/packages/constants.py @@ -5,7 +5,10 @@ # ============================================================================= # App from project_manager.common.constants import ( - ALLOWED_FILE_TYPES, IMAGE_URL, LOGO_URL, READABLE_DATA_FILE_TYPES, + ALLOWED_FILE_TYPES, + IMAGE_URL, + LOGO_URL, + READABLE_DATA_FILE_TYPES, RELEASE_URL, ) diff --git a/project_manager/sub_plugins/constants.py b/project_manager/sub_plugins/constants.py index a613e9c6..b50a990b 100644 --- a/project_manager/sub_plugins/constants.py +++ b/project_manager/sub_plugins/constants.py @@ -5,7 +5,10 @@ # ============================================================================= # App from project_manager.common.constants import ( - ALLOWED_FILE_TYPES, IMAGE_URL, LOGO_URL, READABLE_DATA_FILE_TYPES, + ALLOWED_FILE_TYPES, + IMAGE_URL, + LOGO_URL, + READABLE_DATA_FILE_TYPES, RELEASE_URL, ) from project_manager.plugins.constants import PLUGIN_PATH, PLUGIN_DATA_PATH From 5062ffc14592e2a790b208ac10ac3f431c9679a7 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 22:08:38 -0400 Subject: [PATCH 127/211] Fixed some imports and some tests. --- .../common/api/serializers/__init__.py | 37 +++++----- .../common/api/serializers/mixins.py | 5 -- .../common/api/tests/test_serializers.py | 69 +++++++------------ project_manager/common/api/views/__init__.py | 3 +- 4 files changed, 44 insertions(+), 70 deletions(-) diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/common/api/serializers/__init__.py index 9a382b78..e0b9b7fa 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/common/api/serializers/__init__.py @@ -22,7 +22,6 @@ # App from project_manager.common.api.serializers.mixins import ( - AddProjectToViewMixin, CreateRequirementsMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, @@ -32,14 +31,12 @@ RELEASE_NOTES_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from games.api.serializers import GameSerializer +from games.api.common.serializers import MinimalGameSerializer from games.constants import GAME_SLUG_MAX_LENGTH from games.models import Game from tags.constants import TAG_NAME_MAX_LENGTH from tags.models import Tag -from users.api.serializers.common import ( - ForumUserContributorSerializer, -) +from users.api.common.serializers import ForumUserContributorSerializer from users.constants import USER_USERNAME_MAX_LENGTH from users.models import ForumUser @@ -317,21 +314,15 @@ class Meta: 'image', ) - def create(self, validated_data): - """Add the project to the validated_data when creating the image.""" - view = self.context['view'] - validated_data[view.project_type.replace('-', '_')] = view.project - return super().create(validated_data=validated_data) - -class ProjectGameSerializer(ProjectThroughMixin, AddProjectToViewMixin): +class ProjectGameSerializer(ProjectThroughMixin): """Base ProjectGame Serializer.""" game_slug = CharField( max_length=GAME_SLUG_MAX_LENGTH, write_only=True, ) - game = GameSerializer( + game = MinimalGameSerializer( read_only=True, ) @@ -347,21 +338,23 @@ def validate(self, attrs): """Validate the given game.""" name = attrs.pop('game_slug') view = self.context['view'] - if name in view.project.supported_games.values_list('slug', flat=True): + if view.project.supported_games.filter(slug=name).exists(): raise ValidationError({ 'game': f'Game already linked to {view.project_type}.', }) + try: game = Game.objects.get(basename=name) except Game.DoesNotExist as exception: raise ValidationError({ 'game': f'Invalid game "{name}".' }) from exception + attrs['game'] = game return super().validate(attrs=attrs) -class ProjectTagSerializer(ProjectThroughMixin, AddProjectToViewMixin): +class ProjectTagSerializer(ProjectThroughMixin): """Base ProjectTag Serializer.""" tag = CharField( @@ -379,10 +372,11 @@ def validate(self, attrs): """Validate the given tag.""" name = attrs['tag'] view = self.context['view'] - if name in view.project.tags.values_list('name', flat=True): + if view.project.tags.filter(name=name).exists(): raise ValidationError({ 'tag': f'Tag already linked to {view.project_type}.', }) + tag, created = Tag.objects.get_or_create( name=name, defaults={ @@ -393,11 +387,12 @@ def validate(self, attrs): raise ValidationError({ 'tag': f"Tag '{name}' is black-listed, unable to add.", }) + attrs['tag'] = tag return super().validate(attrs=attrs) -class ProjectContributorSerializer(ProjectThroughMixin, AddProjectToViewMixin): +class ProjectContributorSerializer(ProjectThroughMixin): """Base ProjectContributor Serializer.""" username = CharField( @@ -420,13 +415,11 @@ def validate(self, attrs): """Validate the given username.""" username = attrs.pop('username') view = self.context['view'] - if username in view.project.contributors.values_list( - 'user__username', - flat=True, - ): + if view.project.contributors.filter(user__username=username).exists(): raise ValidationError({ 'username': f'User {username} is already a contributor', }) + if username == view.project.owner.user.username: raise ValidationError({ 'username': ( @@ -434,11 +427,13 @@ def validate(self, attrs): f'cannot add as a contributor' ), }) + try: user = ForumUser.objects.get(user__username=username) except ForumUser.DoesNotExist as exception: raise ValidationError({ 'username': f'No user named "{username}".' }) from exception + attrs['user'] = user return super().validate(attrs=attrs) diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 6c1738af..2fbf5a1c 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -18,7 +18,6 @@ # ALL DECLARATION # ============================================================================= __all__ = ( - 'AddProjectToViewMixin', 'CreateRequirementsMixin', 'ProjectLocaleMixin', 'ProjectReleaseCreationMixin', @@ -233,10 +232,6 @@ def get_field_names(self, declared_fields, info): return field_names + ('id',) return field_names - -class AddProjectToViewMixin(ModelSerializer): - """Mixin used to add the project to the serialized data.""" - def validate(self, attrs): """Add the project to the validated data.""" view = self.context['view'] diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/common/api/tests/test_serializers.py index f1e359b3..21cd9509 100644 --- a/project_manager/common/api/tests/test_serializers.py +++ b/project_manager/common/api/tests/test_serializers.py @@ -19,7 +19,7 @@ from rest_framework.serializers import ModelSerializer # App -from games.api.serializers import GameSerializer +from games.api.common.serializers import MinimalGameSerializer from games.constants import GAME_SLUG_MAX_LENGTH from project_manager.common.api.serializers import ( ProjectContributorSerializer, @@ -31,7 +31,6 @@ ProjectTagSerializer, ) from project_manager.common.api.serializers.mixins import ( - AddProjectToViewMixin, CreateRequirementsMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, @@ -43,7 +42,7 @@ ) from tags.constants import TAG_NAME_MAX_LENGTH from test_utils.factories.users import ForumUserFactory -from users.api.serializers.common import ForumUserContributorSerializer +from users.api.common.serializers import ForumUserContributorSerializer from users.constants import USER_USERNAME_MAX_LENGTH @@ -182,6 +181,29 @@ def test_get_field_names_contributor_owner_only(self): tuple2=self.field_names, ) + def test_validate(self): + project_type = 'test-type' + project = mock.Mock() + obj = ProjectThroughMixin( + context={ + 'view': mock.Mock( + project_type=project_type, + project=project, + ), + }, + ) + original_attrs = { + 'field': 'value', + } + return_attrs = dict(original_attrs) + return_attrs.update({ + project_type.replace('-', '_'): project, + }) + self.assertDictEqual( + d1=obj.validate(original_attrs), + d2=return_attrs, + ) + class ProjectReleaseCreationMixinTestCase(TestCase): def test_class_inheritance(self): @@ -245,44 +267,11 @@ def test_get_project_kwargs_required(self): ) -class AddProjectToViewMixinTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(AddProjectToViewMixin, ModelSerializer), - ) - - def test_validate(self): - project_type = 'test-type' - project = mock.Mock() - obj = AddProjectToViewMixin( - context={ - 'view': mock.Mock( - project_type=project_type, - project=project, - ), - }, - ) - original_attrs = { - 'field': 'value', - } - return_attrs = dict(original_attrs) - return_attrs.update({ - project_type.replace('-', '_'): project, - }) - self.assertDictEqual( - d1=obj.validate(original_attrs), - d2=return_attrs, - ) - - class ProjectContributorSerializerTestCase(TestCase): def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectContributorSerializer, ProjectThroughMixin), ) - self.assertTrue( - expr=issubclass(ProjectContributorSerializer, AddProjectToViewMixin), - ) def test_declared_fields(self): declared_fields = getattr(ProjectContributorSerializer, '_declared_fields') @@ -390,9 +379,6 @@ def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectGameSerializer, ProjectThroughMixin), ) - self.assertTrue( - expr=issubclass(ProjectGameSerializer, AddProjectToViewMixin), - ) def test_declared_fields(self): declared_fields = getattr(ProjectGameSerializer, '_declared_fields') @@ -423,7 +409,7 @@ def test_declared_fields(self): field = declared_fields['game'] self.assertIsInstance( obj=field, - cls=GameSerializer, + cls=MinimalGameSerializer, ) self.assertTrue(expr=field.read_only) @@ -622,9 +608,6 @@ def test_class_inheritance(self): self.assertTrue( expr=issubclass(ProjectTagSerializer, ProjectThroughMixin), ) - self.assertTrue( - expr=issubclass(ProjectTagSerializer, AddProjectToViewMixin), - ) def test_declared_fields(self): declared_fields = getattr(ProjectTagSerializer, '_declared_fields') diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index 7b2b30f5..b3e5f54b 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -134,9 +134,10 @@ def check_object_permissions(self, request, obj): user_id = request.user.id if ( user_id != obj.owner.user.id and - user_id not in obj.contributors.values_list('user', flat=True) + not obj.contributors.filter(user=user_id).exists() ): raise PermissionDenied + return super().check_object_permissions( request=request, obj=obj, From 08fafbc9a2384eb3a099707f44ee1e2e6e6c6188 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 22:09:17 -0400 Subject: [PATCH 128/211] Updated a query to use exists instead of 'in '. --- project_manager/common/helpers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 3203c25b..895f737f 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -286,11 +286,10 @@ def _validate_custom_requirement(self, item): version = item.get('version') # TODO: update this logic to work with all version operators - available_versions = package.releases.values_list( - 'version', - flat=True, - ) - if version is not None and version not in available_versions: + if ( + version is not None and + not package.releases.filter(version=version).exists() + ): self.requirements_errors.append( f'Custom Package "{basename}" version "{version}", ' f'from requirements json file, not found.' From 8a9800e0ac59f1df8881b04943d69423b3d589c4 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 23:04:58 -0400 Subject: [PATCH 129/211] Removed a number of abstract classes to make the code more readable. --- project_manager/common/helpers.py | 6 - project_manager/common/models.py | 220 --------- project_manager/common/tests/test_helpers.py | 11 - project_manager/common/tests/test_models.py | 416 ------------------ project_manager/migrations/0001_initial.py | 44 +- .../{models/__init__.py => models.py} | 215 +++++++-- project_manager/packages/models/abstract.py | 52 --- project_manager/packages/tests/test_models.py | 84 +--- .../plugins/{models/__init__.py => models.py} | 214 +++++++-- project_manager/plugins/models/abstract.py | 52 --- project_manager/plugins/tests/test_models.py | 83 +--- .../{models/__init__.py => models.py} | 216 +++++++-- .../sub_plugins/models/abstract.py | 52 --- .../sub_plugins/tests/test_models.py | 84 +--- 14 files changed, 601 insertions(+), 1148 deletions(-) rename project_manager/packages/{models/__init__.py => models.py} (53%) delete mode 100644 project_manager/packages/models/abstract.py rename project_manager/plugins/{models/__init__.py => models.py} (61%) delete mode 100644 project_manager/plugins/models/abstract.py rename project_manager/sub_plugins/{models/__init__.py => models.py} (56%) delete mode 100644 project_manager/sub_plugins/models/abstract.py diff --git a/project_manager/common/helpers.py b/project_manager/common/helpers.py index 895f737f..2dbabe74 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/common/helpers.py @@ -25,7 +25,6 @@ 'GROUP_QUERYSET_NAMES', 'ProjectZipFile', 'find_image_number', - 'handle_project_image_upload', 'handle_project_logo_upload', 'handle_release_zip_file_upload', ) @@ -345,11 +344,6 @@ def find_image_number(directory, slug): return f'{max(map(int, current_files or [0])) + 1:04}' -def handle_project_image_upload(instance, filename): - """Handle uploading the image by directing to the proper directory.""" - return instance.handle_image_upload(filename) - - def handle_project_logo_upload(instance, filename): """Handle uploading the logo by directing to the proper directory.""" return instance.handle_logo_upload(filename) diff --git a/project_manager/common/models.py b/project_manager/common/models.py index d7a3c5c7..8c070d98 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -32,7 +32,6 @@ RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.helpers import ( - handle_project_image_upload, handle_project_logo_upload, handle_release_zip_file_upload, ) @@ -45,15 +44,7 @@ __all__ = ( 'AbstractUUIDPrimaryKeyModel', 'Project', - 'ProjectContributor', - 'ProjectGame', - 'ProjectImage', 'ProjectRelease', - 'ProjectReleaseDownloadRequirement', - 'ProjectReleasePackageRequirement', - 'ProjectReleasePyPiRequirement', - 'ProjectReleaseVersionControlRequirement', - 'ProjectTag', ) @@ -331,214 +322,3 @@ def save(self, *args, **kwargs): ).update( updated=self.created, ) - - -class ProjectImage(AbstractUUIDPrimaryKeyModel): - """Base model for project images.""" - - image = models.ImageField( - upload_to=handle_project_image_upload, - ) - created = AutoCreatedField( - verbose_name='created', - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - verbose_name = 'Image' - verbose_name_plural = 'Images' - - @property - def handle_image_upload(self): - """Return the function to use for handling image uploads.""" - raise NotImplementedError( - f'Class "{self.__class__.__name__}" must implement a ' - '"handle_image_upload" attribute.' - ) - - -class ProjectContributor(AbstractUUIDPrimaryKeyModel): - """Base through model for project contributors.""" - - user = models.ForeignKey( - to='users.ForumUser', - on_delete=models.CASCADE, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the base string.""" - return 'Project Contributor' - - @property - def project(self): - """Return the project's class.""" - raise NotImplementedError( - f'Class "{self.__class__.__name__}" must implement a ' - f'"project" property.' - ) - - def clean(self): - """Validate that the project's owner cannot be a contributor.""" - if hasattr(self, 'user') and self.project.owner == self.user: - raise ValidationError({ - 'user': ( - f'{self.user} is the owner and cannot be added ' - f'as a contributor.' - ) - }) - return super().clean() - - -class ProjectGame(AbstractUUIDPrimaryKeyModel): - """Base through model for project supported_games.""" - - game = models.ForeignKey( - to='games.Game', - on_delete=models.CASCADE, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the base string.""" - return 'Project Game' - - -class ProjectTag(AbstractUUIDPrimaryKeyModel): - """Base through model for project tags.""" - - tag = models.ForeignKey( - to='tags.Tag', - on_delete=models.CASCADE, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the base string.""" - return 'Project Tag' - - -class ProjectReleasePackageRequirement(AbstractUUIDPrimaryKeyModel): - """Base Package requirement model.""" - - package_requirement = models.ForeignKey( - to='project_manager.Package', - on_delete=models.CASCADE, - ) - version = models.CharField( - max_length=RELEASE_VERSION_MAX_LENGTH, - validators=[version_validator], - help_text=( - 'The version of the custom package for this release ' - 'of the project.' - ), - blank=True, - null=True, - ) - optional = models.BooleanField( - default=False, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the requirement's name and version.""" - return f'{self.package_requirement.name} - {self.version}' - - -class ProjectReleasePyPiRequirement(AbstractUUIDPrimaryKeyModel): - """Base PyPi requirement model.""" - - pypi_requirement = models.ForeignKey( - to='requirements.PyPiRequirement', - on_delete=models.CASCADE, - ) - version = models.CharField( - max_length=RELEASE_VERSION_MAX_LENGTH, - validators=[version_validator], - help_text=( - 'The version of the PyPi package for this release of the project.' - ), - blank=True, - null=True, - ) - optional = models.BooleanField( - default=False, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the requirement's name and version.""" - return f'{self.pypi_requirement.name} - {self.version}' - - -class ProjectReleaseVersionControlRequirement(AbstractUUIDPrimaryKeyModel): - """Base VCS requirement model.""" - - vcs_requirement = models.ForeignKey( - to='requirements.VersionControlRequirement', - on_delete=models.CASCADE, - ) - version = models.CharField( - max_length=RELEASE_VERSION_MAX_LENGTH, - validators=[version_validator], - help_text=( - 'The version of the VCS package for this release of the project.' - ), - blank=True, - null=True, - ) - optional = models.BooleanField( - default=False, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the requirement's name and version.""" - return f'{self.vcs_requirement.url} - {self.version}' - - -class ProjectReleaseDownloadRequirement(AbstractUUIDPrimaryKeyModel): - """Base Download requirement model.""" - - download_requirement = models.ForeignKey( - to='requirements.DownloadRequirement', - on_delete=models.CASCADE, - ) - optional = models.BooleanField( - default=False, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - def __str__(self): - """Return the requirement's url.""" - return self.download_requirement.url diff --git a/project_manager/common/tests/test_helpers.py b/project_manager/common/tests/test_helpers.py index 3beddc2b..ec92cef5 100644 --- a/project_manager/common/tests/test_helpers.py +++ b/project_manager/common/tests/test_helpers.py @@ -18,7 +18,6 @@ from project_manager.common.helpers import ( ProjectZipFile, find_image_number, - handle_project_image_upload, handle_project_logo_upload, handle_release_zip_file_upload, ) @@ -231,16 +230,6 @@ def test_find_image_number(self, mock_media_root): second=f'{max_value + 1:04}', ) - @staticmethod - def test_handle_project_image_upload(): - obj = mock.Mock() - filename = 'test.zip' - handle_project_image_upload( - instance=obj, - filename=filename, - ) - obj.handle_image_upload.assert_called_once_with(filename) - @staticmethod def test_handle_project_logo_upload(): obj = mock.Mock() diff --git a/project_manager/common/tests/test_models.py b/project_manager/common/tests/test_models.py index 98fa32c8..abd7f539 100644 --- a/project_manager/common/tests/test_models.py +++ b/project_manager/common/tests/test_models.py @@ -23,22 +23,13 @@ RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.helpers import ( - handle_project_image_upload, handle_project_logo_upload, handle_release_zip_file_upload, ) from project_manager.common.models import ( AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, - ProjectGame, - ProjectImage, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) from project_manager.common.validators import version_validator @@ -281,134 +272,6 @@ def test_meta_class(self): ) -class ProjectContributorTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectContributor, AbstractUUIDPrimaryKeyModel), - ) - - def test_user_field(self): - field = ProjectContributor._meta.get_field('user') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='users.ForumUser', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_project_required(self): - obj = '' - with self.assertRaises(NotImplementedError) as context: - ProjectContributor.project.fget(obj) - - self.assertEqual( - first=str(context.exception), - second=( - f'Class "{obj.__class__.__name__}" must implement a "project"' - f' property.' - ), - ) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectContributor._meta.abstract - ) - - -class ProjectGameTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectGame, AbstractUUIDPrimaryKeyModel), - ) - - def test_game_field(self): - field = ProjectGame._meta.get_field('game') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='games.Game', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectGame._meta.abstract - ) - - -class ProjectImageTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectImage, AbstractUUIDPrimaryKeyModel), - ) - - def test_image_field(self): - field = ProjectImage._meta.get_field('image') - self.assertIsInstance( - obj=field, - cls=models.ImageField, - ) - self.assertEqual( - first=field.upload_to, - second=handle_project_image_upload, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_created_field(self): - field = ProjectImage._meta.get_field('created') - self.assertIsInstance( - obj=field, - cls=AutoCreatedField, - ) - self.assertEqual( - first=field.verbose_name, - second='created', - ) - - def test_handle_image_upload_required(self): - obj = '' - with self.assertRaises(NotImplementedError) as context: - ProjectImage.handle_image_upload.fget(obj) - - self.assertEqual( - first=str(context.exception), - second=( - f'Class "{obj.__class__.__name__}" must implement a ' - f'"handle_image_upload" attribute.' - ), - ) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectImage._meta.abstract - ) - self.assertEqual( - first=ProjectImage._meta.verbose_name, - second='Image', - ) - self.assertEqual( - first=ProjectImage._meta.verbose_name_plural, - second='Images', - ) - - class ProjectReleaseTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( @@ -562,282 +425,3 @@ def test_meta_class(self): first=ProjectRelease._meta.verbose_name_plural, second='Releases', ) - - -class ProjectReleaseDownloadRequirementTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - ProjectReleaseDownloadRequirement, - AbstractUUIDPrimaryKeyModel, - ), - ) - - def test_download_requirement_field(self): - field = ProjectReleaseDownloadRequirement._meta.get_field( - 'download_requirement', - ) - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='requirements.DownloadRequirement', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_optional_field(self): - field = ProjectReleaseDownloadRequirement._meta.get_field('optional') - self.assertIsInstance( - obj=field, - cls=models.BooleanField, - ) - self.assertFalse(expr=field.default) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectReleaseDownloadRequirement._meta.abstract - ) - - -class ProjectReleasePackageRequirementTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - ProjectReleasePackageRequirement, - AbstractUUIDPrimaryKeyModel, - ), - ) - - def test_package_requirement_field(self): - field = ProjectReleasePackageRequirement._meta.get_field( - 'package_requirement', - ) - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.Package', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_version_field(self): - field = ProjectReleasePackageRequirement._meta.get_field('version') - self.assertIsInstance( - obj=field, - cls=models.CharField, - ) - self.assertEqual( - first=field.max_length, - second=RELEASE_VERSION_MAX_LENGTH, - ) - self.assertIn( - member=version_validator, - container=field.validators, - ) - self.assertEqual( - first=field.help_text, - second=( - 'The version of the custom package for this release of the ' - 'project.' - ) - ) - self.assertTrue(expr=field.blank) - self.assertTrue(expr=field.null) - - def test_optional_field(self): - field = ProjectReleasePackageRequirement._meta.get_field('optional') - self.assertIsInstance( - obj=field, - cls=models.BooleanField, - ) - self.assertFalse(expr=field.default) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectReleasePackageRequirement._meta.abstract - ) - - -class ProjectReleasePyPiRequirementTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - ProjectReleasePyPiRequirement, - AbstractUUIDPrimaryKeyModel, - ), - ) - - def test_pypi_requirement_field(self): - field = ProjectReleasePyPiRequirement._meta.get_field( - 'pypi_requirement', - ) - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='requirements.PyPiRequirement', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_version_field(self): - field = ProjectReleasePyPiRequirement._meta.get_field('version') - self.assertIsInstance( - obj=field, - cls=models.CharField, - ) - self.assertEqual( - first=field.max_length, - second=RELEASE_VERSION_MAX_LENGTH, - ) - self.assertIn( - member=version_validator, - container=field.validators, - ) - self.assertEqual( - first=field.help_text, - second=( - 'The version of the PyPi package for this release of the ' - 'project.' - ) - ) - self.assertTrue(expr=field.blank) - self.assertTrue(expr=field.null) - - def test_optional_field(self): - field = ProjectReleasePyPiRequirement._meta.get_field('optional') - self.assertIsInstance( - obj=field, - cls=models.BooleanField, - ) - self.assertFalse(expr=field.default) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectReleasePyPiRequirement._meta.abstract - ) - - -class ProjectReleaseVersionControlRequirementTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - ProjectReleaseVersionControlRequirement, - AbstractUUIDPrimaryKeyModel, - ), - ) - - def test_vcs_requirement_field(self): - field = ProjectReleaseVersionControlRequirement._meta.get_field( - 'vcs_requirement', - ) - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='requirements.VersionControlRequirement', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_version_field(self): - field = ProjectReleaseVersionControlRequirement._meta.get_field( - 'version', - ) - self.assertIsInstance( - obj=field, - cls=models.CharField, - ) - self.assertEqual( - first=field.max_length, - second=RELEASE_VERSION_MAX_LENGTH, - ) - self.assertIn( - member=version_validator, - container=field.validators, - ) - self.assertEqual( - first=field.help_text, - second=( - 'The version of the VCS package for this release of the ' - 'project.' - ) - ) - self.assertTrue(expr=field.blank) - self.assertTrue(expr=field.null) - - def test_optional_field(self): - field = ProjectReleaseVersionControlRequirement._meta.get_field( - 'optional', - ) - self.assertIsInstance( - obj=field, - cls=models.BooleanField, - ) - self.assertFalse(expr=field.default) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=ProjectReleaseVersionControlRequirement._meta.abstract - ) - - -class ProjectTagTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectTag, AbstractUUIDPrimaryKeyModel), - ) - - def test_tag_field(self): - field = ProjectTag._meta.get_field('tag') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='tags.Tag', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue(expr=ProjectTag._meta.abstract) diff --git a/project_manager/migrations/0001_initial.py b/project_manager/migrations/0001_initial.py index ecb4e37e..d581f042 100644 --- a/project_manager/migrations/0001_initial.py +++ b/project_manager/migrations/0001_initial.py @@ -7,7 +7,13 @@ import embed_video.fields import model_utils.fields import precise_bbcode.fields -import project_manager.common.helpers +from project_manager.common.helpers import ( + handle_project_logo_upload, + handle_release_zip_file_upload, +) +from project_manager.packages.helpers import handle_package_image_upload +from project_manager.plugins.helpers import handle_plugin_image_upload +from project_manager.sub_plugins.helpers import handle_sub_plugin_image_upload import uuid @@ -27,7 +33,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -57,7 +63,7 @@ class Migration(migrations.Migration): name='PackageImage', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), + ('image', models.ImageField(upload_to=handle_package_image_upload)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ @@ -73,7 +79,7 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], @@ -94,7 +100,7 @@ class Migration(migrations.Migration): name='PackageReleasePackageRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the package.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -102,7 +108,7 @@ class Migration(migrations.Migration): name='PackageReleasePyPiRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the package.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -110,7 +116,7 @@ class Migration(migrations.Migration): name='PackageReleaseVersionControlRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the package.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -128,7 +134,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -158,7 +164,7 @@ class Migration(migrations.Migration): name='PluginImage', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), + ('image', models.ImageField(upload_to=handle_plugin_image_upload)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ @@ -174,7 +180,7 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], @@ -195,7 +201,7 @@ class Migration(migrations.Migration): name='PluginReleasePackageRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -203,7 +209,7 @@ class Migration(migrations.Migration): name='PluginReleasePyPiRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -211,7 +217,7 @@ class Migration(migrations.Migration): name='PluginReleaseVersionControlRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -229,7 +235,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -261,7 +267,7 @@ class Migration(migrations.Migration): name='SubPluginImage', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=project_manager.common.helpers.handle_project_image_upload)), + ('image', models.ImageField(upload_to=handle_sub_plugin_image_upload)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ @@ -291,7 +297,7 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], @@ -312,7 +318,7 @@ class Migration(migrations.Migration): name='SubPluginReleasePackageRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the sub_plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -320,7 +326,7 @@ class Migration(migrations.Migration): name='SubPluginReleasePyPiRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the sub_plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), @@ -328,7 +334,7 @@ class Migration(migrations.Migration): name='SubPluginReleaseVersionControlRequirement', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the project.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), + ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the sub_plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], ), diff --git a/project_manager/packages/models/__init__.py b/project_manager/packages/models.py similarity index 53% rename from project_manager/packages/models/__init__.py rename to project_manager/packages/models.py index 6a6373eb..11ade44c 100644 --- a/project_manager/packages/models/__init__.py +++ b/project_manager/packages/models.py @@ -4,40 +4,35 @@ # IMPORTS # ============================================================================= # Django -from django.urls import reverse +from django.core.exceptions import ValidationError from django.db import models +from django.urls import reverse # Third Party Django +from model_utils.fields import AutoCreatedField from model_utils.tracker import FieldTracker # App from project_manager.common.constants import ( PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, + RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, - ProjectGame, - ProjectImage, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) -from project_manager.common.validators import basename_validator +from project_manager.common.validators import ( + basename_validator, + version_validator, +) from project_manager.packages.constants import PACKAGE_LOGO_URL from project_manager.packages.helpers import ( handle_package_image_upload, handle_package_logo_upload, handle_package_zip_upload, ) -from project_manager.packages.models.abstract import ( - PackageReleaseThroughBase, - PackageThroughBase, -) # ============================================================================= @@ -172,7 +167,7 @@ def get_absolute_url(https://github.com/self): ) -class PackageImage(ProjectImage): +class PackageImage(AbstractUUIDPrimaryKeyModel): """Package image type model.""" package = models.ForeignKey( @@ -180,76 +175,234 @@ class PackageImage(ProjectImage): related_name='images', on_delete=models.CASCADE, ) + image = models.ImageField( + upload_to=handle_package_image_upload, + ) + created = AutoCreatedField( + verbose_name='created', + ) - handle_image_upload = handle_package_image_upload + class Meta: + verbose_name = 'Image' + verbose_name_plural = 'Images' -class PackageContributor(PackageThroughBase, ProjectContributor): +class PackageContributor(AbstractUUIDPrimaryKeyModel): """Package contributors through model.""" + package = models.ForeignKey( + to='project_manager.Package', + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + to='users.ForumUser', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package', 'user') + @property + def project(self): + """Return the Package.""" + return self.package + + def __str__(self): + """Return the base string.""" + return 'Package Contributor' + + def clean(self): + """Validate that the package's owner cannot be a contributor.""" + if hasattr(self, 'user') and self.package.owner == self.user: + raise ValidationError({ + 'user': ( + f'{self.user} is the owner and cannot be added ' + f'as a contributor.' + ) + }) + + return super().clean() + -class PackageGame(ProjectGame, PackageThroughBase): +class PackageGame(AbstractUUIDPrimaryKeyModel): """Package supported_games through model.""" + package = models.ForeignKey( + to='project_manager.Package', + on_delete=models.CASCADE, + ) + game = models.ForeignKey( + to='games.Game', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package', 'game') + @property + def project(self): + """Return the Package.""" + return self.package + + def __str__(self): + """Return the base string.""" + return 'Package Game' + -class PackageTag(ProjectTag, PackageThroughBase): +class PackageTag(AbstractUUIDPrimaryKeyModel): """Package tags through model.""" + package = models.ForeignKey( + to='project_manager.Package', + on_delete=models.CASCADE, + ) + tag = models.ForeignKey( + to='tags.Tag', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package', 'tag') + @property + def project(self): + """Return the Package.""" + return self.package + + def __str__(self): + """Return the base string.""" + return 'Package Tag' + -class PackageReleaseDownloadRequirement( - ProjectReleaseDownloadRequirement, PackageReleaseThroughBase -): +class PackageReleaseDownloadRequirement(AbstractUUIDPrimaryKeyModel): """Package Download Requirement for Release model.""" + package_release = models.ForeignKey( + to='project_manager.PackageRelease', + on_delete=models.CASCADE, + ) + download_requirement = models.ForeignKey( + to='requirements.DownloadRequirement', + on_delete=models.CASCADE, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'download_requirement') + def __str__(self): + """Return the requirement's url.""" + return self.download_requirement.url + -class PackageReleasePackageRequirement( - ProjectReleasePackageRequirement, PackageReleaseThroughBase -): +class PackageReleasePackageRequirement(AbstractUUIDPrimaryKeyModel): """Package Package Requirement for Release model.""" + package_release = models.ForeignKey( + to='project_manager.PackageRelease', + on_delete=models.CASCADE, + ) + package_requirement = models.ForeignKey( + to='project_manager.Package', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the custom package for this release ' + 'of the package.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'package_requirement') + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.package_requirement.name} - {self.version}' + -class PackageReleasePyPiRequirement( - ProjectReleasePyPiRequirement, PackageReleaseThroughBase -): +class PackageReleasePyPiRequirement(AbstractUUIDPrimaryKeyModel): """Package PyPi Requirement for Release model.""" + package_release = models.ForeignKey( + to='project_manager.PackageRelease', + on_delete=models.CASCADE, + ) + pypi_requirement = models.ForeignKey( + to='requirements.PyPiRequirement', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the PyPi package for this release of the package.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'pypi_requirement') + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.pypi_requirement.name} - {self.version}' -class PackageReleaseVersionControlRequirement( - ProjectReleaseVersionControlRequirement, PackageReleaseThroughBase -): + +class PackageReleaseVersionControlRequirement(AbstractUUIDPrimaryKeyModel): """Package VCS Requirement for Release model.""" + package_release = models.ForeignKey( + to='project_manager.PackageRelease', + on_delete=models.CASCADE, + ) + vcs_requirement = models.ForeignKey( + to='requirements.VersionControlRequirement', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the VCS package for this release of the package.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'vcs_requirement') + + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.vcs_requirement.url} - {self.version}' diff --git a/project_manager/packages/models/abstract.py b/project_manager/packages/models/abstract.py deleted file mode 100644 index 86ba5a4f..00000000 --- a/project_manager/packages/models/abstract.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Base models for Packages.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.db import models - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'PackageReleaseThroughBase', - 'PackageThroughBase', -) - - -# ============================================================================= -# MODELS -# ============================================================================= -class PackageThroughBase(models.Model): - """Base through model class for Packages.""" - - package = models.ForeignKey( - to='project_manager.Package', - on_delete=models.CASCADE, - ) - - @property - def project(self): - """Return the Package.""" - return self.package - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - -class PackageReleaseThroughBase(models.Model): - """Base through model class for Packages.""" - - package_release = models.ForeignKey( - to='project_manager.PackageRelease', - on_delete=models.CASCADE, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index 70d9f596..906ecef1 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -26,10 +26,9 @@ PROJECT_SLUG_MAX_LENGTH, ) from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, ProjectGame, - ProjectImage, ProjectRelease, ProjectReleaseDownloadRequirement, ProjectReleasePackageRequirement, @@ -56,10 +55,6 @@ PackageReleaseVersionControlRequirement, PackageTag, ) -from project_manager.packages.models.abstract import ( - PackageReleaseThroughBase, - PackageThroughBase, -) from requirements.models import ( DownloadRequirement, PyPiRequirement, @@ -89,64 +84,6 @@ # ============================================================================= # TEST CASES # ============================================================================= -class PackageReleaseThroughBaseTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PackageReleaseThroughBase, models.Model) - ) - - def test_id_field(self): - field = PackageReleaseThroughBase._meta.get_field('package_release') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.PackageRelease', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=PackageReleaseThroughBase._meta.abstract - ) - - -class PackageThroughBaseTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PackageThroughBase, models.Model) - ) - - def test_id_field(self): - field = PackageThroughBase._meta.get_field('package') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.Package', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=PackageThroughBase._meta.abstract - ) - - class PackageTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( @@ -579,7 +516,7 @@ def test_meta_class(self): class PackageImageTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( - expr=issubclass(PackageImage, ProjectImage) + expr=issubclass(PackageImage, AbstractUUIDPrimaryKeyModel) ) def test_package_field(self): @@ -613,10 +550,7 @@ def test_primary_attributes(self): class PackageContributorTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( - expr=issubclass(PackageContributor, ProjectContributor) - ) - self.assertTrue( - expr=issubclass(PackageContributor, PackageThroughBase) + expr=issubclass(PackageContributor, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -672,7 +606,7 @@ def test_model_inheritance(self): expr=issubclass(PackageGame, ProjectGame) ) self.assertTrue( - expr=issubclass(PackageGame, PackageThroughBase) + expr=issubclass(PackageGame, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -694,7 +628,7 @@ def test_model_inheritance(self): expr=issubclass(PackageTag, ProjectTag) ) self.assertTrue( - expr=issubclass(PackageTag, PackageThroughBase) + expr=issubclass(PackageTag, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -721,7 +655,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleaseDownloadRequirement, - PackageReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -754,7 +688,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleasePackageRequirement, - PackageReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -789,7 +723,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleasePyPiRequirement, - PackageReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -824,7 +758,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleaseVersionControlRequirement, - PackageReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) diff --git a/project_manager/plugins/models/__init__.py b/project_manager/plugins/models.py similarity index 61% rename from project_manager/plugins/models/__init__.py rename to project_manager/plugins/models.py index 878073c6..f0746c59 100644 --- a/project_manager/plugins/models/__init__.py +++ b/project_manager/plugins/models.py @@ -5,41 +5,34 @@ # ============================================================================= # Django from django.core.exceptions import ValidationError -from django.urls import reverse from django.db import models +from django.urls import reverse # Third Party Django +from model_utils.fields import AutoCreatedField from model_utils.tracker import FieldTracker # App from project_manager.common.constants import ( PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, + RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, - ProjectGame, - ProjectImage, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) -from project_manager.common.validators import basename_validator -from project_manager.common.models import AbstractUUIDPrimaryKeyModel +from project_manager.common.validators import ( + basename_validator, + version_validator, +) from project_manager.plugins.constants import PLUGIN_LOGO_URL, PATH_MAX_LENGTH from project_manager.plugins.helpers import ( handle_plugin_image_upload, handle_plugin_logo_upload, handle_plugin_zip_upload, ) -from project_manager.plugins.models.abstract import ( - PluginReleaseThroughBase, - PluginThroughBase, -) from project_manager.plugins.validators import sub_plugin_path_validator @@ -176,7 +169,7 @@ def get_absolute_url(https://github.com/self): ) -class PluginImage(ProjectImage): +class PluginImage(AbstractUUIDPrimaryKeyModel): """Plugin image type model.""" plugin = models.ForeignKey( @@ -184,36 +177,109 @@ class PluginImage(ProjectImage): related_name='images', on_delete=models.CASCADE, ) + image = models.ImageField( + upload_to=handle_plugin_image_upload, + ) + created = AutoCreatedField( + verbose_name='created', + ) - handle_image_upload = handle_plugin_image_upload + class Meta: + verbose_name = 'Image' + verbose_name_plural = 'Images' -class PluginContributor(PluginThroughBase, ProjectContributor): +class PluginContributor(AbstractUUIDPrimaryKeyModel): """Plugin contributors through model.""" + plugin = models.ForeignKey( + to='project_manager.Plugin', + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + to='users.ForumUser', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin', 'user') + @property + def project(self): + """Return the Plugin.""" + return self.plugin + + def __str__(self): + """Return the base string.""" + return 'Plugin Contributor' + + def clean(self): + """Validate that the plugin's owner cannot be a contributor.""" + if hasattr(self, 'user') and self.plugin.owner == self.user: + raise ValidationError({ + 'user': ( + f'{self.user} is the owner and cannot be added ' + f'as a contributor.' + ) + }) + return super().clean() + -class PluginGame(ProjectGame, PluginThroughBase): +class PluginGame(AbstractUUIDPrimaryKeyModel): """Plugin supported_games through model.""" + plugin = models.ForeignKey( + to='project_manager.Plugin', + on_delete=models.CASCADE, + ) + game = models.ForeignKey( + to='games.Game', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin', 'game') + @property + def project(self): + """Return the Plugin.""" + return self.plugin -class PluginTag(ProjectTag, PluginThroughBase): + def __str__(self): + """Return the base string.""" + return 'Plugin Game' + + +class PluginTag(AbstractUUIDPrimaryKeyModel): """Plugin tags through model.""" + plugin = models.ForeignKey( + to='project_manager.Plugin', + on_delete=models.CASCADE, + ) + tag = models.ForeignKey( + to='tags.Tag', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin', 'tag') + @property + def project(self): + """Return the Plugin.""" + return self.plugin + + def __str__(self): + """Return the base string.""" + return 'Plugin Tag' + class SubPluginPath(AbstractUUIDPrimaryKeyModel): """Model to store SubPlugin paths for a Plugin.""" @@ -292,45 +358,129 @@ def get_absolute_url(https://github.com/self): ) -class PluginReleaseDownloadRequirement( - ProjectReleaseDownloadRequirement, PluginReleaseThroughBase -): +class PluginReleaseDownloadRequirement(AbstractUUIDPrimaryKeyModel): """Plugin Download Requirement for Release model.""" + plugin_release = models.ForeignKey( + to='project_manager.PluginRelease', + on_delete=models.CASCADE, + ) + download_requirement = models.ForeignKey( + to='requirements.DownloadRequirement', + on_delete=models.CASCADE, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'download_requirement') + def __str__(self): + """Return the requirement's url.""" + return self.download_requirement.url + -class PluginReleasePackageRequirement( - ProjectReleasePackageRequirement, PluginReleaseThroughBase -): +class PluginReleasePackageRequirement(AbstractUUIDPrimaryKeyModel): """Plugin Package Requirement for Release model.""" + plugin_release = models.ForeignKey( + to='project_manager.PluginRelease', + on_delete=models.CASCADE, + ) + package_requirement = models.ForeignKey( + to='project_manager.Package', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the custom package for this release ' + 'of the plugin.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'package_requirement') + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.package_requirement.name} - {self.version}' -class PluginReleasePyPiRequirement( - ProjectReleasePyPiRequirement, PluginReleaseThroughBase -): + +class PluginReleasePyPiRequirement(AbstractUUIDPrimaryKeyModel): """Plugin PyPi Requirement for Release model.""" + plugin_release = models.ForeignKey( + to='project_manager.PluginRelease', + on_delete=models.CASCADE, + ) + pypi_requirement = models.ForeignKey( + to='requirements.PyPiRequirement', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the PyPi package for this release of the plugin.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'pypi_requirement') + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.pypi_requirement.name} - {self.version}' + -class PluginReleaseVersionControlRequirement( - ProjectReleaseVersionControlRequirement, PluginReleaseThroughBase -): +class PluginReleaseVersionControlRequirement(AbstractUUIDPrimaryKeyModel): """Plugin VCS Requirement for Release model.""" + plugin_release = models.ForeignKey( + to='project_manager.PluginRelease', + on_delete=models.CASCADE, + ) + vcs_requirement = models.ForeignKey( + to='requirements.VersionControlRequirement', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the VCS package for this release of the plugin.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'vcs_requirement') + + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.vcs_requirement.url} - {self.version}' diff --git a/project_manager/plugins/models/abstract.py b/project_manager/plugins/models/abstract.py deleted file mode 100644 index 1b956256..00000000 --- a/project_manager/plugins/models/abstract.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Base models for Plugins.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.db import models - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'PluginReleaseThroughBase', - 'PluginThroughBase', -) - - -# ============================================================================= -# MODELS -# ============================================================================= -class PluginThroughBase(models.Model): - """Base through model class for Plugins.""" - - plugin = models.ForeignKey( - to='project_manager.Plugin', - on_delete=models.CASCADE, - ) - - @property - def project(self): - """Return the Plugin.""" - return self.plugin - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - -class PluginReleaseThroughBase(models.Model): - """Base through model class for Packages.""" - - plugin_release = models.ForeignKey( - to='project_manager.PluginRelease', - on_delete=models.CASCADE, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 83bc2f92..4124ac9b 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -28,9 +28,7 @@ from project_manager.common.models import ( AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, ProjectGame, - ProjectImage, ProjectRelease, ProjectReleaseDownloadRequirement, ProjectReleasePackageRequirement, @@ -59,10 +57,6 @@ PluginTag, SubPluginPath, ) -from project_manager.plugins.models.abstract import ( - PluginReleaseThroughBase, - PluginThroughBase, -) from project_manager.plugins.validators import sub_plugin_path_validator from requirements.models import ( DownloadRequirement, @@ -95,64 +89,6 @@ # ============================================================================= # TEST CASES # ============================================================================= -class PluginReleaseThroughBaseTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PluginReleaseThroughBase, models.Model) - ) - - def test_id_field(self): - field = PluginReleaseThroughBase._meta.get_field('plugin_release') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.PluginRelease', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=PluginReleaseThroughBase._meta.abstract - ) - - -class PluginThroughBaseTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PluginThroughBase, models.Model) - ) - - def test_id_field(self): - field = PluginThroughBase._meta.get_field('plugin') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.Plugin', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=PluginThroughBase._meta.abstract - ) - - class PluginTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( @@ -585,7 +521,7 @@ def test_meta_class(self): class PluginImageTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( - expr=issubclass(PluginImage, ProjectImage) + expr=issubclass(PluginImage, AbstractUUIDPrimaryKeyModel) ) def test_plugin_field(self): @@ -619,10 +555,7 @@ def test_primary_attributes(self): class PluginContributorTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( - expr=issubclass(PluginContributor, ProjectContributor) - ) - self.assertTrue( - expr=issubclass(PluginContributor, PluginThroughBase) + expr=issubclass(PluginContributor, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -678,7 +611,7 @@ def test_model_inheritance(self): expr=issubclass(PluginGame, ProjectGame) ) self.assertTrue( - expr=issubclass(PluginGame, PluginThroughBase) + expr=issubclass(PluginGame, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -700,7 +633,7 @@ def test_model_inheritance(self): expr=issubclass(PluginTag, ProjectTag) ) self.assertTrue( - expr=issubclass(PluginTag, PluginThroughBase) + expr=issubclass(PluginTag, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -727,7 +660,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleaseDownloadRequirement, - PluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -760,7 +693,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleasePackageRequirement, - PluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -795,7 +728,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleasePyPiRequirement, - PluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -830,7 +763,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleaseVersionControlRequirement, - PluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) diff --git a/project_manager/sub_plugins/models/__init__.py b/project_manager/sub_plugins/models.py similarity index 56% rename from project_manager/sub_plugins/models/__init__.py rename to project_manager/sub_plugins/models.py index 0a6716be..b152752d 100644 --- a/project_manager/sub_plugins/models/__init__.py +++ b/project_manager/sub_plugins/models.py @@ -4,40 +4,35 @@ # IMPORTS # ============================================================================= # Django -from django.urls import reverse +from django.core.exceptions import ValidationError from django.db import models +from django.urls import reverse # Third Party Django +from model_utils.fields import AutoCreatedField from model_utils.tracker import FieldTracker # App from project_manager.common.constants import ( PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, + RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, - ProjectGame, - ProjectImage, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) -from project_manager.common.validators import basename_validator +from project_manager.common.validators import ( + basename_validator, + version_validator, +) from project_manager.sub_plugins.constants import SUB_PLUGIN_LOGO_URL from project_manager.sub_plugins.helpers import ( handle_sub_plugin_image_upload, handle_sub_plugin_logo_upload, handle_sub_plugin_zip_upload, ) -from project_manager.sub_plugins.models.abstract import ( - SubPluginReleaseThroughBase, - SubPluginThroughBase, -) # ============================================================================= @@ -128,7 +123,7 @@ def get_absolute_url(https://github.com/self): ) def save(self, *args, **kwargs): - """Set the id using the plugin's slug and the project's slug.""" + """Set the id using the plugin's slug and the sub_plugin's slug.""" self.id = f'{self.plugin.slug}.{self.get_slug_value()}' super().save(*args, **kwargs) @@ -195,7 +190,7 @@ def get_absolute_url(https://github.com/self): ) -class SubPluginImage(ProjectImage): +class SubPluginImage(AbstractUUIDPrimaryKeyModel): """SubPlugin image type model.""" sub_plugin = models.ForeignKey( @@ -203,76 +198,233 @@ class SubPluginImage(ProjectImage): related_name='images', on_delete=models.CASCADE, ) + image = models.ImageField( + upload_to=handle_sub_plugin_image_upload, + ) + created = AutoCreatedField( + verbose_name='created', + ) - handle_image_upload = handle_sub_plugin_image_upload + class Meta: + verbose_name = 'Image' + verbose_name_plural = 'Images' -class SubPluginContributor(SubPluginThroughBase, ProjectContributor): +class SubPluginContributor(AbstractUUIDPrimaryKeyModel): """SubPlugin contributors through model.""" + sub_plugin = models.ForeignKey( + to='project_manager.SubPlugin', + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + to='users.ForumUser', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin', 'user') + @property + def project(self): + """Return the SubPlugin.""" + return self.sub_plugin -class SubPluginGame(ProjectGame, SubPluginThroughBase): + def __str__(self): + """Return the base string.""" + return 'SubPlugin Contributor' + + def clean(self): + """Validate that the sub_plugin's owner cannot be a contributor.""" + if hasattr(self, 'user') and self.sub_plugin.owner == self.user: + raise ValidationError({ + 'user': ( + f'{self.user} is the owner and cannot be added ' + f'as a contributor.' + ) + }) + return super().clean() + + +class SubPluginGame(AbstractUUIDPrimaryKeyModel): """SubPlugin supported_games through model.""" + sub_plugin = models.ForeignKey( + to='project_manager.SubPlugin', + on_delete=models.CASCADE, + ) + game = models.ForeignKey( + to='games.Game', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin', 'game') + @property + def project(self): + """Return the SubPlugin.""" + return self.sub_plugin + + def __str__(self): + """Return the base string.""" + return 'SubPlugin Game' + -class SubPluginTag(ProjectTag, SubPluginThroughBase): +class SubPluginTag(AbstractUUIDPrimaryKeyModel): """SubPlugin tags through model.""" + sub_plugin = models.ForeignKey( + to='project_manager.SubPlugin', + on_delete=models.CASCADE, + ) + tag = models.ForeignKey( + to='tags.Tag', + on_delete=models.CASCADE, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin', 'tag') + @property + def project(self): + """Return the SubPlugin.""" + return self.sub_plugin + + def __str__(self): + """Return the base string.""" + return 'SubPlugin Tag' + -class SubPluginReleaseDownloadRequirement( - ProjectReleaseDownloadRequirement, SubPluginReleaseThroughBase -): +class SubPluginReleaseDownloadRequirement(AbstractUUIDPrimaryKeyModel): """SubPlugin Download Requirement for Release model.""" + sub_plugin_release = models.ForeignKey( + to='project_manager.SubPluginRelease', + on_delete=models.CASCADE, + ) + download_requirement = models.ForeignKey( + to='requirements.DownloadRequirement', + on_delete=models.CASCADE, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'download_requirement') + def __str__(self): + """Return the requirement's url.""" + return self.download_requirement.url -class SubPluginReleasePackageRequirement( - ProjectReleasePackageRequirement, SubPluginReleaseThroughBase -): + +class SubPluginReleasePackageRequirement(AbstractUUIDPrimaryKeyModel): """SubPlugin Package Requirement for Release model.""" + sub_plugin_release = models.ForeignKey( + to='project_manager.SubPluginRelease', + on_delete=models.CASCADE, + ) + package_requirement = models.ForeignKey( + to='project_manager.Package', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the custom package for this release ' + 'of the sub_plugin.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'package_requirement') + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.package_requirement.name} - {self.version}' + -class SubPluginReleasePyPiRequirement( - ProjectReleasePyPiRequirement, SubPluginReleaseThroughBase -): +class SubPluginReleasePyPiRequirement(AbstractUUIDPrimaryKeyModel): """SubPlugin PyPi Requirement for Release model.""" + sub_plugin_release = models.ForeignKey( + to='project_manager.SubPluginRelease', + on_delete=models.CASCADE, + ) + pypi_requirement = models.ForeignKey( + to='requirements.PyPiRequirement', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the PyPi package for this release of the sub_plugin.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'pypi_requirement') + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.pypi_requirement.name} - {self.version}' + -class SubPluginReleaseVersionControlRequirement( - ProjectReleaseVersionControlRequirement, SubPluginReleaseThroughBase -): +class SubPluginReleaseVersionControlRequirement(AbstractUUIDPrimaryKeyModel): """SubPlugin VCS Requirement for Release model.""" + sub_plugin_release = models.ForeignKey( + to='project_manager.SubPluginRelease', + on_delete=models.CASCADE, + ) + vcs_requirement = models.ForeignKey( + to='requirements.VersionControlRequirement', + on_delete=models.CASCADE, + ) + version = models.CharField( + max_length=RELEASE_VERSION_MAX_LENGTH, + validators=[version_validator], + help_text=( + 'The version of the VCS package for this release of the sub_plugin.' + ), + blank=True, + null=True, + ) + optional = models.BooleanField( + default=False, + ) + class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'vcs_requirement') + + def __str__(self): + """Return the requirement's name and version.""" + return f'{self.vcs_requirement.url} - {self.version}' diff --git a/project_manager/sub_plugins/models/abstract.py b/project_manager/sub_plugins/models/abstract.py deleted file mode 100644 index 96be72eb..00000000 --- a/project_manager/sub_plugins/models/abstract.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Base models for SubPlugins.""" - -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.db import models - - -# ============================================================================= -# ALL DECLARATION -# ============================================================================= -__all__ = ( - 'SubPluginReleaseThroughBase', - 'SubPluginThroughBase', -) - - -# ============================================================================= -# MODELS -# ============================================================================= -class SubPluginThroughBase(models.Model): - """Base through model class for SubPlugins.""" - - sub_plugin = models.ForeignKey( - to='project_manager.SubPlugin', - on_delete=models.CASCADE, - ) - - @property - def project(self): - """Return the SubPlugin.""" - return self.sub_plugin - - class Meta: - """Define metaclass attributes.""" - - abstract = True - - -class SubPluginReleaseThroughBase(models.Model): - """Base through model class for Packages.""" - - sub_plugin_release = models.ForeignKey( - to='project_manager.SubPluginRelease', - on_delete=models.CASCADE, - ) - - class Meta: - """Define metaclass attributes.""" - - abstract = True diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index b3033773..f8d39147 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -26,10 +26,9 @@ PROJECT_SLUG_MAX_LENGTH, ) from project_manager.common.models import ( + AbstractUUIDPrimaryKeyModel, Project, - ProjectContributor, ProjectGame, - ProjectImage, ProjectRelease, ProjectReleaseDownloadRequirement, ProjectReleasePackageRequirement, @@ -58,10 +57,6 @@ SubPluginReleaseVersionControlRequirement, SubPluginTag, ) -from project_manager.sub_plugins.models.abstract import ( - SubPluginReleaseThroughBase, - SubPluginThroughBase, -) from requirements.models import ( DownloadRequirement, PyPiRequirement, @@ -92,64 +87,6 @@ # ============================================================================= # TEST CASES # ============================================================================= -class SubPluginReleaseThroughBaseTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginReleaseThroughBase, models.Model) - ) - - def test_id_field(self): - field = SubPluginReleaseThroughBase._meta.get_field('sub_plugin_release') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.SubPluginRelease', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=SubPluginReleaseThroughBase._meta.abstract - ) - - -class SubPluginThroughBaseTestCase(TestCase): - def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginThroughBase, models.Model) - ) - - def test_id_field(self): - field = SubPluginThroughBase._meta.get_field('sub_plugin') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='project_manager.SubPlugin', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - - def test_meta_class(self): - self.assertTrue( - expr=SubPluginThroughBase._meta.abstract - ) - - class SubPluginTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( @@ -610,7 +547,7 @@ def test_meta_class(self): class SubPluginImageTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( - expr=issubclass(SubPluginImage, ProjectImage) + expr=issubclass(SubPluginImage, AbstractUUIDPrimaryKeyModel) ) def test_plugin_field(self): @@ -644,10 +581,7 @@ def test_primary_attributes(self): class SubPluginContributorTestCase(TestCase): def test_model_inheritance(self): self.assertTrue( - expr=issubclass(SubPluginContributor, ProjectContributor) - ) - self.assertTrue( - expr=issubclass(SubPluginContributor, SubPluginThroughBase) + expr=issubclass(SubPluginContributor, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -703,7 +637,7 @@ def test_model_inheritance(self): expr=issubclass(SubPluginGame, ProjectGame) ) self.assertTrue( - expr=issubclass(SubPluginGame, SubPluginThroughBase) + expr=issubclass(SubPluginGame, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -725,7 +659,7 @@ def test_model_inheritance(self): expr=issubclass(SubPluginTag, ProjectTag) ) self.assertTrue( - expr=issubclass(SubPluginTag, SubPluginThroughBase) + expr=issubclass(SubPluginTag, AbstractUUIDPrimaryKeyModel) ) def test__str__(self): @@ -752,7 +686,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleaseDownloadRequirement, - SubPluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -785,7 +719,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleasePackageRequirement, - SubPluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -820,7 +754,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleasePyPiRequirement, - SubPluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) @@ -855,7 +789,7 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleaseVersionControlRequirement, - SubPluginReleaseThroughBase, + AbstractUUIDPrimaryKeyModel, ) ) From 617e3b282416da6f4187768b9dde264cfbce96ab Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 26 Mar 2022 23:55:45 -0400 Subject: [PATCH 130/211] Updated tests for model fields moving from abstract classes. --- project_manager/packages/models.py | 2 + project_manager/packages/tests/test_models.py | 435 +++++++++++++++-- project_manager/plugins/models.py | 2 + project_manager/plugins/tests/test_models.py | 435 +++++++++++++++-- project_manager/sub_plugins/models.py | 2 + .../sub_plugins/tests/test_models.py | 437 ++++++++++++++++-- 6 files changed, 1192 insertions(+), 121 deletions(-) diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index 11ade44c..42baf7a9 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -183,6 +183,8 @@ class PackageImage(AbstractUUIDPrimaryKeyModel): ) class Meta: + """Define metaclass attributes.""" + verbose_name = 'Image' verbose_name_plural = 'Images' diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index 906ecef1..91cc5649 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -14,6 +14,7 @@ from django.utils.timezone import now # Third Party Django +from model_utils.fields import AutoCreatedField from model_utils.tracker import FieldTracker # App @@ -23,20 +24,14 @@ LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, PROJECT_BASENAME_MAX_LENGTH, - PROJECT_SLUG_MAX_LENGTH, + PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.models import ( AbstractUUIDPrimaryKeyModel, Project, - ProjectGame, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) -from project_manager.common.validators import basename_validator +from project_manager.common.validators import basename_validator, version_validator from project_manager.packages.constants import PACKAGE_LOGO_URL from project_manager.packages.helpers import ( handle_package_image_upload, @@ -540,11 +535,39 @@ def test_package_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) - def test_primary_attributes(self): + def test_image_field(self): + field = PackageImage._meta.get_field('image') + self.assertIsInstance( + obj=field, + cls=models.ImageField, + ) self.assertEqual( - first=PackageImage.handle_image_upload, + first=field.upload_to, second=handle_package_image_upload, ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_created_field(self): + field = PackageImage._meta.get_field('created') + self.assertIsInstance( + obj=field, + cls=AutoCreatedField, + ) + self.assertEqual( + first=field.verbose_name, + second='created', + ) + + def test_meta_class(self): + self.assertEqual( + first=PackageImage._meta.verbose_name, + second='Image', + ) + self.assertEqual( + first=PackageImage._meta.verbose_name_plural, + second='Images', + ) class PackageContributorTestCase(TestCase): @@ -553,10 +576,44 @@ def test_model_inheritance(self): expr=issubclass(PackageContributor, AbstractUUIDPrimaryKeyModel) ) + def test_package_field(self): + field = PackageContributor._meta.get_field('package') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_user_field(self): + field = PackageContributor._meta.get_field('user') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(PackageContributorFactory()), - second='Project Contributor', + second='Package Contributor', ) def test_clean(self): @@ -602,17 +659,48 @@ def test_meta_class(self): class PackageGameTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PackageGame, ProjectGame) - ) self.assertTrue( expr=issubclass(PackageGame, AbstractUUIDPrimaryKeyModel) ) + def test_package_field(self): + field = PackageGame._meta.get_field('package') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_game_field(self): + field = PackageGame._meta.get_field('game') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Game, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(PackageGameFactory()), - second='Project Game', + second='Package Game', ) def test_meta_class(self): @@ -624,17 +712,48 @@ def test_meta_class(self): class PackageTagTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PackageTag, ProjectTag) - ) self.assertTrue( expr=issubclass(PackageTag, AbstractUUIDPrimaryKeyModel) ) + def test_package_field(self): + field = PackageTag._meta.get_field('package') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_tag_field(self): + field = PackageTag._meta.get_field('tag') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Tag, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(PackageTagFactory()), - second='Project Tag', + second='Package Tag', ) def test_meta_class(self): @@ -646,12 +765,6 @@ def test_meta_class(self): class PackageReleaseDownloadRequirementTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - PackageReleaseDownloadRequirement, - ProjectReleaseDownloadRequirement, - ) - ) self.assertTrue( expr=issubclass( PackageReleaseDownloadRequirement, @@ -659,6 +772,52 @@ def test_model_inheritance(self): ) ) + def test_package_release_field(self): + field = PackageReleaseDownloadRequirement._meta.get_field('package_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PackageRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_requirement_field(self): + field = PackageReleaseDownloadRequirement._meta.get_field( + 'download_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=DownloadRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_optional_field(self): + field = PackageReleaseDownloadRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): requirement = DownloadRequirementFactory() self.assertEqual( @@ -682,15 +841,79 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleasePackageRequirement, - ProjectReleasePackageRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - PackageReleasePackageRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_package_release_field(self): + field = PackageReleasePackageRequirement._meta.get_field('package_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PackageRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_package_requirement_field(self): + field = PackageReleasePackageRequirement._meta.get_field( + 'package_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = PackageReleasePackageRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the custom package for this release of the ' + 'package.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = PackageReleasePackageRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = PackageFactory() @@ -717,15 +940,79 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleasePyPiRequirement, - ProjectReleasePyPiRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - PackageReleasePyPiRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_package_release_field(self): + field = PackageReleasePyPiRequirement._meta.get_field('package_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PackageRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_pypi_requirement_field(self): + field = PackageReleasePyPiRequirement._meta.get_field( + 'pypi_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PyPiRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = PackageReleasePyPiRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the PyPi package for this release of the ' + 'package.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = PackageReleasePyPiRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = PyPiRequirementFactory() @@ -752,15 +1039,83 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PackageReleaseVersionControlRequirement, - ProjectReleaseVersionControlRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - PackageReleaseVersionControlRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_package_release_field(self): + field = PackageReleaseVersionControlRequirement._meta.get_field('package_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PackageRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_vcs_requirement_field(self): + field = PackageReleaseVersionControlRequirement._meta.get_field( + 'vcs_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=VersionControlRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = PackageReleaseVersionControlRequirement._meta.get_field( + 'version', + ) + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the VCS package for this release of the ' + 'package.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = PackageReleaseVersionControlRequirement._meta.get_field( + 'optional', + ) + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = VersionControlRequirementFactory() diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index f0746c59..261ad149 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -185,6 +185,8 @@ class PluginImage(AbstractUUIDPrimaryKeyModel): ) class Meta: + """Define metaclass attributes.""" + verbose_name = 'Image' verbose_name_plural = 'Images' diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 4124ac9b..15413e5a 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -14,6 +14,7 @@ from django.utils.timezone import now # Third Party Django +from model_utils.fields import AutoCreatedField from model_utils.tracker import FieldTracker # App @@ -23,20 +24,14 @@ LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, PROJECT_BASENAME_MAX_LENGTH, - PROJECT_SLUG_MAX_LENGTH, + PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.models import ( AbstractUUIDPrimaryKeyModel, Project, - ProjectGame, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) -from project_manager.common.validators import basename_validator +from project_manager.common.validators import basename_validator, version_validator from project_manager.packages.models import Package from project_manager.plugins.constants import PLUGIN_LOGO_URL, PATH_MAX_LENGTH from project_manager.plugins.helpers import ( @@ -545,11 +540,39 @@ def test_plugin_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) - def test_primary_attributes(self): + def test_image_field(self): + field = PluginImage._meta.get_field('image') + self.assertIsInstance( + obj=field, + cls=models.ImageField, + ) self.assertEqual( - first=PluginImage.handle_image_upload, + first=field.upload_to, second=handle_plugin_image_upload, ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_created_field(self): + field = PluginImage._meta.get_field('created') + self.assertIsInstance( + obj=field, + cls=AutoCreatedField, + ) + self.assertEqual( + first=field.verbose_name, + second='created', + ) + + def test_meta_class(self): + self.assertEqual( + first=PluginImage._meta.verbose_name, + second='Image', + ) + self.assertEqual( + first=PluginImage._meta.verbose_name_plural, + second='Images', + ) class PluginContributorTestCase(TestCase): @@ -558,10 +581,44 @@ def test_model_inheritance(self): expr=issubclass(PluginContributor, AbstractUUIDPrimaryKeyModel) ) + def test_plugin_field(self): + field = PluginContributor._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_user_field(self): + field = PluginContributor._meta.get_field('user') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(PluginContributorFactory()), - second='Project Contributor', + second='Plugin Contributor', ) def test_clean(self): @@ -607,17 +664,48 @@ def test_meta_class(self): class PluginGameTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PluginGame, ProjectGame) - ) self.assertTrue( expr=issubclass(PluginGame, AbstractUUIDPrimaryKeyModel) ) + def test_plugin_field(self): + field = PluginGame._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_game_field(self): + field = PluginGame._meta.get_field('game') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Game, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(PluginGameFactory()), - second='Project Game', + second='Plugin Game', ) def test_meta_class(self): @@ -629,17 +717,48 @@ def test_meta_class(self): class PluginTagTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(PluginTag, ProjectTag) - ) self.assertTrue( expr=issubclass(PluginTag, AbstractUUIDPrimaryKeyModel) ) + def test_plugin_field(self): + field = PluginTag._meta.get_field('plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Plugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_tag_field(self): + field = PluginTag._meta.get_field('tag') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Tag, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(PluginTagFactory()), - second='Project Tag', + second='Plugin Tag', ) def test_meta_class(self): @@ -651,12 +770,6 @@ def test_meta_class(self): class PluginReleaseDownloadRequirementTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - PluginReleaseDownloadRequirement, - ProjectReleaseDownloadRequirement, - ) - ) self.assertTrue( expr=issubclass( PluginReleaseDownloadRequirement, @@ -664,6 +777,52 @@ def test_model_inheritance(self): ) ) + def test_plugin_release_field(self): + field = PluginReleaseDownloadRequirement._meta.get_field('plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_requirement_field(self): + field = PluginReleaseDownloadRequirement._meta.get_field( + 'download_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=DownloadRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_optional_field(self): + field = PluginReleaseDownloadRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): requirement = DownloadRequirementFactory() self.assertEqual( @@ -687,15 +846,79 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleasePackageRequirement, - ProjectReleasePackageRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - PluginReleasePackageRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_plugin_release_field(self): + field = PluginReleasePackageRequirement._meta.get_field('plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_package_requirement_field(self): + field = PluginReleasePackageRequirement._meta.get_field( + 'package_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = PluginReleasePackageRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the custom package for this release of the ' + 'plugin.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = PluginReleasePackageRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = PackageFactory() @@ -722,15 +945,79 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleasePyPiRequirement, - ProjectReleasePyPiRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - PluginReleasePyPiRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_plugin_release_field(self): + field = PluginReleasePyPiRequirement._meta.get_field('plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_pypi_requirement_field(self): + field = PluginReleasePyPiRequirement._meta.get_field( + 'pypi_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PyPiRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = PluginReleasePyPiRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the PyPi package for this release of the ' + 'plugin.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = PluginReleasePyPiRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = PyPiRequirementFactory() @@ -757,15 +1044,83 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( PluginReleaseVersionControlRequirement, - ProjectReleaseVersionControlRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - PluginReleaseVersionControlRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_plugin_release_field(self): + field = PluginReleaseVersionControlRequirement._meta.get_field('plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_vcs_requirement_field(self): + field = PluginReleaseVersionControlRequirement._meta.get_field( + 'vcs_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=VersionControlRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = PluginReleaseVersionControlRequirement._meta.get_field( + 'version', + ) + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the VCS package for this release of the ' + 'plugin.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = PluginReleaseVersionControlRequirement._meta.get_field( + 'optional', + ) + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = VersionControlRequirementFactory() diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index b152752d..d42b9455 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -206,6 +206,8 @@ class SubPluginImage(AbstractUUIDPrimaryKeyModel): ) class Meta: + """Define metaclass attributes.""" + verbose_name = 'Image' verbose_name_plural = 'Images' diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index f8d39147..b1471dd7 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -14,6 +14,7 @@ from django.utils.timezone import now # Third Party Django +from model_utils.fields import AutoCreatedField from model_utils.tracker import FieldTracker # App @@ -23,20 +24,14 @@ LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, PROJECT_BASENAME_MAX_LENGTH, - PROJECT_SLUG_MAX_LENGTH, + PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) from project_manager.common.models import ( AbstractUUIDPrimaryKeyModel, Project, - ProjectGame, ProjectRelease, - ProjectReleaseDownloadRequirement, - ProjectReleasePackageRequirement, - ProjectReleasePyPiRequirement, - ProjectReleaseVersionControlRequirement, - ProjectTag, ) -from project_manager.common.validators import basename_validator +from project_manager.common.validators import basename_validator, version_validator from project_manager.packages.models import Package from project_manager.plugins.models import Plugin from project_manager.sub_plugins.constants import SUB_PLUGIN_LOGO_URL @@ -550,7 +545,7 @@ def test_model_inheritance(self): expr=issubclass(SubPluginImage, AbstractUUIDPrimaryKeyModel) ) - def test_plugin_field(self): + def test_sub_plugin_field(self): field = SubPluginImage._meta.get_field('sub_plugin') self.assertIsInstance( obj=field, @@ -571,11 +566,39 @@ def test_plugin_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) - def test_primary_attributes(self): + def test_image_field(self): + field = SubPluginImage._meta.get_field('image') + self.assertIsInstance( + obj=field, + cls=models.ImageField, + ) self.assertEqual( - first=SubPluginImage.handle_image_upload, + first=field.upload_to, second=handle_sub_plugin_image_upload, ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_created_field(self): + field = SubPluginImage._meta.get_field('created') + self.assertIsInstance( + obj=field, + cls=AutoCreatedField, + ) + self.assertEqual( + first=field.verbose_name, + second='created', + ) + + def test_meta_class(self): + self.assertEqual( + first=SubPluginImage._meta.verbose_name, + second='Image', + ) + self.assertEqual( + first=SubPluginImage._meta.verbose_name_plural, + second='Images', + ) class SubPluginContributorTestCase(TestCase): @@ -584,10 +607,44 @@ def test_model_inheritance(self): expr=issubclass(SubPluginContributor, AbstractUUIDPrimaryKeyModel) ) + def test_sub_plugin_field(self): + field = SubPluginContributor._meta.get_field('sub_plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPlugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_user_field(self): + field = SubPluginContributor._meta.get_field('user') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(SubPluginContributorFactory()), - second='Project Contributor', + second='SubPlugin Contributor', ) def test_clean(self): @@ -633,17 +690,48 @@ def test_meta_class(self): class SubPluginGameTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginGame, ProjectGame) - ) self.assertTrue( expr=issubclass(SubPluginGame, AbstractUUIDPrimaryKeyModel) ) + def test_sub_plugin_field(self): + field = SubPluginGame._meta.get_field('sub_plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPlugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_game_field(self): + field = SubPluginGame._meta.get_field('game') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Game, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(SubPluginGameFactory()), - second='Project Game', + second='SubPlugin Game', ) def test_meta_class(self): @@ -655,17 +743,48 @@ def test_meta_class(self): class SubPluginTagTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass(SubPluginTag, ProjectTag) - ) self.assertTrue( expr=issubclass(SubPluginTag, AbstractUUIDPrimaryKeyModel) ) + def test_sub_plugin_field(self): + field = SubPluginTag._meta.get_field('sub_plugin') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPlugin, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_tag_field(self): + field = SubPluginTag._meta.get_field('tag') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Tag, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): self.assertEqual( first=str(SubPluginTagFactory()), - second='Project Tag', + second='SubPlugin Tag', ) def test_meta_class(self): @@ -677,12 +796,6 @@ def test_meta_class(self): class SubPluginReleaseDownloadRequirementTestCase(TestCase): def test_model_inheritance(self): - self.assertTrue( - expr=issubclass( - SubPluginReleaseDownloadRequirement, - ProjectReleaseDownloadRequirement, - ) - ) self.assertTrue( expr=issubclass( SubPluginReleaseDownloadRequirement, @@ -690,6 +803,52 @@ def test_model_inheritance(self): ) ) + def test_sub_plugin_release_field(self): + field = SubPluginReleaseDownloadRequirement._meta.get_field('sub_plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_download_requirement_field(self): + field = SubPluginReleaseDownloadRequirement._meta.get_field( + 'download_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=DownloadRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_optional_field(self): + field = SubPluginReleaseDownloadRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test__str__(self): requirement = DownloadRequirementFactory() self.assertEqual( @@ -713,15 +872,79 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleasePackageRequirement, - ProjectReleasePackageRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - SubPluginReleasePackageRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_sub_plugin_release_field(self): + field = SubPluginReleasePackageRequirement._meta.get_field('sub_plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_package_requirement_field(self): + field = SubPluginReleasePackageRequirement._meta.get_field( + 'package_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=Package, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = SubPluginReleasePackageRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the custom package for this release of the ' + 'sub_plugin.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = SubPluginReleasePackageRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = PackageFactory() @@ -748,15 +971,79 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleasePyPiRequirement, - ProjectReleasePyPiRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - SubPluginReleasePyPiRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_sub_plugin_release_field(self): + field = SubPluginReleasePyPiRequirement._meta.get_field('sub_plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_pypi_requirement_field(self): + field = SubPluginReleasePyPiRequirement._meta.get_field( + 'pypi_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=PyPiRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = SubPluginReleasePyPiRequirement._meta.get_field('version') + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the PyPi package for this release of the ' + 'sub_plugin.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = SubPluginReleasePyPiRequirement._meta.get_field('optional') + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = PyPiRequirementFactory() @@ -783,15 +1070,83 @@ def test_model_inheritance(self): self.assertTrue( expr=issubclass( SubPluginReleaseVersionControlRequirement, - ProjectReleaseVersionControlRequirement, + AbstractUUIDPrimaryKeyModel, ) ) - self.assertTrue( - expr=issubclass( - SubPluginReleaseVersionControlRequirement, - AbstractUUIDPrimaryKeyModel, + + def test_sub_plugin_release_field(self): + field = SubPluginReleaseVersionControlRequirement._meta.get_field('sub_plugin_release') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=SubPluginRelease, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_vcs_requirement_field(self): + field = SubPluginReleaseVersionControlRequirement._meta.get_field( + 'vcs_requirement', + ) + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=VersionControlRequirement, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + + def test_version_field(self): + field = SubPluginReleaseVersionControlRequirement._meta.get_field( + 'version', + ) + self.assertIsInstance( + obj=field, + cls=models.CharField, + ) + self.assertEqual( + first=field.max_length, + second=RELEASE_VERSION_MAX_LENGTH, + ) + self.assertIn( + member=version_validator, + container=field.validators, + ) + self.assertEqual( + first=field.help_text, + second=( + 'The version of the VCS package for this release of the ' + 'sub_plugin.' ) ) + self.assertTrue(expr=field.blank) + self.assertTrue(expr=field.null) + + def test_optional_field(self): + field = SubPluginReleaseVersionControlRequirement._meta.get_field( + 'optional', + ) + self.assertIsInstance( + obj=field, + cls=models.BooleanField, + ) + self.assertFalse(expr=field.default) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) def test__str__(self): requirement = VersionControlRequirementFactory() From 42aeb9ab50b4cfee146dd18e0a3d4bda3c08d01b Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 00:55:31 -0400 Subject: [PATCH 131/211] Moved Project.owner and ProjectRelease.created_by to each of their respective sub-classes in order to utilize related_name better. --- project_manager/common/models.py | 11 ---- project_manager/common/tests/test_models.py | 42 ---------------- project_manager/packages/models.py | 11 ++++ project_manager/packages/tests/test_models.py | 42 ++++++++++++++++ project_manager/plugins/models.py | 11 ++++ project_manager/plugins/tests/test_models.py | 42 ++++++++++++++++ project_manager/sub_plugins/models.py | 17 +++++-- .../sub_plugins/tests/test_models.py | 50 +++++++++++++++++-- 8 files changed, 166 insertions(+), 60 deletions(-) diff --git a/project_manager/common/models.py b/project_manager/common/models.py index 8c070d98..373f97a2 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -105,11 +105,6 @@ class Project(models.Model): null=True, help_text="The project's video." ) - owner = models.ForeignKey( - to='users.ForumUser', - related_name='%(class)ss', - on_delete=models.CASCADE, - ) synopsis = BBCodeTextField( max_length=PROJECT_SYNOPSIS_MAX_LENGTH, blank=True, @@ -252,12 +247,6 @@ class ProjectRelease(AbstractUUIDPrimaryKeyModel): created = AutoCreatedField( verbose_name='created', ) - created_by = models.ForeignKey( - to='users.ForumUser', - related_name='%(class)ss', - on_delete=models.SET_NULL, - null=True, - ) field_tracker = None diff --git a/project_manager/common/tests/test_models.py b/project_manager/common/tests/test_models.py index abd7f539..ce91e16c 100644 --- a/project_manager/common/tests/test_models.py +++ b/project_manager/common/tests/test_models.py @@ -163,27 +163,6 @@ def test_video_field(self): self.assertFalse(expr=field.blank) self.assertTrue(expr=field.null) - def test_owner_field(self): - field = Project._meta.get_field('owner') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='users.ForumUser', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.CASCADE, - ) - self.assertEqual( - first=field.remote_field.related_name, - second='%(class)ss', - ) - self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) - def test_synopsis_field(self): field = Project._meta.get_field('synopsis') self.assertIsInstance( @@ -353,27 +332,6 @@ def test_created_field(self): second='created', ) - def test_created_by_field(self): - field = ProjectRelease._meta.get_field('created_by') - self.assertIsInstance( - obj=field, - cls=models.ForeignKey, - ) - self.assertEqual( - first=field.remote_field.model, - second='users.ForumUser', - ) - self.assertEqual( - first=field.remote_field.on_delete, - second=models.SET_NULL, - ) - self.assertEqual( - first=field.remote_field.related_name, - second='%(class)ss', - ) - self.assertFalse(expr=field.blank) - self.assertTrue(expr=field.null) - def test_project_class_required(self): obj = '' with self.assertRaises(NotImplementedError) as context: diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index 42baf7a9..d12f5b73 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -64,6 +64,11 @@ class Package(Project): unique=True, blank=True, ) + owner = models.ForeignKey( + to='users.ForumUser', + related_name='packages', + on_delete=models.CASCADE, + ) contributors = models.ManyToManyField( to='users.ForumUser', related_name='package_contributions', @@ -114,6 +119,12 @@ class PackageRelease(ProjectRelease): related_name='releases', on_delete=models.CASCADE, ) + created_by = models.ForeignKey( + to='users.ForumUser', + related_name='package_releases', + on_delete=models.SET_NULL, + null=True, + ) download_requirements = models.ManyToManyField( to='requirements.DownloadRequirement', related_name='required_in_package_releases', diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index 91cc5649..f146351b 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -103,6 +103,27 @@ def test_basename_field(self): self.assertTrue(expr=field.blank) self.assertFalse(expr=field.null) + def test_owner_field(self): + field = Package._meta.get_field('owner') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='packages', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test_contributors_field(self): field = Package._meta.get_field('contributors') self.assertIsInstance( @@ -325,6 +346,27 @@ def test_package_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_created_by_field(self): + field = PackageRelease._meta.get_field('created_by') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.SET_NULL, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='package_releases', + ) + self.assertFalse(expr=field.blank) + self.assertTrue(expr=field.null) + def test_download_requirements_field(self): field = PackageRelease._meta.get_field('download_requirements') self.assertIsInstance( diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index 261ad149..b8746d61 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -66,6 +66,11 @@ class Plugin(Project): unique=True, blank=True, ) + owner = models.ForeignKey( + to='users.ForumUser', + related_name='plugins', + on_delete=models.CASCADE, + ) contributors = models.ManyToManyField( to='users.ForumUser', related_name='plugin_contributions', @@ -116,6 +121,12 @@ class PluginRelease(ProjectRelease): related_name='releases', on_delete=models.CASCADE, ) + created_by = models.ForeignKey( + to='users.ForumUser', + related_name='plugin_releases', + on_delete=models.SET_NULL, + null=True, + ) download_requirements = models.ManyToManyField( to='requirements.DownloadRequirement', related_name='required_in_plugin_releases', diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 15413e5a..6db09272 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -108,6 +108,27 @@ def test_basename_field(self): self.assertTrue(expr=field.blank) self.assertFalse(expr=field.null) + def test_owner_field(self): + field = Plugin._meta.get_field('owner') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='plugins', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test_contributors_field(self): field = Plugin._meta.get_field('contributors') self.assertIsInstance( @@ -330,6 +351,27 @@ def test_plugin_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_created_by_field(self): + field = PluginRelease._meta.get_field('created_by') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.SET_NULL, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='plugin_releases', + ) + self.assertFalse(expr=field.blank) + self.assertTrue(expr=field.null) + def test_download_requirements_field(self): field = PluginRelease._meta.get_field('download_requirements') self.assertIsInstance( diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index d42b9455..82309aea 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -68,9 +68,14 @@ class SubPlugin(Project): validators=[basename_validator], blank=True, ) + owner = models.ForeignKey( + to='users.ForumUser', + related_name='sub_plugins', + on_delete=models.CASCADE, + ) contributors = models.ManyToManyField( to='users.ForumUser', - related_name='subplugin_contributions', + related_name='sub_plugin_contributions', through='project_manager.SubPluginContributor', ) slug = models.SlugField( @@ -84,12 +89,12 @@ class SubPlugin(Project): ) supported_games = models.ManyToManyField( to='games.Game', - related_name='subplugins', + related_name='sub_plugins', through='project_manager.SubPluginGame', ) tags = models.ManyToManyField( to='tags.Tag', - related_name='subplugins', + related_name='sub_plugins', through='project_manager.SubPluginTag', ) @@ -136,6 +141,12 @@ class SubPluginRelease(ProjectRelease): related_name='releases', on_delete=models.CASCADE, ) + created_by = models.ForeignKey( + to='users.ForumUser', + related_name='sub_plugin_releases', + on_delete=models.SET_NULL, + null=True, + ) download_requirements = models.ManyToManyField( to='requirements.DownloadRequirement', related_name='required_in_sub_plugin_releases', diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index b1471dd7..dea9af72 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -106,6 +106,27 @@ def test_basename_field(self): self.assertTrue(expr=field.blank) self.assertFalse(expr=field.null) + def test_owner_field(self): + field = SubPlugin._meta.get_field('owner') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.CASCADE, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='sub_plugins', + ) + self.assertFalse(expr=field.blank) + self.assertFalse(expr=field.null) + def test_contributors_field(self): field = SubPlugin._meta.get_field('contributors') self.assertIsInstance( @@ -118,7 +139,7 @@ def test_contributors_field(self): ) self.assertEqual( first=field.remote_field.related_name, - second='subplugin_contributions', + second='sub_plugin_contributions', ) self.assertEqual( first=field.remote_field.through, @@ -171,7 +192,7 @@ def test_supported_games_field(self): ) self.assertEqual( first=field.remote_field.related_name, - second='subplugins', + second='sub_plugins', ) self.assertEqual( first=field.remote_field.through, @@ -190,7 +211,7 @@ def test_tags_field(self): ) self.assertEqual( first=field.remote_field.related_name, - second='subplugins', + second='sub_plugins', ) self.assertEqual( first=field.remote_field.through, @@ -334,7 +355,7 @@ def test_model_inheritance(self): expr=issubclass(SubPluginRelease, ProjectRelease) ) - def test_plugin_field(self): + def test_sub_plugin_field(self): field = SubPluginRelease._meta.get_field('sub_plugin') self.assertIsInstance( obj=field, @@ -355,6 +376,27 @@ def test_plugin_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_created_by_field(self): + field = SubPluginRelease._meta.get_field('created_by') + self.assertIsInstance( + obj=field, + cls=models.ForeignKey, + ) + self.assertEqual( + first=field.remote_field.model, + second=ForumUser, + ) + self.assertEqual( + first=field.remote_field.on_delete, + second=models.SET_NULL, + ) + self.assertEqual( + first=field.remote_field.related_name, + second='sub_plugin_releases', + ) + self.assertFalse(expr=field.blank) + self.assertTrue(expr=field.null) + def test_download_requirements_field(self): field = SubPluginRelease._meta.get_field('download_requirements') self.assertIsInstance( From 51f22889a4d4a8d6dc5449aa7c6dbb61f72a1b02 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 00:56:17 -0400 Subject: [PATCH 132/211] Updated most instances of subplugin -> sub_plugin. --- games/api/serializers.py | 8 ++-- games/api/tests/test_serializers.py | 8 ++-- games/api/tests/test_views.py | 38 +++++++++---------- games/api/views.py | 8 ++-- project_manager/migrations/0003_initial.py | 6 +-- .../migrations/0005_release_created_by.py | 6 +-- ...lter_packagerelease_created_by_and_more.py | 12 +++--- project_manager/views.py | 4 +- tags/api/serializers.py | 8 ++-- tags/api/tests/test_serializers.py | 8 ++-- tags/api/tests/test_views.py | 38 +++++++++---------- tags/api/views.py | 8 ++-- users/api/filtersets.py | 4 +- users/api/serializers.py | 8 ++-- users/api/tests/test_serializers.py | 28 +++++++------- users/api/views.py | 4 +- 16 files changed, 98 insertions(+), 98 deletions(-) diff --git a/games/api/serializers.py b/games/api/serializers.py index f6d9cd50..ed58c8db 100644 --- a/games/api/serializers.py +++ b/games/api/serializers.py @@ -31,7 +31,7 @@ class GameRetrieveSerializer(ModelSerializer): packages = MinimalPackageSerializer(many=True, read_only=True) plugins = MinimalPluginSerializer(many=True, read_only=True) - subplugins = MinimalSubPluginSerializer(many=True, read_only=True) + sub_plugins = MinimalSubPluginSerializer(many=True, read_only=True) class Meta: """Define metaclass attributes.""" @@ -43,7 +43,7 @@ class Meta: 'icon', 'packages', 'plugins', - 'subplugins', + 'sub_plugins', ) @@ -52,7 +52,7 @@ class GameListSerializer(ModelSerializer): package_count = IntegerField() plugin_count = IntegerField() - subplugin_count = IntegerField() + sub_plugin_count = IntegerField() project_count = IntegerField() class Meta: @@ -65,6 +65,6 @@ class Meta: 'icon', 'package_count', 'plugin_count', - 'subplugin_count', + 'sub_plugin_count', 'project_count', ) diff --git a/games/api/tests/test_serializers.py b/games/api/tests/test_serializers.py index 3f19b570..9981979b 100644 --- a/games/api/tests/test_serializers.py +++ b/games/api/tests/test_serializers.py @@ -58,7 +58,7 @@ def test_declared_fields(self): for field, cls in ( ('packages', MinimalPackageSerializer), ('plugins', MinimalPluginSerializer), - ('subplugins', MinimalSubPluginSerializer), + ('sub_plugins', MinimalSubPluginSerializer), ): self.assertIn( member=field, @@ -88,7 +88,7 @@ def test_meta_class(self): 'icon', 'packages', 'plugins', - 'subplugins', + 'sub_plugins', ), ) @@ -109,7 +109,7 @@ def test_declared_fields(self): for field in ( 'package_count', 'plugin_count', - 'subplugin_count', + 'sub_plugin_count', 'project_count', ): self.assertIn( @@ -135,7 +135,7 @@ def test_meta_class(self): 'icon', 'package_count', 'plugin_count', - 'subplugin_count', + 'sub_plugin_count', 'project_count', ), ) diff --git a/games/api/tests/test_views.py b/games/api/tests/test_views.py index 27f709d4..309e804d 100644 --- a/games/api/tests/test_views.py +++ b/games/api/tests/test_views.py @@ -129,7 +129,7 @@ def test_get_queryset(self): for n, lookup_name in enumerate([ 'packages', 'plugins', - 'subplugins', + 'sub_plugins', ]): lookup = prefetch_lookups[n] self.assertEqual( @@ -185,17 +185,17 @@ def test_get_queryset(self): ) self.assertIn( - member='subplugin_count', + member='sub_plugin_count', container=annotations, ) - subplugin_count = annotations['subplugin_count'] - self.assertTrue(expr=subplugin_count.distinct) + sub_plugin_count = annotations['sub_plugin_count'] + self.assertTrue(expr=sub_plugin_count.distinct) self.assertEqual( - first=len(subplugin_count.source_expressions), + first=len(sub_plugin_count.source_expressions), second=1, ) self.assertIs( - expr1=subplugin_count.source_expressions[0].target, + expr1=sub_plugin_count.source_expressions[0].target, expr2=getattr(SubPluginGame.sub_plugin, 'field'), ) @@ -214,7 +214,7 @@ def test_get_queryset(self): ) self.assertEqual( first=project_count.rhs, - second=subplugin_count, + second=sub_plugin_count, ) lhs = project_count.lhs self.assertIsInstance( @@ -256,7 +256,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_1.icon.url}', 'package_count': 1, 'plugin_count': 2, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 3, }, ) @@ -268,7 +268,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_2.icon.url}', 'package_count': 2, 'plugin_count': 1, - 'subplugin_count': 2, + 'sub_plugin_count': 2, 'project_count': 5, }, ) @@ -280,7 +280,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_3.icon.url}', 'package_count': 0, 'plugin_count': 1, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 1, }, ) @@ -292,7 +292,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_4.icon.url}', 'package_count': 0, 'plugin_count': 0, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 0, }, ) @@ -318,7 +318,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_2.icon.url}', 'package_count': 2, 'plugin_count': 1, - 'subplugin_count': 2, + 'sub_plugin_count': 2, 'project_count': 5, }, ) @@ -330,7 +330,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_1.icon.url}', 'package_count': 1, 'plugin_count': 2, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 3, }, ) @@ -342,7 +342,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_3.icon.url}', 'package_count': 0, 'plugin_count': 1, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 1, }, ) @@ -354,7 +354,7 @@ def test_list(self): 'icon': f'{icon_base_url}{self.game_4.icon.url}', 'package_count': 0, 'plugin_count': 0, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 0, }, ) @@ -389,7 +389,7 @@ def test_retrieve(self): 'slug': self.plugin_2.slug, }, ], - 'subplugins': [], + 'sub_plugins': [], } ) @@ -420,7 +420,7 @@ def test_retrieve(self): 'slug': self.plugin_1.slug, }, ], - 'subplugins': [ + 'sub_plugins': [ { 'name': self.sub_plugin_1.name, 'slug': self.sub_plugin_1.slug, @@ -459,7 +459,7 @@ def test_retrieve(self): 'slug': self.plugin_2.slug, }, ], - 'subplugins': [], + 'sub_plugins': [], } ) @@ -476,7 +476,7 @@ def test_retrieve(self): 'icon': f'{icon_base_url}{self.game_4.icon.url}', 'packages': [], 'plugins': [], - 'subplugins': [], + 'sub_plugins': [], } ) diff --git a/games/api/views.py b/games/api/views.py index 7dcc493f..bf384f89 100644 --- a/games/api/views.py +++ b/games/api/views.py @@ -81,7 +81,7 @@ def get_queryset(self): queryset=Plugin.objects.order_by('name'), ), Prefetch( - lookup='subplugins', + lookup='sub_plugins', queryset=SubPlugin.objects.select_related( 'plugin', ).order_by( @@ -92,11 +92,11 @@ def get_queryset(self): package_count = Count('packages', distinct=True) plugin_count = Count('plugins', distinct=True) - subplugin_count = Count('subplugins', distinct=True) + sub_plugin_count = Count('sub_plugins', distinct=True) return queryset.annotate( package_count=package_count, plugin_count=plugin_count, - subplugin_count=subplugin_count, + sub_plugin_count=sub_plugin_count, ).annotate( - project_count=F('package_count') + F('plugin_count') + F('subplugin_count'), + project_count=F('package_count') + F('plugin_count') + F('sub_plugin_count'), ) diff --git a/project_manager/migrations/0003_initial.py b/project_manager/migrations/0003_initial.py index 989fbca2..ef2aedf1 100644 --- a/project_manager/migrations/0003_initial.py +++ b/project_manager/migrations/0003_initial.py @@ -25,7 +25,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subplugin', name='contributors', - field=models.ManyToManyField(related_name='subplugin_contributions', through='project_manager.SubPluginContributor', to='users.ForumUser'), + field=models.ManyToManyField(related_name='sub_plugin_contributions', through='project_manager.SubPluginContributor', to='users.ForumUser'), ), migrations.AddField( model_name='subplugin', @@ -40,12 +40,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subplugin', name='supported_games', - field=models.ManyToManyField(related_name='subplugins', through='project_manager.SubPluginGame', to='games.Game'), + field=models.ManyToManyField(related_name='sub_plugins', through='project_manager.SubPluginGame', to='games.Game'), ), migrations.AddField( model_name='subplugin', name='tags', - field=models.ManyToManyField(related_name='subplugins', through='project_manager.SubPluginTag', to='tags.Tag'), + field=models.ManyToManyField(related_name='sub_plugins', through='project_manager.SubPluginTag', to='tags.Tag'), ), migrations.AddField( model_name='plugintag', diff --git a/project_manager/migrations/0005_release_created_by.py b/project_manager/migrations/0005_release_created_by.py index dffcb71a..33146b01 100644 --- a/project_manager/migrations/0005_release_created_by.py +++ b/project_manager/migrations/0005_release_created_by.py @@ -15,16 +15,16 @@ class Migration(migrations.Migration): migrations.AddField( model_name='packagerelease', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='packagereleases', to='users.forumuser'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='package_releases', to='users.forumuser'), ), migrations.AddField( model_name='pluginrelease', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pluginreleases', to='users.forumuser'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_releases', to='users.forumuser'), ), migrations.AddField( model_name='subpluginrelease', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subpluginreleases', to='users.forumuser'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugin_releases', to='users.forumuser'), ), ] diff --git a/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py b/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py index 1395f02e..e234340f 100644 --- a/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py +++ b/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py @@ -15,31 +15,31 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='package', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='users.forumuser'), ), migrations.AlterField( model_name='packagerelease', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='users.forumuser'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='package_releases', to='users.forumuser'), ), migrations.AlterField( model_name='plugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='users.forumuser'), ), migrations.AlterField( model_name='pluginrelease', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='users.forumuser'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_releases', to='users.forumuser'), ), migrations.AlterField( model_name='subplugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_plugins', to='users.forumuser'), ), migrations.AlterField( model_name='subpluginrelease', name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)ss', to='users.forumuser'), + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugin_releases', to='users.forumuser'), ), ] diff --git a/project_manager/views.py b/project_manager/views.py index 0ec51584..d0674a35 100644 --- a/project_manager/views.py +++ b/project_manager/views.py @@ -55,8 +55,8 @@ def get_context_data(self, **kwargs): users = ForumUser.objects.filter( Q(plugins__isnull=False) | Q(plugin_contributions__isnull=False) | - Q(subplugins__isnull=False) | - Q(subplugin_contributions__isnull=False) | + Q(sub_plugins__isnull=False) | + Q(sub_plugin_contributions__isnull=False) | Q(packages__isnull=False) | Q(package_contributions__isnull=False) ).distinct().count() diff --git a/tags/api/serializers.py b/tags/api/serializers.py index 844b4043..b5c1a46d 100644 --- a/tags/api/serializers.py +++ b/tags/api/serializers.py @@ -31,7 +31,7 @@ class TagRetrieveSerializer(ModelSerializer): packages = MinimalPackageSerializer(many=True, read_only=True) plugins = MinimalPluginSerializer(many=True, read_only=True) - subplugins = MinimalSubPluginSerializer(many=True, read_only=True) + sub_plugins = MinimalSubPluginSerializer(many=True, read_only=True) class Meta: """Define metaclass attributes.""" @@ -41,7 +41,7 @@ class Meta: 'name', 'packages', 'plugins', - 'subplugins', + 'sub_plugins', ) @@ -50,7 +50,7 @@ class TagListSerializer(ModelSerializer): package_count = IntegerField() plugin_count = IntegerField() - subplugin_count = IntegerField() + sub_plugin_count = IntegerField() project_count = IntegerField() class Meta: @@ -61,6 +61,6 @@ class Meta: 'name', 'package_count', 'plugin_count', - 'subplugin_count', + 'sub_plugin_count', 'project_count', ) diff --git a/tags/api/tests/test_serializers.py b/tags/api/tests/test_serializers.py index e3c97745..e45d9b27 100644 --- a/tags/api/tests/test_serializers.py +++ b/tags/api/tests/test_serializers.py @@ -35,7 +35,7 @@ def test_declared_fields(self): for field, cls in ( ('packages', MinimalPackageSerializer), ('plugins', MinimalPluginSerializer), - ('subplugins', MinimalSubPluginSerializer), + ('sub_plugins', MinimalSubPluginSerializer), ): self.assertIn( member=field, @@ -63,7 +63,7 @@ def test_meta_class(self): 'name', 'packages', 'plugins', - 'subplugins', + 'sub_plugins', ), ) @@ -84,7 +84,7 @@ def test_declared_fields(self): for field in ( 'package_count', 'plugin_count', - 'subplugin_count', + 'sub_plugin_count', 'project_count', ): self.assertIn( @@ -108,7 +108,7 @@ def test_meta_class(self): 'name', 'package_count', 'plugin_count', - 'subplugin_count', + 'sub_plugin_count', 'project_count', ), ) diff --git a/tags/api/tests/test_views.py b/tags/api/tests/test_views.py index d04ab132..20b9d3d8 100644 --- a/tags/api/tests/test_views.py +++ b/tags/api/tests/test_views.py @@ -133,7 +133,7 @@ def test_get_queryset(self): for n, lookup_name in enumerate([ 'packages', 'plugins', - 'subplugins', + 'sub_plugins', ]): lookup = prefetch_lookups[n] self.assertEqual( @@ -189,17 +189,17 @@ def test_get_queryset(self): ) self.assertIn( - member='subplugin_count', + member='sub_plugin_count', container=annotations, ) - subplugin_count = annotations['subplugin_count'] - self.assertTrue(expr=subplugin_count.distinct) + sub_plugin_count = annotations['sub_plugin_count'] + self.assertTrue(expr=sub_plugin_count.distinct) self.assertEqual( - first=len(subplugin_count.source_expressions), + first=len(sub_plugin_count.source_expressions), second=1, ) self.assertIs( - expr1=subplugin_count.source_expressions[0].target, + expr1=sub_plugin_count.source_expressions[0].target, expr2=getattr(SubPluginTag.sub_plugin, 'field'), ) @@ -218,7 +218,7 @@ def test_get_queryset(self): ) self.assertEqual( first=project_count.rhs, - second=subplugin_count, + second=sub_plugin_count, ) lhs = project_count.lhs self.assertIsInstance( @@ -256,7 +256,7 @@ def test_list(self): 'name': self.tag_1.name, 'package_count': 1, 'plugin_count': 2, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 3, }, ) @@ -266,7 +266,7 @@ def test_list(self): 'name': self.tag_2.name, 'package_count': 2, 'plugin_count': 1, - 'subplugin_count': 2, + 'sub_plugin_count': 2, 'project_count': 5, }, ) @@ -276,7 +276,7 @@ def test_list(self): 'name': self.tag_3.name, 'package_count': 0, 'plugin_count': 1, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 1, }, ) @@ -286,7 +286,7 @@ def test_list(self): 'name': self.tag_4.name, 'package_count': 0, 'plugin_count': 0, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 0, }, ) @@ -310,7 +310,7 @@ def test_list(self): 'name': self.tag_2.name, 'package_count': 2, 'plugin_count': 1, - 'subplugin_count': 2, + 'sub_plugin_count': 2, 'project_count': 5, }, ) @@ -320,7 +320,7 @@ def test_list(self): 'name': self.tag_1.name, 'package_count': 1, 'plugin_count': 2, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 3, }, ) @@ -330,7 +330,7 @@ def test_list(self): 'name': self.tag_3.name, 'package_count': 0, 'plugin_count': 1, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 1, }, ) @@ -340,7 +340,7 @@ def test_list(self): 'name': self.tag_4.name, 'package_count': 0, 'plugin_count': 0, - 'subplugin_count': 0, + 'sub_plugin_count': 0, 'project_count': 0, }, ) @@ -371,7 +371,7 @@ def test_retrieve(self): 'slug': self.plugin_2.slug, }, ], - 'subplugins': [], + 'sub_plugins': [], } ) @@ -400,7 +400,7 @@ def test_retrieve(self): 'slug': self.plugin_1.slug, }, ], - 'subplugins': [ + 'sub_plugins': [ { 'name': self.sub_plugin_1.name, 'slug': self.sub_plugin_1.slug, @@ -437,7 +437,7 @@ def test_retrieve(self): 'slug': self.plugin_2.slug, }, ], - 'subplugins': [], + 'sub_plugins': [], } ) @@ -452,7 +452,7 @@ def test_retrieve(self): 'name': self.tag_4.name, 'packages': [], 'plugins': [], - 'subplugins': [], + 'sub_plugins': [], } ) diff --git a/tags/api/views.py b/tags/api/views.py index 5b4ac4ad..7c945143 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -83,7 +83,7 @@ def get_queryset(self): queryset=Plugin.objects.order_by('name'), ), Prefetch( - lookup='subplugins', + lookup='sub_plugins', queryset=SubPlugin.objects.select_related( 'plugin', ).order_by( @@ -94,11 +94,11 @@ def get_queryset(self): package_count = Count('packages', distinct=True) plugin_count = Count('plugins', distinct=True) - subplugin_count = Count('subplugins', distinct=True) + sub_plugin_count = Count('sub_plugins', distinct=True) return queryset.annotate( package_count=package_count, plugin_count=plugin_count, - subplugin_count=subplugin_count, + sub_plugin_count=sub_plugin_count, ).annotate( - project_count=F('package_count') + F('plugin_count') + F('subplugin_count'), + project_count=F('package_count') + F('plugin_count') + F('sub_plugin_count'), ) diff --git a/users/api/filtersets.py b/users/api/filtersets.py index ea280282..1624e6d8 100644 --- a/users/api/filtersets.py +++ b/users/api/filtersets.py @@ -48,8 +48,8 @@ def filter_has_contributions(queryset, name, value): plugin_contribution_count=Count('plugin_contributions'), package_count=Count('packages'), package_contribution_count=Count('package_contributions'), - sub_plugin_count=Count('subplugins'), - sub_plugin_contribution_count=Count('subplugin_contributions'), + sub_plugin_count=Count('sub_plugins'), + sub_plugin_contribution_count=Count('sub_plugin_contributions'), ) method = queryset.filter if value else queryset.exclude return method( diff --git a/users/api/serializers.py b/users/api/serializers.py index e6590eec..170091c8 100644 --- a/users/api/serializers.py +++ b/users/api/serializers.py @@ -45,11 +45,11 @@ class ForumUserSerializer(ModelSerializer): many=True, read_only=True, ) - subplugins = MinimalSubPluginSerializer( + sub_plugins = MinimalSubPluginSerializer( many=True, read_only=True, ) - subplugin_contributions = MinimalSubPluginSerializer( + sub_plugin_contributions = MinimalSubPluginSerializer( many=True, read_only=True, ) @@ -65,8 +65,8 @@ class Meta: 'package_contributions', 'plugins', 'plugin_contributions', - 'subplugins', - 'subplugin_contributions', + 'sub_plugins', + 'sub_plugin_contributions', ) @staticmethod diff --git a/users/api/tests/test_serializers.py b/users/api/tests/test_serializers.py index 71afca28..9a7d90bf 100644 --- a/users/api/tests/test_serializers.py +++ b/users/api/tests/test_serializers.py @@ -107,36 +107,36 @@ def test_declared_fields(self): self.assertTrue(expr=declared_fields['plugin_contributions'].child.read_only) self.assertIn( - member='subplugins', + member='sub_plugins', container=declared_fields, ) self.assertIsInstance( - obj=declared_fields['subplugins'], + obj=declared_fields['sub_plugins'], cls=ListSerializer, ) - self.assertTrue(expr=declared_fields['subplugins'].many) - self.assertTrue(expr=declared_fields['subplugins'].read_only) + self.assertTrue(expr=declared_fields['sub_plugins'].many) + self.assertTrue(expr=declared_fields['sub_plugins'].read_only) self.assertIsInstance( - obj=declared_fields['subplugins'].child, + obj=declared_fields['sub_plugins'].child, cls=MinimalSubPluginSerializer, ) - self.assertTrue(expr=declared_fields['subplugins'].child.read_only) + self.assertTrue(expr=declared_fields['sub_plugins'].child.read_only) self.assertIn( - member='subplugin_contributions', + member='sub_plugin_contributions', container=declared_fields, ) self.assertIsInstance( - obj=declared_fields['subplugin_contributions'], + obj=declared_fields['sub_plugin_contributions'], cls=ListSerializer, ) - self.assertTrue(expr=declared_fields['subplugin_contributions'].many) - self.assertTrue(expr=declared_fields['subplugin_contributions'].read_only) + self.assertTrue(expr=declared_fields['sub_plugin_contributions'].many) + self.assertTrue(expr=declared_fields['sub_plugin_contributions'].read_only) self.assertIsInstance( - obj=declared_fields['subplugin_contributions'].child, + obj=declared_fields['sub_plugin_contributions'].child, cls=MinimalSubPluginSerializer, ) - self.assertTrue(expr=declared_fields['subplugin_contributions'].child.read_only) + self.assertTrue(expr=declared_fields['sub_plugin_contributions'].child.read_only) def test_meta_class(self): self.assertEqual( @@ -152,8 +152,8 @@ def test_meta_class(self): 'package_contributions', 'plugins', 'plugin_contributions', - 'subplugins', - 'subplugin_contributions', + 'sub_plugins', + 'sub_plugin_contributions', ), ) diff --git a/users/api/views.py b/users/api/views.py index 78a6b905..94b4acb8 100644 --- a/users/api/views.py +++ b/users/api/views.py @@ -64,7 +64,7 @@ class ForumUserViewSet(ModelViewSet): ) ), Prefetch( - lookup='subplugins', + lookup='sub_plugins', queryset=SubPlugin.objects.order_by( 'name', ).select_related( @@ -84,7 +84,7 @@ class ForumUserViewSet(ModelViewSet): ) ), Prefetch( - lookup='subplugin_contributions', + lookup='sub_plugin_contributions', queryset=SubPlugin.objects.order_by( 'name', ).select_related( From b2c5f66eb8e88e254de153f550cc5439a09f1f87 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 10:03:04 -0400 Subject: [PATCH 133/211] Added verbose names to all model.Meta classes. Updated a few methods. Updated tests accordingly. --- games/migrations/0001_initial.py | 6 +- games/migrations/0002_alter_game_options.py | 17 -- project_manager/common/models.py | 2 - project_manager/common/tests/test_models.py | 8 - project_manager/migrations/0001_initial.py | 148 ++++++++++++++---- project_manager/migrations/0002_initial.py | 55 +------ project_manager/migrations/0003_initial.py | 117 +++++++++++--- .../migrations/0004_auto_20211103_1055.py | 29 ---- .../migrations/0005_release_created_by.py | 30 ---- ...lter_packagerelease_created_by_and_more.py | 45 ------ ...7_alter_packagerelease_options_and_more.py | 25 --- project_manager/packages/models.py | 38 +++-- project_manager/packages/tests/test_models.py | 85 ++++++++-- project_manager/plugins/models.py | 40 +++-- project_manager/plugins/tests/test_models.py | 93 +++++++++-- project_manager/sub_plugins/models.py | 42 +++-- .../sub_plugins/tests/test_models.py | 101 ++++++++++-- requirements/migrations/0001_initial.py | 2 +- tags/migrations/0001_initial.py | 6 +- .../{0002_tag_creator.py => 0002_initial.py} | 2 +- tags/migrations/0003_alter_tag_options.py | 17 -- users/migrations/0001_initial.py | 9 +- users/models.py | 6 + users/tests/test_models.py | 10 ++ 24 files changed, 571 insertions(+), 362 deletions(-) delete mode 100644 games/migrations/0002_alter_game_options.py delete mode 100644 project_manager/migrations/0004_auto_20211103_1055.py delete mode 100644 project_manager/migrations/0005_release_created_by.py delete mode 100644 project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py delete mode 100644 project_manager/migrations/0007_alter_packagerelease_options_and_more.py rename tags/migrations/{0002_tag_creator.py => 0002_initial.py} (91%) delete mode 100644 tags/migrations/0003_alter_tag_options.py diff --git a/games/migrations/0001_initial.py b/games/migrations/0001_initial.py index 127dc6ce..1f14ecd7 100644 --- a/games/migrations/0001_initial.py +++ b/games/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 from django.db import migrations, models @@ -19,5 +19,9 @@ class Migration(migrations.Migration): ('slug', models.CharField(blank=True, max_length=16, primary_key=True, serialize=False, unique=True)), ('icon', models.ImageField(upload_to='')), ], + options={ + 'verbose_name': 'Game', + 'verbose_name_plural': 'Games', + }, ), ] diff --git a/games/migrations/0002_alter_game_options.py b/games/migrations/0002_alter_game_options.py deleted file mode 100644 index d2aef368..00000000 --- a/games/migrations/0002_alter_game_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.8 on 2021-11-03 11:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('games', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='game', - options={'verbose_name': 'Game', 'verbose_name_plural': 'Games'}, - ), - ] diff --git a/project_manager/common/models.py b/project_manager/common/models.py index 373f97a2..c7b70a09 100644 --- a/project_manager/common/models.py +++ b/project_manager/common/models.py @@ -254,8 +254,6 @@ class Meta: """Define metaclass attributes.""" abstract = True - verbose_name = 'Release' - verbose_name_plural = 'Releases' @property def project_class(self): diff --git a/project_manager/common/tests/test_models.py b/project_manager/common/tests/test_models.py index ce91e16c..e55c05f6 100644 --- a/project_manager/common/tests/test_models.py +++ b/project_manager/common/tests/test_models.py @@ -375,11 +375,3 @@ def test_meta_class(self): self.assertTrue( expr=ProjectRelease._meta.abstract ) - self.assertEqual( - first=ProjectRelease._meta.verbose_name, - second='Release', - ) - self.assertEqual( - first=ProjectRelease._meta.verbose_name_plural, - second='Releases', - ) diff --git a/project_manager/migrations/0001_initial.py b/project_manager/migrations/0001_initial.py index d581f042..8282fc7a 100644 --- a/project_manager/migrations/0001_initial.py +++ b/project_manager/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 import django.core.validators from django.db import migrations, models @@ -7,13 +7,10 @@ import embed_video.fields import model_utils.fields import precise_bbcode.fields -from project_manager.common.helpers import ( - handle_project_logo_upload, - handle_release_zip_file_upload, -) -from project_manager.packages.helpers import handle_package_image_upload -from project_manager.plugins.helpers import handle_plugin_image_upload -from project_manager.sub_plugins.helpers import handle_sub_plugin_image_upload +import project_manager.common.helpers +import project_manager.packages.helpers +import project_manager.plugins.helpers +import project_manager.sub_plugins.helpers import uuid @@ -33,7 +30,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -44,7 +41,8 @@ class Migration(migrations.Migration): ('slug', models.SlugField(blank=True, max_length=32, primary_key=True, serialize=False, unique=True)), ], options={ - 'abstract': False, + 'verbose_name': 'Package', + 'verbose_name_plural': 'Packages', }, ), migrations.CreateModel( @@ -52,24 +50,31 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'Package Contributor', + 'verbose_name_plural': 'Package Contributors', + }, ), migrations.CreateModel( name='PackageGame', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'Package Game', + 'verbose_name_plural': 'Package Games', + }, ), migrations.CreateModel( name='PackageImage', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=handle_package_image_upload)), + ('image', models.ImageField(upload_to=project_manager.packages.helpers.handle_package_image_upload)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ - 'verbose_name': 'Image', - 'verbose_name_plural': 'Images', - 'abstract': False, + 'verbose_name': 'Package Image', + 'verbose_name_plural': 'Package Images', }, ), migrations.CreateModel( @@ -79,13 +84,13 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ - 'verbose_name': 'Release', - 'verbose_name_plural': 'Releases', + 'verbose_name': 'Package Release', + 'verbose_name_plural': 'Package Releases', 'abstract': False, }, ), @@ -95,6 +100,10 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Package Release Download Requirement', + 'verbose_name_plural': 'Package Release Download Requirements', + }, ), migrations.CreateModel( name='PackageReleasePackageRequirement', @@ -103,6 +112,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the package.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Package Release Package Requirement', + 'verbose_name_plural': 'Package Release Package Requirements', + }, ), migrations.CreateModel( name='PackageReleasePyPiRequirement', @@ -111,6 +124,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the package.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Package Release PyPi Requirement', + 'verbose_name_plural': 'Package Release PyPi Requirements', + }, ), migrations.CreateModel( name='PackageReleaseVersionControlRequirement', @@ -119,12 +136,20 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the package.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Package Release Version Control Requirement', + 'verbose_name_plural': 'Package Release Version Control Requirements', + }, ), migrations.CreateModel( name='PackageTag', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'Package Tag', + 'verbose_name_plural': 'Package Tags', + }, ), migrations.CreateModel( name='Plugin', @@ -134,7 +159,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -145,7 +170,8 @@ class Migration(migrations.Migration): ('slug', models.SlugField(blank=True, max_length=32, primary_key=True, serialize=False, unique=True)), ], options={ - 'abstract': False, + 'verbose_name': 'Plugin', + 'verbose_name_plural': 'Plugins', }, ), migrations.CreateModel( @@ -153,24 +179,31 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'Plugin Contributor', + 'verbose_name_plural': 'Plugin Contributors', + }, ), migrations.CreateModel( name='PluginGame', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'Plugin Game', + 'verbose_name_plural': 'Plugin Games', + }, ), migrations.CreateModel( name='PluginImage', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=handle_plugin_image_upload)), + ('image', models.ImageField(upload_to=project_manager.plugins.helpers.handle_plugin_image_upload)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ - 'verbose_name': 'Image', - 'verbose_name_plural': 'Images', - 'abstract': False, + 'verbose_name': 'Plugin Image', + 'verbose_name_plural': 'Plugin Images', }, ), migrations.CreateModel( @@ -180,13 +213,13 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ - 'verbose_name': 'Release', - 'verbose_name_plural': 'Releases', + 'verbose_name': 'Plugin Release', + 'verbose_name_plural': 'Plugin Releases', 'abstract': False, }, ), @@ -196,6 +229,10 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Plugin Release Download Requirement', + 'verbose_name_plural': 'Plugin Release Download Requirements', + }, ), migrations.CreateModel( name='PluginReleasePackageRequirement', @@ -204,6 +241,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Plugin Release Package Requirement', + 'verbose_name_plural': 'Plugin Release Package Requirements', + }, ), migrations.CreateModel( name='PluginReleasePyPiRequirement', @@ -212,6 +253,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Plugin Release PyPi Requirement', + 'verbose_name_plural': 'Plugin Release PyPi Requirements', + }, ), migrations.CreateModel( name='PluginReleaseVersionControlRequirement', @@ -220,12 +265,20 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Plugin Release Version Control Requirement', + 'verbose_name_plural': 'Plugin Release Version Control Requirements', + }, ), migrations.CreateModel( name='PluginTag', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'Plugin Tag', + 'verbose_name_plural': 'Plugin Tags', + }, ), migrations.CreateModel( name='SubPlugin', @@ -235,7 +288,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -256,24 +309,31 @@ class Migration(migrations.Migration): fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'SubPlugin Contributor', + 'verbose_name_plural': 'SubPlugin Contributors', + }, ), migrations.CreateModel( name='SubPluginGame', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ], + options={ + 'verbose_name': 'SubPlugin Game', + 'verbose_name_plural': 'SubPlugin Games', + }, ), migrations.CreateModel( name='SubPluginImage', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), - ('image', models.ImageField(upload_to=handle_sub_plugin_image_upload)), + ('image', models.ImageField(upload_to=project_manager.sub_plugins.helpers.handle_sub_plugin_image_upload)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ - 'verbose_name': 'Image', - 'verbose_name_plural': 'Images', - 'abstract': False, + 'verbose_name': 'SubPlugin Image', + 'verbose_name_plural': 'SubPlugin Images', }, ), migrations.CreateModel( @@ -297,13 +357,13 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], options={ - 'verbose_name': 'Release', - 'verbose_name_plural': 'Releases', + 'verbose_name': 'SubPlugin Release', + 'verbose_name_plural': 'SubPlugin Releases', 'abstract': False, }, ), @@ -313,6 +373,10 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'SubPlugin Release Download Requirement', + 'verbose_name_plural': 'SubPlugin Release Download Requirements', + }, ), migrations.CreateModel( name='SubPluginReleasePackageRequirement', @@ -321,6 +385,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the custom package for this release of the sub_plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'SubPlugin Release Package Requirement', + 'verbose_name_plural': 'SubPlugin Release Package Requirements', + }, ), migrations.CreateModel( name='SubPluginReleasePyPiRequirement', @@ -329,6 +397,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the PyPi package for this release of the sub_plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'SubPlugin Release PyPi Requirement', + 'verbose_name_plural': 'SubPlugin Release PyPi Requirements', + }, ), migrations.CreateModel( name='SubPluginReleaseVersionControlRequirement', @@ -337,6 +409,10 @@ class Migration(migrations.Migration): ('version', models.CharField(blank=True, help_text='The version of the VCS package for this release of the sub_plugin.', max_length=8, null=True, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('optional', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'SubPlugin Release Version Control Requirement', + 'verbose_name_plural': 'SubPlugin Release Version Control Requirements', + }, ), migrations.CreateModel( name='SubPluginTag', @@ -344,5 +420,9 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')), ('sub_plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin')), ], + options={ + 'verbose_name': 'SubPlugin Tag', + 'verbose_name_plural': 'SubPlugin Tags', + }, ), ] diff --git a/project_manager/migrations/0002_initial.py b/project_manager/migrations/0002_initial.py index dbc3c789..fced5723 100644 --- a/project_manager/migrations/0002_initial.py +++ b/project_manager/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 from django.db import migrations, models import django.db.models.deletion @@ -9,10 +9,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('tags', '0001_initial'), ('project_manager', '0001_initial'), + ('tags', '0001_initial'), ('requirements', '0001_initial'), - ('games', '0001_initial'), ] operations = [ @@ -61,54 +60,4 @@ class Migration(migrations.Migration): name='sub_plugin_release', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subpluginrelease'), ), - migrations.AddField( - model_name='subpluginrelease', - name='download_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleaseDownloadRequirement', to='requirements.DownloadRequirement'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='package_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleasePackageRequirement', to='project_manager.Package'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='pypi_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleasePyPiRequirement', to='requirements.PyPiRequirement'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='sub_plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='project_manager.subplugin'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='vcs_requirements', - field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), - ), - migrations.AddField( - model_name='subpluginpath', - name='plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paths', to='project_manager.plugin'), - ), - migrations.AddField( - model_name='subpluginimage', - name='sub_plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='project_manager.subplugin'), - ), - migrations.AddField( - model_name='subplugingame', - name='game', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game'), - ), - migrations.AddField( - model_name='subplugingame', - name='sub_plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin'), - ), - migrations.AddField( - model_name='subplugincontributor', - name='sub_plugin', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin'), - ), ] diff --git a/project_manager/migrations/0003_initial.py b/project_manager/migrations/0003_initial.py index ef2aedf1..e4a2da9b 100644 --- a/project_manager/migrations/0003_initial.py +++ b/project_manager/migrations/0003_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 from django.db import migrations, models import django.db.models.deletion @@ -9,14 +9,69 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('tags', '0001_initial'), - ('users', '0001_initial'), ('requirements', '0001_initial'), + ('tags', '0001_initial'), ('project_manager', '0002_initial'), ('games', '0001_initial'), + ('users', '0001_initial'), ] operations = [ + migrations.AddField( + model_name='subpluginrelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugin_releases', to='users.forumuser'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='download_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleaseDownloadRequirement', to='requirements.downloadrequirement'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='package_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleasePackageRequirement', to='project_manager.package'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='pypi_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleasePyPiRequirement', to='requirements.pypirequirement'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='releases', to='project_manager.subplugin'), + ), + migrations.AddField( + model_name='subpluginrelease', + name='vcs_requirements', + field=models.ManyToManyField(related_name='required_in_sub_plugin_releases', through='project_manager.SubPluginReleaseVersionControlRequirement', to='requirements.versioncontrolrequirement'), + ), + migrations.AddField( + model_name='subpluginpath', + name='plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paths', to='project_manager.plugin'), + ), + migrations.AddField( + model_name='subpluginimage', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='project_manager.subplugin'), + ), + migrations.AddField( + model_name='subplugingame', + name='game', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.game'), + ), + migrations.AddField( + model_name='subplugingame', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin'), + ), + migrations.AddField( + model_name='subplugincontributor', + name='sub_plugin', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.subplugin'), + ), migrations.AddField( model_name='subplugincontributor', name='user', @@ -25,12 +80,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subplugin', name='contributors', - field=models.ManyToManyField(related_name='sub_plugin_contributions', through='project_manager.SubPluginContributor', to='users.ForumUser'), + field=models.ManyToManyField(related_name='sub_plugin_contributions', through='project_manager.SubPluginContributor', to='users.forumuser'), ), migrations.AddField( model_name='subplugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subplugins', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_plugins', to='users.forumuser'), ), migrations.AddField( model_name='subplugin', @@ -40,12 +95,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subplugin', name='supported_games', - field=models.ManyToManyField(related_name='sub_plugins', through='project_manager.SubPluginGame', to='games.Game'), + field=models.ManyToManyField(related_name='sub_plugins', through='project_manager.SubPluginGame', to='games.game'), ), migrations.AddField( model_name='subplugin', name='tags', - field=models.ManyToManyField(related_name='sub_plugins', through='project_manager.SubPluginTag', to='tags.Tag'), + field=models.ManyToManyField(related_name='sub_plugins', through='project_manager.SubPluginTag', to='tags.tag'), ), migrations.AddField( model_name='plugintag', @@ -97,15 +152,20 @@ class Migration(migrations.Migration): name='plugin_release', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.pluginrelease'), ), + migrations.AddField( + model_name='pluginrelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_releases', to='users.forumuser'), + ), migrations.AddField( model_name='pluginrelease', name='download_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleaseDownloadRequirement', to='requirements.DownloadRequirement'), + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleaseDownloadRequirement', to='requirements.downloadrequirement'), ), migrations.AddField( model_name='pluginrelease', name='package_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleasePackageRequirement', to='project_manager.Package'), + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleasePackageRequirement', to='project_manager.package'), ), migrations.AddField( model_name='pluginrelease', @@ -115,12 +175,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='pluginrelease', name='pypi_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleasePyPiRequirement', to='requirements.PyPiRequirement'), + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleasePyPiRequirement', to='requirements.pypirequirement'), ), migrations.AddField( model_name='pluginrelease', name='vcs_requirements', - field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), + field=models.ManyToManyField(related_name='required_in_plugin_releases', through='project_manager.PluginReleaseVersionControlRequirement', to='requirements.versioncontrolrequirement'), ), migrations.AddField( model_name='pluginimage', @@ -150,7 +210,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='plugin', name='contributors', - field=models.ManyToManyField(related_name='plugin_contributions', through='project_manager.PluginContributor', to='users.ForumUser'), + field=models.ManyToManyField(related_name='plugin_contributions', through='project_manager.PluginContributor', to='users.forumuser'), ), migrations.AddField( model_name='plugin', @@ -160,12 +220,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='plugin', name='supported_games', - field=models.ManyToManyField(related_name='plugins', through='project_manager.PluginGame', to='games.Game'), + field=models.ManyToManyField(related_name='plugins', through='project_manager.PluginGame', to='games.game'), ), migrations.AddField( model_name='plugin', name='tags', - field=models.ManyToManyField(related_name='plugins', through='project_manager.PluginTag', to='tags.Tag'), + field=models.ManyToManyField(related_name='plugins', through='project_manager.PluginTag', to='tags.tag'), ), migrations.AddField( model_name='packagetag', @@ -217,10 +277,15 @@ class Migration(migrations.Migration): name='package_release', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project_manager.packagerelease'), ), + migrations.AddField( + model_name='packagerelease', + name='created_by', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='package_releases', to='users.forumuser'), + ), migrations.AddField( model_name='packagerelease', name='download_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleaseDownloadRequirement', to='requirements.DownloadRequirement'), + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleaseDownloadRequirement', to='requirements.downloadrequirement'), ), migrations.AddField( model_name='packagerelease', @@ -230,17 +295,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='packagerelease', name='package_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleasePackageRequirement', to='project_manager.Package'), + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleasePackageRequirement', to='project_manager.package'), ), migrations.AddField( model_name='packagerelease', name='pypi_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleasePyPiRequirement', to='requirements.PyPiRequirement'), + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleasePyPiRequirement', to='requirements.pypirequirement'), ), migrations.AddField( model_name='packagerelease', name='vcs_requirements', - field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleaseVersionControlRequirement', to='requirements.VersionControlRequirement'), + field=models.ManyToManyField(related_name='required_in_package_releases', through='project_manager.PackageReleaseVersionControlRequirement', to='requirements.versioncontrolrequirement'), ), migrations.AddField( model_name='packageimage', @@ -270,7 +335,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='package', name='contributors', - field=models.ManyToManyField(related_name='package_contributions', through='project_manager.PackageContributor', to='users.ForumUser'), + field=models.ManyToManyField(related_name='package_contributions', through='project_manager.PackageContributor', to='users.forumuser'), ), migrations.AddField( model_name='package', @@ -280,12 +345,12 @@ class Migration(migrations.Migration): migrations.AddField( model_name='package', name='supported_games', - field=models.ManyToManyField(related_name='packages', through='project_manager.PackageGame', to='games.Game'), + field=models.ManyToManyField(related_name='packages', through='project_manager.PackageGame', to='games.game'), ), migrations.AddField( model_name='package', name='tags', - field=models.ManyToManyField(related_name='packages', through='project_manager.PackageTag', to='tags.Tag'), + field=models.ManyToManyField(related_name='packages', through='project_manager.PackageTag', to='tags.tag'), ), migrations.AlterUniqueTogether( name='subplugintag', @@ -307,6 +372,10 @@ class Migration(migrations.Migration): name='subpluginreleasedownloadrequirement', unique_together={('sub_plugin_release', 'download_requirement')}, ), + migrations.AlterUniqueTogether( + name='subpluginrelease', + unique_together={('sub_plugin', 'version')}, + ), migrations.AlterUniqueTogether( name='subpluginpath', unique_together={('path', 'plugin')}, @@ -321,7 +390,7 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='subplugin', - unique_together={('plugin', 'slug'), ('plugin', 'basename'), ('plugin', 'name')}, + unique_together={('plugin', 'basename'), ('plugin', 'name'), ('plugin', 'slug')}, ), migrations.AlterUniqueTogether( name='plugintag', @@ -375,6 +444,10 @@ class Migration(migrations.Migration): name='packagereleasedownloadrequirement', unique_together={('package_release', 'download_requirement')}, ), + migrations.AlterUniqueTogether( + name='packagerelease', + unique_together={('package', 'version')}, + ), migrations.AlterUniqueTogether( name='packagegame', unique_together={('package', 'game')}, diff --git a/project_manager/migrations/0004_auto_20211103_1055.py b/project_manager/migrations/0004_auto_20211103_1055.py deleted file mode 100644 index f69db531..00000000 --- a/project_manager/migrations/0004_auto_20211103_1055.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 3.2.8 on 2021-11-03 14:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('project_manager', '0003_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='package', - options={'verbose_name': 'Package', 'verbose_name_plural': 'Packages'}, - ), - migrations.AlterModelOptions( - name='plugin', - options={'verbose_name': 'Plugin', 'verbose_name_plural': 'Plugins'}, - ), - migrations.AlterUniqueTogether( - name='packagerelease', - unique_together={('package', 'version')}, - ), - migrations.AlterUniqueTogether( - name='subpluginrelease', - unique_together={('sub_plugin', 'version')}, - ), - ] diff --git a/project_manager/migrations/0005_release_created_by.py b/project_manager/migrations/0005_release_created_by.py deleted file mode 100644 index 33146b01..00000000 --- a/project_manager/migrations/0005_release_created_by.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.2.9 on 2021-11-13 22:37 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ('project_manager', '0004_auto_20211103_1055'), - ] - - operations = [ - migrations.AddField( - model_name='packagerelease', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='package_releases', to='users.forumuser'), - ), - migrations.AddField( - model_name='pluginrelease', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_releases', to='users.forumuser'), - ), - migrations.AddField( - model_name='subpluginrelease', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugin_releases', to='users.forumuser'), - ), - ] diff --git a/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py b/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py deleted file mode 100644 index e234340f..00000000 --- a/project_manager/migrations/0006_alter_package_owner_alter_packagerelease_created_by_and_more.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 4.0.2 on 2022-02-05 21:16 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0001_initial'), - ('project_manager', '0005_release_created_by'), - ] - - operations = [ - migrations.AlterField( - model_name='package', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='users.forumuser'), - ), - migrations.AlterField( - model_name='packagerelease', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='package_releases', to='users.forumuser'), - ), - migrations.AlterField( - model_name='plugin', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='users.forumuser'), - ), - migrations.AlterField( - model_name='pluginrelease', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plugin_releases', to='users.forumuser'), - ), - migrations.AlterField( - model_name='subplugin', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_plugins', to='users.forumuser'), - ), - migrations.AlterField( - model_name='subpluginrelease', - name='created_by', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugin_releases', to='users.forumuser'), - ), - ] diff --git a/project_manager/migrations/0007_alter_packagerelease_options_and_more.py b/project_manager/migrations/0007_alter_packagerelease_options_and_more.py deleted file mode 100644 index a83745c4..00000000 --- a/project_manager/migrations/0007_alter_packagerelease_options_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.0.2 on 2022-03-19 19:38 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('project_manager', '0006_alter_package_owner_alter_packagerelease_created_by_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='packagerelease', - options={'verbose_name': 'Package Release', 'verbose_name_plural': 'Package Releases'}, - ), - migrations.AlterModelOptions( - name='pluginrelease', - options={'verbose_name': 'Plugin Release', 'verbose_name_plural': 'Plugin Releases'}, - ), - migrations.AlterModelOptions( - name='subpluginrelease', - options={'verbose_name': 'SubPlugin Release', 'verbose_name_plural': 'SubPlugin Releases'}, - ), - ] diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index d12f5b73..bd79f895 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -155,11 +155,6 @@ class PackageRelease(ProjectRelease): ] ) - @property - def project(self): - """Return the Package.""" - return self.package - class Meta(ProjectRelease.Meta): """Define metaclass attributes.""" @@ -167,6 +162,11 @@ class Meta(ProjectRelease.Meta): verbose_name = 'Package Release' verbose_name_plural = 'Package Releases' + @property + def project(self): + """Return the Package.""" + return self.package + def get_absolute_url(https://github.com/self): """Return the URL for the PackageRelease.""" return reverse( @@ -196,8 +196,12 @@ class PackageImage(AbstractUUIDPrimaryKeyModel): class Meta: """Define metaclass attributes.""" - verbose_name = 'Image' - verbose_name_plural = 'Images' + verbose_name = 'Package Image' + verbose_name_plural = 'Package Images' + + def __str__(self): + """Return the proper str value of the object.""" + return f'{self.package} - {self.image}' class PackageContributor(AbstractUUIDPrimaryKeyModel): @@ -216,6 +220,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package', 'user') + verbose_name = 'Package Contributor' + verbose_name_plural = 'Package Contributors' @property def project(self): @@ -224,7 +230,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'Package Contributor' + return f'{self.package} Contributor: {self.user}' def clean(self): """Validate that the package's owner cannot be a contributor.""" @@ -255,6 +261,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package', 'game') + verbose_name = 'Package Game' + verbose_name_plural = 'Package Games' @property def project(self): @@ -263,7 +271,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'Package Game' + return f'{self.package} Game: {self.game}' class PackageTag(AbstractUUIDPrimaryKeyModel): @@ -282,6 +290,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package', 'tag') + verbose_name = 'Package Tag' + verbose_name_plural = 'Package Tags' @property def project(self): @@ -290,7 +300,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'Package Tag' + return f'{self.package} Tag: {self.tag}' class PackageReleaseDownloadRequirement(AbstractUUIDPrimaryKeyModel): @@ -312,6 +322,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'download_requirement') + verbose_name = 'Package Release Download Requirement' + verbose_name_plural = 'Package Release Download Requirements' def __str__(self): """Return the requirement's url.""" @@ -347,6 +359,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'package_requirement') + verbose_name = 'Package Release Package Requirement' + verbose_name_plural = 'Package Release Package Requirements' def __str__(self): """Return the requirement's name and version.""" @@ -381,6 +395,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'pypi_requirement') + verbose_name = 'Package Release PyPi Requirement' + verbose_name_plural = 'Package Release PyPi Requirements' def __str__(self): """Return the requirement's name and version.""" @@ -415,6 +431,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('package_release', 'vcs_requirement') + verbose_name = 'Package Release Version Control Requirement' + verbose_name_plural = 'Package Release Version Control Requirements' def __str__(self): """Return the requirement's name and version.""" diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index f146351b..9dd0bc85 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -60,6 +60,7 @@ PackageContributorFactory, PackageFactory, PackageGameFactory, + PackageImageFactory, PackageReleaseFactory, PackageReleaseDownloadRequirementFactory, PackageReleasePackageRequirementFactory, @@ -478,7 +479,7 @@ def test__str__(self): release = PackageReleaseFactory() self.assertEqual( first=str(release), - second=f'{release.project} - {release.version}', + second=f'{release.package} - {release.version}', ) def test_clean(self): @@ -601,14 +602,21 @@ def test_created_field(self): second='created', ) + def test__str__(self): + obj = PackageImageFactory() + self.assertEqual( + first=str(obj), + second=f'{obj.package} - {obj.image}', + ) + def test_meta_class(self): self.assertEqual( first=PackageImage._meta.verbose_name, - second='Image', + second='Package Image', ) self.assertEqual( first=PackageImage._meta.verbose_name_plural, - second='Images', + second='Package Images', ) @@ -653,9 +661,10 @@ def test_user_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = PackageContributorFactory() self.assertEqual( - first=str(PackageContributorFactory()), - second='Package Contributor', + first=str(obj), + second=f'{obj.package} Contributor: {obj.user}', ) def test_clean(self): @@ -697,6 +706,14 @@ def test_meta_class(self): tuple1=PackageContributor._meta.unique_together, tuple2=(('package', 'user'),), ) + self.assertEqual( + first=PackageContributor._meta.verbose_name, + second='Package Contributor', + ) + self.assertEqual( + first=PackageContributor._meta.verbose_name_plural, + second='Package Contributors', + ) class PackageGameTestCase(TestCase): @@ -740,9 +757,10 @@ def test_game_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = PackageGameFactory() self.assertEqual( - first=str(PackageGameFactory()), - second='Package Game', + first=str(obj), + second=f'{obj.package} Game: {obj.game}', ) def test_meta_class(self): @@ -750,6 +768,14 @@ def test_meta_class(self): tuple1=PackageGame._meta.unique_together, tuple2=(('package', 'game'),), ) + self.assertEqual( + first=PackageGame._meta.verbose_name, + second='Package Game', + ) + self.assertEqual( + first=PackageGame._meta.verbose_name_plural, + second='Package Games', + ) class PackageTagTestCase(TestCase): @@ -793,9 +819,10 @@ def test_tag_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = PackageTagFactory() self.assertEqual( - first=str(PackageTagFactory()), - second='Package Tag', + first=str(obj), + second=f'{obj.package} Tag: {obj.tag}', ) def test_meta_class(self): @@ -803,6 +830,14 @@ def test_meta_class(self): tuple1=PackageTag._meta.unique_together, tuple2=(('package', 'tag'),), ) + self.assertEqual( + first=PackageTag._meta.verbose_name, + second='Package Tag', + ) + self.assertEqual( + first=PackageTag._meta.verbose_name_plural, + second='Package Tags', + ) class PackageReleaseDownloadRequirementTestCase(TestCase): @@ -876,6 +911,14 @@ def test_meta_class(self): tuple1=PackageReleaseDownloadRequirement._meta.unique_together, tuple2=(('package_release', 'download_requirement'),), ) + self.assertEqual( + first=PackageReleaseDownloadRequirement._meta.verbose_name, + second='Package Release Download Requirement', + ) + self.assertEqual( + first=PackageReleaseDownloadRequirement._meta.verbose_name_plural, + second='Package Release Download Requirements', + ) class PackageReleasePackageRequirementTestCase(TestCase): @@ -975,6 +1018,14 @@ def test_meta_class(self): tuple1=PackageReleasePackageRequirement._meta.unique_together, tuple2=(('package_release', 'package_requirement'),), ) + self.assertEqual( + first=PackageReleasePackageRequirement._meta.verbose_name, + second='Package Release Package Requirement', + ) + self.assertEqual( + first=PackageReleasePackageRequirement._meta.verbose_name_plural, + second='Package Release Package Requirements', + ) class PackageReleasePyPiRequirementTestCase(TestCase): @@ -1074,6 +1125,14 @@ def test_meta_class(self): tuple1=PackageReleasePyPiRequirement._meta.unique_together, tuple2=(('package_release', 'pypi_requirement'),), ) + self.assertEqual( + first=PackageReleasePyPiRequirement._meta.verbose_name, + second='Package Release PyPi Requirement', + ) + self.assertEqual( + first=PackageReleasePyPiRequirement._meta.verbose_name_plural, + second='Package Release PyPi Requirements', + ) class PackageReleaseVersionControlRequirementTestCase(TestCase): @@ -1177,3 +1236,11 @@ def test_meta_class(self): tuple1=PackageReleaseVersionControlRequirement._meta.unique_together, tuple2=(('package_release', 'vcs_requirement'),), ) + self.assertEqual( + first=PackageReleaseVersionControlRequirement._meta.verbose_name, + second='Package Release Version Control Requirement', + ) + self.assertEqual( + first=PackageReleaseVersionControlRequirement._meta.verbose_name_plural, + second='Package Release Version Control Requirements', + ) diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index b8746d61..7e0f9326 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -157,11 +157,6 @@ class PluginRelease(ProjectRelease): ] ) - @property - def project(self): - """Return the Plugin.""" - return self.plugin - class Meta(ProjectRelease.Meta): """Define metaclass attributes.""" @@ -169,6 +164,11 @@ class Meta(ProjectRelease.Meta): verbose_name = 'Plugin Release' verbose_name_plural = 'Plugin Releases' + @property + def project(self): + """Return the Plugin.""" + return self.plugin + def get_absolute_url(https://github.com/self): """Return the URL for the PluginRelease.""" return reverse( @@ -198,8 +198,12 @@ class PluginImage(AbstractUUIDPrimaryKeyModel): class Meta: """Define metaclass attributes.""" - verbose_name = 'Image' - verbose_name_plural = 'Images' + verbose_name = 'Plugin Image' + verbose_name_plural = 'Plugin Images' + + def __str__(self): + """Return the proper str value of the object.""" + return f'{self.plugin} - {self.image}' class PluginContributor(AbstractUUIDPrimaryKeyModel): @@ -218,6 +222,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin', 'user') + verbose_name = 'Plugin Contributor' + verbose_name_plural = 'Plugin Contributors' @property def project(self): @@ -226,7 +232,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'Plugin Contributor' + return f'{self.plugin} Contributor: {self.user}' def clean(self): """Validate that the plugin's owner cannot be a contributor.""" @@ -256,6 +262,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin', 'game') + verbose_name = 'Plugin Game' + verbose_name_plural = 'Plugin Games' @property def project(self): @@ -264,7 +272,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'Plugin Game' + return f'{self.plugin} Game: {self.game}' class PluginTag(AbstractUUIDPrimaryKeyModel): @@ -283,6 +291,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin', 'tag') + verbose_name = 'Plugin Tag' + verbose_name_plural = 'Plugin Tags' @property def project(self): @@ -291,7 +301,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'Plugin Tag' + return f'{self.plugin} Tag: {self.tag}' class SubPluginPath(AbstractUUIDPrimaryKeyModel): @@ -325,9 +335,9 @@ class SubPluginPath(AbstractUUIDPrimaryKeyModel): class Meta: """Define metaclass attributes.""" + unique_together = ('path', 'plugin') verbose_name = 'SubPlugin Path' verbose_name_plural = 'SubPlugin Paths' - unique_together = ('path', 'plugin') def __str__(self): """Return the path.""" @@ -390,6 +400,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'download_requirement') + verbose_name = 'Plugin Release Download Requirement' + verbose_name_plural = 'Plugin Release Download Requirements' def __str__(self): """Return the requirement's url.""" @@ -425,6 +437,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'package_requirement') + verbose_name = 'Plugin Release Package Requirement' + verbose_name_plural = 'Plugin Release Package Requirements' def __str__(self): """Return the requirement's name and version.""" @@ -459,6 +473,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'pypi_requirement') + verbose_name = 'Plugin Release PyPi Requirement' + verbose_name_plural = 'Plugin Release PyPi Requirements' def __str__(self): """Return the requirement's name and version.""" @@ -493,6 +509,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('plugin_release', 'vcs_requirement') + verbose_name = 'Plugin Release Version Control Requirement' + verbose_name_plural = 'Plugin Release Version Control Requirements' def __str__(self): """Return the requirement's name and version.""" diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 6db09272..14dd3bf1 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -64,6 +64,7 @@ PluginContributorFactory, PluginFactory, PluginGameFactory, + PluginImageFactory, PluginReleaseFactory, PluginReleaseDownloadRequirementFactory, PluginReleasePackageRequirementFactory, @@ -483,7 +484,7 @@ def test__str__(self): release = PluginReleaseFactory() self.assertEqual( first=str(release), - second=f'{release.project} - {release.version}', + second=f'{release.plugin} - {release.version}', ) def test_clean(self): @@ -606,14 +607,21 @@ def test_created_field(self): second='created', ) + def test__str__(self): + obj = PluginImageFactory() + self.assertEqual( + first=str(obj), + second=f'{obj.plugin} - {obj.image}', + ) + def test_meta_class(self): self.assertEqual( first=PluginImage._meta.verbose_name, - second='Image', + second='Plugin Image', ) self.assertEqual( first=PluginImage._meta.verbose_name_plural, - second='Images', + second='Plugin Images', ) @@ -658,9 +666,10 @@ def test_user_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = PluginContributorFactory() self.assertEqual( - first=str(PluginContributorFactory()), - second='Plugin Contributor', + first=str(obj), + second=f'{obj.plugin} Contributor: {obj.user}', ) def test_clean(self): @@ -702,6 +711,14 @@ def test_meta_class(self): tuple1=PluginContributor._meta.unique_together, tuple2=(('plugin', 'user'),), ) + self.assertEqual( + first=PluginContributor._meta.verbose_name, + second='Plugin Contributor', + ) + self.assertEqual( + first=PluginContributor._meta.verbose_name_plural, + second='Plugin Contributors', + ) class PluginGameTestCase(TestCase): @@ -745,9 +762,10 @@ def test_game_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = PluginGameFactory() self.assertEqual( - first=str(PluginGameFactory()), - second='Plugin Game', + first=str(obj), + second=f'{obj.plugin} Game: {obj.game}', ) def test_meta_class(self): @@ -755,6 +773,14 @@ def test_meta_class(self): tuple1=PluginGame._meta.unique_together, tuple2=(('plugin', 'game'),), ) + self.assertEqual( + first=PluginGame._meta.verbose_name, + second='Plugin Game', + ) + self.assertEqual( + first=PluginGame._meta.verbose_name_plural, + second='Plugin Games', + ) class PluginTagTestCase(TestCase): @@ -798,9 +824,10 @@ def test_tag_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = PluginTagFactory() self.assertEqual( - first=str(PluginTagFactory()), - second='Plugin Tag', + first=str(obj), + second=f'{obj.plugin} Tag: {obj.tag}', ) def test_meta_class(self): @@ -808,6 +835,14 @@ def test_meta_class(self): tuple1=PluginTag._meta.unique_together, tuple2=(('plugin', 'tag'),), ) + self.assertEqual( + first=PluginTag._meta.verbose_name, + second='Plugin Tag', + ) + self.assertEqual( + first=PluginTag._meta.verbose_name_plural, + second='Plugin Tags', + ) class PluginReleaseDownloadRequirementTestCase(TestCase): @@ -881,6 +916,14 @@ def test_meta_class(self): tuple1=PluginReleaseDownloadRequirement._meta.unique_together, tuple2=(('plugin_release', 'download_requirement'),), ) + self.assertEqual( + first=PluginReleaseDownloadRequirement._meta.verbose_name, + second='Plugin Release Download Requirement', + ) + self.assertEqual( + first=PluginReleaseDownloadRequirement._meta.verbose_name_plural, + second='Plugin Release Download Requirements', + ) class PluginReleasePackageRequirementTestCase(TestCase): @@ -980,6 +1023,14 @@ def test_meta_class(self): tuple1=PluginReleasePackageRequirement._meta.unique_together, tuple2=(('plugin_release', 'package_requirement'),), ) + self.assertEqual( + first=PluginReleasePackageRequirement._meta.verbose_name, + second='Plugin Release Package Requirement', + ) + self.assertEqual( + first=PluginReleasePackageRequirement._meta.verbose_name_plural, + second='Plugin Release Package Requirements', + ) class PluginReleasePyPiRequirementTestCase(TestCase): @@ -1079,6 +1130,14 @@ def test_meta_class(self): tuple1=PluginReleasePyPiRequirement._meta.unique_together, tuple2=(('plugin_release', 'pypi_requirement'),), ) + self.assertEqual( + first=PluginReleasePyPiRequirement._meta.verbose_name, + second='Plugin Release PyPi Requirement', + ) + self.assertEqual( + first=PluginReleasePyPiRequirement._meta.verbose_name_plural, + second='Plugin Release PyPi Requirements', + ) class PluginReleaseVersionControlRequirementTestCase(TestCase): @@ -1182,6 +1241,14 @@ def test_meta_class(self): tuple1=PluginReleaseVersionControlRequirement._meta.unique_together, tuple2=(('plugin_release', 'vcs_requirement'),), ) + self.assertEqual( + first=PluginReleaseVersionControlRequirement._meta.verbose_name, + second='Plugin Release Version Control Requirement', + ) + self.assertEqual( + first=PluginReleaseVersionControlRequirement._meta.verbose_name_plural, + second='Plugin Release Version Control Requirements', + ) class SubPluginPathTestCase(TestCase): @@ -1310,6 +1377,10 @@ def test_clean(self): ) def test_meta_class(self): + self.assertTupleEqual( + tuple1=SubPluginPath._meta.unique_together, + tuple2=(('path', 'plugin'),), + ) self.assertEqual( first=SubPluginPath._meta.verbose_name, second='SubPlugin Path', @@ -1318,7 +1389,3 @@ def test_meta_class(self): first=SubPluginPath._meta.verbose_name_plural, second='SubPlugin Paths', ) - self.assertTupleEqual( - tuple1=SubPluginPath._meta.unique_together, - tuple2=(('path', 'plugin'),), - ) diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index 82309aea..0e293693 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -104,13 +104,13 @@ class SubPlugin(Project): class Meta: """Define metaclass attributes.""" - verbose_name = 'SubPlugin' - verbose_name_plural = 'SubPlugins' unique_together = ( ('plugin', 'basename'), ('plugin', 'name'), ('plugin', 'slug'), ) + verbose_name = 'SubPlugin' + verbose_name_plural = 'SubPlugins' def __str__(self): """Return the string formatted name for the sub-plugin.""" @@ -177,11 +177,6 @@ class SubPluginRelease(ProjectRelease): ] ) - @property - def project(self): - """Return the SubPlugin.""" - return self.sub_plugin - class Meta(ProjectRelease.Meta): """Define metaclass attributes.""" @@ -189,6 +184,11 @@ class Meta(ProjectRelease.Meta): verbose_name = 'SubPlugin Release' verbose_name_plural = 'SubPlugin Releases' + @property + def project(self): + """Return the SubPlugin.""" + return self.sub_plugin + def get_absolute_url(https://github.com/self): """Return the URL for the SubPluginRelease.""" return reverse( @@ -219,8 +219,12 @@ class SubPluginImage(AbstractUUIDPrimaryKeyModel): class Meta: """Define metaclass attributes.""" - verbose_name = 'Image' - verbose_name_plural = 'Images' + verbose_name = 'SubPlugin Image' + verbose_name_plural = 'SubPlugin Images' + + def __str__(self): + """Return the proper str value of the object.""" + return f'{self.sub_plugin} - {self.image}' class SubPluginContributor(AbstractUUIDPrimaryKeyModel): @@ -239,6 +243,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin', 'user') + verbose_name = 'SubPlugin Contributor' + verbose_name_plural = 'SubPlugin Contributors' @property def project(self): @@ -247,7 +253,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'SubPlugin Contributor' + return f'{self.sub_plugin} Contributor: {self.user}' def clean(self): """Validate that the sub_plugin's owner cannot be a contributor.""" @@ -277,6 +283,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin', 'game') + verbose_name = 'SubPlugin Game' + verbose_name_plural = 'SubPlugin Games' @property def project(self): @@ -285,7 +293,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'SubPlugin Game' + return f'{self.sub_plugin} Game: {self.game}' class SubPluginTag(AbstractUUIDPrimaryKeyModel): @@ -304,6 +312,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin', 'tag') + verbose_name = 'SubPlugin Tag' + verbose_name_plural = 'SubPlugin Tags' @property def project(self): @@ -312,7 +322,7 @@ def project(self): def __str__(self): """Return the base string.""" - return 'SubPlugin Tag' + return f'{self.sub_plugin} Tag: {self.tag}' class SubPluginReleaseDownloadRequirement(AbstractUUIDPrimaryKeyModel): @@ -334,6 +344,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'download_requirement') + verbose_name = 'SubPlugin Release Download Requirement' + verbose_name_plural = 'SubPlugin Release Download Requirements' def __str__(self): """Return the requirement's url.""" @@ -369,6 +381,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'package_requirement') + verbose_name = 'SubPlugin Release Package Requirement' + verbose_name_plural = 'SubPlugin Release Package Requirements' def __str__(self): """Return the requirement's name and version.""" @@ -403,6 +417,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'pypi_requirement') + verbose_name = 'SubPlugin Release PyPi Requirement' + verbose_name_plural = 'SubPlugin Release PyPi Requirements' def __str__(self): """Return the requirement's name and version.""" @@ -437,6 +453,8 @@ class Meta: """Define metaclass attributes.""" unique_together = ('sub_plugin_release', 'vcs_requirement') + verbose_name = 'SubPlugin Release Version Control Requirement' + verbose_name_plural = 'SubPlugin Release Version Control Requirements' def __str__(self): """Return the requirement's name and version.""" diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index dea9af72..c2ad5fd4 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -68,6 +68,7 @@ SubPluginContributorFactory, SubPluginFactory, SubPluginGameFactory, + SubPluginImageFactory, SubPluginReleaseFactory, SubPluginReleaseDownloadRequirementFactory, SubPluginReleasePackageRequirementFactory, @@ -331,14 +332,6 @@ def test_get_forum_url(https://github.com/self): def test_meta_class(self): self.assertTrue(issubclass(SubPlugin.Meta, Project.Meta)) - self.assertEqual( - first=SubPlugin._meta.verbose_name, - second='SubPlugin', - ) - self.assertEqual( - first=SubPlugin._meta.verbose_name_plural, - second='SubPlugins', - ) self.assertTupleEqual( tuple1=SubPlugin._meta.unique_together, tuple2=( @@ -347,6 +340,14 @@ def test_meta_class(self): ('plugin', 'slug'), ) ) + self.assertEqual( + first=SubPlugin._meta.verbose_name, + second='SubPlugin', + ) + self.assertEqual( + first=SubPlugin._meta.verbose_name_plural, + second='SubPlugins', + ) class SubPluginReleaseTestCase(TestCase): @@ -508,7 +509,7 @@ def test__str__(self): release = SubPluginReleaseFactory() self.assertEqual( first=str(release), - second=f'{release.project} - {release.version}', + second=f'{release.sub_plugin} - {release.version}', ) def test_clean(self): @@ -632,14 +633,21 @@ def test_created_field(self): second='created', ) + def test__str__(self): + obj = SubPluginImageFactory() + self.assertEqual( + first=str(obj), + second=f'{obj.sub_plugin} - {obj.image}', + ) + def test_meta_class(self): self.assertEqual( first=SubPluginImage._meta.verbose_name, - second='Image', + second='SubPlugin Image', ) self.assertEqual( first=SubPluginImage._meta.verbose_name_plural, - second='Images', + second='SubPlugin Images', ) @@ -684,9 +692,10 @@ def test_user_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = SubPluginContributorFactory() self.assertEqual( - first=str(SubPluginContributorFactory()), - second='SubPlugin Contributor', + first=str(obj), + second=f'{obj.sub_plugin} Contributor: {obj.user}', ) def test_clean(self): @@ -728,6 +737,14 @@ def test_meta_class(self): tuple1=SubPluginContributor._meta.unique_together, tuple2=(('sub_plugin', 'user'),), ) + self.assertEqual( + first=SubPluginContributor._meta.verbose_name, + second='SubPlugin Contributor', + ) + self.assertEqual( + first=SubPluginContributor._meta.verbose_name_plural, + second='SubPlugin Contributors', + ) class SubPluginGameTestCase(TestCase): @@ -771,9 +788,10 @@ def test_game_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = SubPluginGameFactory() self.assertEqual( - first=str(SubPluginGameFactory()), - second='SubPlugin Game', + first=str(obj), + second=f'{obj.sub_plugin} Game: {obj.game}', ) def test_meta_class(self): @@ -781,6 +799,14 @@ def test_meta_class(self): tuple1=SubPluginGame._meta.unique_together, tuple2=(('sub_plugin', 'game'),), ) + self.assertEqual( + first=SubPluginGame._meta.verbose_name, + second='SubPlugin Game', + ) + self.assertEqual( + first=SubPluginGame._meta.verbose_name_plural, + second='SubPlugin Games', + ) class SubPluginTagTestCase(TestCase): @@ -824,9 +850,10 @@ def test_tag_field(self): self.assertFalse(expr=field.null) def test__str__(self): + obj = SubPluginTagFactory() self.assertEqual( - first=str(SubPluginTagFactory()), - second='SubPlugin Tag', + first=str(obj), + second=f'{obj.sub_plugin} Tag: {obj.tag}', ) def test_meta_class(self): @@ -834,6 +861,14 @@ def test_meta_class(self): tuple1=SubPluginTag._meta.unique_together, tuple2=(('sub_plugin', 'tag'),), ) + self.assertEqual( + first=SubPluginTag._meta.verbose_name, + second='SubPlugin Tag', + ) + self.assertEqual( + first=SubPluginTag._meta.verbose_name_plural, + second='SubPlugin Tags', + ) class SubPluginReleaseDownloadRequirementTestCase(TestCase): @@ -907,6 +942,14 @@ def test_meta_class(self): tuple1=SubPluginReleaseDownloadRequirement._meta.unique_together, tuple2=(('sub_plugin_release', 'download_requirement'),), ) + self.assertEqual( + first=SubPluginReleaseDownloadRequirement._meta.verbose_name, + second='SubPlugin Release Download Requirement', + ) + self.assertEqual( + first=SubPluginReleaseDownloadRequirement._meta.verbose_name_plural, + second='SubPlugin Release Download Requirements', + ) class SubPluginReleasePackageRequirementTestCase(TestCase): @@ -1006,6 +1049,14 @@ def test_meta_class(self): tuple1=SubPluginReleasePackageRequirement._meta.unique_together, tuple2=(('sub_plugin_release', 'package_requirement'),), ) + self.assertEqual( + first=SubPluginReleasePackageRequirement._meta.verbose_name, + second='SubPlugin Release Package Requirement', + ) + self.assertEqual( + first=SubPluginReleasePackageRequirement._meta.verbose_name_plural, + second='SubPlugin Release Package Requirements', + ) class SubPluginReleasePyPiRequirementTestCase(TestCase): @@ -1105,6 +1156,14 @@ def test_meta_class(self): tuple1=SubPluginReleasePyPiRequirement._meta.unique_together, tuple2=(('sub_plugin_release', 'pypi_requirement'),), ) + self.assertEqual( + first=SubPluginReleasePyPiRequirement._meta.verbose_name, + second='SubPlugin Release PyPi Requirement', + ) + self.assertEqual( + first=SubPluginReleasePyPiRequirement._meta.verbose_name_plural, + second='SubPlugin Release PyPi Requirements', + ) class SubPluginReleaseVersionControlRequirementTestCase(TestCase): @@ -1208,3 +1267,11 @@ def test_meta_class(self): tuple1=SubPluginReleaseVersionControlRequirement._meta.unique_together, tuple2=(('sub_plugin_release', 'vcs_requirement'),), ) + self.assertEqual( + first=SubPluginReleaseVersionControlRequirement._meta.verbose_name, + second='SubPlugin Release Version Control Requirement', + ) + self.assertEqual( + first=SubPluginReleaseVersionControlRequirement._meta.verbose_name_plural, + second='SubPlugin Release Version Control Requirements', + ) diff --git a/requirements/migrations/0001_initial.py b/requirements/migrations/0001_initial.py index 01c46286..2e53ba22 100644 --- a/requirements/migrations/0001_initial.py +++ b/requirements/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 from django.db import migrations, models diff --git a/tags/migrations/0001_initial.py b/tags/migrations/0001_initial.py index 36a3720f..8c935bde 100644 --- a/tags/migrations/0001_initial.py +++ b/tags/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 import django.core.validators from django.db import migrations, models @@ -18,5 +18,9 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=16, primary_key=True, serialize=False, unique=True, validators=[django.core.validators.RegexValidator('^[a-z]*')])), ('black_listed', models.BooleanField(default=False)), ], + options={ + 'verbose_name': 'Tag', + 'verbose_name_plural': 'Tags', + }, ), ] diff --git a/tags/migrations/0002_tag_creator.py b/tags/migrations/0002_initial.py similarity index 91% rename from tags/migrations/0002_tag_creator.py rename to tags/migrations/0002_initial.py index ff693841..6c15fddc 100644 --- a/tags/migrations/0002_tag_creator.py +++ b/tags/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 from django.db import migrations, models import django.db.models.deletion diff --git a/tags/migrations/0003_alter_tag_options.py b/tags/migrations/0003_alter_tag_options.py deleted file mode 100644 index 14cb696c..00000000 --- a/tags/migrations/0003_alter_tag_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.8 on 2021-11-03 11:46 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('tags', '0002_tag_creator'), - ] - - operations = [ - migrations.AlterModelOptions( - name='tag', - options={'verbose_name': 'Tag', 'verbose_name_plural': 'Tags'}, - ), - ] diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 0db509d8..e797439f 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.8 on 2021-10-22 20:02 +# Generated by Django 4.0.3 on 2022-03-27 13:20 from django.conf import settings import django.contrib.auth.models @@ -25,11 +25,12 @@ class Migration(migrations.Migration): ('username', models.CharField(editable=False, max_length=30, unique=True)), ('email', models.EmailField(blank=True, max_length=256)), ('is_staff', models.BooleanField(default=False)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ - 'abstract': False, + 'verbose_name': 'User', + 'verbose_name_plural': 'Users', }, managers=[ ('objects', django.contrib.auth.models.UserManager()), diff --git a/users/models.py b/users/models.py index 3247d4f6..9e3d962e 100644 --- a/users/models.py +++ b/users/models.py @@ -54,6 +54,12 @@ class User(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = 'username' + class Meta: + """Define metaclass attributes.""" + + verbose_name = 'User' + verbose_name_plural = 'Users' + def get_short_name(self): """Return the short name for the user.""" return self.username diff --git a/users/tests/test_models.py b/users/tests/test_models.py index 6bb0cf23..3affc82f 100644 --- a/users/tests/test_models.py +++ b/users/tests/test_models.py @@ -140,6 +140,16 @@ def test_is_staff_field(self): self.assertFalse(expr=field.blank) self.assertFalse(expr=field.null) + def test_meta_class(self): + self.assertEqual( + first=User._meta.verbose_name, + second='User', + ) + self.assertEqual( + first=User._meta.verbose_name_plural, + second='Users', + ) + def test_objects(self): self.assertIsInstance( obj=User.objects, From e45cedc684b4ac3f4b292899553f9f424ffcfdb5 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 10:20:00 -0400 Subject: [PATCH 134/211] Changed some on_delete values to SET_NULL instead of CASCADE in case a ForumUser is ever actually deleted. --- project_manager/migrations/0003_initial.py | 6 +++--- project_manager/packages/models.py | 2 +- project_manager/packages/tests/test_models.py | 2 +- project_manager/plugins/models.py | 2 +- project_manager/plugins/tests/test_models.py | 2 +- project_manager/sub_plugins/models.py | 2 +- project_manager/sub_plugins/tests/test_models.py | 2 +- tags/migrations/0002_initial.py | 2 +- tags/models.py | 2 +- tags/tests/test_models.py | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/project_manager/migrations/0003_initial.py b/project_manager/migrations/0003_initial.py index e4a2da9b..50fe54e3 100644 --- a/project_manager/migrations/0003_initial.py +++ b/project_manager/migrations/0003_initial.py @@ -85,7 +85,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subplugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_plugins', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugins', to='users.forumuser'), ), migrations.AddField( model_name='subplugin', @@ -215,7 +215,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='plugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plugins', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='plugins', to='users.forumuser'), ), migrations.AddField( model_name='plugin', @@ -340,7 +340,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='package', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='packages', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='packages', to='users.forumuser'), ), migrations.AddField( model_name='package', diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index bd79f895..c241da56 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -67,7 +67,7 @@ class Package(Project): owner = models.ForeignKey( to='users.ForumUser', related_name='packages', - on_delete=models.CASCADE, + on_delete=models.SET_NULL, ) contributors = models.ManyToManyField( to='users.ForumUser', diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index 9dd0bc85..2c9a3bdb 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -116,7 +116,7 @@ def test_owner_field(self): ) self.assertEqual( first=field.remote_field.on_delete, - second=models.CASCADE, + second=models.SET_NULL, ) self.assertEqual( first=field.remote_field.related_name, diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index 7e0f9326..1312d581 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -69,7 +69,7 @@ class Plugin(Project): owner = models.ForeignKey( to='users.ForumUser', related_name='plugins', - on_delete=models.CASCADE, + on_delete=models.SET_NULL, ) contributors = models.ManyToManyField( to='users.ForumUser', diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 14dd3bf1..6ab5310e 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -121,7 +121,7 @@ def test_owner_field(self): ) self.assertEqual( first=field.remote_field.on_delete, - second=models.CASCADE, + second=models.SET_NULL, ) self.assertEqual( first=field.remote_field.related_name, diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index 0e293693..9047eb31 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -71,7 +71,7 @@ class SubPlugin(Project): owner = models.ForeignKey( to='users.ForumUser', related_name='sub_plugins', - on_delete=models.CASCADE, + on_delete=models.SET_NULL, ) contributors = models.ManyToManyField( to='users.ForumUser', diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index c2ad5fd4..1e19e44e 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -119,7 +119,7 @@ def test_owner_field(self): ) self.assertEqual( first=field.remote_field.on_delete, - second=models.CASCADE, + second=models.SET_NULL, ) self.assertEqual( first=field.remote_field.related_name, diff --git a/tags/migrations/0002_initial.py b/tags/migrations/0002_initial.py index 6c15fddc..e4c1b2df 100644 --- a/tags/migrations/0002_initial.py +++ b/tags/migrations/0002_initial.py @@ -17,6 +17,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='tag', name='creator', - field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='created_tags', to='users.forumuser'), + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tags', to='users.forumuser'), ), ] diff --git a/tags/models.py b/tags/models.py index 2593bd4d..4d45a7e0 100644 --- a/tags/models.py +++ b/tags/models.py @@ -38,7 +38,7 @@ class Tag(models.Model): to='users.ForumUser', related_name='created_tags', blank=True, - on_delete=models.CASCADE, + on_delete=models.SET_NULL, ) class Meta: diff --git a/tags/tests/test_models.py b/tags/tests/test_models.py index 2ac8bbee..ec2056fa 100644 --- a/tags/tests/test_models.py +++ b/tags/tests/test_models.py @@ -66,7 +66,7 @@ def test_creator_field(self): ) self.assertEqual( first=field.remote_field.on_delete, - second=models.CASCADE, + second=models.SET_NULL, ) self.assertEqual( first=field.remote_field.related_name, From 05773d4b0677588b8e7e66ce94ef9b662be1afa6 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 11:04:34 -0400 Subject: [PATCH 135/211] Fixed SET_NULL fields to allow for null. --- project_manager/migrations/0003_initial.py | 6 +++--- project_manager/packages/models.py | 1 + project_manager/packages/tests/test_models.py | 2 +- project_manager/plugins/models.py | 1 + project_manager/plugins/tests/test_models.py | 2 +- project_manager/sub_plugins/models.py | 1 + project_manager/sub_plugins/tests/test_models.py | 2 +- tags/migrations/0002_initial.py | 2 +- tags/models.py | 1 + tags/tests/test_models.py | 2 +- 10 files changed, 12 insertions(+), 8 deletions(-) diff --git a/project_manager/migrations/0003_initial.py b/project_manager/migrations/0003_initial.py index 50fe54e3..f19504be 100644 --- a/project_manager/migrations/0003_initial.py +++ b/project_manager/migrations/0003_initial.py @@ -85,7 +85,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='subplugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='sub_plugins', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, related_name='sub_plugins', to='users.forumuser'), ), migrations.AddField( model_name='subplugin', @@ -215,7 +215,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='plugin', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='plugins', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, related_name='plugins', to='users.forumuser'), ), migrations.AddField( model_name='plugin', @@ -340,7 +340,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='package', name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='packages', to='users.forumuser'), + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, related_name='packages', to='users.forumuser'), ), migrations.AddField( model_name='package', diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index c241da56..6225138b 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -68,6 +68,7 @@ class Package(Project): to='users.ForumUser', related_name='packages', on_delete=models.SET_NULL, + null=True, ) contributors = models.ManyToManyField( to='users.ForumUser', diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index 2c9a3bdb..f47f9862 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -123,7 +123,7 @@ def test_owner_field(self): second='packages', ) self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) + self.assertTrue(expr=field.null) def test_contributors_field(self): field = Package._meta.get_field('contributors') diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index 1312d581..5eafc52d 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -70,6 +70,7 @@ class Plugin(Project): to='users.ForumUser', related_name='plugins', on_delete=models.SET_NULL, + null=True, ) contributors = models.ManyToManyField( to='users.ForumUser', diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index 6ab5310e..d550053a 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -128,7 +128,7 @@ def test_owner_field(self): second='plugins', ) self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) + self.assertTrue(expr=field.null) def test_contributors_field(self): field = Plugin._meta.get_field('contributors') diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index 9047eb31..c2f4797b 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -72,6 +72,7 @@ class SubPlugin(Project): to='users.ForumUser', related_name='sub_plugins', on_delete=models.SET_NULL, + null=True, ) contributors = models.ManyToManyField( to='users.ForumUser', diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index 1e19e44e..8436c299 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -126,7 +126,7 @@ def test_owner_field(self): second='sub_plugins', ) self.assertFalse(expr=field.blank) - self.assertFalse(expr=field.null) + self.assertTrue(expr=field.null) def test_contributors_field(self): field = SubPlugin._meta.get_field('contributors') diff --git a/tags/migrations/0002_initial.py b/tags/migrations/0002_initial.py index e4c1b2df..603550fe 100644 --- a/tags/migrations/0002_initial.py +++ b/tags/migrations/0002_initial.py @@ -17,6 +17,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='tag', name='creator', - field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tags', to='users.forumuser'), + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.SET_NULL, null=True, related_name='created_tags', to='users.forumuser'), ), ] diff --git a/tags/models.py b/tags/models.py index 4d45a7e0..e960eca4 100644 --- a/tags/models.py +++ b/tags/models.py @@ -39,6 +39,7 @@ class Tag(models.Model): related_name='created_tags', blank=True, on_delete=models.SET_NULL, + null=True, ) class Meta: diff --git a/tags/tests/test_models.py b/tags/tests/test_models.py index ec2056fa..473e2f41 100644 --- a/tags/tests/test_models.py +++ b/tags/tests/test_models.py @@ -73,7 +73,7 @@ def test_creator_field(self): second='created_tags', ) self.assertTrue(expr=field.blank) - self.assertFalse(expr=field.null) + self.assertTrue(expr=field.null) def test_meta_class(self): self.assertEqual( From dbd4b574857a32bbf03f259dd68fb935b600bf26 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 27 Mar 2022 17:54:35 -0400 Subject: [PATCH 136/211] Removed unnecessary Prefetch calls now that the queryset is only used in retrieve. --- games/api/tests/test_views.py | 29 +++++++++++------------------ games/api/views.py | 19 +++---------------- tags/api/tests/test_views.py | 29 +++++++++++------------------ tags/api/views.py | 19 +++---------------- 4 files changed, 28 insertions(+), 68 deletions(-) diff --git a/games/api/tests/test_views.py b/games/api/tests/test_views.py index 309e804d..33520506 100644 --- a/games/api/tests/test_views.py +++ b/games/api/tests/test_views.py @@ -124,24 +124,17 @@ def test_get_queryset(self): prefetch_lookups = queryset._prefetch_related_lookups self.assertEqual( first=len(prefetch_lookups), - second=3, - ) - for n, lookup_name in enumerate([ - 'packages', - 'plugins', - 'sub_plugins', - ]): - lookup = prefetch_lookups[n] - self.assertEqual( - first=lookup.prefetch_to, - second=lookup_name, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('name',), - ) - - lookup = prefetch_lookups[2] + second=1, + ) + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='sub_plugins', + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('name',), + ) self.assertEqual( first=lookup.queryset.query.select_related, second={'plugin': {}} diff --git a/games/api/views.py b/games/api/views.py index bf384f89..cf2c9b04 100644 --- a/games/api/views.py +++ b/games/api/views.py @@ -14,8 +14,6 @@ # App from games.api.serializers import GameListSerializer, GameRetrieveSerializer from games.models import Game -from project_manager.packages.models import Package -from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin @@ -72,14 +70,6 @@ def get_queryset(self): queryset = super().get_queryset() if self.action == 'retrieve': return queryset.prefetch_related( - Prefetch( - lookup='packages', - queryset=Package.objects.order_by('name'), - ), - Prefetch( - lookup='plugins', - queryset=Plugin.objects.order_by('name'), - ), Prefetch( lookup='sub_plugins', queryset=SubPlugin.objects.select_related( @@ -90,13 +80,10 @@ def get_queryset(self): ), ) - package_count = Count('packages', distinct=True) - plugin_count = Count('plugins', distinct=True) - sub_plugin_count = Count('sub_plugins', distinct=True) return queryset.annotate( - package_count=package_count, - plugin_count=plugin_count, - sub_plugin_count=sub_plugin_count, + package_count=Count('packages', distinct=True), + plugin_count=Count('plugins', distinct=True), + sub_plugin_count=Count('sub_plugins', distinct=True), ).annotate( project_count=F('package_count') + F('plugin_count') + F('sub_plugin_count'), ) diff --git a/tags/api/tests/test_views.py b/tags/api/tests/test_views.py index 20b9d3d8..d3987d8d 100644 --- a/tags/api/tests/test_views.py +++ b/tags/api/tests/test_views.py @@ -128,24 +128,17 @@ def test_get_queryset(self): prefetch_lookups = queryset._prefetch_related_lookups self.assertEqual( first=len(prefetch_lookups), - second=3, - ) - for n, lookup_name in enumerate([ - 'packages', - 'plugins', - 'sub_plugins', - ]): - lookup = prefetch_lookups[n] - self.assertEqual( - first=lookup.prefetch_to, - second=lookup_name, - ) - self.assertEqual( - first=lookup.queryset.query.order_by, - second=('name',), - ) - - lookup = prefetch_lookups[2] + second=1, + ) + lookup = prefetch_lookups[0] + self.assertEqual( + first=lookup.prefetch_to, + second='sub_plugins', + ) + self.assertEqual( + first=lookup.queryset.query.order_by, + second=('name',), + ) self.assertEqual( first=lookup.queryset.query.select_related, second={'plugin': {}} diff --git a/tags/api/views.py b/tags/api/views.py index 7c945143..1c8d4e9e 100644 --- a/tags/api/views.py +++ b/tags/api/views.py @@ -13,8 +13,6 @@ from rest_framework.viewsets import GenericViewSet # App -from project_manager.packages.models import Package -from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin from tags.api.serializers import TagListSerializer, TagRetrieveSerializer from tags.models import Tag @@ -74,14 +72,6 @@ def get_queryset(self): ) if self.action == 'retrieve': return queryset.prefetch_related( - Prefetch( - lookup='packages', - queryset=Package.objects.order_by('name'), - ), - Prefetch( - lookup='plugins', - queryset=Plugin.objects.order_by('name'), - ), Prefetch( lookup='sub_plugins', queryset=SubPlugin.objects.select_related( @@ -92,13 +82,10 @@ def get_queryset(self): ), ) - package_count = Count('packages', distinct=True) - plugin_count = Count('plugins', distinct=True) - sub_plugin_count = Count('sub_plugins', distinct=True) return queryset.annotate( - package_count=package_count, - plugin_count=plugin_count, - sub_plugin_count=sub_plugin_count, + package_count=Count('packages', distinct=True), + plugin_count=Count('plugins', distinct=True), + sub_plugin_count=Count('sub_plugins', distinct=True), ).annotate( project_count=F('package_count') + F('plugin_count') + F('sub_plugin_count'), ) From b9d9089d390f0c223471d941f05520df282a6a46 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 20:15:46 -0400 Subject: [PATCH 137/211] Removed unused properties. Made manage.py executable. --- manage.py | 0 project_manager/packages/models.py | 15 --------------- project_manager/plugins/models.py | 15 --------------- project_manager/sub_plugins/models.py | 15 --------------- 4 files changed, 45 deletions(-) mode change 100644 => 100755 manage.py diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index 6225138b..838e8602 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -224,11 +224,6 @@ class Meta: verbose_name = 'Package Contributor' verbose_name_plural = 'Package Contributors' - @property - def project(self): - """Return the Package.""" - return self.package - def __str__(self): """Return the base string.""" return f'{self.package} Contributor: {self.user}' @@ -265,11 +260,6 @@ class Meta: verbose_name = 'Package Game' verbose_name_plural = 'Package Games' - @property - def project(self): - """Return the Package.""" - return self.package - def __str__(self): """Return the base string.""" return f'{self.package} Game: {self.game}' @@ -294,11 +284,6 @@ class Meta: verbose_name = 'Package Tag' verbose_name_plural = 'Package Tags' - @property - def project(self): - """Return the Package.""" - return self.package - def __str__(self): """Return the base string.""" return f'{self.package} Tag: {self.tag}' diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index 5eafc52d..c71e4c4f 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -226,11 +226,6 @@ class Meta: verbose_name = 'Plugin Contributor' verbose_name_plural = 'Plugin Contributors' - @property - def project(self): - """Return the Plugin.""" - return self.plugin - def __str__(self): """Return the base string.""" return f'{self.plugin} Contributor: {self.user}' @@ -266,11 +261,6 @@ class Meta: verbose_name = 'Plugin Game' verbose_name_plural = 'Plugin Games' - @property - def project(self): - """Return the Plugin.""" - return self.plugin - def __str__(self): """Return the base string.""" return f'{self.plugin} Game: {self.game}' @@ -295,11 +285,6 @@ class Meta: verbose_name = 'Plugin Tag' verbose_name_plural = 'Plugin Tags' - @property - def project(self): - """Return the Plugin.""" - return self.plugin - def __str__(self): """Return the base string.""" return f'{self.plugin} Tag: {self.tag}' diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index c2f4797b..69ab0991 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -247,11 +247,6 @@ class Meta: verbose_name = 'SubPlugin Contributor' verbose_name_plural = 'SubPlugin Contributors' - @property - def project(self): - """Return the SubPlugin.""" - return self.sub_plugin - def __str__(self): """Return the base string.""" return f'{self.sub_plugin} Contributor: {self.user}' @@ -287,11 +282,6 @@ class Meta: verbose_name = 'SubPlugin Game' verbose_name_plural = 'SubPlugin Games' - @property - def project(self): - """Return the SubPlugin.""" - return self.sub_plugin - def __str__(self): """Return the base string.""" return f'{self.sub_plugin} Game: {self.game}' @@ -316,11 +306,6 @@ class Meta: verbose_name = 'SubPlugin Tag' verbose_name_plural = 'SubPlugin Tags' - @property - def project(self): - """Return the SubPlugin.""" - return self.sub_plugin - def __str__(self): """Return the base string.""" return f'{self.sub_plugin} Tag: {self.tag}' From 428014fad626b21b34400b2fc34ee8f3c1cf1bb8 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 27 Mar 2022 20:16:10 -0400 Subject: [PATCH 138/211] Added a few instructions to the readme. --- readme.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/readme.md b/readme.md index 594dd293..213b5d4a 100644 --- a/readme.md +++ b/readme.md @@ -13,23 +13,27 @@ If you wish to contribute to this application, follow the instructions below on 2. If you have to set it up yourself, there are plenty of online guides to help you. 3. We may Docker-ize the app in the future, which will make this a little simpler, but will require you to install/run Docker. 3. Log into your virtual environment to complete the rest of these steps. -4. Run `pip install -r pip-requirements/local.txt` to install all the Python/Django requirements. -5. Run the [migrate](https://docs.djangoproject.com/en/dev/ref/django-admin/#migrate) management command to create the tables/columns in your database. -6. Run the `create_game_instances` management command to create the Game objects. -7. Run the [createsuperuser](https://docs.djangoproject.com/en/dev/ref/django-admin/#createsuperuser) management command to create your main user. -8. Run the `associate_super_user` management command to associate the Super User you just created with a ForumUser object. - 1. Arguments for the command are: - 1. **username** - The username of the Super User. - 2. **forum_id** - The user id from the Source.Python forums. -9. If you want to create a test (non-Super User) User, run the `create_test_user` management command. - 1. Arguments for the command are: - 1. **username** - The username of the User. - 2. **password** - The password to use for the User. - 3. **forum_id** - The user id from the Source.Python forums. -10. If you want additional users to test with, run the `create_random_users` management command. +4. In order for things to function correctly, set the environment variable **DJANGO_SETTINGS_MODULE** to `SPPM.settings.local` +5. Run `pip install -r pip-requirements/local.txt` to install all the Python/Django requirements. + 1. Be mindful in the future that when you `git pull`, you will want to update the requirements by running the above command again. +6. Run the [makemigrations](https://docs.djangoproject.com/en/dev/ref/django-admin/#makemigrations) management command in case any of the newly installed requirements has any to create. + 1. Any time the requirements are updated, you should attempt to run this command again, and `migrate` if there were any new migrations found. +7. Run the [migrate](https://docs.djangoproject.com/en/dev/ref/django-admin/#migrate) management command to create the tables/columns in your database. +8. Run the `create_game_instances` management command to create the Game objects. +9. Run the [createsuperuser](https://docs.djangoproject.com/en/dev/ref/django-admin/#createsuperuser) management command to create your main user. +10. Run the `associate_super_user` management command to associate the Super User you just created with a ForumUser object. + 1. Arguments for the command are: + 1. **username** - The username of the Super User. + 2. **forum_id** - The user id from the Source.Python forums. +11. If you want to create a test (non-Super User) User, run the `create_test_user` management command. + 1. Arguments for the command are: + 1. **username** - The username of the User. + 2. **password** - The password to use for the User. + 3. **forum_id** - The user id from the Source.Python forums. +12. If you want additional users to test with, run the `create_random_users` management command. 1. Arguments for the command are: 1. **count** - The number of random Users to create. -11. Run the server using the [runserver](https://docs.djangoproject.com/en/dev/ref/django-admin/#runserver) management command. +13. Run the server using the [runserver](https://docs.djangoproject.com/en/dev/ref/django-admin/#runserver) management command. 1. Some IDEs, like Pycharm, have tools to run the server instead of manually running the command in a console window. ## Authentication (logging in) From 8daadb8b9d013086dea95c1ab7323edf757d3704 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sat, 2 Apr 2022 08:33:23 -0400 Subject: [PATCH 139/211] Added an ordering filter for ForumUserViewSet to allow ordering by username instead of user__username. --- users/api/ordering.py | 33 +++++++++ users/api/tests/test_views.py | 128 ++++++++++++++++++++++++++++++---- users/api/views.py | 12 ++-- 3 files changed, 155 insertions(+), 18 deletions(-) create mode 100644 users/api/ordering.py diff --git a/users/api/ordering.py b/users/api/ordering.py new file mode 100644 index 00000000..57895024 --- /dev/null +++ b/users/api/ordering.py @@ -0,0 +1,33 @@ +"""User custom ordering filters.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Third Party Django +from rest_framework.filters import OrderingFilter + + +# ============================================================================= +# ALL DECLARATION +# ============================================================================= +__all__ = ( + 'ForumUserOrderingFilter', +) + + +# ============================================================================= +# ORDERING +# ============================================================================= +class ForumUserOrderingFilter(OrderingFilter): + """Custom ForumUser ordering filter.""" + + def get_ordering(self, request, queryset, view): + """Allow username in place of user__username.""" + ordering = list(super().get_ordering(request, queryset, view)) + for index, item in enumerate(ordering): + prefix = '-' if item.startswith('-') else '' + item_name = item[1:] if prefix == '-' else item + if item_name == 'username': + ordering[index] = f'{prefix}user__username' + + return tuple(ordering) diff --git a/users/api/tests/test_views.py b/users/api/tests/test_views.py index d4a2e34f..68d24d45 100644 --- a/users/api/tests/test_views.py +++ b/users/api/tests/test_views.py @@ -4,12 +4,12 @@ # Third Party Django from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status -from rest_framework.filters import OrderingFilter from rest_framework.test import APITestCase # App -from test_utils.factories.users import ForumUserFactory +from test_utils.factories.users import NonAdminUserFactory, ForumUserFactory from users.api.filtersets import ForumUserFilterSet +from users.api.ordering import ForumUserOrderingFilter from users.api.serializers import ForumUserSerializer from users.api.views import ForumUserViewSet @@ -24,7 +24,7 @@ class ForumUserViewSetTestCase(APITestCase): def test_filter_backends(self): self.assertTupleEqual( tuple1=ForumUserViewSet.filter_backends, - tuple2=(OrderingFilter, DjangoFilterBackend) + tuple2=(ForumUserOrderingFilter, DjangoFilterBackend) ) def test_filterset_class(self): @@ -48,17 +48,32 @@ def test_serializer_class(self): def test_ordering(self): self.assertTupleEqual( tuple1=ForumUserViewSet.ordering, - tuple2=('user__username',), + tuple2=('username',), ) def test_ordering_fields(self): self.assertTupleEqual( tuple1=ForumUserViewSet.ordering_fields, - tuple2=('forum_id', 'user__username'), + tuple2=('forum_id', 'username'), ) def test_get_list(self): - user = ForumUserFactory() + user_1 = NonAdminUserFactory( + username='Alfred', + ) + forum_user_1 = ForumUserFactory( + forum_id=1, + user=user_1, + ) + user_2 = NonAdminUserFactory( + username='Zach', + ) + forum_user_2 = ForumUserFactory( + forum_id=2, + user=user_2, + ) + + # Test default ordering response = self.client.get(path=self.api_path) self.assertEqual( first=response.status_code, @@ -67,17 +82,106 @@ def test_get_list(self): content = response.json() self.assertEqual( first=content['count'], - second=1, + second=2, + ) + for n, forum_user in enumerate([forum_user_1, forum_user_2]): + content_user = content['results'][n] + self.assertEqual( + first=content_user['forum_id'], + second=forum_user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=forum_user.user.username, + ) + + # Test alphabetized custom ordering + response = self.client.get(path=f'{self.api_path}?ordering=username') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, ) - content_user = content['results'][0] + content = response.json() self.assertEqual( - first=content_user['forum_id'], - second=user.forum_id, + first=content['count'], + second=2, + ) + for n, forum_user in enumerate([forum_user_1, forum_user_2]): + content_user = content['results'][n] + self.assertEqual( + first=content_user['forum_id'], + second=forum_user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=forum_user.user.username, + ) + + # Test reverse alphabetized custom ordering + response = self.client.get(path=f'{self.api_path}?ordering=-username') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() self.assertEqual( - first=content_user['username'], - second=user.user.username, + first=content['count'], + second=2, + ) + for n, forum_user in enumerate([forum_user_2, forum_user_1]): + content_user = content['results'][n] + self.assertEqual( + first=content_user['forum_id'], + second=forum_user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=forum_user.user.username, + ) + + # Test forum_id ordering + response = self.client.get(path=f'{self.api_path}?ordering=forum_id') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, ) + content = response.json() + self.assertEqual( + first=content['count'], + second=2, + ) + for n, forum_user in enumerate([forum_user_1, forum_user_2]): + content_user = content['results'][n] + self.assertEqual( + first=content_user['forum_id'], + second=forum_user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=forum_user.user.username, + ) + + # Test reverse forum_id ordering + response = self.client.get(path=f'{self.api_path}?ordering=-forum_id') + self.assertEqual( + first=response.status_code, + second=status.HTTP_200_OK, + ) + content = response.json() + self.assertEqual( + first=content['count'], + second=2, + ) + for n, forum_user in enumerate([forum_user_2, forum_user_1]): + content_user = content['results'][n] + self.assertEqual( + first=content_user['forum_id'], + second=forum_user.forum_id, + ) + self.assertEqual( + first=content_user['username'], + second=forum_user.user.username, + ) def test_get_details(self): user = ForumUserFactory() diff --git a/users/api/views.py b/users/api/views.py index 94b4acb8..1099fb2a 100644 --- a/users/api/views.py +++ b/users/api/views.py @@ -8,7 +8,6 @@ # Third Party Django from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.filters import OrderingFilter from rest_framework.viewsets import ModelViewSet # App @@ -16,6 +15,7 @@ from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin from users.api.filtersets import ForumUserFilterSet +from users.api.ordering import ForumUserOrderingFilter from users.api.serializers import ForumUserSerializer from users.models import ForumUser @@ -37,19 +37,19 @@ class ForumUserViewSet(ModelViewSet): ###Available Ordering: * **forum_id** (descending) or **-forum_id** (ascending) - * **user__username** (descending) or **-user__username** (ascending) + * **username** (descending) or **-username** (ascending) ####Example: `?ordering=forum_id` - `?ordering=-user__username` + `?ordering=-username` """ - filter_backends = (OrderingFilter, DjangoFilterBackend) + filter_backends = (ForumUserOrderingFilter, DjangoFilterBackend) filterset_class = ForumUserFilterSet http_method_names = ('get', 'options') - ordering = ('user__username',) - ordering_fields = ('forum_id', 'user__username') + ordering = ('username',) + ordering_fields = ('forum_id', 'username') queryset = ForumUser.objects.prefetch_related( Prefetch( lookup='packages', From 32f3349052efa79dfc2a209f1c654a6e71b6429d Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Apr 2022 14:51:00 -0400 Subject: [PATCH 140/211] Updated base requirements. --- pip-requirements/base.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pip-requirements/base.txt b/pip-requirements/base.txt index 3978b6d6..8702af10 100644 --- a/pip-requirements/base.txt +++ b/pip-requirements/base.txt @@ -1,5 +1,5 @@ -django==4.0.3 -django-embed-video==1.4.2 +django==4.0.4 +django-embed-video==1.4.4 django-extensions==3.1.5 django-filter==21.1 django-model-utils==4.2.0 From f51f319d87c9c4e01eb5d9a7b312058b6b2dbfa2 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Apr 2022 14:51:34 -0400 Subject: [PATCH 141/211] Added version to release ordering. --- project_manager/common/api/tests/test_views.py | 2 +- project_manager/common/api/views/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project_manager/common/api/tests/test_views.py b/project_manager/common/api/tests/test_views.py index 391ccaea..5d4764c9 100644 --- a/project_manager/common/api/tests/test_views.py +++ b/project_manager/common/api/tests/test_views.py @@ -181,7 +181,7 @@ def test_base_attributes(self): ) self.assertTupleEqual( tuple1=ProjectReleaseViewSet.ordering_fields, - tuple2=('created',), + tuple2=('created', 'version'), ) self.assertEqual( first=ProjectReleaseViewSet.lookup_value_regex, diff --git a/project_manager/common/api/views/__init__.py b/project_manager/common/api/views/__init__.py index b3e5f54b..44e89394 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/common/api/views/__init__.py @@ -201,7 +201,7 @@ class ProjectReleaseViewSet(ProjectRelatedInfoMixin): """ http_method_names = ('get', 'post', 'options') ordering = ('-created',) - ordering_fields = ('created',) + ordering_fields = ('created', 'version') lookup_value_regex = RELEASE_VERSION_REGEX lookup_field = 'version' related_model_type = 'Release' From d8ad7398725538cec883851cfa6ab8774b39e3bc Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sat, 23 Apr 2022 14:51:51 -0400 Subject: [PATCH 142/211] Minor update. --- users/tests/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/tests/test_models.py b/users/tests/test_models.py index 3affc82f..29f86582 100644 --- a/users/tests/test_models.py +++ b/users/tests/test_models.py @@ -22,7 +22,7 @@ # ============================================================================= -# GLOBALS +# GLOBAL VARIABLES # ============================================================================= UserModel = get_user_model() From 32b885e365b1c07d7eb0430b31fee004b66e6d6e Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Apr 2022 09:32:18 -0400 Subject: [PATCH 143/211] Updated pylint to the newest version. Moved all pylint configurations into the .pylintrc file. Fixed several pylint issues. --- .pylintrc | 11 +++++++---- pip-requirements/local.txt | 2 +- project_manager/common/api/serializers/mixins.py | 5 +++-- project_manager/packages/api/serializers/mixins.py | 2 +- project_manager/plugins/api/serializers/mixins.py | 2 +- project_manager/sub_plugins/api/serializers/mixins.py | 4 ++-- prospector.yaml | 9 --------- users/management/commands/associate_super_user.py | 5 +++-- users/management/commands/create_random_users.py | 5 ++++- users/management/commands/create_test_user.py | 5 +++-- users/tests/test_commands.py | 11 ++++++++--- 11 files changed, 33 insertions(+), 28 deletions(-) diff --git a/.pylintrc b/.pylintrc index 14ea522e..8a0f0045 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,6 +4,8 @@ ignore= tests, migrations, +django-settings-module=SPPM.settings.local + [MESSAGES CONTROL] # Definitions of codes disabled @@ -11,12 +13,13 @@ ignore= # http://pylint-messages.wikidot.com/all-codes disable= abstract-method, + astroid-error, fixme, - locally-disabled, - no-member, - too-few-public-methods, too-many-ancestors, - logging-fstring-interpolation, + + +[FORMAT] +max-line-length=100 [BASIC] diff --git a/pip-requirements/local.txt b/pip-requirements/local.txt index f2f7501c..879a6b06 100644 --- a/pip-requirements/local.txt +++ b/pip-requirements/local.txt @@ -6,7 +6,7 @@ prospector==1.7.7 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pylint==2.12.2 +pylint==2.13.7 pytest-cov==3.0.0 pytest-django==4.5.2 random-username==1.0.2 diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/common/api/serializers/mixins.py index 2fbf5a1c..897edcf2 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/common/api/serializers/mixins.py @@ -54,6 +54,7 @@ def get_date_display(date, date_format): ) if date else date +# pylint: disable=too-few-public-methods class CreateRequirementsMixin: """Mixin for creating the requirement relationships for releases.""" @@ -155,8 +156,8 @@ def validate(self, attrs): return attrs - @staticmethod - def get_zip_file_args(zip_file): + # pylint: disable=no-self-use + def get_zip_file_args(self, zip_file): """Return the arguments necessary to instantiate the ZipFile class.""" return [zip_file] diff --git a/project_manager/packages/api/serializers/mixins.py b/project_manager/packages/api/serializers/mixins.py index 33c7a7b2..a8b59b17 100644 --- a/project_manager/packages/api/serializers/mixins.py +++ b/project_manager/packages/api/serializers/mixins.py @@ -33,5 +33,5 @@ def zip_parser(self): def get_project_kwargs(self): """Return kwargs for the project.""" return { - 'pk': self.context['view'].kwargs.get('package_slug') + 'pk': getattr(self, 'context')['view'].kwargs.get('package_slug') } diff --git a/project_manager/plugins/api/serializers/mixins.py b/project_manager/plugins/api/serializers/mixins.py index 04b2de51..5395c48c 100644 --- a/project_manager/plugins/api/serializers/mixins.py +++ b/project_manager/plugins/api/serializers/mixins.py @@ -33,5 +33,5 @@ def zip_parser(self): def get_project_kwargs(self): """Return kwargs for the project.""" return { - 'pk': self.context['view'].kwargs.get('plugin_slug') + 'pk': getattr(self, 'context')['view'].kwargs.get('plugin_slug') } diff --git a/project_manager/sub_plugins/api/serializers/mixins.py b/project_manager/sub_plugins/api/serializers/mixins.py index 6365d659..3032575e 100644 --- a/project_manager/sub_plugins/api/serializers/mixins.py +++ b/project_manager/sub_plugins/api/serializers/mixins.py @@ -32,7 +32,7 @@ class SubPluginReleaseBase: @property def parent_project(self): """Return the parent plugin.""" - kwargs = self.context['view'].kwargs + kwargs = getattr(self, 'context')['view'].kwargs plugin_slug = kwargs.get('plugin_slug') try: plugin = Plugin.objects.get(slug=plugin_slug) @@ -49,7 +49,7 @@ def zip_parser(self): def get_project_kwargs(self): """Return kwargs for the project.""" - kwargs = self.context['view'].kwargs + kwargs = getattr(self, 'context')['view'].kwargs return { 'slug': kwargs.get('sub_plugin_slug'), 'plugin': self.parent_project, diff --git a/prospector.yaml b/prospector.yaml index 0e290119..4ea91d45 100644 --- a/prospector.yaml +++ b/prospector.yaml @@ -37,12 +37,3 @@ mccabe: pylint: run: true - disable: - - astroid-error - - fixme - - django-not-configured - - ungrouped-imports - - wrong-import-order - - wrong-import-position - options: - max-line-length: 100 diff --git a/users/management/commands/associate_super_user.py b/users/management/commands/associate_super_user.py index 3d3ac7c2..c411ba9e 100644 --- a/users/management/commands/associate_super_user.py +++ b/users/management/commands/associate_super_user.py @@ -71,6 +71,7 @@ def handle(self, *args, **options): forum_id=forum_id, ) logger.info( - f'User "{username}" successfully associated with forum id ' - f'"{forum_id}".' + 'User "%s" successfully associated with forum id "%s".', + username, + forum_id, ) diff --git a/users/management/commands/create_random_users.py b/users/management/commands/create_random_users.py index 2e9c2059..0c47febc 100644 --- a/users/management/commands/create_random_users.py +++ b/users/management/commands/create_random_users.py @@ -91,7 +91,10 @@ def handle(self, *args, **options): objs=obj_list, ) - logger.info(f'Successfully created "{count}" users.') + logger.info( + 'Successfully created "%s" users.', + count, + ) @staticmethod def validate_unique_list(username_list, current_usernames, count): diff --git a/users/management/commands/create_test_user.py b/users/management/commands/create_test_user.py index 14884a46..c7612292 100644 --- a/users/management/commands/create_test_user.py +++ b/users/management/commands/create_test_user.py @@ -82,6 +82,7 @@ def handle(self, *args, **options): forum_id=forum_id, ) logger.info( - f'Successfully created user "{username}" and associated it with ' - f'forum id "{forum_id}".' + 'Successfully created user "%s" and associated it with forum id "%s".', + username, + forum_id, ) diff --git a/users/tests/test_commands.py b/users/tests/test_commands.py index b5b4e1a2..be3b8160 100644 --- a/users/tests/test_commands.py +++ b/users/tests/test_commands.py @@ -46,7 +46,9 @@ def test_associate_super_user(self, mock_logger): second=forum_id, ) mock_logger.info.assert_called_once_with( - f'User "{user.username}" successfully associated with forum id "{forum_id}".' + 'User "%s" successfully associated with forum id "%s".', + user.username, + forum_id ) @override_settings(LOCAL=False) @@ -118,7 +120,8 @@ def test_create_random_users(self, mock_logger): list2=id_list, ) mock_logger.info.assert_called_once_with( - f'Successfully created "{count}" users.' + 'Successfully created "%s" users.', + count, ) @override_settings(LOCAL=False) @@ -156,7 +159,9 @@ def test_create_test_user(self, mock_logger): second=username, ) mock_logger.info.assert_called_once_with( - f'Successfully created user "{username}" and associated it with forum id "{forum_id}".' + 'Successfully created user "%s" and associated it with forum id "%s".', + username, + forum_id, ) @override_settings(LOCAL=False) From 18215bf5b0926217805c5f082ec3de12e38c4fb0 Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Apr 2022 10:53:22 -0400 Subject: [PATCH 144/211] Moved all modules out of project_manager.common and into the base project_manager. --- .pylintrc | 2 - SPPM/settings/base.py | 2 +- .../{admin.py => admin/__init__.py} | 0 .../admin/__init__.py => admin/base.py} | 0 project_manager/{common => }/admin/inlines.py | 0 .../{common/api => api/common}/__init__.py | 0 .../{common/api => api/common}/filtersets.py | 0 .../common}/serializers/__init__.py | 4 +- .../api => api/common}/serializers/mixins.py | 2 +- .../api => api/common}/tests/__init__.py | 0 .../common}/tests/test_filtersets.py | 2 +- .../common}/tests/test_serializers.py | 6 +- .../api => api/common}/tests/test_views.py | 6 +- .../api => api/common}/views/__init__.py | 4 +- .../api => api/common}/views/mixins.py | 0 project_manager/common/__init__.py | 1 - project_manager/common/tests/__init__.py | 0 project_manager/common/tests/test_admin.py | 264 ------------------ project_manager/{common => }/constants.py | 0 .../{common => }/context_processors.py | 0 project_manager/{common => }/helpers.py | 2 +- project_manager/migrations/0001_initial.py | 14 +- project_manager/{common => }/mixins.py | 0 project_manager/models/__init__.py | 1 + .../{common/models.py => models/abstract.py} | 6 +- project_manager/packages/admin/__init__.py | 2 +- project_manager/packages/admin/inlines.py | 2 +- project_manager/packages/api/filtersets.py | 2 +- .../packages/api/serializers/__init__.py | 2 +- .../packages/api/tests/test_filtersets.py | 2 +- .../packages/api/tests/test_project_views.py | 2 +- .../packages/api/tests/test_related_views.py | 2 +- .../packages/api/tests/test_release_views.py | 2 +- .../packages/api/tests/test_serializers.py | 4 +- .../packages/api/tests/test_views.py | 2 +- project_manager/packages/api/views.py | 2 +- project_manager/packages/constants.py | 2 +- project_manager/packages/helpers.py | 2 +- project_manager/packages/models.py | 6 +- project_manager/packages/tests/test_admin.py | 4 +- .../packages/tests/test_helpers.py | 20 +- project_manager/packages/tests/test_models.py | 13 +- project_manager/packages/tests/test_views.py | 4 +- project_manager/packages/views.py | 2 +- project_manager/plugins/admin/__init__.py | 2 +- project_manager/plugins/admin/inlines.py | 2 +- project_manager/plugins/api/filtersets.py | 2 +- .../plugins/api/serializers/__init__.py | 4 +- .../plugins/api/tests/test_filtersets.py | 2 +- .../plugins/api/tests/test_project_views.py | 2 +- .../plugins/api/tests/test_related_views.py | 4 +- .../plugins/api/tests/test_release_views.py | 2 +- .../plugins/api/tests/test_serializers.py | 6 +- .../plugins/api/tests/test_views.py | 2 +- project_manager/plugins/api/views.py | 4 +- project_manager/plugins/constants.py | 2 +- project_manager/plugins/helpers.py | 2 +- project_manager/plugins/models.py | 6 +- project_manager/plugins/tests/test_admin.py | 4 +- project_manager/plugins/tests/test_helpers.py | 20 +- project_manager/plugins/tests/test_models.py | 13 +- project_manager/plugins/tests/test_views.py | 4 +- project_manager/plugins/views.py | 2 +- project_manager/sub_plugins/admin/__init__.py | 2 +- project_manager/sub_plugins/admin/inlines.py | 2 +- project_manager/sub_plugins/api/filtersets.py | 2 +- .../sub_plugins/api/serializers/__init__.py | 2 +- .../sub_plugins/api/tests/test_filtersets.py | 2 +- .../api/tests/test_project_views.py | 2 +- .../api/tests/test_related_views.py | 2 +- .../api/tests/test_release_views.py | 2 +- .../sub_plugins/api/tests/test_serializers.py | 4 +- .../sub_plugins/api/tests/test_views.py | 2 +- project_manager/sub_plugins/api/views.py | 2 +- project_manager/sub_plugins/constants.py | 2 +- project_manager/sub_plugins/helpers.py | 2 +- project_manager/sub_plugins/models.py | 6 +- .../sub_plugins/tests/test_admin.py | 4 +- .../sub_plugins/tests/test_helpers.py | 16 +- .../sub_plugins/tests/test_models.py | 13 +- .../sub_plugins/tests/test_views.py | 4 +- project_manager/sub_plugins/views.py | 2 +- project_manager/tests/test_admin.py | 253 +++++++++++++++++ .../{common => }/tests/test_helpers.py | 8 +- .../{common => }/tests/test_mixins.py | 2 +- .../{common => }/tests/test_models.py | 8 +- project_manager/{common => }/validators.py | 2 +- 87 files changed, 410 insertions(+), 414 deletions(-) rename project_manager/{admin.py => admin/__init__.py} (100%) rename project_manager/{common/admin/__init__.py => admin/base.py} (100%) rename project_manager/{common => }/admin/inlines.py (100%) rename project_manager/{common/api => api/common}/__init__.py (100%) rename project_manager/{common/api => api/common}/filtersets.py (100%) rename project_manager/{common/api => api/common}/serializers/__init__.py (99%) rename project_manager/{common/api => api/common}/serializers/mixins.py (99%) rename project_manager/{common/api => api/common}/tests/__init__.py (100%) rename project_manager/{common/api => api/common}/tests/test_filtersets.py (97%) rename project_manager/{common/api => api/common}/tests/test_serializers.py (99%) rename project_manager/{common/api => api/common}/tests/test_views.py (98%) rename project_manager/{common/api => api/common}/views/__init__.py (98%) rename project_manager/{common/api => api/common}/views/mixins.py (100%) delete mode 100644 project_manager/common/__init__.py delete mode 100644 project_manager/common/tests/__init__.py delete mode 100644 project_manager/common/tests/test_admin.py rename project_manager/{common => }/constants.py (100%) rename project_manager/{common => }/context_processors.py (100%) rename project_manager/{common => }/helpers.py (99%) rename project_manager/{common => }/mixins.py (100%) create mode 100644 project_manager/models/__init__.py rename project_manager/{common/models.py => models/abstract.py} (98%) rename project_manager/{common => }/tests/test_helpers.py (97%) rename project_manager/{common => }/tests/test_mixins.py (97%) rename project_manager/{common => }/tests/test_models.py (98%) rename project_manager/{common => }/validators.py (94%) diff --git a/.pylintrc b/.pylintrc index 8a0f0045..bdc66db3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -13,8 +13,6 @@ django-settings-module=SPPM.settings.local # http://pylint-messages.wikidot.com/all-codes disable= abstract-method, - astroid-error, - fixme, too-many-ancestors, diff --git a/SPPM/settings/base.py b/SPPM/settings/base.py index 4e0e7996..5569b7ea 100644 --- a/SPPM/settings/base.py +++ b/SPPM/settings/base.py @@ -84,7 +84,7 @@ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'project_manager.common.context_processors.add_common_context_processors', + 'project_manager.context_processors.add_common_context_processors', ], }, }, diff --git a/project_manager/admin.py b/project_manager/admin/__init__.py similarity index 100% rename from project_manager/admin.py rename to project_manager/admin/__init__.py diff --git a/project_manager/common/admin/__init__.py b/project_manager/admin/base.py similarity index 100% rename from project_manager/common/admin/__init__.py rename to project_manager/admin/base.py diff --git a/project_manager/common/admin/inlines.py b/project_manager/admin/inlines.py similarity index 100% rename from project_manager/common/admin/inlines.py rename to project_manager/admin/inlines.py diff --git a/project_manager/common/api/__init__.py b/project_manager/api/common/__init__.py similarity index 100% rename from project_manager/common/api/__init__.py rename to project_manager/api/common/__init__.py diff --git a/project_manager/common/api/filtersets.py b/project_manager/api/common/filtersets.py similarity index 100% rename from project_manager/common/api/filtersets.py rename to project_manager/api/common/filtersets.py diff --git a/project_manager/common/api/serializers/__init__.py b/project_manager/api/common/serializers/__init__.py similarity index 99% rename from project_manager/common/api/serializers/__init__.py rename to project_manager/api/common/serializers/__init__.py index e0b9b7fa..e4608954 100644 --- a/project_manager/common/api/serializers/__init__.py +++ b/project_manager/api/common/serializers/__init__.py @@ -21,13 +21,13 @@ from rest_framework.serializers import ModelSerializer # App -from project_manager.common.api.serializers.mixins import ( +from project_manager.api.common.serializers.mixins import ( CreateRequirementsMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, ProjectThroughMixin, ) -from project_manager.common.constants import ( +from project_manager.constants import ( RELEASE_NOTES_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) diff --git a/project_manager/common/api/serializers/mixins.py b/project_manager/api/common/serializers/mixins.py similarity index 99% rename from project_manager/common/api/serializers/mixins.py rename to project_manager/api/common/serializers/mixins.py index 897edcf2..3055d3d9 100644 --- a/project_manager/common/api/serializers/mixins.py +++ b/project_manager/api/common/serializers/mixins.py @@ -11,7 +11,7 @@ from rest_framework.serializers import ModelSerializer # App -from project_manager.common.helpers import GROUP_QUERYSET_NAMES +from project_manager.helpers import GROUP_QUERYSET_NAMES # ============================================================================= diff --git a/project_manager/common/api/tests/__init__.py b/project_manager/api/common/tests/__init__.py similarity index 100% rename from project_manager/common/api/tests/__init__.py rename to project_manager/api/common/tests/__init__.py diff --git a/project_manager/common/api/tests/test_filtersets.py b/project_manager/api/common/tests/test_filtersets.py similarity index 97% rename from project_manager/common/api/tests/test_filtersets.py rename to project_manager/api/common/tests/test_filtersets.py index 6fa5f441..df25be72 100644 --- a/project_manager/common/api/tests/test_filtersets.py +++ b/project_manager/api/common/tests/test_filtersets.py @@ -9,7 +9,7 @@ from django_filters.filterset import FilterSet # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet # ============================================================================= diff --git a/project_manager/common/api/tests/test_serializers.py b/project_manager/api/common/tests/test_serializers.py similarity index 99% rename from project_manager/common/api/tests/test_serializers.py rename to project_manager/api/common/tests/test_serializers.py index 21cd9509..b1f83527 100644 --- a/project_manager/common/api/tests/test_serializers.py +++ b/project_manager/api/common/tests/test_serializers.py @@ -21,7 +21,7 @@ # App from games.api.common.serializers import MinimalGameSerializer from games.constants import GAME_SLUG_MAX_LENGTH -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, @@ -30,13 +30,13 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.common.api.serializers.mixins import ( +from project_manager.api.common.serializers.mixins import ( CreateRequirementsMixin, ProjectLocaleMixin, ProjectReleaseCreationMixin, ProjectThroughMixin, ) -from project_manager.common.constants import ( +from project_manager.constants import ( RELEASE_NOTES_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) diff --git a/project_manager/common/api/tests/test_views.py b/project_manager/api/common/tests/test_views.py similarity index 98% rename from project_manager/common/api/tests/test_views.py rename to project_manager/api/common/tests/test_views.py index 5d4764c9..5d0ef766 100644 --- a/project_manager/common/api/tests/test_views.py +++ b/project_manager/api/common/tests/test_views.py @@ -13,7 +13,7 @@ from rest_framework.viewsets import ModelViewSet # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectAPIView, ProjectContributorViewSet, ProjectGameViewSet, @@ -22,11 +22,11 @@ ProjectTagViewSet, ProjectViewSet, ) -from project_manager.common.api.views.mixins import ( +from project_manager.api.common.views.mixins import ( ProjectRelatedInfoMixin, ProjectThroughModelMixin, ) -from project_manager.common.constants import RELEASE_VERSION_REGEX +from project_manager.constants import RELEASE_VERSION_REGEX # ============================================================================= diff --git a/project_manager/common/api/views/__init__.py b/project_manager/api/common/views/__init__.py similarity index 98% rename from project_manager/common/api/views/__init__.py rename to project_manager/api/common/views/__init__.py index 44e89394..d84ab3c0 100644 --- a/project_manager/common/api/views/__init__.py +++ b/project_manager/api/common/views/__init__.py @@ -18,11 +18,11 @@ from rest_framework.viewsets import ModelViewSet # App -from project_manager.common.api.views.mixins import ( +from project_manager.api.common.views.mixins import ( ProjectRelatedInfoMixin, ProjectThroughModelMixin, ) -from project_manager.common.constants import RELEASE_VERSION_REGEX +from project_manager.constants import RELEASE_VERSION_REGEX # ============================================================================= diff --git a/project_manager/common/api/views/mixins.py b/project_manager/api/common/views/mixins.py similarity index 100% rename from project_manager/common/api/views/mixins.py rename to project_manager/api/common/views/mixins.py diff --git a/project_manager/common/__init__.py b/project_manager/common/__init__.py deleted file mode 100644 index f2a653cd..00000000 --- a/project_manager/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Common functionalities for use in multiple apps.""" diff --git a/project_manager/common/tests/__init__.py b/project_manager/common/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/project_manager/common/tests/test_admin.py b/project_manager/common/tests/test_admin.py deleted file mode 100644 index 2adab343..00000000 --- a/project_manager/common/tests/test_admin.py +++ /dev/null @@ -1,264 +0,0 @@ -# ============================================================================= -# IMPORTS -# ============================================================================= -# Django -from django.contrib import admin -from django.test import TestCase - -# App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin -from project_manager.common.admin.inlines import ( - ProjectContributorInline, - ProjectGameInline, - ProjectImageInline, - ProjectTagInline, -) -from project_manager.common.models import Project - - -# ============================================================================= -# TEST CASES -# ============================================================================= -class ProjectAdminTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectAdmin, admin.ModelAdmin), - ) - - def test_actions(self): - self.assertIsNone(obj=ProjectAdmin.actions) - - def test_fieldsets(self): - self.assertTupleEqual( - tuple1=ProjectAdmin.fieldsets, - tuple2=( - ( - 'Project Info', - { - 'classes': ('wide',), - 'fields': ( - 'name', - 'owner', - 'configuration', - 'description', - 'synopsis', - 'logo', - 'topic', - ), - } - ), - ( - 'Metadata', - { - 'classes': ('collapse',), - 'fields': ( - 'basename', - 'slug', - 'created', - 'updated', - ), - }, - ) - ), - ) - - def test_list_display(self): - self.assertTupleEqual( - tuple1=ProjectAdmin.list_display, - tuple2=( - 'name', - 'basename', - 'owner', - ), - ) - - def test_raw_id_fields(self): - self.assertTupleEqual( - tuple1=ProjectAdmin.raw_id_fields, - tuple2=('owner',), - ) - - def test_readonly_fields(self): - self.assertTupleEqual( - tuple1=ProjectAdmin.readonly_fields, - tuple2=( - 'basename', - 'created', - 'slug', - 'updated', - ), - ) - - def test_search_fields(self): - self.assertTupleEqual( - tuple1=ProjectAdmin.search_fields, - tuple2=( - 'name', - 'basename', - 'owner__user__username', - 'contributors__user__username', - ) - ) - - def test_has_add_permission(self): - self.assertFalse( - expr=ProjectAdmin(Project, '').has_add_permission(''), - ) - - def test_has_delete_permission(self): - self.assertFalse( - expr=ProjectAdmin(Project, '').has_delete_permission(''), - ) - - -class ProjectReleaseAdminTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue(expr=issubclass(ProjectReleaseAdmin, admin.ModelAdmin)) - - def test_fieldsets(self): - self.assertTupleEqual( - tuple1=ProjectReleaseAdmin.fieldsets, - tuple2=( - ( - 'Release Info', - { - 'classes': ('wide',), - 'fields': ( - 'version', - 'notes', - 'zip_file', - ), - } - ), - ( - 'Metadata', - { - 'classes': ('collapse',), - 'fields': ( - 'created', - 'created_by', - 'download_count', - ), - }, - ) - ) - ) - - def test_list_display(self): - self.assertTupleEqual( - tuple1=ProjectReleaseAdmin.list_display, - tuple2=( - 'version', - 'created', - ) - ) - - def test_readonly_fields(self): - self.assertTupleEqual( - tuple1=ProjectReleaseAdmin.readonly_fields, - tuple2=( - 'zip_file', - 'download_count', - 'created', - 'created_by', - ) - ) - - def test_search_fields(self): - self.assertTupleEqual( - tuple1=ProjectReleaseAdmin.search_fields, - tuple2=( - 'version', - ) - ) - - def test_view_on_site(self): - self.assertFalse(expr=ProjectReleaseAdmin.view_on_site) - - -class ProjectContributorInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectContributorInline, admin.TabularInline), - ) - - def test_extra(self): - self.assertEqual( - first=ProjectContributorInline.extra, - second=0, - ) - - def test_fields(self): - self.assertTupleEqual( - tuple1=ProjectContributorInline.fields, - tuple2=('user',), - ) - - def test_raw_id_fields(self): - self.assertTupleEqual( - tuple1=ProjectContributorInline.raw_id_fields, - tuple2=('user',), - ) - - -class ProjectGameInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectGameInline, admin.TabularInline), - ) - - def test_fields(self): - self.assertTupleEqual( - tuple1=ProjectGameInline.fields, - tuple2=('game',), - ) - - def test_readonly_fields(self): - self.assertTupleEqual( - tuple1=ProjectGameInline.readonly_fields, - tuple2=('game',), - ) - - -class ProjectImageInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectImageInline, admin.TabularInline), - ) - - def test_fields(self): - self.assertTupleEqual( - tuple1=ProjectImageInline.fields, - tuple2=( - 'image', - 'created', - ), - ) - - def test_readonly_fields(self): - self.assertTupleEqual( - tuple1=ProjectImageInline.readonly_fields, - tuple2=( - 'image', - 'created', - ), - ) - - -class ProjectTagInlineTestCase(TestCase): - def test_class_inheritance(self): - self.assertTrue( - expr=issubclass(ProjectTagInline, admin.TabularInline), - ) - - def test_fields(self): - self.assertTupleEqual( - tuple1=ProjectTagInline.fields, - tuple2=('tag',), - ) - - def test_readonly_fields(self): - self.assertTupleEqual( - tuple1=ProjectTagInline.readonly_fields, - tuple2=('tag',), - ) diff --git a/project_manager/common/constants.py b/project_manager/constants.py similarity index 100% rename from project_manager/common/constants.py rename to project_manager/constants.py diff --git a/project_manager/common/context_processors.py b/project_manager/context_processors.py similarity index 100% rename from project_manager/common/context_processors.py rename to project_manager/context_processors.py diff --git a/project_manager/common/helpers.py b/project_manager/helpers.py similarity index 99% rename from project_manager/common/helpers.py rename to project_manager/helpers.py index 2dbabe74..e7b3c00b 100644 --- a/project_manager/common/helpers.py +++ b/project_manager/helpers.py @@ -15,7 +15,7 @@ from django.core.exceptions import ValidationError # App -from project_manager.common.constants import CANNOT_BE_NAMED, CANNOT_START_WITH +from project_manager.constants import CANNOT_BE_NAMED, CANNOT_START_WITH # ============================================================================= diff --git a/project_manager/migrations/0001_initial.py b/project_manager/migrations/0001_initial.py index 8282fc7a..aa640ea0 100644 --- a/project_manager/migrations/0001_initial.py +++ b/project_manager/migrations/0001_initial.py @@ -7,7 +7,7 @@ import embed_video.fields import model_utils.fields import precise_bbcode.fields -import project_manager.common.helpers +import project_manager.helpers import project_manager.packages.helpers import project_manager.plugins.helpers import project_manager.sub_plugins.helpers @@ -30,7 +30,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.helpers.handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -84,7 +84,7 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=project_manager.helpers.handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], @@ -159,7 +159,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.helpers.handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -213,7 +213,7 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=project_manager.helpers.handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], @@ -288,7 +288,7 @@ class Migration(migrations.Migration): ('configuration', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The configuration of the project. If too long, post on the forum and provide the link here. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), ('_description_rendered', models.TextField(blank=True, editable=False, null=True)), ('description', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The full description of the project. BBCode is allowed. 1024 char limit.', max_length=1024, no_rendered_field=True, null=True)), - ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.common.helpers.handle_project_logo_upload)), + ('logo', models.ImageField(blank=True, help_text="The project's logo image.", null=True, upload_to=project_manager.helpers.handle_project_logo_upload)), ('video', embed_video.fields.EmbedVideoField(help_text="The project's video.", null=True)), ('_synopsis_rendered', models.TextField(blank=True, editable=False, null=True)), ('synopsis', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='A brief description of the project. BBCode is allowed. 128 char limit.', max_length=128, no_rendered_field=True, null=True)), @@ -357,7 +357,7 @@ class Migration(migrations.Migration): ('version', models.CharField(help_text='The version for this release of the project.', max_length=8, validators=[django.core.validators.RegexValidator('^[0-9][0-9a-z.]*[0-9a-z]')])), ('_notes_rendered', models.TextField(blank=True, editable=False, null=True)), ('notes', precise_bbcode.fields.BBCodeTextField(blank=True, help_text='The notes for this particular release of the project.', max_length=512, no_rendered_field=True, null=True)), - ('zip_file', models.FileField(upload_to=project_manager.common.helpers.handle_release_zip_file_upload)), + ('zip_file', models.FileField(upload_to=project_manager.helpers.handle_release_zip_file_upload)), ('download_count', models.PositiveIntegerField(default=0)), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ], diff --git a/project_manager/common/mixins.py b/project_manager/mixins.py similarity index 100% rename from project_manager/common/mixins.py rename to project_manager/mixins.py diff --git a/project_manager/models/__init__.py b/project_manager/models/__init__.py new file mode 100644 index 00000000..e801ca7b --- /dev/null +++ b/project_manager/models/__init__.py @@ -0,0 +1 @@ +"""Base models.""" diff --git a/project_manager/common/models.py b/project_manager/models/abstract.py similarity index 98% rename from project_manager/common/models.py rename to project_manager/models/abstract.py index c7b70a09..46146263 100644 --- a/project_manager/common/models.py +++ b/project_manager/models/abstract.py @@ -20,7 +20,7 @@ from precise_bbcode.fields import BBCodeTextField # App -from project_manager.common.constants import ( +from project_manager.constants import ( FORUM_THREAD_URL, LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, @@ -31,11 +31,11 @@ RELEASE_NOTES_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.helpers import ( +from project_manager.helpers import ( handle_project_logo_upload, handle_release_zip_file_upload, ) -from project_manager.common.validators import version_validator +from project_manager.validators import version_validator # ============================================================================= diff --git a/project_manager/packages/admin/__init__.py b/project_manager/packages/admin/__init__.py index a8a8c2e7..2d25bedc 100644 --- a/project_manager/packages/admin/__init__.py +++ b/project_manager/packages/admin/__init__.py @@ -10,7 +10,7 @@ from django.contrib import admin # App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin from project_manager.packages.admin.inlines import ( PackageContributorInline, PackageImageInline, diff --git a/project_manager/packages/admin/inlines.py b/project_manager/packages/admin/inlines.py index 47fc9491..9eeb0813 100644 --- a/project_manager/packages/admin/inlines.py +++ b/project_manager/packages/admin/inlines.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.admin.inlines import ( +from project_manager.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, diff --git a/project_manager/packages/api/filtersets.py b/project_manager/packages/api/filtersets.py index 3e0a25ce..b58bb359 100644 --- a/project_manager/packages/api/filtersets.py +++ b/project_manager/packages/api/filtersets.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet from project_manager.packages.models import Package diff --git a/project_manager/packages/api/serializers/__init__.py b/project_manager/packages/api/serializers/__init__.py index e692910b..ebb6fb0d 100644 --- a/project_manager/packages/api/serializers/__init__.py +++ b/project_manager/packages/api/serializers/__init__.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, diff --git a/project_manager/packages/api/tests/test_filtersets.py b/project_manager/packages/api/tests/test_filtersets.py index a3589cad..61a551e5 100644 --- a/project_manager/packages/api/tests/test_filtersets.py +++ b/project_manager/packages/api/tests/test_filtersets.py @@ -5,7 +5,7 @@ from django.test import TestCase # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet from project_manager.packages.api.filtersets import PackageFilterSet from project_manager.packages.models import Package diff --git a/project_manager/packages/api/tests/test_project_views.py b/project_manager/packages/api/tests/test_project_views.py index 2b740313..1d5e626f 100644 --- a/project_manager/packages/api/tests/test_project_views.py +++ b/project_manager/packages/api/tests/test_project_views.py @@ -19,7 +19,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectViewSet +from project_manager.api.common.views import ProjectViewSet from project_manager.packages.api.filtersets import PackageFilterSet from project_manager.packages.api.serializers import ( PackageCreateSerializer, diff --git a/project_manager/packages/api/tests/test_related_views.py b/project_manager/packages/api/tests/test_related_views.py index d40b7f53..85d331d4 100644 --- a/project_manager/packages/api/tests/test_related_views.py +++ b/project_manager/packages/api/tests/test_related_views.py @@ -19,7 +19,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectContributorViewSet, ProjectGameViewSet, ProjectImageViewSet, diff --git a/project_manager/packages/api/tests/test_release_views.py b/project_manager/packages/api/tests/test_release_views.py index 59e8a246..7f34765a 100644 --- a/project_manager/packages/api/tests/test_release_views.py +++ b/project_manager/packages/api/tests/test_release_views.py @@ -19,7 +19,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectReleaseViewSet +from project_manager.api.common.views import ProjectReleaseViewSet from project_manager.packages.api.serializers import PackageReleaseSerializer from project_manager.packages.api.views import PackageReleaseViewSet from project_manager.packages.models import ( diff --git a/project_manager/packages/api/tests/test_serializers.py b/project_manager/packages/api/tests/test_serializers.py index db029305..386c5235 100644 --- a/project_manager/packages/api/tests/test_serializers.py +++ b/project_manager/packages/api/tests/test_serializers.py @@ -12,7 +12,7 @@ from rest_framework.serializers import ListSerializer, ModelSerializer # App -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, @@ -120,7 +120,7 @@ def test_class_inheritance(self): ) @mock.patch( - target='project_manager.common.api.serializers.ProjectSerializer.get_extra_kwargs', + target='project_manager.api.common.serializers.ProjectSerializer.get_extra_kwargs', return_value={}, ) def test_releases(self, _): diff --git a/project_manager/packages/api/tests/test_views.py b/project_manager/packages/api/tests/test_views.py index a32b2e4c..a3772718 100644 --- a/project_manager/packages/api/tests/test_views.py +++ b/project_manager/packages/api/tests/test_views.py @@ -7,7 +7,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectAPIView +from project_manager.api.common.views import ProjectAPIView from project_manager.packages.api.views import PackageAPIView diff --git a/project_manager/packages/api/views.py b/project_manager/packages/api/views.py index e515f9ea..1e8f281b 100644 --- a/project_manager/packages/api/views.py +++ b/project_manager/packages/api/views.py @@ -7,7 +7,7 @@ from django.db.models import Prefetch # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectAPIView, ProjectContributorViewSet, ProjectGameViewSet, diff --git a/project_manager/packages/constants.py b/project_manager/packages/constants.py index a70e77d7..d2276a47 100644 --- a/project_manager/packages/constants.py +++ b/project_manager/packages/constants.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.constants import ( +from project_manager.constants import ( ALLOWED_FILE_TYPES, IMAGE_URL, LOGO_URL, diff --git a/project_manager/packages/helpers.py b/project_manager/packages/helpers.py index d4d88d19..201023fb 100644 --- a/project_manager/packages/helpers.py +++ b/project_manager/packages/helpers.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError # App -from project_manager.common.helpers import ProjectZipFile, find_image_number +from project_manager.helpers import ProjectZipFile, find_image_number from project_manager.packages.constants import ( PACKAGE_ALLOWED_FILE_TYPES, PACKAGE_IMAGE_URL, diff --git a/project_manager/packages/models.py b/project_manager/packages/models.py index 838e8602..94922bc8 100644 --- a/project_manager/packages/models.py +++ b/project_manager/packages/models.py @@ -13,17 +13,17 @@ from model_utils.tracker import FieldTracker # App -from project_manager.common.constants import ( +from project_manager.constants import ( PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import ( +from project_manager.validators import ( basename_validator, version_validator, ) diff --git a/project_manager/packages/tests/test_admin.py b/project_manager/packages/tests/test_admin.py index 7cb820d1..b39abde6 100644 --- a/project_manager/packages/tests/test_admin.py +++ b/project_manager/packages/tests/test_admin.py @@ -9,8 +9,8 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin -from project_manager.common.admin.inlines import ( +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, diff --git a/project_manager/packages/tests/test_helpers.py b/project_manager/packages/tests/test_helpers.py index fea4f802..d63a0a01 100644 --- a/project_manager/packages/tests/test_helpers.py +++ b/project_manager/packages/tests/test_helpers.py @@ -11,7 +11,7 @@ from django.test import TestCase # App -from project_manager.common.helpers import ProjectZipFile +from project_manager.helpers import ProjectZipFile from project_manager.packages.constants import ( PACKAGE_ALLOWED_FILE_TYPES, PACKAGE_IMAGE_URL, @@ -44,7 +44,7 @@ class PackageZipFileTestCase(TestCase): def setUp(self) -> None: super().setUp() self.mock_get_file_list = mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', + target='project_manager.helpers.ProjectZipFile.get_file_list', ).start() def tearDown(self) -> None: @@ -93,7 +93,7 @@ def test_file_types(self): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_find_base_info(self, _): package_basename = 'test_package_as_module' @@ -137,7 +137,7 @@ def test_find_base_info(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_get_base_paths(self, _): package_basename = 'test_package_as_module' @@ -166,7 +166,7 @@ def test_get_base_paths(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_base_file_in_zip(self, _): package_basename = 'test_package_as_module' @@ -191,7 +191,7 @@ def test_validate_base_file_in_zip(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_get_requirement_path(self, _): package_basename = 'test_package_as_module' @@ -217,7 +217,7 @@ def test_get_requirement_path(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_file_paths(self, _): package_basename = 'test_package_as_module' @@ -284,7 +284,7 @@ def test_validate_file_paths(self, _): ) @mock.patch( - target='project_manager.common.helpers.logger', + target='project_manager.helpers.logger', ) def test_validate_requirements_file_failures(self, mock_logger): base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'packages' @@ -310,10 +310,10 @@ def test_validate_requirements_file_failures(self, mock_logger): ) @mock.patch( - target='project_manager.common.helpers.json.loads', + target='project_manager.helpers.json.loads', ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_requirements_file_item_failures(self, _, mock_json_loads): custom_package_basename = 'test_custom_package' diff --git a/project_manager/packages/tests/test_models.py b/project_manager/packages/tests/test_models.py index f47f9862..4609b0ce 100644 --- a/project_manager/packages/tests/test_models.py +++ b/project_manager/packages/tests/test_models.py @@ -19,19 +19,22 @@ # App from games.models import Game -from project_manager.common.constants import ( +from project_manager.constants import ( FORUM_THREAD_URL, LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import basename_validator, version_validator +from project_manager.validators import ( + basename_validator, + version_validator, +) from project_manager.packages.constants import PACKAGE_LOGO_URL from project_manager.packages.helpers import ( handle_package_image_upload, @@ -250,7 +253,7 @@ def test_total_downloads(self): ) @mock.patch( - target='project_manager.common.models.Image.open', + target='project_manager.models.abstract.Image.open', ) def test_clean_logo(self, mock_image_open): Package().clean() @@ -281,7 +284,7 @@ def test_clean_logo(self, mock_image_open): ) @mock.patch( - target='project_manager.common.models.settings.MEDIA_ROOT', + target='project_manager.models.abstract.settings.MEDIA_ROOT', ) def test_save(self, mock_media_root): basename = 'test' diff --git a/project_manager/packages/tests/test_views.py b/project_manager/packages/tests/test_views.py index 2ed2dfb3..b8c15d9f 100644 --- a/project_manager/packages/tests/test_views.py +++ b/project_manager/packages/tests/test_views.py @@ -12,7 +12,7 @@ from rest_framework import status # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin from project_manager.packages.constants import PACKAGE_RELEASE_URL from project_manager.packages.models import Package, PackageRelease from project_manager.packages.views import PackageReleaseDownloadView @@ -72,7 +72,7 @@ def test_base_attributes(self): ) @mock.patch( - target='project_manager.common.mixins.DownloadMixin.full_path', + target='project_manager.mixins.DownloadMixin.full_path', ) def test_get_failure(self, mock_full_path): mock_full_path.isfile.return_value = False diff --git a/project_manager/packages/views.py b/project_manager/packages/views.py index c9b9d2fc..23abf4cf 100644 --- a/project_manager/packages/views.py +++ b/project_manager/packages/views.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin from project_manager.packages.constants import PACKAGE_RELEASE_URL from project_manager.packages.models import Package, PackageRelease diff --git a/project_manager/plugins/admin/__init__.py b/project_manager/plugins/admin/__init__.py index 9be8f4f8..58635bd3 100644 --- a/project_manager/plugins/admin/__init__.py +++ b/project_manager/plugins/admin/__init__.py @@ -10,7 +10,7 @@ from django.contrib import admin # App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin from project_manager.plugins.admin.inlines import ( PluginContributorInline, PluginGameInline, diff --git a/project_manager/plugins/admin/inlines.py b/project_manager/plugins/admin/inlines.py index 55464f95..e15d1aa6 100644 --- a/project_manager/plugins/admin/inlines.py +++ b/project_manager/plugins/admin/inlines.py @@ -7,7 +7,7 @@ from django.contrib import admin # App -from project_manager.common.admin.inlines import ( +from project_manager.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, diff --git a/project_manager/plugins/api/filtersets.py b/project_manager/plugins/api/filtersets.py index ca403f01..c13bf53c 100644 --- a/project_manager/plugins/api/filtersets.py +++ b/project_manager/plugins/api/filtersets.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet from project_manager.plugins.models import Plugin diff --git a/project_manager/plugins/api/serializers/__init__.py b/project_manager/plugins/api/serializers/__init__.py index 4d70b120..27b16c01 100644 --- a/project_manager/plugins/api/serializers/__init__.py +++ b/project_manager/plugins/api/serializers/__init__.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError # App -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, @@ -16,7 +16,7 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.common.api.serializers.mixins import ProjectThroughMixin +from project_manager.api.common.serializers.mixins import ProjectThroughMixin from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer from project_manager.plugins.api.serializers.mixins import PluginReleaseBase from project_manager.plugins.models import ( diff --git a/project_manager/plugins/api/tests/test_filtersets.py b/project_manager/plugins/api/tests/test_filtersets.py index 5ff4c517..b5b438f6 100644 --- a/project_manager/plugins/api/tests/test_filtersets.py +++ b/project_manager/plugins/api/tests/test_filtersets.py @@ -5,7 +5,7 @@ from django.test import TestCase # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet from project_manager.plugins.api.filtersets import PluginFilterSet from project_manager.plugins.models import Plugin diff --git a/project_manager/plugins/api/tests/test_project_views.py b/project_manager/plugins/api/tests/test_project_views.py index 4bf6f3ee..e3052005 100644 --- a/project_manager/plugins/api/tests/test_project_views.py +++ b/project_manager/plugins/api/tests/test_project_views.py @@ -19,7 +19,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectViewSet +from project_manager.api.common.views import ProjectViewSet from project_manager.plugins.api.filtersets import PluginFilterSet from project_manager.plugins.api.serializers import ( PluginCreateSerializer, diff --git a/project_manager/plugins/api/tests/test_related_views.py b/project_manager/plugins/api/tests/test_related_views.py index 608b7a58..d28072a7 100644 --- a/project_manager/plugins/api/tests/test_related_views.py +++ b/project_manager/plugins/api/tests/test_related_views.py @@ -19,13 +19,13 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectContributorViewSet, ProjectGameViewSet, ProjectImageViewSet, ProjectTagViewSet, ) -from project_manager.common.api.views.mixins import ProjectThroughModelMixin +from project_manager.api.common.views.mixins import ProjectThroughModelMixin from project_manager.plugins.api.serializers import ( PluginContributorSerializer, PluginGameSerializer, diff --git a/project_manager/plugins/api/tests/test_release_views.py b/project_manager/plugins/api/tests/test_release_views.py index 00254c7e..acca6ba0 100644 --- a/project_manager/plugins/api/tests/test_release_views.py +++ b/project_manager/plugins/api/tests/test_release_views.py @@ -19,7 +19,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectReleaseViewSet +from project_manager.api.common.views import ProjectReleaseViewSet from project_manager.plugins.api.serializers import PluginReleaseSerializer from project_manager.plugins.api.views import PluginReleaseViewSet from project_manager.plugins.models import ( diff --git a/project_manager/plugins/api/tests/test_serializers.py b/project_manager/plugins/api/tests/test_serializers.py index 7f590a0f..ef1d225d 100644 --- a/project_manager/plugins/api/tests/test_serializers.py +++ b/project_manager/plugins/api/tests/test_serializers.py @@ -13,7 +13,7 @@ from rest_framework.serializers import ListSerializer, ModelSerializer # App -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, @@ -22,7 +22,7 @@ ProjectSerializer, ProjectTagSerializer, ) -from project_manager.common.api.serializers.mixins import ProjectThroughMixin +from project_manager.api.common.serializers.mixins import ProjectThroughMixin from project_manager.packages.api.common.serializers import ReleasePackageRequirementSerializer from project_manager.plugins.api.serializers import ( PluginContributorSerializer, @@ -123,7 +123,7 @@ def test_class_inheritance(self): ) @mock.patch( - target='project_manager.common.api.serializers.ProjectSerializer.get_extra_kwargs', + target='project_manager.api.common.serializers.ProjectSerializer.get_extra_kwargs', return_value={}, ) def test_releases(self, _): diff --git a/project_manager/plugins/api/tests/test_views.py b/project_manager/plugins/api/tests/test_views.py index 4a151c2a..6c4354a3 100644 --- a/project_manager/plugins/api/tests/test_views.py +++ b/project_manager/plugins/api/tests/test_views.py @@ -7,7 +7,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectAPIView +from project_manager.api.common.views import ProjectAPIView from project_manager.plugins.api.views import PluginAPIView diff --git a/project_manager/plugins/api/views.py b/project_manager/plugins/api/views.py index a53f55e3..e8443c92 100644 --- a/project_manager/plugins/api/views.py +++ b/project_manager/plugins/api/views.py @@ -10,7 +10,7 @@ from rest_framework.reverse import reverse # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectAPIView, ProjectContributorViewSet, ProjectGameViewSet, @@ -19,7 +19,7 @@ ProjectTagViewSet, ProjectViewSet, ) -from project_manager.common.api.views.mixins import ProjectThroughModelMixin +from project_manager.api.common.views.mixins import ProjectThroughModelMixin from project_manager.plugins.api.filtersets import PluginFilterSet from project_manager.plugins.api.serializers import ( PluginContributorSerializer, diff --git a/project_manager/plugins/constants.py b/project_manager/plugins/constants.py index 8eaf4b35..f9aceb7a 100644 --- a/project_manager/plugins/constants.py +++ b/project_manager/plugins/constants.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.constants import ( +from project_manager.constants import ( ALLOWED_FILE_TYPES, IMAGE_URL, LOGO_URL, diff --git a/project_manager/plugins/helpers.py b/project_manager/plugins/helpers.py index 2d57dfe3..9a6d0b4c 100644 --- a/project_manager/plugins/helpers.py +++ b/project_manager/plugins/helpers.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError # App -from project_manager.common.helpers import ProjectZipFile, find_image_number +from project_manager.helpers import ProjectZipFile, find_image_number from project_manager.plugins.constants import ( PLUGIN_ALLOWED_FILE_TYPES, PLUGIN_IMAGE_URL, diff --git a/project_manager/plugins/models.py b/project_manager/plugins/models.py index c71e4c4f..8b26d8ff 100644 --- a/project_manager/plugins/models.py +++ b/project_manager/plugins/models.py @@ -13,17 +13,17 @@ from model_utils.tracker import FieldTracker # App -from project_manager.common.constants import ( +from project_manager.constants import ( PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import ( +from project_manager.validators import ( basename_validator, version_validator, ) diff --git a/project_manager/plugins/tests/test_admin.py b/project_manager/plugins/tests/test_admin.py index 4ca8a2d4..30f518be 100644 --- a/project_manager/plugins/tests/test_admin.py +++ b/project_manager/plugins/tests/test_admin.py @@ -9,8 +9,8 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin -from project_manager.common.admin.inlines import ( +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, diff --git a/project_manager/plugins/tests/test_helpers.py b/project_manager/plugins/tests/test_helpers.py index e2496b91..e0b43291 100644 --- a/project_manager/plugins/tests/test_helpers.py +++ b/project_manager/plugins/tests/test_helpers.py @@ -11,7 +11,7 @@ from django.test import TestCase # App -from project_manager.common.helpers import ProjectZipFile +from project_manager.helpers import ProjectZipFile from project_manager.plugins.constants import ( PLUGIN_ALLOWED_FILE_TYPES, PLUGIN_IMAGE_URL, @@ -46,7 +46,7 @@ class PluginZipFileTestCase(TestCase): def setUp(self) -> None: super().setUp() self.mock_get_file_list = mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', + target='project_manager.helpers.ProjectZipFile.get_file_list', ).start() def tearDown(self) -> None: @@ -84,7 +84,7 @@ def test_file_types(self): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_find_base_info(self, _): plugin_basename = 'test_plugin' @@ -115,7 +115,7 @@ def test_find_base_info(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_get_base_paths(self, _): plugin_basename = 'test_plugin' @@ -130,7 +130,7 @@ def test_get_base_paths(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_base_file_in_zip(self, _): plugin_basename = 'test_plugin' @@ -155,7 +155,7 @@ def test_validate_base_file_in_zip(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_get_requirement_path(self, _): plugin_basename = 'test_plugin' @@ -170,7 +170,7 @@ def test_get_requirement_path(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_file_paths(self, _): plugin_basename = 'test_plugin' @@ -237,7 +237,7 @@ def test_validate_file_paths(self, _): ) @mock.patch( - target='project_manager.common.helpers.logger', + target='project_manager.helpers.logger', ) def test_validate_requirements_file_failures(self, mock_logger): base_path = settings.BASE_DIR / 'fixtures' / 'releases' / 'plugins' @@ -263,10 +263,10 @@ def test_validate_requirements_file_failures(self, mock_logger): ) @mock.patch( - target='project_manager.common.helpers.json.loads', + target='project_manager.helpers.json.loads', ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_requirements_file_item_failures(self, _, mock_json_loads): custom_package_basename = 'test_custom_package' diff --git a/project_manager/plugins/tests/test_models.py b/project_manager/plugins/tests/test_models.py index d550053a..65f83928 100644 --- a/project_manager/plugins/tests/test_models.py +++ b/project_manager/plugins/tests/test_models.py @@ -19,19 +19,22 @@ # App from games.models import Game -from project_manager.common.constants import ( +from project_manager.constants import ( FORUM_THREAD_URL, LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import basename_validator, version_validator +from project_manager.validators import ( + basename_validator, + version_validator, +) from project_manager.packages.models import Package from project_manager.plugins.constants import PLUGIN_LOGO_URL, PATH_MAX_LENGTH from project_manager.plugins.helpers import ( @@ -255,7 +258,7 @@ def test_total_downloads(self): ) @mock.patch( - target='project_manager.common.models.Image.open', + target='project_manager.models.abstract.Image.open', ) def test_clean_logo(self, mock_image_open): Plugin().clean() @@ -286,7 +289,7 @@ def test_clean_logo(self, mock_image_open): ) @mock.patch( - target='project_manager.common.models.settings.MEDIA_ROOT', + target='project_manager.models.abstract.settings.MEDIA_ROOT', ) def test_save(self, mock_media_root): basename = 'test' diff --git a/project_manager/plugins/tests/test_views.py b/project_manager/plugins/tests/test_views.py index 043e0c60..dadb3c73 100644 --- a/project_manager/plugins/tests/test_views.py +++ b/project_manager/plugins/tests/test_views.py @@ -12,7 +12,7 @@ from rest_framework import status # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin from project_manager.plugins.constants import PLUGIN_RELEASE_URL from project_manager.plugins.models import Plugin, PluginRelease from project_manager.plugins.views import PluginReleaseDownloadView @@ -72,7 +72,7 @@ def test_base_attributes(self): ) @mock.patch( - target='project_manager.common.mixins.DownloadMixin.full_path', + target='project_manager.mixins.DownloadMixin.full_path', ) def test_get_failure(self, mock_full_path): mock_full_path.isfile.return_value = False diff --git a/project_manager/plugins/views.py b/project_manager/plugins/views.py index 063e3a80..0357ddbc 100644 --- a/project_manager/plugins/views.py +++ b/project_manager/plugins/views.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin from project_manager.plugins.constants import PLUGIN_RELEASE_URL from project_manager.plugins.models import Plugin, PluginRelease diff --git a/project_manager/sub_plugins/admin/__init__.py b/project_manager/sub_plugins/admin/__init__.py index 507d6883..c47d8368 100644 --- a/project_manager/sub_plugins/admin/__init__.py +++ b/project_manager/sub_plugins/admin/__init__.py @@ -10,7 +10,7 @@ from django.contrib import admin # App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin from project_manager.sub_plugins.admin.inlines import ( SubPluginContributorInline, SubPluginGameInline, diff --git a/project_manager/sub_plugins/admin/inlines.py b/project_manager/sub_plugins/admin/inlines.py index 5d51d81b..55d328f6 100644 --- a/project_manager/sub_plugins/admin/inlines.py +++ b/project_manager/sub_plugins/admin/inlines.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.admin.inlines import ( +from project_manager.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, diff --git a/project_manager/sub_plugins/api/filtersets.py b/project_manager/sub_plugins/api/filtersets.py index a4069ad4..c4477877 100644 --- a/project_manager/sub_plugins/api/filtersets.py +++ b/project_manager/sub_plugins/api/filtersets.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet from project_manager.sub_plugins.models import SubPlugin diff --git a/project_manager/sub_plugins/api/serializers/__init__.py b/project_manager/sub_plugins/api/serializers/__init__.py index a3ecd59c..0ee08c81 100644 --- a/project_manager/sub_plugins/api/serializers/__init__.py +++ b/project_manager/sub_plugins/api/serializers/__init__.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError # App -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, diff --git a/project_manager/sub_plugins/api/tests/test_filtersets.py b/project_manager/sub_plugins/api/tests/test_filtersets.py index da437533..e41bc4c5 100644 --- a/project_manager/sub_plugins/api/tests/test_filtersets.py +++ b/project_manager/sub_plugins/api/tests/test_filtersets.py @@ -5,7 +5,7 @@ from django.test import TestCase # App -from project_manager.common.api.filtersets import ProjectFilterSet +from project_manager.api.common.filtersets import ProjectFilterSet from project_manager.sub_plugins.api.filtersets import SubPluginFilterSet from project_manager.sub_plugins.models import SubPlugin diff --git a/project_manager/sub_plugins/api/tests/test_project_views.py b/project_manager/sub_plugins/api/tests/test_project_views.py index b1c15cb7..a9fdc39f 100644 --- a/project_manager/sub_plugins/api/tests/test_project_views.py +++ b/project_manager/sub_plugins/api/tests/test_project_views.py @@ -20,7 +20,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectViewSet +from project_manager.api.common.views import ProjectViewSet from project_manager.sub_plugins.api.filtersets import SubPluginFilterSet from project_manager.sub_plugins.api.serializers import ( SubPluginCreateSerializer, diff --git a/project_manager/sub_plugins/api/tests/test_related_views.py b/project_manager/sub_plugins/api/tests/test_related_views.py index be6d9ac6..0b02b3da 100644 --- a/project_manager/sub_plugins/api/tests/test_related_views.py +++ b/project_manager/sub_plugins/api/tests/test_related_views.py @@ -20,7 +20,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectContributorViewSet, ProjectGameViewSet, ProjectImageViewSet, diff --git a/project_manager/sub_plugins/api/tests/test_release_views.py b/project_manager/sub_plugins/api/tests/test_release_views.py index e2e921d1..acc3eaf4 100644 --- a/project_manager/sub_plugins/api/tests/test_release_views.py +++ b/project_manager/sub_plugins/api/tests/test_release_views.py @@ -20,7 +20,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectReleaseViewSet +from project_manager.api.common.views import ProjectReleaseViewSet from project_manager.sub_plugins.api.serializers import SubPluginReleaseSerializer from project_manager.sub_plugins.api.views import SubPluginReleaseViewSet from project_manager.sub_plugins.models import ( diff --git a/project_manager/sub_plugins/api/tests/test_serializers.py b/project_manager/sub_plugins/api/tests/test_serializers.py index 31528015..68eb260f 100644 --- a/project_manager/sub_plugins/api/tests/test_serializers.py +++ b/project_manager/sub_plugins/api/tests/test_serializers.py @@ -14,7 +14,7 @@ from rest_framework.serializers import ListSerializer, ModelSerializer # App -from project_manager.common.api.serializers import ( +from project_manager.api.common.serializers import ( ProjectContributorSerializer, ProjectCreateReleaseSerializer, ProjectGameSerializer, @@ -124,7 +124,7 @@ def test_class_inheritance(self): ) @mock.patch( - target='project_manager.common.api.serializers.ProjectSerializer.get_extra_kwargs', + target='project_manager.api.common.serializers.ProjectSerializer.get_extra_kwargs', return_value={}, ) def test_releases(self, _): diff --git a/project_manager/sub_plugins/api/tests/test_views.py b/project_manager/sub_plugins/api/tests/test_views.py index 4e02f0d5..c79a18fc 100644 --- a/project_manager/sub_plugins/api/tests/test_views.py +++ b/project_manager/sub_plugins/api/tests/test_views.py @@ -7,7 +7,7 @@ from rest_framework.test import APITestCase # App -from project_manager.common.api.views import ProjectAPIView +from project_manager.api.common.views import ProjectAPIView from project_manager.sub_plugins.api.views import SubPluginAPIView diff --git a/project_manager/sub_plugins/api/views.py b/project_manager/sub_plugins/api/views.py index 97d08400..e44a8ab3 100644 --- a/project_manager/sub_plugins/api/views.py +++ b/project_manager/sub_plugins/api/views.py @@ -12,7 +12,7 @@ from rest_framework.reverse import reverse # App -from project_manager.common.api.views import ( +from project_manager.api.common.views import ( ProjectAPIView, ProjectContributorViewSet, ProjectGameViewSet, diff --git a/project_manager/sub_plugins/constants.py b/project_manager/sub_plugins/constants.py index b50a990b..87f087af 100644 --- a/project_manager/sub_plugins/constants.py +++ b/project_manager/sub_plugins/constants.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.constants import ( +from project_manager.constants import ( ALLOWED_FILE_TYPES, IMAGE_URL, LOGO_URL, diff --git a/project_manager/sub_plugins/helpers.py b/project_manager/sub_plugins/helpers.py index 4b9782da..e80da340 100644 --- a/project_manager/sub_plugins/helpers.py +++ b/project_manager/sub_plugins/helpers.py @@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError # App -from project_manager.common.helpers import ProjectZipFile, find_image_number +from project_manager.helpers import ProjectZipFile, find_image_number from project_manager.plugins.constants import PLUGIN_PATH from project_manager.sub_plugins.constants import ( SUB_PLUGIN_ALLOWED_FILE_TYPES, diff --git a/project_manager/sub_plugins/models.py b/project_manager/sub_plugins/models.py index 69ab0991..3a9dc267 100644 --- a/project_manager/sub_plugins/models.py +++ b/project_manager/sub_plugins/models.py @@ -13,17 +13,17 @@ from model_utils.tracker import FieldTracker # App -from project_manager.common.constants import ( +from project_manager.constants import ( PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import ( +from project_manager.validators import ( basename_validator, version_validator, ) diff --git a/project_manager/sub_plugins/tests/test_admin.py b/project_manager/sub_plugins/tests/test_admin.py index e6153f82..244ed474 100644 --- a/project_manager/sub_plugins/tests/test_admin.py +++ b/project_manager/sub_plugins/tests/test_admin.py @@ -9,8 +9,8 @@ from django.test import TestCase # App -from project_manager.common.admin import ProjectAdmin, ProjectReleaseAdmin -from project_manager.common.admin.inlines import ( +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.inlines import ( ProjectContributorInline, ProjectGameInline, ProjectImageInline, diff --git a/project_manager/sub_plugins/tests/test_helpers.py b/project_manager/sub_plugins/tests/test_helpers.py index f7b355ad..d89630ff 100644 --- a/project_manager/sub_plugins/tests/test_helpers.py +++ b/project_manager/sub_plugins/tests/test_helpers.py @@ -11,7 +11,7 @@ from django.test import TestCase # App -from project_manager.common.helpers import ProjectZipFile +from project_manager.helpers import ProjectZipFile from project_manager.plugins.constants import PLUGIN_PATH from project_manager.sub_plugins.constants import ( SUB_PLUGIN_ALLOWED_FILE_TYPES, @@ -63,7 +63,7 @@ def setUpTestData(cls): def setUp(self) -> None: super().setUp() self.mock_get_file_list = mock.patch( - target='project_manager.common.helpers.ProjectZipFile.get_file_list', + target='project_manager.helpers.ProjectZipFile.get_file_list', ).start() def tearDown(self) -> None: @@ -112,7 +112,7 @@ def test_file_types(self): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_find_base_info(self, _): sub_plugin_basename = 'test_sub_plugin' @@ -143,7 +143,7 @@ def test_find_base_info(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_base_file_in_zip(self, _): sub_plugin_basename = 'test_plugin' @@ -213,7 +213,7 @@ def test_validate_base_file_in_zip(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_get_requirement_paths(self, _): sub_plugin_basename = 'test_sub_plugin' @@ -249,7 +249,7 @@ def test_get_requirement_paths(self, _): ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) def test_validate_file_paths(self, _): sub_plugin_basename = 'test_plugin' @@ -358,10 +358,10 @@ def test_validate_requirements_file_failures(self, mock_logger): ) @mock.patch( - target='project_manager.common.helpers.json.loads', + target='project_manager.helpers.json.loads', ) @mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ) @mock.patch( target='project_manager.sub_plugins.helpers.ZipFile', diff --git a/project_manager/sub_plugins/tests/test_models.py b/project_manager/sub_plugins/tests/test_models.py index 8436c299..5812ef76 100644 --- a/project_manager/sub_plugins/tests/test_models.py +++ b/project_manager/sub_plugins/tests/test_models.py @@ -19,19 +19,22 @@ # App from games.models import Game -from project_manager.common.constants import ( +from project_manager.constants import ( FORUM_THREAD_URL, LOGO_MAX_HEIGHT, LOGO_MAX_WIDTH, PROJECT_BASENAME_MAX_LENGTH, PROJECT_SLUG_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import basename_validator, version_validator +from project_manager.validators import ( + basename_validator, + version_validator, +) from project_manager.packages.models import Package from project_manager.plugins.models import Plugin from project_manager.sub_plugins.constants import SUB_PLUGIN_LOGO_URL @@ -272,7 +275,7 @@ def test_total_downloads(self): ) @mock.patch( - target='project_manager.common.models.Image.open', + target='project_manager.models.abstract.Image.open', ) def test_clean_logo(self, mock_image_open): SubPlugin().clean() @@ -303,7 +306,7 @@ def test_clean_logo(self, mock_image_open): ) @mock.patch( - target='project_manager.common.models.settings.MEDIA_ROOT', + target='project_manager.models.abstract.settings.MEDIA_ROOT', ) def test_save(self, mock_media_root): basename = 'test' diff --git a/project_manager/sub_plugins/tests/test_views.py b/project_manager/sub_plugins/tests/test_views.py index e7848aee..ca7682fb 100644 --- a/project_manager/sub_plugins/tests/test_views.py +++ b/project_manager/sub_plugins/tests/test_views.py @@ -12,7 +12,7 @@ from rest_framework import status # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin from project_manager.plugins.models import Plugin from project_manager.sub_plugins.constants import SUB_PLUGIN_RELEASE_URL from project_manager.sub_plugins.models import SubPluginRelease @@ -82,7 +82,7 @@ def test_base_attributes(self): ) @mock.patch( - target='project_manager.common.mixins.DownloadMixin.full_path', + target='project_manager.mixins.DownloadMixin.full_path', ) def test_get_failure(self, mock_full_path): mock_full_path.isfile.return_value = False diff --git a/project_manager/sub_plugins/views.py b/project_manager/sub_plugins/views.py index d36c6e2e..d6f9457e 100644 --- a/project_manager/sub_plugins/views.py +++ b/project_manager/sub_plugins/views.py @@ -4,7 +4,7 @@ # IMPORTS # ============================================================================= # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin from project_manager.plugins.models import Plugin from project_manager.sub_plugins.constants import SUB_PLUGIN_RELEASE_URL from project_manager.sub_plugins.models import SubPlugin, SubPluginRelease diff --git a/project_manager/tests/test_admin.py b/project_manager/tests/test_admin.py index a389b170..bccfc5ff 100644 --- a/project_manager/tests/test_admin.py +++ b/project_manager/tests/test_admin.py @@ -10,6 +10,14 @@ from precise_bbcode.models import BBCodeTag, SmileyTag # App +from project_manager.admin.base import ProjectAdmin, ProjectReleaseAdmin +from project_manager.admin.inlines import ( + ProjectContributorInline, + ProjectGameInline, + ProjectImageInline, + ProjectTagInline, +) +from project_manager.models.abstract import Project from project_manager.packages.models import Package from project_manager.plugins.models import Plugin from project_manager.sub_plugins.models import SubPlugin @@ -46,3 +54,248 @@ def test_third_party_models_not_registered(self): member=SmileyTag, container=admin.site._registry, ) + + +class ProjectAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectAdmin, admin.ModelAdmin), + ) + + def test_actions(self): + self.assertIsNone(obj=ProjectAdmin.actions) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.fieldsets, + tuple2=( + ( + 'Project Info', + { + 'classes': ('wide',), + 'fields': ( + 'name', + 'owner', + 'configuration', + 'description', + 'synopsis', + 'logo', + 'topic', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'basename', + 'slug', + 'created', + 'updated', + ), + }, + ) + ), + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.list_display, + tuple2=( + 'name', + 'basename', + 'owner', + ), + ) + + def test_raw_id_fields(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.raw_id_fields, + tuple2=('owner',), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.readonly_fields, + tuple2=( + 'basename', + 'created', + 'slug', + 'updated', + ), + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=ProjectAdmin.search_fields, + tuple2=( + 'name', + 'basename', + 'owner__user__username', + 'contributors__user__username', + ) + ) + + def test_has_add_permission(self): + self.assertFalse( + expr=ProjectAdmin(Project, '').has_add_permission(''), + ) + + def test_has_delete_permission(self): + self.assertFalse( + expr=ProjectAdmin(Project, '').has_delete_permission(''), + ) + + +class ProjectReleaseAdminTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue(expr=issubclass(ProjectReleaseAdmin, admin.ModelAdmin)) + + def test_fieldsets(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.fieldsets, + tuple2=( + ( + 'Release Info', + { + 'classes': ('wide',), + 'fields': ( + 'version', + 'notes', + 'zip_file', + ), + } + ), + ( + 'Metadata', + { + 'classes': ('collapse',), + 'fields': ( + 'created', + 'created_by', + 'download_count', + ), + }, + ) + ) + ) + + def test_list_display(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.list_display, + tuple2=( + 'version', + 'created', + ) + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.readonly_fields, + tuple2=( + 'zip_file', + 'download_count', + 'created', + 'created_by', + ) + ) + + def test_search_fields(self): + self.assertTupleEqual( + tuple1=ProjectReleaseAdmin.search_fields, + tuple2=( + 'version', + ) + ) + + def test_view_on_site(self): + self.assertFalse(expr=ProjectReleaseAdmin.view_on_site) + + +class ProjectContributorInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectContributorInline, admin.TabularInline), + ) + + def test_extra(self): + self.assertEqual( + first=ProjectContributorInline.extra, + second=0, + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectContributorInline.fields, + tuple2=('user',), + ) + + def test_raw_id_fields(self): + self.assertTupleEqual( + tuple1=ProjectContributorInline.raw_id_fields, + tuple2=('user',), + ) + + +class ProjectGameInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectGameInline, admin.TabularInline), + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectGameInline.fields, + tuple2=('game',), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectGameInline.readonly_fields, + tuple2=('game',), + ) + + +class ProjectImageInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectImageInline, admin.TabularInline), + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectImageInline.fields, + tuple2=( + 'image', + 'created', + ), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectImageInline.readonly_fields, + tuple2=( + 'image', + 'created', + ), + ) + + +class ProjectTagInlineTestCase(TestCase): + def test_class_inheritance(self): + self.assertTrue( + expr=issubclass(ProjectTagInline, admin.TabularInline), + ) + + def test_fields(self): + self.assertTupleEqual( + tuple1=ProjectTagInline.fields, + tuple2=('tag',), + ) + + def test_readonly_fields(self): + self.assertTupleEqual( + tuple1=ProjectTagInline.readonly_fields, + tuple2=('tag',), + ) diff --git a/project_manager/common/tests/test_helpers.py b/project_manager/tests/test_helpers.py similarity index 97% rename from project_manager/common/tests/test_helpers.py rename to project_manager/tests/test_helpers.py index ec92cef5..24da0f20 100644 --- a/project_manager/common/tests/test_helpers.py +++ b/project_manager/tests/test_helpers.py @@ -11,11 +11,11 @@ from django.test import TestCase # App -from project_manager.common.constants import ( +from project_manager.constants import ( CANNOT_BE_NAMED, CANNOT_START_WITH, ) -from project_manager.common.helpers import ( +from project_manager.helpers import ( ProjectZipFile, find_image_number, handle_project_logo_upload, @@ -31,7 +31,7 @@ class ProjectZipFileTestCase(TestCase): def setUp(self) -> None: super().setUp() self.mock_zip_file = mock.patch( - target='project_manager.common.helpers.ZipFile', + target='project_manager.helpers.ZipFile', ).start() def tearDown(self) -> None: @@ -201,7 +201,7 @@ class TestProjectZipFile(ProjectZipFile): class CommonHelperFunctionsTestCase(TestCase): @mock.patch( - target='project_manager.common.models.settings.MEDIA_ROOT', + target='project_manager.models.abstract.settings.MEDIA_ROOT', ) def test_find_image_number(self, mock_media_root): base_directory = mock_media_root.__truediv__.return_value diff --git a/project_manager/common/tests/test_mixins.py b/project_manager/tests/test_mixins.py similarity index 97% rename from project_manager/common/tests/test_mixins.py rename to project_manager/tests/test_mixins.py index 149cfab3..69bf2c65 100644 --- a/project_manager/common/tests/test_mixins.py +++ b/project_manager/tests/test_mixins.py @@ -6,7 +6,7 @@ from django.views.generic import View # App -from project_manager.common.mixins import DownloadMixin +from project_manager.mixins import DownloadMixin # ============================================================================= diff --git a/project_manager/common/tests/test_models.py b/project_manager/tests/test_models.py similarity index 98% rename from project_manager/common/tests/test_models.py rename to project_manager/tests/test_models.py index e55c05f6..a6b74041 100644 --- a/project_manager/common/tests/test_models.py +++ b/project_manager/tests/test_models.py @@ -14,7 +14,7 @@ from precise_bbcode.fields import BBCodeTextField # App -from project_manager.common.constants import ( +from project_manager.constants import ( PROJECT_CONFIGURATION_MAX_LENGTH, PROJECT_DESCRIPTION_MAX_LENGTH, PROJECT_NAME_MAX_LENGTH, @@ -22,16 +22,16 @@ RELEASE_NOTES_MAX_LENGTH, RELEASE_VERSION_MAX_LENGTH, ) -from project_manager.common.helpers import ( +from project_manager.helpers import ( handle_project_logo_upload, handle_release_zip_file_upload, ) -from project_manager.common.models import ( +from project_manager.models.abstract import ( AbstractUUIDPrimaryKeyModel, Project, ProjectRelease, ) -from project_manager.common.validators import version_validator +from project_manager.validators import version_validator # ============================================================================= diff --git a/project_manager/common/validators.py b/project_manager/validators.py similarity index 94% rename from project_manager/common/validators.py rename to project_manager/validators.py index 2f758ac9..4a2770fc 100644 --- a/project_manager/common/validators.py +++ b/project_manager/validators.py @@ -7,7 +7,7 @@ from django.core.validators import RegexValidator # App -from project_manager.common.constants import RELEASE_VERSION_REGEX +from project_manager.constants import RELEASE_VERSION_REGEX # ============================================================================= From f84e2b6e484132812f7361fd732c495f5f9b650d Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Apr 2022 16:19:20 -0400 Subject: [PATCH 145/211] Improved some of the queries for the statistics view. --- project_manager/views.py | 58 +++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/project_manager/views.py b/project_manager/views.py index d0674a35..55590d03 100644 --- a/project_manager/views.py +++ b/project_manager/views.py @@ -4,13 +4,14 @@ # IMPORTS # ============================================================================= # Django -from django.db.models import Q +from django.db.models import Count, Q, Sum +from django.db.models.functions import Coalesce from django.views.generic import TemplateView # App -from project_manager.packages.models import Package, PackageRelease -from project_manager.plugins.models import Plugin, PluginRelease -from project_manager.sub_plugins.models import SubPlugin, SubPluginRelease +from project_manager.packages.models import PackageRelease +from project_manager.plugins.models import PluginRelease +from project_manager.sub_plugins.models import SubPluginRelease from users.models import ForumUser @@ -34,23 +35,17 @@ class StatisticsView(TemplateView): def get_context_data(self, **kwargs): """Return all statistical context data.""" context = super().get_context_data(**kwargs) - package_downloads = sum( - PackageRelease.objects.values_list( - 'download_count', - flat=True, - ) + package_info = PackageRelease.objects.aggregate( + download_count=Coalesce(Sum('download_count'), 0), + project_count=Count('package', distinct=True), ) - plugin_downloads = sum( - PluginRelease.objects.values_list( - 'download_count', - flat=True, - ) + plugin_info = PluginRelease.objects.aggregate( + download_count=Coalesce(Sum('download_count'), 0), + project_count=Count('plugin', distinct=True), ) - sub_plugin_downloads = sum( - SubPluginRelease.objects.values_list( - 'download_count', - flat=True, - ) + sub_plugin_info = SubPluginRelease.objects.aggregate( + download_count=Coalesce(Sum('download_count'), 0), + project_count=Count('sub_plugin', distinct=True), ) users = ForumUser.objects.filter( Q(plugins__isnull=False) | @@ -60,20 +55,23 @@ def get_context_data(self, **kwargs): Q(packages__isnull=False) | Q(package_contributions__isnull=False) ).distinct().count() - packages = Package.objects.count() - plugins = Plugin.objects.count() - sub_plugins = SubPlugin.objects.count() context.update({ 'users': users, - 'package_count': packages, - 'plugin_count': plugins, - 'sub_plugin_count': sub_plugins, - 'total_projects': packages + plugins + sub_plugins, - 'package_downloads': package_downloads, - 'plugin_downloads': plugin_downloads, - 'sub_plugin_downloads': sub_plugin_downloads, + 'package_count': package_info['project_count'], + 'plugin_count': plugin_info['project_count'], + 'sub_plugin_count': sub_plugin_info['project_count'], + 'total_projects': sum([ + package_info['project_count'], + plugin_info['project_count'], + sub_plugin_info['project_count'], + ]), + 'package_downloads': package_info['download_count'], + 'plugin_downloads': plugin_info['download_count'], + 'sub_plugin_downloads': sub_plugin_info['download_count'], 'total_downloads': sum([ - package_downloads, plugin_downloads, sub_plugin_downloads, + package_info['download_count'], + plugin_info['download_count'], + sub_plugin_info['download_count'], ]) }) return context From 9c2ee6d347463b33624aac5191b1b4b9f21b9c1f Mon Sep 17 00:00:00 2001 From: satoon101 Date: Sun, 24 Apr 2022 19:14:47 -0400 Subject: [PATCH 146/211] Implemented basic frontend views for packages/plugins. Updated base.html to show the username if logged in and login/register if not logged in. Re-enabled links to packages/plugins views in base.html. --- project_manager/context_processors.py | 1 + project_manager/packages/urls.py | 37 ++++++++++++++ project_manager/packages/views.py | 39 +++++++++++++++ project_manager/plugins/urls.py | 37 ++++++++++++++ project_manager/plugins/views.py | 39 +++++++++++++++ .../static/project_manager/css/custom.css | 1 - project_manager/urls.py | 21 ++++++-- templates/base.html | 50 ++++++------------- templates/packages.html | 9 ++++ templates/plugins.html | 9 ++++ 10 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 project_manager/packages/urls.py create mode 100644 project_manager/plugins/urls.py create mode 100644 templates/packages.html create mode 100644 templates/plugins.html diff --git a/project_manager/context_processors.py b/project_manager/context_processors.py index a53b20ba..cc9368fb 100644 --- a/project_manager/context_processors.py +++ b/project_manager/context_processors.py @@ -27,4 +27,5 @@ def add_common_context_processors(request): 'MEDIA_URL': settings.MEDIA_URL, 'WIKI_URL': settings.WIKI_URL, 'username': str(request.user), + 'user_authenticated': request.user.is_authenticated, } diff --git a/project_manager/packages/urls.py b/project_manager/packages/urls.py new file mode 100644 index 00000000..90ab6307 --- /dev/null +++ b/project_manager/packages/urls.py @@ -0,0 +1,37 @@ +"""Base App URLs.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.urls import path + +# App +from project_manager.packages.views import PackageCreateView, PackageView + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +app_name = 'packages' + +urlpatterns = [ + path( + # /packages + route='', + view=PackageView.as_view(), + name='list', + ), + path( + # /packages/create + route='create', + view=PackageCreateView.as_view(), + name='create', + ), + path( + # /packages/ + route='', + view=PackageView.as_view(), + name='detail', + ), +] diff --git a/project_manager/packages/views.py b/project_manager/packages/views.py index 23abf4cf..4b8fc889 100644 --- a/project_manager/packages/views.py +++ b/project_manager/packages/views.py @@ -3,6 +3,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Django +from django.views.generic import TemplateView + # App from project_manager.mixins import DownloadMixin from project_manager.packages.constants import PACKAGE_RELEASE_URL @@ -14,6 +17,8 @@ # ============================================================================= __all__ = ( 'PackageReleaseDownloadView', + 'PackageCreateView', + 'PackageView', ) @@ -27,3 +32,37 @@ class PackageReleaseDownloadView(DownloadMixin): project_model = Package model_kwarg = 'package' base_url = PACKAGE_RELEASE_URL + + +class PackageView(TemplateView): + """Frontend view for viewing Packages.""" + + template_name = 'packages.html' + http_method_names = ('get', 'options') + + def get_context_data(self, **kwargs): + """Add the page title to the context.""" + context = super().get_context_data(**kwargs) + slug = context.get('slug') + if slug is None: + context['title'] = 'Package Listing' + else: + try: + package = Package.objects.get(slug=slug) + context['title'] = package.name + except Package.DoesNotExist: + context['title'] = f'Package "{slug}" not found.' + return context + + +class PackageCreateView(TemplateView): + """Frontend view for creating Packages.""" + + template_name = 'packages.html' + http_method_names = ('get', 'post', 'options') + + def get_context_data(self, **kwargs): + """Add the page title to the context.""" + context = super().get_context_data(**kwargs) + context['title'] = 'Create a Package' + return context diff --git a/project_manager/plugins/urls.py b/project_manager/plugins/urls.py new file mode 100644 index 00000000..676a5f23 --- /dev/null +++ b/project_manager/plugins/urls.py @@ -0,0 +1,37 @@ +"""Base App URLs.""" + +# ============================================================================= +# IMPORTS +# ============================================================================= +# Django +from django.urls import path + +# App +from project_manager.plugins.views import PluginCreateView, PluginView + + +# ============================================================================= +# GLOBAL VARIABLES +# ============================================================================= +app_name = 'plugins' + +urlpatterns = [ + path( + # /plugins + route='', + view=PluginView.as_view(), + name='list', + ), + path( + # /plugins/create + route='create', + view=PluginCreateView.as_view(), + name='create', + ), + path( + # /plugins/ + route='', + view=PluginView.as_view(), + name='detail', + ), +] diff --git a/project_manager/plugins/views.py b/project_manager/plugins/views.py index 0357ddbc..4af77ef5 100644 --- a/project_manager/plugins/views.py +++ b/project_manager/plugins/views.py @@ -3,6 +3,9 @@ # ============================================================================= # IMPORTS # ============================================================================= +# Django +from django.views.generic import TemplateView + # App from project_manager.mixins import DownloadMixin from project_manager.plugins.constants import PLUGIN_RELEASE_URL @@ -14,6 +17,8 @@ # ============================================================================= __all__ = ( 'PluginReleaseDownloadView', + 'PluginCreateView', + 'PluginView', ) @@ -27,3 +32,37 @@ class PluginReleaseDownloadView(DownloadMixin): project_model = Plugin model_kwarg = 'plugin' base_url = PLUGIN_RELEASE_URL + + +class PluginView(TemplateView): + """Frontend view for viewing Plugins.""" + + template_name = 'plugins.html' + http_method_names = ('get', 'options') + + def get_context_data(self, **kwargs): + """Add the page title to the context.""" + context = super().get_context_data(**kwargs) + slug = context.get('slug') + if slug is None: + context['title'] = 'Plugin Listing' + else: + try: + plugin = Plugin.objects.get(slug=slug) + context['title'] = plugin.name + except Plugin.DoesNotExist: + context['title'] = f'Plugin "{slug}" not found.' + return context + + +class PluginCreateView(TemplateView): + """Frontend view for creating Plugins.""" + + template_name = 'plugins.html' + http_method_names = ('get', 'post', 'options') + + def get_context_data(self, **kwargs): + """Add the page title to the context.""" + context = super().get_context_data(**kwargs) + context['title'] = 'Create a Plugin' + return context diff --git a/project_manager/static/project_manager/css/custom.css b/project_manager/static/project_manager/css/custom.css index c11fd37f..5a89ab70 100644 --- a/project_manager/static/project_manager/css/custom.css +++ b/project_manager/static/project_manager/css/custom.css @@ -127,7 +127,6 @@ ul.nav.navbar-nav.navbar-right { margin-top: 5px; } .topbar { - text-transform: uppercase; font-size: 13px; } #search{ diff --git a/project_manager/urls.py b/project_manager/urls.py index d226d36a..d9f4593e 100644 --- a/project_manager/urls.py +++ b/project_manager/urls.py @@ -49,11 +49,22 @@ ), name='api', ), - # path( - # route='plugins/', - # view=, - # name='plugins', - # ), + path( + route='packages/', + view=include( + 'project_manager.packages.urls', + namespace='packages', + ), + name='packages', + ), + path( + route='plugins/', + view=include( + 'project_manager.plugins.urls', + namespace='plugins', + ), + name='plugins', + ), path( # /media/releases/packages// route='media/releases/packages//', diff --git a/templates/base.html b/templates/base.html index f2f8533f..42ee35aa 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,9 +1,6 @@ {% load static %} - - -{{ username|json_script:"username" }} @@ -11,7 +8,6 @@ -{# #} @@ -23,20 +19,17 @@