From ed490aa7033cbe44f2d7cee10220e48be7226280 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 7 Nov 2021 07:36:52 -0500 Subject: [PATCH] initial commit of M4 --- .all-contributorsrc | 134 -- .coveragerc | 11 - .deepsource.toml | 28 - .env-example | 64 - .env.local | 1 - .env.production | 1 - .env.test | 8 - .env.testing | 8 - .gitignore | 37 +- CHANGELOG.md | 485 ----- CONTRIBUTING.md | 143 +- LICENSE | 21 - MANIFEST.in | 24 +- Makefile | 38 +- README.md | 361 +--- SECURITY.md | 2 +- WHITEPAPER.md | 333 ++++ app/User.py | 11 - app/http/controllers/ConfirmController.py | 68 - app/http/controllers/ControllerTest.py | 32 - app/http/controllers/TestController.py | 59 - app/http/controllers/UnitTestController.py | 63 - .../controllers/UserResourceController.py | 61 - app/http/controllers/WelcomeController.py | 23 - .../controllers/subdirectory/SubController.py | 5 - .../subdirectory/deep/DeepController.py | 5 - app/http/middleware/AddAttributeMiddleware.py | 19 - .../middleware/AuthenticationMiddleware.py | 24 - app/http/middleware/CsrfMiddleware.py | 18 - app/http/middleware/LoadUserMiddleware.py | 35 - app/http/middleware/MiddlewareTest.py | 19 - app/http/middleware/TestHttpMiddleware.py | 13 - app/http/middleware/TestMiddleware.py | 13 - app/http/middleware/VerifyEmailMiddleware.py | 26 - app/http/test_controllers/Request.py | 2 - app/http/test_controllers/TestController.py | 6 - app/jobs/TestJob.py | 21 - config/application.py | 65 - config/auth.py | 37 - config/broadcast.py | 36 - config/cache.py | 26 - config/database.py | 60 - config/factories.py | 15 - config/mail.py | 42 - config/middleware.py | 64 - config/packages.py | 18 - config/providers.py | 44 - config/queue.py | 30 - config/session.py | 19 - config/storage.py | 93 - craft | 13 +- database.sqlite3 | Bin 0 -> 348160 bytes .../2018_01_09_043202_create_users_table.py | 20 - ...9_02_07_015506_create_failed_jobs_table.py | 22 - ...20_09_18_233149_create_queue_jobs_table.py | 19 - databases/migrations/__init__.py | 3 - databases/seeds/__init__.py | 3 - databases/seeds/user_table_seeder.py | 22 - masonite.sqlite3 | Bin 65536 -> 0 bytes package.json | 20 - pytest.ini | 4 +- requirements.txt | 57 +- resources/__init__.py | 5 - resources/templates/__init__.py | 7 - resources/templates/admin_test.html | 1 - resources/templates/auth/confirm.html | 1 - resources/templates/auth/error.html | 1 - resources/templates/base.html | 29 - resources/templates/csrf_field.html | 1 - resources/templates/emails/test.html | 1 - resources/templates/filter.html | 1 - resources/templates/index.html | 1 - resources/templates/line-statements.html | 3 - resources/templates/mail/composers.html | 1 - resources/templates/mail/share.html | 1 - resources/templates/mail/welcome.html | 66 - resources/templates/mail/welcome.txt | 2 - resources/templates/pug/hello.pug | 1 - resources/templates/test_cache.html | 1 - resources/templates/test_exception.html | 1 - resources/templates/welcome.html | 36 - routes/web.py | 52 - setup.cfg | 5 - setup.py | 241 ++- src/masonite/__init__.py | 24 - src/masonite/__version__.py | 10 - src/masonite/auth/Auth.py | 27 - src/masonite/auth/Csrf.py | 54 - src/masonite/auth/Sign.py | 13 +- src/masonite/auth/__init__.py | 2 - .../auth/guards/AuthenticationGuard.py | 56 - src/masonite/auth/guards/Guard.py | 124 -- src/masonite/auth/guards/WebGuard.py | 157 -- src/masonite/auth/guards/__init__.py | 3 - src/masonite/authentication/Auth.py | 155 ++ src/masonite/authentication/__init__.py | 2 + .../authentication/guards/WebGuard.py | 80 + .../authentication/guards/__init__.py | 1 + .../authentication/models/__init__.py | 1 + .../authentication/models/authenticates.py | 66 + .../authorization/AuthorizationResponse.py | 30 + .../authorization/AuthorizesRequest.py | 7 + src/masonite/authorization/Gate.py | 170 ++ src/masonite/authorization/Policy.py | 9 + src/masonite/authorization/__init__.py | 5 + src/masonite/authorization/models/__init__.py | 1 + .../authorization/models/authorizes.py | 12 + src/masonite/autoload.py | 177 -- src/masonite/broadcasting/Broadcast.py | 74 + src/masonite/broadcasting/CanBroadcast.py | 9 + src/masonite/broadcasting/Channel.py | 6 + src/masonite/broadcasting/PresenceChannel.py | 9 + src/masonite/broadcasting/PrivateChannel.py | 9 + src/masonite/broadcasting/__init__.py | 5 + .../controllers/BroadcastingController.py | 10 + .../broadcasting/controllers/__init__.py | 1 + .../broadcasting/drivers/PusherDriver.py | 35 + src/masonite/broadcasting/drivers/__init__.py | 1 + .../providers/BroadcastProvider.py | 20 + .../broadcasting/providers/__init__.py | 1 + src/masonite/cache/Cache.py | 59 + src/masonite/cache/__init__.py | 1 + src/masonite/cache/drivers/FileDriver.py | 106 + src/masonite/cache/drivers/MemcacheDriver.py | 90 + src/masonite/cache/drivers/RedisDriver.py | 95 + src/masonite/cache/drivers/__init__.py | 3 + src/masonite/cli.py | 27 - src/masonite/commands/AuthCommand.py | 65 +- src/masonite/commands/BaseScaffoldCommand.py | 54 - src/masonite/commands/CommandCapsule.py | 12 + src/masonite/commands/CommandCommand.py | 16 - src/masonite/commands/ControllerCommand.py | 42 - src/masonite/commands/DownCommand.py | 15 - src/masonite/commands/Entry.py | 23 + src/masonite/commands/InfoCommand.py | 60 - src/masonite/commands/InstallCommand.py | 6 +- src/masonite/commands/JobCommand.py | 16 - src/masonite/commands/KeyCommand.py | 4 +- src/masonite/commands/MailableCommand.py | 16 - .../commands/MakeControllerCommand.py | 41 + src/masonite/commands/MakeJobCommand.py | 38 + src/masonite/commands/MakeMailableCommand.py | 39 + src/masonite/commands/MakePolicyCommand.py | 57 + src/masonite/commands/MakeProviderCommand.py | 39 + src/masonite/commands/MiddlewareCommand.py | 16 - src/masonite/commands/ModelCommand.py | 51 - .../commands/ModelDocstringCommand.py | 34 - src/masonite/commands/PresetCommand.py | 71 - .../{NewCommand.py => ProjectCommand.py} | 59 +- src/masonite/commands/ProviderCommand.py | 16 - src/masonite/commands/PublishCommand.py | 28 - .../commands/PublishPackageCommand.py | 46 + src/masonite/commands/QueueFailedCommand.py | 33 + src/masonite/commands/QueueRetryCommand.py | 28 + src/masonite/commands/QueueTableCommand.py | 40 +- src/masonite/commands/QueueWorkCommand.py | 45 +- src/masonite/commands/RoutesCommand.py | 31 - src/masonite/commands/SeedCommand.py | 31 - src/masonite/commands/SeedRunCommand.py | 30 - src/masonite/commands/ServeCommand.py | 79 +- src/masonite/commands/TestCommand.py | 18 - src/masonite/commands/TinkerCommand.py | 55 +- src/masonite/commands/UpCommand.py | 16 - src/masonite/commands/ViewCommand.py | 16 - src/masonite/commands/__init__.py | 34 +- src/masonite/commands/_devserver.py | 140 -- src/masonite/commands/presets/Bootstrap.py | 35 - src/masonite/commands/presets/Preset.py | 45 - src/masonite/commands/presets/React.py | 64 - src/masonite/commands/presets/Remove.py | 58 - src/masonite/commands/presets/Tailwind.py | 61 - src/masonite/commands/presets/Vue.py | 62 - src/masonite/commands/presets/Vue3.py | 79 - src/masonite/commands/presets/__init__.py | 6 - .../presets/bootstrap-stubs/_variables.scss | 19 - .../commands/presets/bootstrap-stubs/app.scss | 13 - .../commands/presets/react-stubs/Example.js | 22 - .../commands/presets/react-stubs/app.js | 15 - .../presets/react-stubs/webpack.mix.js | 15 - .../commands/presets/remove-stubs/app.js | 7 - .../presets/remove-stubs/bootstrap.js | 25 - .../presets/remove-stubs/webpack.mix.js | 15 - .../presets/shared-stubs/bootstrap.js | 25 - .../commands/presets/tailwind-stubs/base.html | 27 - .../presets/tailwind-stubs/style.scss | 7 - .../presets/tailwind-stubs/tailwind.config.js | 14 - .../presets/tailwind-stubs/webpack.mix.js | 22 - .../presets/tailwind-stubs/welcome.html | 39 - .../presets/vue-stubs/ExampleComponent.vue | 21 - .../commands/presets/vue-stubs/app.js | 35 - .../commands/presets/vue-stubs/webpack.mix.js | 17 - .../commands/presets/vue3-stubs/App.vue | 26 - .../presets/vue3-stubs/HelloWorld.vue | 35 - .../commands/presets/vue3-stubs/app.html | 19 - .../commands/presets/vue3-stubs/app.js | 50 - .../presets/vue3-stubs/webpack.mix.js | 31 - src/masonite/configuration/Configuration.py | 74 + src/masonite/configuration/__init__.py | 2 + src/masonite/configuration/helpers.py | 5 + .../providers/ConfigurationProvider.py | 16 + .../configuration/providers/__init__.py | 1 + src/masonite/container/__init__.py | 1 + .../{app.py => container/container.py} | 81 +- src/masonite/contracts/AuthContract.py | 15 - src/masonite/contracts/BroadcastContract.py | 11 - src/masonite/contracts/CacheContract.py | 23 - src/masonite/contracts/MailContract.py | 19 - src/masonite/contracts/QueueContract.py | 27 - src/masonite/contracts/SessionContract.py | 35 - src/masonite/contracts/StorageContract.py | 140 -- src/masonite/contracts/UploadContract.py | 15 - src/masonite/contracts/__init__.py | 16 - .../managers/BroadcastManagerContract.py | 5 - .../managers/CacheManagerContract.py | 5 - .../contracts/managers/MailManagerContract.py | 5 - .../managers/QueueManagerContract.py | 5 - .../managers/SessionManagerContract.py | 5 - .../managers/StorageManagerContract.py | 5 - .../managers/UploadManagerContract.py | 5 - .../controllers/RedirectController.py | 10 + src/masonite/controllers/__init__.py | 1 + src/masonite/cookies/Cookie.py | 2 +- src/masonite/cookies/CookieJar.py | 30 +- src/masonite/drivers/BaseDriver.py | 31 - src/masonite/drivers/__init__.py | 25 - .../authentication/AuthCookieDriver.py | 64 - .../drivers/authentication/AuthJwtDriver.py | 105 - .../drivers/broadcast/BroadcastAblyDriver.py | 69 - .../broadcast/BroadcastPubNubDriver.py | 80 - .../broadcast/BroadcastPusherDriver.py | 74 - src/masonite/drivers/cache/BaseCacheDriver.py | 34 - src/masonite/drivers/cache/CacheDiskDriver.py | 182 -- .../drivers/cache/CacheRedisDriver.py | 121 -- src/masonite/drivers/mail/BaseMailDriver.py | 220 --- src/masonite/drivers/mail/MailLogDriver.py | 58 - .../drivers/mail/MailMailgunDriver.py | 77 - src/masonite/drivers/mail/MailSmtpDriver.py | 111 -- .../drivers/mail/MailTerminalDriver.py | 44 - src/masonite/drivers/mail/Mailable.py | 26 - src/masonite/drivers/queue/AMQPDriver.py | 185 ++ .../{QueueAsyncDriver.py => AsyncDriver.py} | 90 +- src/masonite/drivers/queue/BaseQueueDriver.py | 66 - src/masonite/drivers/queue/DatabaseDriver.py | 205 ++ src/masonite/drivers/queue/QueueAmqpDriver.py | 183 -- .../drivers/queue/QueueDatabaseDriver.py | 137 -- src/masonite/drivers/queue/QueueJobsModel.py | 6 - src/masonite/drivers/queue/__init__.py | 3 + src/masonite/drivers/session/CookieDriver.py | 58 + .../drivers/session/SessionCookieDriver.py | 203 -- .../drivers/session/SessionMemoryDriver.py | 207 -- src/masonite/drivers/session/__init__.py | 1 + .../drivers/storage/StorageDiskDriver.py | 75 - .../drivers/upload/BaseUploadDriver.py | 78 - .../drivers/upload/UploadDiskDriver.py | 56 - src/masonite/drivers/upload/UploadS3Driver.py | 76 - src/masonite/environment/__init__.py | 1 + src/masonite/{ => environment}/environment.py | 3 + src/masonite/essentials/helpers/__init__.py | 1 + src/masonite/essentials/helpers/hashid.py | 34 + .../essentials/middleware/HashIDMiddleware.py | 17 + .../essentials/middleware/__init__.py | 1 + .../essentials/providers/HashIDProvider.py | 13 + src/masonite/events/Event.py | 81 + src/masonite/events/Listener.py | 2 + src/masonite/events/__init__.py | 1 + .../events/commands/MakeEventCommand.py | 39 + .../events/commands/MakeListenerCommand.py | 38 + src/masonite/events/commands/__init__.py | 2 + .../events/providers/EventProvider.py | 18 + src/masonite/events/providers/__init__.py | 1 + src/masonite/exception_handler.py | 176 -- src/masonite/exceptions/DD.py | 15 + .../exceptions/DumpExceptionHandler.py | 35 + src/masonite/exceptions/ExceptionHandler.py | 65 + src/masonite/exceptions/__init__.py | 32 + src/masonite/{ => exceptions}/exceptions.py | 53 + src/masonite/facades/Auth.py | 5 + src/masonite/facades/Config.py | 5 + src/masonite/facades/Facade.py | 5 + src/masonite/facades/Gate.py | 5 + src/masonite/facades/Hash.py | 5 + src/masonite/facades/Loader.py | 5 + src/masonite/facades/Mail.py | 5 + src/masonite/facades/Notification.py | 5 + src/masonite/facades/Request.py | 5 + src/masonite/facades/Response.py | 5 + src/masonite/facades/Session.py | 5 + src/masonite/facades/Url.py | 5 + src/masonite/facades/View.py | 5 + src/masonite/facades/__init__.py | 13 + src/masonite/filesystem/File.py | 26 + src/masonite/filesystem/FileStream.py | 16 + src/masonite/filesystem/Storage.py | 35 + src/masonite/filesystem/UploadedFile.py | 30 + src/masonite/filesystem/__init__.py | 3 + .../filesystem/drivers/AmazonS3Driver.py | 146 ++ .../filesystem/drivers/LocalDriver.py | 105 + src/masonite/filesystem/drivers/__init__.py | 2 + .../filesystem/providers/StorageProvider.py | 20 + src/masonite/filesystem/providers/__init__.py | 1 + src/masonite/foundation/Application.py | 77 + src/masonite/foundation/Kernel.py | 79 + src/masonite/foundation/__init__.py | 3 + .../response_handler.py} | 60 +- src/masonite/hashing/Hash.py | 48 + src/masonite/hashing/__init__.py | 1 + src/masonite/hashing/drivers/Argon2Hasher.py | 34 + src/masonite/hashing/drivers/BcryptHasher.py | 28 + src/masonite/hashing/drivers/__init__.py | 2 + src/masonite/headers/HeaderBag.py | 7 + src/masonite/helpers/Extendable.py | 41 - src/masonite/helpers/__init__.py | 19 +- src/masonite/helpers/compact.py | 29 + src/masonite/helpers/filesystem.py | 31 - src/masonite/helpers/migrations.py | 43 - src/masonite/helpers/misc.py | 121 -- src/masonite/helpers/mix.py | 34 + src/masonite/helpers/optional.py | 24 +- src/masonite/helpers/password.py | 19 - src/masonite/helpers/routes.py | 102 - src/masonite/helpers/sign.py | 49 - src/masonite/helpers/static.py | 30 - src/masonite/helpers/structures.py | 219 -- src/masonite/helpers/time.py | 88 - src/masonite/helpers/urls.py | 49 + src/masonite/helpers/view_helpers.py | 62 - src/masonite/hook.py | 24 - src/masonite/input/Input.py | 4 + src/masonite/input/InputBag.py | 114 +- src/masonite/input/__init__.py | 1 + .../listeners/BaseExceptionListener.py | 2 - src/masonite/listeners/__init__.py | 1 - src/masonite/loader/Loader.py | 78 + src/masonite/loader/__init__.py | 1 + src/masonite/mail/Mail.py | 33 + src/masonite/mail/Mailable.py | 102 + src/masonite/mail/MessageAttachment.py | 4 + src/masonite/mail/MockMail.py | 101 + src/masonite/mail/Recipient.py | 18 + src/masonite/mail/__init__.py | 3 + src/masonite/mail/drivers/MailgunDriver.py | 51 + src/masonite/mail/drivers/SMTPDriver.py | 76 + src/masonite/mail/drivers/TerminalDriver.py | 30 + src/masonite/mail/drivers/__init__.py | 3 + src/masonite/managers/AuthManager.py | 20 - src/masonite/managers/BroadcastManager.py | 21 - src/masonite/managers/CacheManager.py | 21 - src/masonite/managers/MailManager.py | 30 - src/masonite/managers/Manager.py | 95 - src/masonite/managers/QueueManager.py | 21 - src/masonite/managers/SessionManager.py | 21 - src/masonite/managers/StorageManager.py | 21 - src/masonite/managers/UploadManager.py | 21 - src/masonite/managers/__init__.py | 9 - src/masonite/middleware/CorsMiddleware.py | 25 - src/masonite/middleware/CsrfMiddleware.py | 95 - src/masonite/middleware/GuardMiddleware.py | 27 - .../middleware/MaintenanceModeMiddleware.py | 16 - src/masonite/middleware/ResponseMiddleware.py | 21 - .../middleware/SecureHeadersMiddleware.py | 40 - src/masonite/middleware/__init__.py | 12 +- src/masonite/middleware/middleware.py | 2 + src/masonite/middleware/middleware_capsule.py | 64 + .../middleware/route/EncryptCookies.py | 21 + .../middleware/route/LoadUserMiddleware.py | 11 + .../middleware/route/SessionMiddleware.py | 29 + .../middleware/route/VerifyCsrfToken.py | 69 + .../masonite/middleware/route}/__init__.py | 0 .../notification/AnonymousNotifiable.py | 39 + .../notification/DatabaseNotification.py | 38 + src/masonite/notification/MockNotification.py | 120 ++ src/masonite/notification/Notifiable.py | 63 + src/masonite/notification/Notification.py | 39 + .../notification/NotificationManager.py | 85 + src/masonite/notification/SlackMessage.py | 166 ++ src/masonite/notification/Sms.py | 54 + src/masonite/notification/Textable.py | 6 + src/masonite/notification/__init__.py | 9 + .../commands/MakeNotificationCommand.py | 42 + .../commands/NotificationTableCommand.py | 37 + .../notification/commands/__init__.py | 2 + .../notification/drivers/BaseDriver.py | 19 + .../notification/drivers/BroadcastDriver.py | 22 + .../notification/drivers/DatabaseDriver.py | 37 + .../notification/drivers/MailDriver.py | 22 + .../notification/drivers/SlackDriver.py | 110 ++ src/masonite/notification/drivers/__init__.py | 5 + .../drivers/vonage/VonageDriver.py | 69 + .../providers/NotificationProvider.py | 41 + .../notification/providers/__init__.py | 1 + src/masonite/packages.py | 139 -- src/masonite/packages/Package.py | 46 + src/masonite/packages/PublishableResource.py | 14 + src/masonite/packages/__init__.py | 1 + .../packages/providers/PackageProvider.py | 176 ++ src/masonite/packages/providers/__init__.py | 1 + src/masonite/packages/reserved_names.py | 1 + src/masonite/pipeline/Pipeline.py | 11 + src/masonite/pipeline/__init__.py | 1 + .../masonite/pipeline/tasks/MiddlewareTask.py | 0 src/masonite/pipeline/tasks/ResponseTask.py | 7 + src/masonite/provider.py | 117 -- src/masonite/providers/AppProvider.py | 93 - .../providers/AuthenticationProvider.py | 31 +- .../providers/AuthorizationProvider.py | 13 + src/masonite/providers/BroadcastProvider.py | 28 - src/masonite/providers/CacheProvider.py | 32 +- src/masonite/providers/CorsProvider.py | 22 - src/masonite/providers/CsrfProvider.py | 16 - src/masonite/providers/ExceptionProvider.py | 21 + src/masonite/providers/FrameworkProvider.py | 17 + src/masonite/providers/HashServiceProvider.py | 20 + src/masonite/providers/HelpersProvider.py | 70 +- src/masonite/providers/MailProvider.py | 40 +- src/masonite/providers/Provider.py | 2 + src/masonite/providers/QueueProvider.py | 31 +- .../providers/RequestHelpersProviders.py | 28 - src/masonite/providers/RouteProvider.py | 168 +- src/masonite/providers/SessionProvider.py | 35 +- src/masonite/providers/StatusCodeProvider.py | 68 - src/masonite/providers/UploadProvider.py | 26 - src/masonite/providers/ViewProvider.py | 26 +- src/masonite/providers/WhitenoiseProvider.py | 40 +- src/masonite/providers/__init__.py | 31 +- src/masonite/queues/Queue.py | 45 + src/masonite/queues/Queueable.py | 14 +- src/masonite/queues/__init__.py | 1 + src/masonite/request.py | 1156 ----------- src/masonite/request/__init__.py | 1 + src/masonite/request/request.py | 137 ++ src/masonite/request/validation.py | 7 + src/masonite/response/__init__.py | 1 + src/masonite/{ => response}/response.py | 156 +- src/masonite/routes.py | 772 -------- src/masonite/routes/HTTPRoute.py | 284 +++ src/masonite/routes/Route.py | 165 ++ src/masonite/routes/Router.py | 93 + src/masonite/routes/__init__.py | 3 + src/masonite/scheduling/CanSchedule.py | 10 + src/masonite/scheduling/CommandTask.py | 13 + src/masonite/scheduling/Task.py | 148 ++ src/masonite/scheduling/TaskHandler.py | 30 + src/masonite/scheduling/__init__.py | 4 + .../scheduling/commands/MakeTaskCommand.py | 46 + .../scheduling/commands/ScheduleRunCommand.py | 21 + src/masonite/scheduling/commands/__init__.py | 2 + .../scheduling/providers/ScheduleProvider.py | 20 + src/masonite/scheduling/providers/__init__.py | 1 + src/masonite/sessions/Session.py | 147 ++ src/masonite/sessions/__init__.py | 1 + .../auth/controllers/ConfirmController.py | 69 - .../auth/controllers/HomeController.py | 17 - .../auth/controllers/LoginController.py | 66 - .../auth/controllers/PasswordController.py | 79 - .../auth/controllers/RegisterController.py | 77 - .../snippets/auth/templates/auth/base.html | 60 - .../snippets/auth/templates/auth/confirm.html | 12 - .../snippets/auth/templates/auth/error.html | 12 - .../snippets/auth/templates/auth/forget.html | 44 - .../snippets/auth/templates/auth/home.html | 10 - .../snippets/auth/templates/auth/login.html | 44 - .../auth/templates/auth/register.html | 43 - .../snippets/auth/templates/auth/reset.html | 34 - .../snippets/auth/templates/auth/verify.html | 13 - .../auth/templates/auth/verifymail.html | 288 --- src/masonite/snippets/exception.html | 281 --- .../snippets/exceptions/css/go-icon.png | Bin 15196 -> 0 bytes .../snippets/exceptions/css/so-icon.png | Bin 3445 -> 0 bytes .../snippets/exceptions/css/style.css | 5 - .../snippets/exceptions/obj_loop.html | 43 - src/masonite/snippets/scaffold/command.html | 15 - .../snippets/scaffold/controller.html | 21 - .../scaffold/controller_resource.html | 61 - src/masonite/snippets/scaffold/job.html | 16 - src/masonite/snippets/scaffold/mailable.html | 21 - .../snippets/scaffold/middleware.html | 24 - src/masonite/snippets/scaffold/model.html | 8 - src/masonite/snippets/scaffold/provider.html | 18 - src/masonite/snippets/scaffold/test.html | 23 - src/masonite/snippets/scaffold/validator.html | 8 - src/masonite/snippets/statuscode.html | 72 - src/masonite/storage.py | 50 - src/masonite/storage/__init__.py | 1 + src/masonite/storage/storage.py | 10 + src/masonite/stubs/controllers/Controller.py | 7 + .../stubs/controllers/auth/HomeController.py | 7 + .../stubs/controllers/auth/LoginController.py | 19 + .../auth/PasswordResetController.py | 28 + .../controllers/auth/RegisterController.py | 20 + src/masonite/stubs/events/event.py | 2 + src/masonite/stubs/events/listener.py | 3 + src/masonite/stubs/jobs/Job.py | 6 + src/masonite/stubs/mailable/Mailable.py | 12 + .../stubs/notification/Notification.py | 15 + .../create_notifications_table.py | 17 + src/masonite/stubs/policies/ModelPolicy.py | 24 + src/masonite/stubs/policies/Policy.py | 6 + src/masonite/stubs/providers/Provider.py | 12 + .../queue}/create_failed_jobs_table.py | 12 +- .../queue}/create_queue_jobs_table.py | 13 +- src/masonite/stubs/scheduling/task.py | 7 + src/masonite/stubs/templates/auth/base.html | 13 + .../stubs/templates/auth/change_password.html | 49 + src/masonite/stubs/templates/auth/home.html | 17 + src/masonite/stubs/templates/auth/login.html | 66 + .../stubs/templates/auth/password_reset.html | 40 + .../stubs/templates/auth/register.html | 72 + src/masonite/stubs/validation/Rule.py | 48 + .../stubs/validation/RuleEnclosure.py | 18 + .../{snippets => templates}/__init__.py | 0 .../exceptions => templates}/dump.html | 0 src/masonite/templates/obj_loop.html | 43 + src/masonite/testing/BaseRequest.py | 18 - src/masonite/testing/MockRoute.py | 381 ---- src/masonite/testing/TestCase.py | 325 --- src/masonite/testing/__init__.py | 4 - src/masonite/testing/create_container.py | 43 - src/masonite/testing/generate_wsgi.py | 39 - src/masonite/tests/DatabaseTransactions.py | 8 + src/masonite/tests/HttpTestResponse.py | 323 +++ src/masonite/tests/MockInput.py | 6 + src/masonite/tests/TestCase.py | 251 +++ src/masonite/tests/TestCommand.py | 58 + src/masonite/tests/TestResponseCapsule.py | 17 + src/masonite/tests/__init__.py | 4 + .../exceptions => utils}/__init__.py | 0 src/masonite/utils/collections.py | 545 +++++ src/masonite/utils/console.py | 14 + src/masonite/utils/filesystem.py | 72 + .../{helpers/status.py => utils/http.py} | 40 +- src/masonite/utils/location.py | 74 + src/masonite/utils/str.py | 58 + src/masonite/utils/structures.py | 93 + src/masonite/utils/time.py | 58 + src/masonite/validation/MessageBag.py | 120 ++ src/masonite/validation/RuleEnclosure.py | 2 + src/masonite/validation/Validator.py | 1308 ++++++++++++ src/masonite/validation/__init__.py | 48 + .../validation/commands/MakeRuleCommand.py | 46 + .../commands/MakeRuleEnclosureCommand.py | 46 + .../masonite/validation/commands}/__init__.py | 0 src/masonite/validation/decorators.py | 20 + .../providers/ValidationProvider.py | 35 + src/masonite/validation/providers/__init__.py | 1 + .../validation/resources/postal_codes.py | 1011 ++++++++++ src/masonite/view.py | 321 --- .../masonite/views/ViewCapsule.py | 0 src/masonite/views/__init__.py | 1 + src/masonite/views/view.py | 275 +++ storage/append_from.txt | 3 - storage/file.txt | 1 - storage/logs/.gitignore | 2 - storage/some_file.txt | 1 - storage/static/__init__.py | 5 - storage/static/profile.jpg | Bin 237344 -> 0 bytes storage/static/sass/style.scss | 54 - storage/templates/tests/test.html | 1 - storage/test_location.html | 1 - templates/test.html | 1 - templates/tests/test.html | 1 - testpackage/test-config.py | 1 - tests/TestCase.py | 11 + tests/__init__.py | 1 + tests/broadcasts/test_sockets.py | 53 - tests/commands/test_new.py | 303 --- tests/core/__init__.py | 0 .../authentication/test_authentication2.py | 44 + tests/core/authorization/test_authorizes.py | 29 + tests/core/authorization/test_gate.py | 191 ++ tests/core/authorization/test_policies.py | 82 + tests/core/configuration/test_config.py | 68 + tests/core/{ => cookies}/test_cookies.py | 45 +- tests/core/foundation/test_app_application.py | 9 + tests/core/foundation/test_application.py | 6 + tests/core/foundation/test_facades.py | 11 + tests/core/headers/test_headers.py | 30 + tests/core/helpers/test_mix.py | 54 + tests/core/helpers/test_optional.py | 28 + tests/core/helpers/test_urls.py | 25 + tests/core/middleware/test_encrypt_cookies.py | 17 + tests/core/middleware/test_middleware.py | 100 + tests/core/request/test_http_requests.py | 7 + tests/core/request/test_input.py | 104 + tests/core/request/test_request.py | 24 + tests/core/request/test_request_input.py | 24 + tests/core/response/test_response_download.py | 20 + tests/core/response/test_response_helpers.py | 21 + .../response/test_response_redirections.py | 27 + tests/core/test_app.py | 136 -- tests/core/test_auth.py | 208 -- tests/core/test_auth_middleware.py | 38 - tests/core/test_autoload.py | 57 - tests/core/test_cache.py | 122 -- tests/core/test_container.py | 226 --- tests/core/test_controllers.py | 44 - tests/core/test_cookie_signing.py | 49 - tests/core/test_csrf.py | 40 - tests/core/test_download.py | 35 - tests/core/test_environment.py | 55 - tests/core/test_exception.py | 65 - tests/core/test_extends.py | 136 -- tests/core/test_headers.py | 27 - tests/core/test_hook.py | 19 - tests/core/test_mail_base_driver.py | 49 - tests/core/test_mail_log_drivers.py | 94 - tests/core/test_mail_smtp_driver.py | 150 -- tests/core/test_mailgun_driver.py | 103 - tests/core/test_managers_mail_manager.py | 127 -- tests/core/test_middleware.py | 55 - tests/core/test_package.py | 28 - tests/core/test_providers.py | 29 - tests/core/test_request_routes.py | 127 -- tests/core/test_requests.py | 861 -------- tests/core/test_responsable.py | 14 - tests/core/test_response.py | 234 --- tests/core/test_routes.py | 353 ---- tests/core/test_service_provider.py | 139 -- tests/core/test_session.py | 185 -- tests/core/test_signing.py | 28 - tests/core/test_upload_manager.py | 196 -- tests/core/test_view.py | 272 --- tests/core/utils/test_location.py | 106 + tests/core/utils/test_str.py | 19 + tests/core/utils/test_structures.py | 44 + tests/core/utils/test_time.py | 60 + tests/core/views/test_view.py | 115 ++ tests/database/test_user.py | 24 - tests/feature/__init__.py | 0 tests/feature/test_feature_works.py | 7 - tests/features/broadcasting/test_pusher.py | 35 + tests/features/cache/test_file_cache.py | 61 + tests/features/cache/test_memcache_cache.py | 63 + tests/features/cache/test_redis_cache.py | 63 + tests/features/event/test_event.py | 92 + .../features/hashid/test_hashid_middleware.py | 34 + tests/features/hashing/test_hashers.py | 33 + tests/features/loader/test_loader.py | 55 + tests/features/mail/test_mailable.py | 90 + tests/features/mail/test_mailgun_driver.py | 22 + tests/features/mail/test_mock_mail.py | 42 + tests/features/mail/test_smtp_driver.py | 20 + tests/features/mail/test_terminal.py | 37 + .../notification/test_anonymous_notifiable.py | 54 + .../notification/test_broadcast_driver.py | 35 + .../notification/test_database_driver.py | 132 ++ .../notification/test_integrations.py | 86 + .../features/notification/test_mail_driver.py | 59 + .../notification/test_mock_notification.py | 166 ++ .../notification/test_notification.py | 63 + .../notification/test_slack_driver.py | 169 ++ .../notification/test_slack_message.py | 64 + tests/features/notification/test_sms.py | 24 + .../notification/test_vonage_driver.py | 101 + .../packages/test_package_provider.py | 43 + .../features/packages/test_publish.py | 0 tests/features/queues/test_async_driver.py | 12 + tests/features/scheduling/test_handler.py | 27 + tests/features/scheduling/test_scheduling.py | 161 ++ tests/features/session/test_cookie_session.py | 119 ++ tests/features/storage/test_local_storage.py | 38 + tests/features/storage/test_s3_storage.py | 41 + tests/features/validation/test_messagebag.py | 116 ++ .../validation/test_messagebag_view.py | 16 + .../validation/test_request_validation.py | 25 + tests/features/validation/test_validation.py | 1755 +++++++++++++++++ tests/helpers/__init__.py | 0 tests/helpers/test_clean_request_input.py | 64 - tests/helpers/test_collect.py | 7 - tests/helpers/test_compact.py | 38 - tests/helpers/test_config.py | 60 - tests/helpers/test_dot_notation.py | 37 - tests/helpers/test_filesystem.py | 18 - tests/helpers/test_instead_of.py | 34 - tests/helpers/test_optional.py | 26 - tests/helpers/test_password.py | 9 - tests/helpers/test_view_helpers.py | 64 - tests/integrations/api.py | 6 + tests/integrations/app/Kernel/Kernel.py | 116 ++ tests/integrations/app/Kernel/__init__.py | 1 + tests/integrations/app/SayHi.py | 6 + tests/integrations/app/User.py | 8 + tests/integrations/config/application.py | 14 + tests/integrations/config/auth.py | 9 + tests/integrations/config/broadcast.py | 15 + tests/integrations/config/cache.py | 24 + tests/integrations/config/database.py | 11 + tests/integrations/config/exceptions.py | 1 + tests/integrations/config/filesystem.py | 17 + tests/integrations/config/mail.py | 16 + tests/integrations/config/notification.py | 21 + tests/integrations/config/package.py | 3 + tests/integrations/config/providers.py | 50 + tests/integrations/config/queue.py | 30 + tests/integrations/config/session.py | 7 + tests/integrations/config/test_package.py | 1 + .../controllers/HelloController.py | 7 + .../controllers/MailableController.py | 19 + .../controllers/WelcomeController.py | 149 ++ .../controllers/api/TestController.py | 6 + .../controllers/auth/HomeController.py | 7 + .../controllers/auth/LoginController.py | 19 + .../auth/PasswordResetController.py | 29 + .../controllers/auth/RegisterController.py | 21 + ...1_09_033202_create_password_reset_table.py | 15 + .../2021_01_09_043202_create_users_table.py | 20 + ...03_18_190410_create_notifications_table.py | 17 + .../integrations/databases/seeds}/__init__.py | 0 .../databases}/seeds/database_seeder.py | 3 +- .../databases/seeds/user_table_seeder.py | 17 + .../notifications/OneTimePassword.py | 19 + tests/integrations/policies/PostPolicy.py | 29 + tests/integrations/providers/AppProvider.py | 15 + tests/integrations/providers/__init__.py | 1 + tests/integrations/storage/invoice.pdf | Bin 0 -> 43627 bytes .../integrations/storage}/public/favicon.ico | Bin tests/integrations/storage/static/main.css | 3 + .../integrations/templates}/__init__.py | 0 tests/integrations/templates/auth/base.html | 13 + .../templates/auth/change_password.html | 49 + tests/integrations/templates/auth/home.html | 17 + tests/integrations/templates/auth/login.html | 64 + .../templates/auth/password_reset.html | 40 + .../integrations/templates/auth/register.html | 72 + .../templates/authorizations.html | 21 + .../integrations/templates/mail/welcome.html | 1 + .../templates/mailables/welcome.html | 1 + .../integrations}/templates/test.html | 0 .../integrations/templates/test_helpers.html | 3 + .../vendor/test_package/admin/settings.html | 1 + tests/integrations/templates/welcome.html | 35 + tests/integrations/test_package/__init__.py | 1 + .../test_package/assets/folder/test.pdf | Bin .../test_package/assets/test.js} | 0 .../test_package/commands/Command1.py | 12 + .../test_package/commands/Command2.py | 12 + .../integrations/test_package/config/test.py | 2 + .../controllers/PackageController.py | 9 + .../migrations/create_some_table.py} | 0 .../providers/MyTestPackageProvider.py | 27 + tests/integrations/test_package/routes/api.py | 5 + tests/integrations/test_package/routes/web.py | 5 + .../templates/admin/settings.html | 1 + .../test_package/templates/package.html | 5 + .../test_package/templates/package_base.html | 13 + tests/integrations/web.py | 22 + tests/listeners/test_exception_listener.py | 47 - tests/middleware/__init__.py | 0 tests/middleware/test_cors_middleware.py | 21 - tests/middleware/test_csrf_middleware.py | 71 - .../test_maintenance_mode_middleware.py | 35 - .../test_secure_headers_middleware.py | 25 - tests/pipeline/test_pipeline.py | 48 + tests/presets/__init__.py | 0 tests/presets/test_bootstrap.py | 40 - tests/presets/test_preset.py | 30 - tests/presets/test_react.py | 64 - tests/presets/test_remove.py | 71 - tests/presets/test_tailwind.py | 50 - tests/presets/test_vue.py | 62 - tests/presets/test_vue3.py | 72 - tests/providers/__init__.py | 0 tests/providers/test_route_provider.py | 115 -- tests/providers/test_statuscode_provider.py | 47 - tests/queues/__init__.py | 0 tests/queues/test_drivers.py | 100 - tests/routes/test_routes.py | 176 ++ tests/static/test.jpg | Bin 50389 -> 0 bytes tests/storage/__init__.py | 0 tests/storage/test_storage_manager.py | 82 - tests/testing/__init__.py | 0 tests/testing/test_database_tests.py | 27 - tests/testing/test_route_tests.py | 175 -- tests/tests/test_commands.py | 47 + tests/tests/test_mock.py | 19 + tests/tests/test_testcase.py | 325 +++ tests/tests/test_transactions.py | 19 + tests/unit/test_works.py | 2 - trigger_build.py | 97 - uploads/.gitkeep | 0 wsgi.py | 56 +- 780 files changed, 20064 insertions(+), 21679 deletions(-) delete mode 100644 .all-contributorsrc delete mode 100644 .coveragerc delete mode 100644 .deepsource.toml delete mode 100644 .env-example delete mode 100644 .env.local delete mode 100644 .env.production delete mode 100644 .env.test delete mode 100644 .env.testing delete mode 100644 CHANGELOG.md delete mode 100644 LICENSE create mode 100644 WHITEPAPER.md delete mode 100644 app/User.py delete mode 100644 app/http/controllers/ConfirmController.py delete mode 100644 app/http/controllers/ControllerTest.py delete mode 100644 app/http/controllers/TestController.py delete mode 100644 app/http/controllers/UnitTestController.py delete mode 100644 app/http/controllers/UserResourceController.py delete mode 100644 app/http/controllers/WelcomeController.py delete mode 100644 app/http/controllers/subdirectory/SubController.py delete mode 100644 app/http/controllers/subdirectory/deep/DeepController.py delete mode 100644 app/http/middleware/AddAttributeMiddleware.py delete mode 100644 app/http/middleware/AuthenticationMiddleware.py delete mode 100644 app/http/middleware/CsrfMiddleware.py delete mode 100644 app/http/middleware/LoadUserMiddleware.py delete mode 100644 app/http/middleware/MiddlewareTest.py delete mode 100644 app/http/middleware/TestHttpMiddleware.py delete mode 100644 app/http/middleware/TestMiddleware.py delete mode 100644 app/http/middleware/VerifyEmailMiddleware.py delete mode 100644 app/http/test_controllers/Request.py delete mode 100644 app/http/test_controllers/TestController.py delete mode 100644 app/jobs/TestJob.py delete mode 100644 config/application.py delete mode 100644 config/auth.py delete mode 100644 config/broadcast.py delete mode 100644 config/cache.py delete mode 100644 config/database.py delete mode 100644 config/factories.py delete mode 100644 config/mail.py delete mode 100644 config/middleware.py delete mode 100644 config/packages.py delete mode 100644 config/providers.py delete mode 100644 config/queue.py delete mode 100644 config/session.py delete mode 100644 config/storage.py create mode 100644 database.sqlite3 delete mode 100644 databases/migrations/2018_01_09_043202_create_users_table.py delete mode 100644 databases/migrations/2019_02_07_015506_create_failed_jobs_table.py delete mode 100644 databases/migrations/2020_09_18_233149_create_queue_jobs_table.py delete mode 100644 databases/migrations/__init__.py delete mode 100644 databases/seeds/__init__.py delete mode 100644 databases/seeds/user_table_seeder.py delete mode 100644 masonite.sqlite3 delete mode 100644 package.json delete mode 100644 resources/__init__.py delete mode 100644 resources/templates/__init__.py delete mode 100644 resources/templates/admin_test.html delete mode 100644 resources/templates/auth/confirm.html delete mode 100644 resources/templates/auth/error.html delete mode 100644 resources/templates/base.html delete mode 100644 resources/templates/csrf_field.html delete mode 100644 resources/templates/emails/test.html delete mode 100644 resources/templates/filter.html delete mode 100644 resources/templates/index.html delete mode 100644 resources/templates/line-statements.html delete mode 100644 resources/templates/mail/composers.html delete mode 100644 resources/templates/mail/share.html delete mode 100644 resources/templates/mail/welcome.html delete mode 100644 resources/templates/mail/welcome.txt delete mode 100644 resources/templates/pug/hello.pug delete mode 100644 resources/templates/test_cache.html delete mode 100644 resources/templates/test_exception.html delete mode 100644 resources/templates/welcome.html delete mode 100644 routes/web.py delete mode 100644 setup.cfg delete mode 100644 src/masonite/__version__.py delete mode 100644 src/masonite/auth/Auth.py delete mode 100644 src/masonite/auth/Csrf.py delete mode 100644 src/masonite/auth/guards/AuthenticationGuard.py delete mode 100644 src/masonite/auth/guards/Guard.py delete mode 100644 src/masonite/auth/guards/WebGuard.py delete mode 100644 src/masonite/auth/guards/__init__.py create mode 100644 src/masonite/authentication/Auth.py create mode 100644 src/masonite/authentication/__init__.py create mode 100644 src/masonite/authentication/guards/WebGuard.py create mode 100644 src/masonite/authentication/guards/__init__.py create mode 100644 src/masonite/authentication/models/__init__.py create mode 100644 src/masonite/authentication/models/authenticates.py create mode 100644 src/masonite/authorization/AuthorizationResponse.py create mode 100644 src/masonite/authorization/AuthorizesRequest.py create mode 100644 src/masonite/authorization/Gate.py create mode 100644 src/masonite/authorization/Policy.py create mode 100644 src/masonite/authorization/__init__.py create mode 100644 src/masonite/authorization/models/__init__.py create mode 100644 src/masonite/authorization/models/authorizes.py delete mode 100644 src/masonite/autoload.py create mode 100644 src/masonite/broadcasting/Broadcast.py create mode 100644 src/masonite/broadcasting/CanBroadcast.py create mode 100644 src/masonite/broadcasting/Channel.py create mode 100644 src/masonite/broadcasting/PresenceChannel.py create mode 100644 src/masonite/broadcasting/PrivateChannel.py create mode 100644 src/masonite/broadcasting/__init__.py create mode 100644 src/masonite/broadcasting/controllers/BroadcastingController.py create mode 100644 src/masonite/broadcasting/controllers/__init__.py create mode 100644 src/masonite/broadcasting/drivers/PusherDriver.py create mode 100644 src/masonite/broadcasting/drivers/__init__.py create mode 100644 src/masonite/broadcasting/providers/BroadcastProvider.py create mode 100644 src/masonite/broadcasting/providers/__init__.py create mode 100644 src/masonite/cache/Cache.py create mode 100644 src/masonite/cache/__init__.py create mode 100644 src/masonite/cache/drivers/FileDriver.py create mode 100644 src/masonite/cache/drivers/MemcacheDriver.py create mode 100644 src/masonite/cache/drivers/RedisDriver.py create mode 100644 src/masonite/cache/drivers/__init__.py delete mode 100644 src/masonite/cli.py delete mode 100644 src/masonite/commands/BaseScaffoldCommand.py create mode 100644 src/masonite/commands/CommandCapsule.py delete mode 100644 src/masonite/commands/CommandCommand.py delete mode 100644 src/masonite/commands/ControllerCommand.py delete mode 100644 src/masonite/commands/DownCommand.py create mode 100644 src/masonite/commands/Entry.py delete mode 100644 src/masonite/commands/InfoCommand.py delete mode 100644 src/masonite/commands/JobCommand.py delete mode 100644 src/masonite/commands/MailableCommand.py create mode 100644 src/masonite/commands/MakeControllerCommand.py create mode 100644 src/masonite/commands/MakeJobCommand.py create mode 100644 src/masonite/commands/MakeMailableCommand.py create mode 100644 src/masonite/commands/MakePolicyCommand.py create mode 100644 src/masonite/commands/MakeProviderCommand.py delete mode 100644 src/masonite/commands/MiddlewareCommand.py delete mode 100644 src/masonite/commands/ModelCommand.py delete mode 100644 src/masonite/commands/ModelDocstringCommand.py delete mode 100644 src/masonite/commands/PresetCommand.py rename src/masonite/commands/{NewCommand.py => ProjectCommand.py} (87%) delete mode 100644 src/masonite/commands/ProviderCommand.py delete mode 100644 src/masonite/commands/PublishCommand.py create mode 100644 src/masonite/commands/PublishPackageCommand.py create mode 100644 src/masonite/commands/QueueFailedCommand.py create mode 100644 src/masonite/commands/QueueRetryCommand.py delete mode 100644 src/masonite/commands/RoutesCommand.py delete mode 100644 src/masonite/commands/SeedCommand.py delete mode 100644 src/masonite/commands/SeedRunCommand.py delete mode 100644 src/masonite/commands/TestCommand.py delete mode 100644 src/masonite/commands/UpCommand.py delete mode 100644 src/masonite/commands/ViewCommand.py delete mode 100644 src/masonite/commands/_devserver.py delete mode 100644 src/masonite/commands/presets/Bootstrap.py delete mode 100644 src/masonite/commands/presets/Preset.py delete mode 100644 src/masonite/commands/presets/React.py delete mode 100644 src/masonite/commands/presets/Remove.py delete mode 100644 src/masonite/commands/presets/Tailwind.py delete mode 100644 src/masonite/commands/presets/Vue.py delete mode 100644 src/masonite/commands/presets/Vue3.py delete mode 100644 src/masonite/commands/presets/__init__.py delete mode 100644 src/masonite/commands/presets/bootstrap-stubs/_variables.scss delete mode 100644 src/masonite/commands/presets/bootstrap-stubs/app.scss delete mode 100644 src/masonite/commands/presets/react-stubs/Example.js delete mode 100644 src/masonite/commands/presets/react-stubs/app.js delete mode 100644 src/masonite/commands/presets/react-stubs/webpack.mix.js delete mode 100644 src/masonite/commands/presets/remove-stubs/app.js delete mode 100644 src/masonite/commands/presets/remove-stubs/bootstrap.js delete mode 100644 src/masonite/commands/presets/remove-stubs/webpack.mix.js delete mode 100644 src/masonite/commands/presets/shared-stubs/bootstrap.js delete mode 100644 src/masonite/commands/presets/tailwind-stubs/base.html delete mode 100644 src/masonite/commands/presets/tailwind-stubs/style.scss delete mode 100644 src/masonite/commands/presets/tailwind-stubs/tailwind.config.js delete mode 100644 src/masonite/commands/presets/tailwind-stubs/webpack.mix.js delete mode 100644 src/masonite/commands/presets/tailwind-stubs/welcome.html delete mode 100644 src/masonite/commands/presets/vue-stubs/ExampleComponent.vue delete mode 100644 src/masonite/commands/presets/vue-stubs/app.js delete mode 100644 src/masonite/commands/presets/vue-stubs/webpack.mix.js delete mode 100644 src/masonite/commands/presets/vue3-stubs/App.vue delete mode 100644 src/masonite/commands/presets/vue3-stubs/HelloWorld.vue delete mode 100644 src/masonite/commands/presets/vue3-stubs/app.html delete mode 100644 src/masonite/commands/presets/vue3-stubs/app.js delete mode 100644 src/masonite/commands/presets/vue3-stubs/webpack.mix.js create mode 100644 src/masonite/configuration/Configuration.py create mode 100644 src/masonite/configuration/__init__.py create mode 100644 src/masonite/configuration/helpers.py create mode 100644 src/masonite/configuration/providers/ConfigurationProvider.py create mode 100644 src/masonite/configuration/providers/__init__.py create mode 100644 src/masonite/container/__init__.py rename src/masonite/{app.py => container/container.py} (89%) delete mode 100644 src/masonite/contracts/AuthContract.py delete mode 100644 src/masonite/contracts/BroadcastContract.py delete mode 100644 src/masonite/contracts/CacheContract.py delete mode 100644 src/masonite/contracts/MailContract.py delete mode 100644 src/masonite/contracts/QueueContract.py delete mode 100644 src/masonite/contracts/SessionContract.py delete mode 100644 src/masonite/contracts/StorageContract.py delete mode 100644 src/masonite/contracts/UploadContract.py delete mode 100644 src/masonite/contracts/__init__.py delete mode 100644 src/masonite/contracts/managers/BroadcastManagerContract.py delete mode 100644 src/masonite/contracts/managers/CacheManagerContract.py delete mode 100644 src/masonite/contracts/managers/MailManagerContract.py delete mode 100644 src/masonite/contracts/managers/QueueManagerContract.py delete mode 100644 src/masonite/contracts/managers/SessionManagerContract.py delete mode 100644 src/masonite/contracts/managers/StorageManagerContract.py delete mode 100644 src/masonite/contracts/managers/UploadManagerContract.py create mode 100644 src/masonite/controllers/RedirectController.py delete mode 100644 src/masonite/drivers/BaseDriver.py delete mode 100644 src/masonite/drivers/__init__.py delete mode 100644 src/masonite/drivers/authentication/AuthCookieDriver.py delete mode 100644 src/masonite/drivers/authentication/AuthJwtDriver.py delete mode 100644 src/masonite/drivers/broadcast/BroadcastAblyDriver.py delete mode 100644 src/masonite/drivers/broadcast/BroadcastPubNubDriver.py delete mode 100644 src/masonite/drivers/broadcast/BroadcastPusherDriver.py delete mode 100644 src/masonite/drivers/cache/BaseCacheDriver.py delete mode 100644 src/masonite/drivers/cache/CacheDiskDriver.py delete mode 100644 src/masonite/drivers/cache/CacheRedisDriver.py delete mode 100644 src/masonite/drivers/mail/BaseMailDriver.py delete mode 100644 src/masonite/drivers/mail/MailLogDriver.py delete mode 100644 src/masonite/drivers/mail/MailMailgunDriver.py delete mode 100644 src/masonite/drivers/mail/MailSmtpDriver.py delete mode 100644 src/masonite/drivers/mail/MailTerminalDriver.py delete mode 100644 src/masonite/drivers/mail/Mailable.py create mode 100644 src/masonite/drivers/queue/AMQPDriver.py rename src/masonite/drivers/queue/{QueueAsyncDriver.py => AsyncDriver.py} (59%) delete mode 100644 src/masonite/drivers/queue/BaseQueueDriver.py create mode 100644 src/masonite/drivers/queue/DatabaseDriver.py delete mode 100644 src/masonite/drivers/queue/QueueAmqpDriver.py delete mode 100644 src/masonite/drivers/queue/QueueDatabaseDriver.py delete mode 100644 src/masonite/drivers/queue/QueueJobsModel.py create mode 100644 src/masonite/drivers/queue/__init__.py create mode 100644 src/masonite/drivers/session/CookieDriver.py delete mode 100644 src/masonite/drivers/session/SessionCookieDriver.py delete mode 100644 src/masonite/drivers/session/SessionMemoryDriver.py create mode 100644 src/masonite/drivers/session/__init__.py delete mode 100644 src/masonite/drivers/storage/StorageDiskDriver.py delete mode 100644 src/masonite/drivers/upload/BaseUploadDriver.py delete mode 100644 src/masonite/drivers/upload/UploadDiskDriver.py delete mode 100644 src/masonite/drivers/upload/UploadS3Driver.py create mode 100644 src/masonite/environment/__init__.py rename src/masonite/{ => environment}/environment.py (92%) create mode 100644 src/masonite/essentials/helpers/__init__.py create mode 100644 src/masonite/essentials/helpers/hashid.py create mode 100644 src/masonite/essentials/middleware/HashIDMiddleware.py create mode 100644 src/masonite/essentials/middleware/__init__.py create mode 100644 src/masonite/essentials/providers/HashIDProvider.py create mode 100644 src/masonite/events/Event.py create mode 100644 src/masonite/events/Listener.py create mode 100644 src/masonite/events/__init__.py create mode 100644 src/masonite/events/commands/MakeEventCommand.py create mode 100644 src/masonite/events/commands/MakeListenerCommand.py create mode 100644 src/masonite/events/commands/__init__.py create mode 100644 src/masonite/events/providers/EventProvider.py create mode 100644 src/masonite/events/providers/__init__.py delete mode 100644 src/masonite/exception_handler.py create mode 100644 src/masonite/exceptions/DD.py create mode 100644 src/masonite/exceptions/DumpExceptionHandler.py create mode 100644 src/masonite/exceptions/ExceptionHandler.py create mode 100644 src/masonite/exceptions/__init__.py rename src/masonite/{ => exceptions}/exceptions.py (62%) create mode 100644 src/masonite/facades/Auth.py create mode 100644 src/masonite/facades/Config.py create mode 100644 src/masonite/facades/Facade.py create mode 100644 src/masonite/facades/Gate.py create mode 100644 src/masonite/facades/Hash.py create mode 100644 src/masonite/facades/Loader.py create mode 100644 src/masonite/facades/Mail.py create mode 100644 src/masonite/facades/Notification.py create mode 100644 src/masonite/facades/Request.py create mode 100644 src/masonite/facades/Response.py create mode 100644 src/masonite/facades/Session.py create mode 100644 src/masonite/facades/Url.py create mode 100644 src/masonite/facades/View.py create mode 100644 src/masonite/facades/__init__.py create mode 100644 src/masonite/filesystem/File.py create mode 100644 src/masonite/filesystem/FileStream.py create mode 100644 src/masonite/filesystem/Storage.py create mode 100644 src/masonite/filesystem/UploadedFile.py create mode 100644 src/masonite/filesystem/__init__.py create mode 100644 src/masonite/filesystem/drivers/AmazonS3Driver.py create mode 100644 src/masonite/filesystem/drivers/LocalDriver.py create mode 100644 src/masonite/filesystem/drivers/__init__.py create mode 100644 src/masonite/filesystem/providers/StorageProvider.py create mode 100644 src/masonite/filesystem/providers/__init__.py create mode 100644 src/masonite/foundation/Application.py create mode 100644 src/masonite/foundation/Kernel.py create mode 100644 src/masonite/foundation/__init__.py rename src/masonite/{wsgi.py => foundation/response_handler.py} (56%) create mode 100644 src/masonite/hashing/Hash.py create mode 100644 src/masonite/hashing/__init__.py create mode 100644 src/masonite/hashing/drivers/Argon2Hasher.py create mode 100644 src/masonite/hashing/drivers/BcryptHasher.py create mode 100644 src/masonite/hashing/drivers/__init__.py delete mode 100644 src/masonite/helpers/Extendable.py create mode 100644 src/masonite/helpers/compact.py delete mode 100644 src/masonite/helpers/filesystem.py delete mode 100644 src/masonite/helpers/migrations.py delete mode 100644 src/masonite/helpers/misc.py create mode 100644 src/masonite/helpers/mix.py delete mode 100644 src/masonite/helpers/password.py delete mode 100644 src/masonite/helpers/routes.py delete mode 100644 src/masonite/helpers/sign.py delete mode 100644 src/masonite/helpers/static.py delete mode 100644 src/masonite/helpers/structures.py delete mode 100644 src/masonite/helpers/time.py create mode 100644 src/masonite/helpers/urls.py delete mode 100644 src/masonite/helpers/view_helpers.py delete mode 100644 src/masonite/hook.py create mode 100644 src/masonite/input/Input.py create mode 100644 src/masonite/input/__init__.py delete mode 100644 src/masonite/listeners/BaseExceptionListener.py delete mode 100644 src/masonite/listeners/__init__.py create mode 100644 src/masonite/loader/Loader.py create mode 100644 src/masonite/loader/__init__.py create mode 100644 src/masonite/mail/Mail.py create mode 100644 src/masonite/mail/Mailable.py create mode 100644 src/masonite/mail/MessageAttachment.py create mode 100644 src/masonite/mail/MockMail.py create mode 100644 src/masonite/mail/Recipient.py create mode 100644 src/masonite/mail/__init__.py create mode 100644 src/masonite/mail/drivers/MailgunDriver.py create mode 100644 src/masonite/mail/drivers/SMTPDriver.py create mode 100644 src/masonite/mail/drivers/TerminalDriver.py create mode 100644 src/masonite/mail/drivers/__init__.py delete mode 100644 src/masonite/managers/AuthManager.py delete mode 100644 src/masonite/managers/BroadcastManager.py delete mode 100644 src/masonite/managers/CacheManager.py delete mode 100644 src/masonite/managers/MailManager.py delete mode 100644 src/masonite/managers/Manager.py delete mode 100644 src/masonite/managers/QueueManager.py delete mode 100644 src/masonite/managers/SessionManager.py delete mode 100644 src/masonite/managers/StorageManager.py delete mode 100644 src/masonite/managers/UploadManager.py delete mode 100644 src/masonite/managers/__init__.py delete mode 100644 src/masonite/middleware/CorsMiddleware.py delete mode 100644 src/masonite/middleware/CsrfMiddleware.py delete mode 100644 src/masonite/middleware/GuardMiddleware.py delete mode 100644 src/masonite/middleware/MaintenanceModeMiddleware.py delete mode 100644 src/masonite/middleware/ResponseMiddleware.py delete mode 100644 src/masonite/middleware/SecureHeadersMiddleware.py create mode 100644 src/masonite/middleware/middleware.py create mode 100644 src/masonite/middleware/middleware_capsule.py create mode 100644 src/masonite/middleware/route/EncryptCookies.py create mode 100644 src/masonite/middleware/route/LoadUserMiddleware.py create mode 100644 src/masonite/middleware/route/SessionMiddleware.py create mode 100644 src/masonite/middleware/route/VerifyCsrfToken.py rename {config => src/masonite/middleware/route}/__init__.py (100%) create mode 100644 src/masonite/notification/AnonymousNotifiable.py create mode 100644 src/masonite/notification/DatabaseNotification.py create mode 100644 src/masonite/notification/MockNotification.py create mode 100644 src/masonite/notification/Notifiable.py create mode 100644 src/masonite/notification/Notification.py create mode 100644 src/masonite/notification/NotificationManager.py create mode 100644 src/masonite/notification/SlackMessage.py create mode 100644 src/masonite/notification/Sms.py create mode 100644 src/masonite/notification/Textable.py create mode 100644 src/masonite/notification/__init__.py create mode 100644 src/masonite/notification/commands/MakeNotificationCommand.py create mode 100644 src/masonite/notification/commands/NotificationTableCommand.py create mode 100644 src/masonite/notification/commands/__init__.py create mode 100644 src/masonite/notification/drivers/BaseDriver.py create mode 100644 src/masonite/notification/drivers/BroadcastDriver.py create mode 100644 src/masonite/notification/drivers/DatabaseDriver.py create mode 100644 src/masonite/notification/drivers/MailDriver.py create mode 100644 src/masonite/notification/drivers/SlackDriver.py create mode 100644 src/masonite/notification/drivers/__init__.py create mode 100644 src/masonite/notification/drivers/vonage/VonageDriver.py create mode 100644 src/masonite/notification/providers/NotificationProvider.py create mode 100644 src/masonite/notification/providers/__init__.py delete mode 100644 src/masonite/packages.py create mode 100644 src/masonite/packages/Package.py create mode 100644 src/masonite/packages/PublishableResource.py create mode 100644 src/masonite/packages/__init__.py create mode 100644 src/masonite/packages/providers/PackageProvider.py create mode 100644 src/masonite/packages/providers/__init__.py create mode 100644 src/masonite/packages/reserved_names.py create mode 100644 src/masonite/pipeline/Pipeline.py create mode 100644 src/masonite/pipeline/__init__.py rename app/providers/.gitignore => src/masonite/pipeline/tasks/MiddlewareTask.py (100%) create mode 100644 src/masonite/pipeline/tasks/ResponseTask.py delete mode 100644 src/masonite/provider.py delete mode 100644 src/masonite/providers/AppProvider.py create mode 100644 src/masonite/providers/AuthorizationProvider.py delete mode 100644 src/masonite/providers/BroadcastProvider.py delete mode 100644 src/masonite/providers/CorsProvider.py delete mode 100644 src/masonite/providers/CsrfProvider.py create mode 100644 src/masonite/providers/ExceptionProvider.py create mode 100644 src/masonite/providers/FrameworkProvider.py create mode 100644 src/masonite/providers/HashServiceProvider.py create mode 100644 src/masonite/providers/Provider.py delete mode 100644 src/masonite/providers/RequestHelpersProviders.py delete mode 100644 src/masonite/providers/StatusCodeProvider.py delete mode 100644 src/masonite/providers/UploadProvider.py create mode 100644 src/masonite/queues/Queue.py delete mode 100644 src/masonite/request.py create mode 100644 src/masonite/request/__init__.py create mode 100644 src/masonite/request/request.py create mode 100644 src/masonite/request/validation.py create mode 100644 src/masonite/response/__init__.py rename src/masonite/{ => response}/response.py (65%) delete mode 100644 src/masonite/routes.py create mode 100644 src/masonite/routes/HTTPRoute.py create mode 100644 src/masonite/routes/Route.py create mode 100644 src/masonite/routes/Router.py create mode 100644 src/masonite/routes/__init__.py create mode 100644 src/masonite/scheduling/CanSchedule.py create mode 100644 src/masonite/scheduling/CommandTask.py create mode 100644 src/masonite/scheduling/Task.py create mode 100644 src/masonite/scheduling/TaskHandler.py create mode 100644 src/masonite/scheduling/__init__.py create mode 100644 src/masonite/scheduling/commands/MakeTaskCommand.py create mode 100644 src/masonite/scheduling/commands/ScheduleRunCommand.py create mode 100644 src/masonite/scheduling/commands/__init__.py create mode 100644 src/masonite/scheduling/providers/ScheduleProvider.py create mode 100644 src/masonite/scheduling/providers/__init__.py create mode 100644 src/masonite/sessions/Session.py create mode 100644 src/masonite/sessions/__init__.py delete mode 100644 src/masonite/snippets/auth/controllers/ConfirmController.py delete mode 100644 src/masonite/snippets/auth/controllers/HomeController.py delete mode 100644 src/masonite/snippets/auth/controllers/LoginController.py delete mode 100644 src/masonite/snippets/auth/controllers/PasswordController.py delete mode 100644 src/masonite/snippets/auth/controllers/RegisterController.py delete mode 100644 src/masonite/snippets/auth/templates/auth/base.html delete mode 100644 src/masonite/snippets/auth/templates/auth/confirm.html delete mode 100644 src/masonite/snippets/auth/templates/auth/error.html delete mode 100644 src/masonite/snippets/auth/templates/auth/forget.html delete mode 100644 src/masonite/snippets/auth/templates/auth/home.html delete mode 100644 src/masonite/snippets/auth/templates/auth/login.html delete mode 100644 src/masonite/snippets/auth/templates/auth/register.html delete mode 100644 src/masonite/snippets/auth/templates/auth/reset.html delete mode 100644 src/masonite/snippets/auth/templates/auth/verify.html delete mode 100644 src/masonite/snippets/auth/templates/auth/verifymail.html delete mode 100644 src/masonite/snippets/exception.html delete mode 100644 src/masonite/snippets/exceptions/css/go-icon.png delete mode 100644 src/masonite/snippets/exceptions/css/so-icon.png delete mode 100644 src/masonite/snippets/exceptions/css/style.css delete mode 100644 src/masonite/snippets/exceptions/obj_loop.html delete mode 100644 src/masonite/snippets/scaffold/command.html delete mode 100644 src/masonite/snippets/scaffold/controller.html delete mode 100644 src/masonite/snippets/scaffold/controller_resource.html delete mode 100644 src/masonite/snippets/scaffold/job.html delete mode 100644 src/masonite/snippets/scaffold/mailable.html delete mode 100644 src/masonite/snippets/scaffold/middleware.html delete mode 100644 src/masonite/snippets/scaffold/model.html delete mode 100644 src/masonite/snippets/scaffold/provider.html delete mode 100644 src/masonite/snippets/scaffold/test.html delete mode 100644 src/masonite/snippets/scaffold/validator.html delete mode 100644 src/masonite/snippets/statuscode.html delete mode 100644 src/masonite/storage.py create mode 100644 src/masonite/storage/__init__.py create mode 100644 src/masonite/storage/storage.py create mode 100644 src/masonite/stubs/controllers/Controller.py create mode 100644 src/masonite/stubs/controllers/auth/HomeController.py create mode 100644 src/masonite/stubs/controllers/auth/LoginController.py create mode 100644 src/masonite/stubs/controllers/auth/PasswordResetController.py create mode 100644 src/masonite/stubs/controllers/auth/RegisterController.py create mode 100644 src/masonite/stubs/events/event.py create mode 100644 src/masonite/stubs/events/listener.py create mode 100644 src/masonite/stubs/jobs/Job.py create mode 100644 src/masonite/stubs/mailable/Mailable.py create mode 100644 src/masonite/stubs/notification/Notification.py create mode 100644 src/masonite/stubs/notification/create_notifications_table.py create mode 100644 src/masonite/stubs/policies/ModelPolicy.py create mode 100644 src/masonite/stubs/policies/Policy.py create mode 100644 src/masonite/stubs/providers/Provider.py rename src/masonite/{snippets/migrations => stubs/queue}/create_failed_jobs_table.py (51%) rename src/masonite/{snippets/migrations => stubs/queue}/create_queue_jobs_table.py (60%) create mode 100644 src/masonite/stubs/scheduling/task.py create mode 100644 src/masonite/stubs/templates/auth/base.html create mode 100644 src/masonite/stubs/templates/auth/change_password.html create mode 100644 src/masonite/stubs/templates/auth/home.html create mode 100644 src/masonite/stubs/templates/auth/login.html create mode 100644 src/masonite/stubs/templates/auth/password_reset.html create mode 100644 src/masonite/stubs/templates/auth/register.html create mode 100644 src/masonite/stubs/validation/Rule.py create mode 100644 src/masonite/stubs/validation/RuleEnclosure.py rename src/masonite/{snippets => templates}/__init__.py (100%) rename src/masonite/{snippets/exceptions => templates}/dump.html (100%) create mode 100644 src/masonite/templates/obj_loop.html delete mode 100644 src/masonite/testing/BaseRequest.py delete mode 100644 src/masonite/testing/MockRoute.py delete mode 100644 src/masonite/testing/TestCase.py delete mode 100644 src/masonite/testing/__init__.py delete mode 100644 src/masonite/testing/create_container.py delete mode 100644 src/masonite/testing/generate_wsgi.py create mode 100644 src/masonite/tests/DatabaseTransactions.py create mode 100644 src/masonite/tests/HttpTestResponse.py create mode 100644 src/masonite/tests/MockInput.py create mode 100644 src/masonite/tests/TestCase.py create mode 100644 src/masonite/tests/TestCommand.py create mode 100644 src/masonite/tests/TestResponseCapsule.py create mode 100644 src/masonite/tests/__init__.py rename src/masonite/{snippets/exceptions => utils}/__init__.py (100%) create mode 100644 src/masonite/utils/collections.py create mode 100644 src/masonite/utils/console.py create mode 100644 src/masonite/utils/filesystem.py rename src/masonite/{helpers/status.py => utils/http.py} (58%) create mode 100644 src/masonite/utils/location.py create mode 100644 src/masonite/utils/str.py create mode 100644 src/masonite/utils/structures.py create mode 100644 src/masonite/utils/time.py create mode 100644 src/masonite/validation/MessageBag.py create mode 100644 src/masonite/validation/RuleEnclosure.py create mode 100644 src/masonite/validation/Validator.py create mode 100644 src/masonite/validation/__init__.py create mode 100644 src/masonite/validation/commands/MakeRuleCommand.py create mode 100644 src/masonite/validation/commands/MakeRuleEnclosureCommand.py rename {storage => src/masonite/validation/commands}/__init__.py (100%) create mode 100644 src/masonite/validation/decorators.py create mode 100644 src/masonite/validation/providers/ValidationProvider.py create mode 100644 src/masonite/validation/providers/__init__.py create mode 100644 src/masonite/validation/resources/postal_codes.py delete mode 100644 src/masonite/view.py rename bootstrap/cache/.gitignore => src/masonite/views/ViewCapsule.py (100%) create mode 100644 src/masonite/views/__init__.py create mode 100644 src/masonite/views/view.py delete mode 100644 storage/append_from.txt delete mode 100644 storage/file.txt delete mode 100644 storage/logs/.gitignore delete mode 100644 storage/some_file.txt delete mode 100644 storage/static/__init__.py delete mode 100644 storage/static/profile.jpg delete mode 100644 storage/static/sass/style.scss delete mode 100644 storage/templates/tests/test.html delete mode 100644 storage/test_location.html delete mode 100644 templates/test.html delete mode 100644 templates/tests/test.html delete mode 100644 testpackage/test-config.py create mode 100644 tests/TestCase.py delete mode 100644 tests/broadcasts/test_sockets.py delete mode 100644 tests/commands/test_new.py delete mode 100644 tests/core/__init__.py create mode 100644 tests/core/authentication/test_authentication2.py create mode 100644 tests/core/authorization/test_authorizes.py create mode 100644 tests/core/authorization/test_gate.py create mode 100644 tests/core/authorization/test_policies.py create mode 100644 tests/core/configuration/test_config.py rename tests/core/{ => cookies}/test_cookies.py (51%) create mode 100644 tests/core/foundation/test_app_application.py create mode 100644 tests/core/foundation/test_application.py create mode 100644 tests/core/foundation/test_facades.py create mode 100644 tests/core/headers/test_headers.py create mode 100644 tests/core/helpers/test_mix.py create mode 100644 tests/core/helpers/test_optional.py create mode 100644 tests/core/helpers/test_urls.py create mode 100644 tests/core/middleware/test_encrypt_cookies.py create mode 100644 tests/core/middleware/test_middleware.py create mode 100644 tests/core/request/test_http_requests.py create mode 100644 tests/core/request/test_input.py create mode 100644 tests/core/request/test_request.py create mode 100644 tests/core/request/test_request_input.py create mode 100644 tests/core/response/test_response_download.py create mode 100644 tests/core/response/test_response_helpers.py create mode 100644 tests/core/response/test_response_redirections.py delete mode 100644 tests/core/test_app.py delete mode 100644 tests/core/test_auth.py delete mode 100644 tests/core/test_auth_middleware.py delete mode 100644 tests/core/test_autoload.py delete mode 100644 tests/core/test_cache.py delete mode 100644 tests/core/test_container.py delete mode 100644 tests/core/test_controllers.py delete mode 100644 tests/core/test_cookie_signing.py delete mode 100644 tests/core/test_csrf.py delete mode 100644 tests/core/test_download.py delete mode 100644 tests/core/test_environment.py delete mode 100644 tests/core/test_exception.py delete mode 100644 tests/core/test_extends.py delete mode 100644 tests/core/test_headers.py delete mode 100644 tests/core/test_hook.py delete mode 100644 tests/core/test_mail_base_driver.py delete mode 100644 tests/core/test_mail_log_drivers.py delete mode 100644 tests/core/test_mail_smtp_driver.py delete mode 100644 tests/core/test_mailgun_driver.py delete mode 100644 tests/core/test_managers_mail_manager.py delete mode 100644 tests/core/test_middleware.py delete mode 100644 tests/core/test_package.py delete mode 100644 tests/core/test_providers.py delete mode 100644 tests/core/test_request_routes.py delete mode 100644 tests/core/test_requests.py delete mode 100644 tests/core/test_responsable.py delete mode 100644 tests/core/test_response.py delete mode 100644 tests/core/test_routes.py delete mode 100644 tests/core/test_service_provider.py delete mode 100644 tests/core/test_session.py delete mode 100644 tests/core/test_signing.py delete mode 100644 tests/core/test_upload_manager.py delete mode 100644 tests/core/test_view.py create mode 100644 tests/core/utils/test_location.py create mode 100644 tests/core/utils/test_str.py create mode 100644 tests/core/utils/test_structures.py create mode 100644 tests/core/utils/test_time.py create mode 100644 tests/core/views/test_view.py delete mode 100644 tests/database/test_user.py delete mode 100644 tests/feature/__init__.py delete mode 100644 tests/feature/test_feature_works.py create mode 100644 tests/features/broadcasting/test_pusher.py create mode 100644 tests/features/cache/test_file_cache.py create mode 100644 tests/features/cache/test_memcache_cache.py create mode 100644 tests/features/cache/test_redis_cache.py create mode 100644 tests/features/event/test_event.py create mode 100644 tests/features/hashid/test_hashid_middleware.py create mode 100644 tests/features/hashing/test_hashers.py create mode 100644 tests/features/loader/test_loader.py create mode 100644 tests/features/mail/test_mailable.py create mode 100644 tests/features/mail/test_mailgun_driver.py create mode 100644 tests/features/mail/test_mock_mail.py create mode 100644 tests/features/mail/test_smtp_driver.py create mode 100644 tests/features/mail/test_terminal.py create mode 100644 tests/features/notification/test_anonymous_notifiable.py create mode 100644 tests/features/notification/test_broadcast_driver.py create mode 100644 tests/features/notification/test_database_driver.py create mode 100644 tests/features/notification/test_integrations.py create mode 100644 tests/features/notification/test_mail_driver.py create mode 100644 tests/features/notification/test_mock_notification.py create mode 100644 tests/features/notification/test_notification.py create mode 100644 tests/features/notification/test_slack_driver.py create mode 100644 tests/features/notification/test_slack_message.py create mode 100644 tests/features/notification/test_sms.py create mode 100644 tests/features/notification/test_vonage_driver.py create mode 100644 tests/features/packages/test_package_provider.py rename src/masonite/snippets/scaffold/view.html => tests/features/packages/test_publish.py (100%) create mode 100644 tests/features/queues/test_async_driver.py create mode 100644 tests/features/scheduling/test_handler.py create mode 100644 tests/features/scheduling/test_scheduling.py create mode 100644 tests/features/session/test_cookie_session.py create mode 100644 tests/features/storage/test_local_storage.py create mode 100644 tests/features/storage/test_s3_storage.py create mode 100644 tests/features/validation/test_messagebag.py create mode 100644 tests/features/validation/test_messagebag_view.py create mode 100644 tests/features/validation/test_request_validation.py create mode 100644 tests/features/validation/test_validation.py delete mode 100644 tests/helpers/__init__.py delete mode 100644 tests/helpers/test_clean_request_input.py delete mode 100644 tests/helpers/test_collect.py delete mode 100644 tests/helpers/test_compact.py delete mode 100644 tests/helpers/test_config.py delete mode 100644 tests/helpers/test_dot_notation.py delete mode 100644 tests/helpers/test_filesystem.py delete mode 100644 tests/helpers/test_instead_of.py delete mode 100644 tests/helpers/test_optional.py delete mode 100644 tests/helpers/test_password.py delete mode 100644 tests/helpers/test_view_helpers.py create mode 100644 tests/integrations/api.py create mode 100644 tests/integrations/app/Kernel/Kernel.py create mode 100644 tests/integrations/app/Kernel/__init__.py create mode 100644 tests/integrations/app/SayHi.py create mode 100644 tests/integrations/app/User.py create mode 100644 tests/integrations/config/application.py create mode 100644 tests/integrations/config/auth.py create mode 100644 tests/integrations/config/broadcast.py create mode 100644 tests/integrations/config/cache.py create mode 100644 tests/integrations/config/database.py create mode 100644 tests/integrations/config/exceptions.py create mode 100644 tests/integrations/config/filesystem.py create mode 100644 tests/integrations/config/mail.py create mode 100644 tests/integrations/config/notification.py create mode 100644 tests/integrations/config/package.py create mode 100644 tests/integrations/config/providers.py create mode 100644 tests/integrations/config/queue.py create mode 100644 tests/integrations/config/session.py create mode 100644 tests/integrations/config/test_package.py create mode 100644 tests/integrations/controllers/HelloController.py create mode 100644 tests/integrations/controllers/MailableController.py create mode 100644 tests/integrations/controllers/WelcomeController.py create mode 100644 tests/integrations/controllers/api/TestController.py create mode 100644 tests/integrations/controllers/auth/HomeController.py create mode 100644 tests/integrations/controllers/auth/LoginController.py create mode 100644 tests/integrations/controllers/auth/PasswordResetController.py create mode 100644 tests/integrations/controllers/auth/RegisterController.py create mode 100644 tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py create mode 100644 tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py create mode 100644 tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py rename {storage/uploads => tests/integrations/databases/seeds}/__init__.py (100%) rename {databases => tests/integrations/databases}/seeds/database_seeder.py (84%) create mode 100644 tests/integrations/databases/seeds/user_table_seeder.py create mode 100644 tests/integrations/notifications/OneTimePassword.py create mode 100644 tests/integrations/policies/PostPolicy.py create mode 100644 tests/integrations/providers/AppProvider.py create mode 100644 tests/integrations/providers/__init__.py create mode 100644 tests/integrations/storage/invoice.pdf rename {storage => tests/integrations/storage}/public/favicon.ico (100%) create mode 100644 tests/integrations/storage/static/main.css rename {templates => tests/integrations/templates}/__init__.py (100%) create mode 100644 tests/integrations/templates/auth/base.html create mode 100644 tests/integrations/templates/auth/change_password.html create mode 100644 tests/integrations/templates/auth/home.html create mode 100644 tests/integrations/templates/auth/login.html create mode 100644 tests/integrations/templates/auth/password_reset.html create mode 100644 tests/integrations/templates/auth/register.html create mode 100644 tests/integrations/templates/authorizations.html create mode 100644 tests/integrations/templates/mail/welcome.html create mode 100644 tests/integrations/templates/mailables/welcome.html rename {resources => tests/integrations}/templates/test.html (100%) create mode 100644 tests/integrations/templates/test_helpers.html create mode 100644 tests/integrations/templates/vendor/test_package/admin/settings.html create mode 100644 tests/integrations/templates/welcome.html create mode 100644 tests/integrations/test_package/__init__.py rename storage/public/robots.txt => tests/integrations/test_package/assets/folder/test.pdf (100%) rename tests/{broadcasts/__init__.py => integrations/test_package/assets/test.js} (100%) create mode 100644 tests/integrations/test_package/commands/Command1.py create mode 100644 tests/integrations/test_package/commands/Command2.py create mode 100644 tests/integrations/test_package/config/test.py create mode 100644 tests/integrations/test_package/controllers/PackageController.py rename tests/{commands/__init__.py => integrations/test_package/migrations/create_some_table.py} (100%) create mode 100644 tests/integrations/test_package/providers/MyTestPackageProvider.py create mode 100644 tests/integrations/test_package/routes/api.py create mode 100644 tests/integrations/test_package/routes/web.py create mode 100644 tests/integrations/test_package/templates/admin/settings.html create mode 100644 tests/integrations/test_package/templates/package.html create mode 100644 tests/integrations/test_package/templates/package_base.html create mode 100644 tests/integrations/web.py delete mode 100644 tests/listeners/test_exception_listener.py delete mode 100644 tests/middleware/__init__.py delete mode 100644 tests/middleware/test_cors_middleware.py delete mode 100644 tests/middleware/test_csrf_middleware.py delete mode 100644 tests/middleware/test_maintenance_mode_middleware.py delete mode 100644 tests/middleware/test_secure_headers_middleware.py create mode 100644 tests/pipeline/test_pipeline.py delete mode 100644 tests/presets/__init__.py delete mode 100644 tests/presets/test_bootstrap.py delete mode 100644 tests/presets/test_preset.py delete mode 100644 tests/presets/test_react.py delete mode 100644 tests/presets/test_remove.py delete mode 100644 tests/presets/test_tailwind.py delete mode 100644 tests/presets/test_vue.py delete mode 100644 tests/presets/test_vue3.py delete mode 100644 tests/providers/__init__.py delete mode 100644 tests/providers/test_route_provider.py delete mode 100644 tests/providers/test_statuscode_provider.py delete mode 100644 tests/queues/__init__.py delete mode 100644 tests/queues/test_drivers.py create mode 100644 tests/routes/test_routes.py delete mode 100644 tests/static/test.jpg delete mode 100644 tests/storage/__init__.py delete mode 100644 tests/storage/test_storage_manager.py delete mode 100644 tests/testing/__init__.py delete mode 100644 tests/testing/test_database_tests.py delete mode 100644 tests/testing/test_route_tests.py create mode 100644 tests/tests/test_commands.py create mode 100644 tests/tests/test_mock.py create mode 100644 tests/tests/test_testcase.py create mode 100644 tests/tests/test_transactions.py delete mode 100644 tests/unit/test_works.py delete mode 100644 trigger_build.py delete mode 100644 uploads/.gitkeep diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index fab08eb2e..000000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,134 +0,0 @@ -{ - "files": [ - "README.md" - ], - "imageSize": 100, - "commit": false, - "contributors": [ - { - "login": "vaibhavmule", - "name": "Vaibhav Mule", - "avatar_url": "https://avatars0.githubusercontent.com/u/6290791?v=4", - "profile": "http://vaibhavmule.com", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "mapeveri", - "name": "Martín Peveri", - "avatar_url": "https://avatars3.githubusercontent.com/u/6276555?v=4", - "profile": "http://martinpeveri.wordpress.com", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "hammacktony", - "name": "Tony Hammack", - "avatar_url": "https://avatars0.githubusercontent.com/u/10157988?v=4", - "profile": "http://tonyhammack.com", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "aisola", - "name": "Abram C. Isola", - "avatar_url": "https://avatars0.githubusercontent.com/u/1970073?v=4", - "profile": "https://inkit.io", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "josephmancuso", - "name": "Joseph Mancuso", - "avatar_url": "https://avatars1.githubusercontent.com/u/20172538?v=4", - "profile": "http://docs.masoniteproject.com", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "mitchdennett", - "name": "Mitch Dennett", - "avatar_url": "https://avatars0.githubusercontent.com/u/16268619?v=4", - "profile": "http://www.mitchdennett.com", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "Marlysson", - "name": "Marlysson Silva", - "avatar_url": "https://avatars3.githubusercontent.com/u/4117999?v=4", - "profile": "http://marlysson.github.io", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "ChrisByrd14", - "name": "Christopher Byrd", - "avatar_url": "https://avatars2.githubusercontent.com/u/7581926?v=4", - "profile": "https://www.linkedin.com/in/christopher-byrd-49726691", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "bjorntheart", - "name": "Björn Theart", - "avatar_url": "https://avatars1.githubusercontent.com/u/53244?v=4", - "profile": "https://github.com/bjorntheart", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - }, - { - "login": "nioperas06", - "name": "Junior Gantin", - "avatar_url": "https://avatars1.githubusercontent.com/u/11293401?v=4", - "profile": "https://nioperas06.github.io", - "contributions": [ - "code", - "bug", - "question", - "ideas" - ] - } - ], - "contributorsPerLine": 7, - "projectName": "masonite", - "projectOwner": "MasoniteFramework", - "repoType": "github", - "repoHost": "https://github.com" -} diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index db24590cf..000000000 --- a/.coveragerc +++ /dev/null @@ -1,11 +0,0 @@ -[run] -source = masonite - -[report] -ignore_errors = True -omit = - */tests/* - masonite/commands/* - masonite/contracts/* - masonite/snippets/* - .travis.yml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index d67f20822..000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,28 +0,0 @@ -# generated by deepsource.io -version = 1 - -test_patterns = [ - 'tests/**/*.py', - 'tests/*', - 'src/masonite/testing/*.py', - 'src/masonite/testsuite/*.py', - 'testpackage/*.py', - 'masonite/*' -] - -exclude_patterns = [ - 'databases/migrations/*' -] - -[[analyzers]] -name = "python" -enabled = true -runtime_version = "3.x.x" - - [analyzers.meta] - max_line_length = 99 - -# Test coverage analyzer -[[analyzers]] -name = "test-coverage" -enabled = true diff --git a/.env-example b/.env-example deleted file mode 100644 index 4f339220d..000000000 --- a/.env-example +++ /dev/null @@ -1,64 +0,0 @@ -APP_NAME=Masonite 2.2 -APP_ENV=local -APP_DEBUG=True -AUTH_DRIVER=cookie -APP_URL=http://localhost:8000 -KEY=your-secret-key - -MAIL_DRIVER=smtp -MAIL_FROM_ADDRESS=sandboxXX.mailgun.org -MAIL_FROM_NAME=Masonite -MAIL_HOST=smtp.mailtrap.io -MAIL_PORT=465 -MAIL_USERNAME= -MAIL_PASSWORD= - -MAILGUN_SECRET=key-xx -MAILGUN_DOMAIN=xx.mailgun.org - -DB_CONNECTION=mysql -DB_DRIVER=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=masonite -DB_USERNAME=root -DB_PASSWORD=root -DB_LOG=True - -STRIPE_CLIENT= -STRIPE_SECRET= - -STORAGE_DRIVER=disk - -S3_CLIENT= -S3_SECRET= -S3_BUCKET= - -RACKSPACE_USERNAME= -RACKSPACE_SECRET= -RACKSPACE_CONTAINER= -RACKSPACE_REGION= - -AZURE_NAME= -AZURE_SECRET= -AZURE_CONNECTION= -AZURE_CONTAINER= - -QUEUE_DRIVER=async -QUEUE_USERNAME= -QUEUE_VHOST= -QUEUE_PASSWORD= -QUEUE_HOST= -QUEUE_PORT= -QUEUE_CHANNEL= - -PUSHER_APP_ID= -PUSHER_CLIENT= -PUSHER_SECRET= -PUSHER_CLUSTER= - -ABLY_SECRET= - -PUBNUB_SECRET= -PUBNUB_PUBLISH_KEY= -PUBNUB_SUBSCRIBE_KEY= \ No newline at end of file diff --git a/.env.local b/.env.local deleted file mode 100644 index 9b625afd7..000000000 --- a/.env.local +++ /dev/null @@ -1 +0,0 @@ -LOCAL=TEST \ No newline at end of file diff --git a/.env.production b/.env.production deleted file mode 100644 index c3f528ed8..000000000 --- a/.env.production +++ /dev/null @@ -1 +0,0 @@ -TEST_PRODUCTION=TEST \ No newline at end of file diff --git a/.env.test b/.env.test deleted file mode 100644 index fddee460b..000000000 --- a/.env.test +++ /dev/null @@ -1,8 +0,0 @@ -DB_DRIVER=sqlite -DB_CONNECTION=sqlite -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=masonite.db -DB_USERNAME=root -DB_PASSWORD= -APP_DEBUG=True \ No newline at end of file diff --git a/.env.testing b/.env.testing deleted file mode 100644 index fddee460b..000000000 --- a/.env.testing +++ /dev/null @@ -1,8 +0,0 @@ -DB_DRIVER=sqlite -DB_CONNECTION=sqlite -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=masonite.db -DB_USERNAME=root -DB_PASSWORD= -APP_DEBUG=True \ No newline at end of file diff --git a/.gitignore b/.gitignore index 161e96d69..5da88d1de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,11 @@ -dist -docs -masonite.egg-info +**/*.pyc venv -venv2 -.vscode -htmlcov -.coverage -**.pyc -**.DS_Store -.DS_store +**/*.DS_Store +.env.* .env -.pytest_cache/ -__pycache__ -# storage/compiled/ -storage/uploads/ -uploads/test.jpg -bootstrap/cache/*.html -bootstrap/cache/*.txt -tests/bootstrap/cache/*.html -tests/bootstrap/cache/*.txt -/uploads/ -.coverage -.idea/ -masonite.db -tests/uploads -build -noorator -venv3 -node_modules +storage/framework/cache +storage/framework/filesystem +src/masonite.egg-info/* +.vscode +build/ +venv4 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index f42cf3212..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,485 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Sentimental Versioning](http://sentimentalversioning.org/). - -## [2.2.b2](https://github.com/MasoniteFramework/core/releases/tag/v2.2.b2) - 2019-04-27 -### Added -- Added a accept(*) to the upload drivers -- Added a lot more view helpers -- Added better test case for database tests - -### Changed -- Changed Craft serve command now reloads by default. You can still pass in the -r flag but it is not required. You can also disable reloading by using craft serve -d for --dont-reload -- Changed all pytest tests to unittests - - -## [2.1.27](https://github.com/MasoniteFramework/core/releases/tag/v2.1.27) - 2019-04-27 -### Added -- [Added collect helper](https://docs.masoniteproject.com/the-basics/helper-functions#collect) -- [Added a new way to add URL params to the route method when not specified.](https://docs.masoniteproject.com/the-basics/requests#route-parsing) - -## [2.1.26](https://github.com/MasoniteFramework/core/releases/tag/v2.1.26) - 2019-03-31 -### Fixed -- Fixed issue where channel was not reopening after closing for AMQP driver - -## [2.1.25](https://github.com/MasoniteFramework/core/releases/tag/v2.1.25) - 2019-03-31 -### Fixed -- Fixed issue where the amqp driver was not closing connections and channels after use - -## [2.1.24](https://github.com/MasoniteFramework/core/releases/tag/v2.1.24) - 2019-03-31 -### Fixed -- Fixed issue where queue jobs would fail because of a Unicode character (the checkmark) being printed out which would cause the job to fail. Now the command will print out a [Y] character if it cannot print out of a check mark. - -## [2.1.23](https://github.com/MasoniteFramework/core/releases/tag/v2.1.23) - 2019-03-24 -### Added -- [Added Cors middleware](https://docs.masoniteproject.com/security/headers#cors). -- [You can now use `@` line statements inside your jinja templates](https://docs.masoniteproject.com/the-basics/views#view-syntax) - -### Changed -- Command command now appends `Command` to the end of the class name -- Exception is now thrown when a route name is not found using the `route` method on the request class - -### Fixed -- [x] Fixed deprecation warnings with regex strings in tests - -## [2.1.22](https://github.com/MasoniteFramework/core/releases/tag/v2.1.22) - 2019-03-17 -### Fixed -- Fixed Auth command not adding route names -- Fixed issue with compact command not working with multiple None types -- Fixed issue Japanese characters not returning correct content length - -## [2.1.21](https://github.com/MasoniteFramework/core/releases/tag/v2.1.21) - 2019-03-14 -### Added -- [Added run_again_on_fail and run_times](https://docs.masoniteproject.com/useful-features/queues-and-jobs#specifying-failed-jobs) - -## [2.1.20](https://github.com/MasoniteFramework/core/releases/tag/v2.1.20) - 2019-03-09 -### Added -- [Added compact helper](https://docs.masoniteproject.com/the-basics/helper-functions#compact) - -### Fixed -- Fixed issue with comments on resource controller being flipped. -- Fixed issue with misspelling of logger - -## [2.1.19](https://github.com/MasoniteFramework/core/releases/tag/v2.1.19) - 2019-03-05 -### Fixed -* Fixed issue with `.` template splices not working - -## [2.1.18](https://github.com/MasoniteFramework/core/releases/tag/v2.1.18) - 2019-03-04 -### Added -- Added a `--connection` option to the `model:doctring` command to use other connections - -### Fixed -- Fixed issue with csrf not being able to correctly detect csrf wildcard routes. - -## [2.1.17](https://github.com/MasoniteFramework/core/releases/tag/v2.1.17) - 2019-02-24 -### Added -- [Added `-m` and `-s` to the model command to create a migration or a seed](https://docs.masoniteproject.com/the-craft-command/introduction#model-shortcuts) -- [Added ability to use dot notation to get a dictionary value](https://docs.masoniteproject.com/the-basics/requests#getting-dictionary-input) -- Added google and stack overflow links to the top of the exception page -- [Added optional helper](https://docs.masoniteproject.com/the-basics/helper-functions#optional) - -### Changed -- [Changed where cleaning happens in the request class. Can now specify on if you want parameters cleaned.](https://docs.masoniteproject.com/the-basics/requests#input-cleaning) - -### Fixed -- Fixed issue with not being able to set 404 status codes - -## [2.1.16](https://github.com/MasoniteFramework/core/releases/tag/v2.1.16) - 2019-02-11 -### Fixed -- Fixed issue with setting status codes on json responses -- Fixed issue with specifying exempt CSRF protection routes that contained route parameters - -## [2.1.15](https://github.com/MasoniteFramework/core/releases/tag/v2.1.15) - 2019-02-10 -### Added -- [Added ability to specify a list as the second parameter to routes instead of a dictionary](https://docs.masoniteproject.com/the-basics/requests#route-parsing) -- [Added ability to return a model which then returns a JSON response](https://docs.masoniteproject.com/the-basics/controllers#returning-json) -- [Added ability to show when you have unmigrated migrations](https://docs.masoniteproject.com/the-craft-command/introduction#running-the-wsgi-server) -- [Added improvements to the queue feature](https://docs.masoniteproject.com/useful-features/queues-and-jobs#queues-and-jobs) -- Added ability to pass in default as the driver method to get the default driver. - -## [2.1.14](https://github.com/MasoniteFramework/core/releases/tag/v2.1.14) - 2019-02-01 -### Fixed -- Fixed issue with login authentication - -## [2.1.13](https://github.com/MasoniteFramework/core/releases/tag/v2.1.13) - 2019-01-26 -### Fixed -- Fixed issue where a JSON null value could raise an exception 71d9016 -- Fixed issue where a body type of 0 would throw an exception with the delete method type 9659363 -- Fixed issue where status code could not be set in a controller 5ede5d8 - -### Added -- Added a better exception when passing in a set instead of a dictionary to the render method. This is a common mistake that would throw an ambiguous error -- [Added route redirection](https://docs.masoniteproject.com/the-basics/routing#redirect-route) - -## [2.1.12](https://github.com/MasoniteFramework/core/releases/tag/v2.1.12) - 2019-01-19 -### Fixed -- Fixed issue where incoming JSON response would only return the first value in a list. - -## [2.1.11](https://github.com/MasoniteFramework/core/releases/tag/v2.1.11) - 2019-01-18 -### Fixed -- Fixed issue when cleaning a multi dimensional dictionary - -## [2.1.10](https://github.com/MasoniteFramework/core/releases/tag/v2.1.10) - 2019-01-11 -### Removed -- Removed ability to set the password column using __password__ - -## [2.1.9](https://github.com/MasoniteFramework/core/releases/tag/v2.1.9) - 2019-01-11 -### Fixed -- Fixed issue with auth requiring a __password__ attribute when it should not have been. - -## [2.1.8](https://github.com/MasoniteFramework/core/releases/tag/v2.1.8) - 2019-01-10 -### Fixed -- Fixed issue with whitenoise not auto refreshing static files - -## [2.1.7](https://github.com/MasoniteFramework/core/releases/tag/v2.1.7) - 2019-01-10 -### Fixed -- Fixed issue with storage folder not updating for static assets - -## [2.1.6](https://github.com/MasoniteFramework/core/releases/tag/v2.1.6) - 2019-01-09 -### Added -- [Added config helper](https://docs.masoniteproject.com/the-basics/helper-functions#config) [#517](https://github.com/MasoniteFramework/core/pull/517) -- [Added ability to use `in` keyword for the container](https://docs.masoniteproject.com/architectural-concepts/service-container#has) [#520](https://github.com/MasoniteFramework/core/pull/520) -- [Added ability to use multiple columns to authenticate](https://docs.masoniteproject.com/security/authentication#multiple-authentication-columns) [#521](https://github.com/MasoniteFramework/core/pull/521) -- [Added ability to specify the user password column](https://docs.masoniteproject.com/security/authentication#changing-the-authentication-password) [#521](https://github.com/MasoniteFramework/core/pull/521) - -## [2.1.5](https://github.com/MasoniteFramework/core/releases/tag/v2.1.5) - 2019-01-03 -### Fixed -- Fixed issue with LoginController not working properly because of incorrectly specified input -- Fixed issue with view render method storing variables from previous renders -- Fixed issue with s3 not working properly when using both location and filename -- Fixed issue with Amazon S3 storing all files in root directory - -### Changed -- Changed how S3 temporarily stores file uploads -- Changed where the exception is thrown in the s3 driver to prevent a temporary file being saved before uploading if the driver is not installed. - -### Added -- Added the ability for disk driver to create directories if they do not exist - -## [2.1.4](https://github.com/MasoniteFramework/core/releases/tag/v2.1.4) - 2018-12-30 -### Fixed -- Fixed issue where uploading a file resulted in None being returned. - -## [2.1.3](https://github.com/MasoniteFramework/core/releases/tag/v2.1.3) - 2018-12-22 -### Security -- Fixed possibility of an XSS attack through query strings -- Fixed possibility of uploading arbitrary files by default - -### Fixed -- Fixed issue where a 400 response was returning a 200 status code - -## [2.1.2](https://github.com/MasoniteFramework/core/releases/tag/v2.1.2) - 2018-12-16 -### Added -- [Added queue drivers so any objects can be queued](https://docs.masoniteproject.com/useful-features/queues-and-jobs#passing-functions-or-methods) -- Added ShouldQueue class -- [Added new redirection method options](https://docs.masoniteproject.com/the-basics/requests#redirection) - -### Fixed -- Fixed issue with deeper module controllers -- Fixed issue when returning integer from a view -- Fixed container error warning - -## [2.1.1](https://github.com/MasoniteFramework/core/releases/tag/v2.1.1) - 2018-12-04 -### Fixed -- Fixed issue with header redirection - -## [2.1.0](https://github.com/MasoniteFramework/core/releases/tag/v2.1.0) - 2018-12-01 -### Added -- Added middleware classes instead of strings. -- Added migrate:status command -- Added a simple container binding -- Added Mail Helper -- Added status code mapping and `request.status(int)` features -- Added several methods to the service provider class to helper bind things into the container -- Added view Routes -- Added request.without() method -- Added port to database dictionary -- Added way to set an integer as a status code -- Added a way to set headers with a dictionary -- Added basic testing framework -- Added Match routes for multiple route methods -- Added Masonite events into core -- Added email verification -- Added request.without -- Added craft middleware command -- Added Headers can be added via a dictionary -- Added views can now use dot notation -- Added swap to container -- Added masonite env function for cast conversions -- Added ability to resolve with normal parameters like `.resolve(obj, var1, var2)` -- Added password reset to auth command -- Added Response Middleware and removed the StartResponse provider -- Added better pep 8 standards -- Added code of conduct -- Added test for file system helpers -- Added Masonite events to core -- Added Response object - -### Removed -- Removed the arbitrary `payload` input when fetching a json response 308b3b1 -- Removed container Resolving - #255 -- Removed the need for the |safe filters on Masonite template helpers -- Removed patch from serve command - -### Fixed -- Fixed param method not working with custom route compilers -- Fixed issue when removing mailprovider from the optional providers section - -### Changed -- Changed `Auth` class into the `auth` directory and removed the facades directory. -- Changed cache_exists to cache -- Changed Request redirections now set status codes -- Changed and refactored commands to inherit from scaffolding based classes -- Changed built in templates to bootstrap 4 -- Changed all scaffolding commands to use view templates now -- Changed routes to work without adding a slash at the end -- Changed all dependencies to the most up to date versions - -## [2.0.36](https://github.com/MasoniteFramework/core/releases/tag/v2.0.36) - 2018-11-16 -### Added -- Added the `-b`, `-p` and `-i` options to the serve command for bind, port and interval. - -### Fixed -- Fixed issue where the server would crash when there was a syntax error. - -### Changed -- Changed the developer server completely and replaced waitress with a different pure python development server. - -## [2.0.35](https://github.com/MasoniteFramework/core/releases/tag/v2.0.35) - 2018-11-10 -### Added -- Added upload driver's abilities to accept an open file as a file item. - -## [2.0.34](https://github.com/MasoniteFramework/core/releases/tag/v2.0.34) - 2018-11-09 -### Fixed -- Fixed dependencies being fixed to a specific version number - -## [2.0.33](https://github.com/MasoniteFramework/core/releases/tag/v2.0.33) - 2018-11-01 -### Fixed -- Fixed issue with mail templates throwing `'function' object has no attribute 'render` - -## [2.0.32](https://github.com/MasoniteFramework/core/releases/tag/v2.0.32) - 2018-10-31 -### Added -- [Added Redis cache driver](https://docs.masoniteproject.com/useful-features/caching#redis) -- [Added `dd()`](https://docs.masoniteproject.com/the-basics/helper-functions#die-and-dump) and [custom exception handlers](https://docs.masoniteproject.com/useful-features/framework-hooks#exception-handlers) -- [Added ability to add jinja2 extensions](https://docs.masoniteproject.com/useful-features/framework-hooks#exception-handlers) - -## [2.0.31](https://github.com/MasoniteFramework/core/releases/tag/v2.0.31) - 2018-10-31 -### Security -- Security fix because of the `requests` package - -## [2.0.30](https://github.com/MasoniteFramework/core/releases/tag/v2.0.30) - 2018-10-16 -### Fixed -- Fixed issue where `amqp` driver was not reconnecting automatically if the connection was lost - -## [2.0.29](https://github.com/MasoniteFramework/core/releases/tag/v2.0.29) - 2018-10-08 -### Fixed -- Fixed `amqp` driver connection credentials when connecting to remote servers - -## [2.0.28](https://github.com/MasoniteFramework/core/releases/tag/v2.0.28) - 2018-10-08 -### Fixed -- Fixed `amqp` driver not requiring a port - -## [2.0.27](https://github.com/MasoniteFramework/core/releases/tag/v2.0.27) - 2018-10-08 -### Fixed -- Fixed `amqp` driver not accepting a vhost - -## [2.0.26](https://github.com/MasoniteFramework/core/releases/tag/v2.0.26) - 2018-10-08 -### Fixed -- [Fixed passing variables into jobs](https://docs.masoniteproject.com/useful-features/queues-and-jobs#passing-variables-into-jobs) - -## [2.0.25](https://github.com/MasoniteFramework/core/releases/tag/v2.0.25) - 2018-10-07 -### Added -- [Added 2 new mail drivers: `terminal` and `log`](https://docs.masoniteproject.com/useful-features/mail#terminal-driver) -- [Added `amqp` queue driver and `queue:work` command](https://docs.masoniteproject.com/useful-features/queues-and-jobs#amqp-driver) -- [Added `model:docstring` command](https://docs.masoniteproject.com/useful-features/queues-and-jobs#amqp-driver) - -## [2.0.24](https://github.com/MasoniteFramework/core/releases/tag/v2.0.24) - 2018-09-30 -### Added -- Added ability to "make" a class from the container -- Added a way to make a full route - -## [2.0.23](https://github.com/MasoniteFramework/core/releases/tag/v2.0.23) - 2018-09-16 -### Fixed -- Fixed Issue where url parameters were not resetting at the end of each request and being carried over when the second route does not have any URL parameters. - -## [2.0.22](https://github.com/MasoniteFramework/core/releases/tag/v2.0.22) - 2018-09-13 -### Fixed -- Fixed Issue where multiple select inputs were not fetching all values and also made it so it will fetch via dot notation - -## [2.0.21](https://github.com/MasoniteFramework/core/releases/tag/v2.0.21) - 2018-09-13 -### Fixed -- Fixed Issue where Masonite was not overriding environment variables that were already set - -## [2.0.20](https://github.com/MasoniteFramework/core/releases/tag/v2.0.20) - 2018-09-09 -### Added -- Contracts to managers - -- Better exception handling for invalid secret keys - -- Python 3.7 to travis.yml file - -- SSL option in config - -- View tests - -## Changed -- Made the view class more modular - -## [2.0.19](https://github.com/MasoniteFramework/core/releases/tag/v2.0.19) - 2018-09-01 -### Fixed -- Fixed Issue where the reset migration command was not throwing `QueryExceptions`. - -## [2.0.18](https://github.com/MasoniteFramework/core/releases/tag/v2.0.18) - 2018-08-30 -### Fixed -- Fixed Issue where route groups were overriding middleware - -## [2.0.17](https://github.com/MasoniteFramework/core/releases/tag/v2.0.17) - 2018-08-27 -### Fixed -- Fixed Issue where the autoloader was loading more directories than it was suppose to - -## [2.0.16](https://github.com/MasoniteFramework/core/releases/tag/v2.0.16) - 2018-08-22 -### Added -- Added docstrings to nearly all classes -- Added container hooks -- Added strict and override options to the container -- Added a validator command -- Added middleware groups -- Added change log -- Added route compilers - -## [2.0.15](https://github.com/MasoniteFramework/core/releases/tag/v2.0.15) - 2018-08-12 - -### Fixed -- Fixed an issue where the craft info command was calling the masonite-cli command prematurely. - -## [2.0.14](https://github.com/MasoniteFramework/core/releases/tag/v2.0.14) - 2018-08-08 - -### Added -- Added casting for validations and added a validation helper -- Added ability to set a dictionary in the session and be able to automatically JSON encode and decode. - -### Fixed -- Fixed cryptography dependency -- Fixed issue where URL endpoints could not have - or . in them. - -## [2.0.13](https://github.com/MasoniteFramework/core/releases/tag/v2.0.13) - 2018-07-28 - -### Fixed -- Fixed seed files not being able to import user models -- Fixed models not being able to be created in deeper directories - -## [2.0.12](https://github.com/MasoniteFramework/core/releases/tag/v2.0.12) - 2018-07-19 - -### Fixed -- Fixed exception thrown when a route inside a group route did not have a name but the route did - -## [2.0.11](https://github.com/MasoniteFramework/core/releases/tag/v2.0.11) - 2018-07-14 - -### Fixed -- Made a hot fix for the .env file not being found on some systems - -## [2.0.10](https://github.com/MasoniteFramework/core/releases/tag/v2.0.10) - 2018-07-10 - -### Added - -- Added back method to request class -- Added ability to add custom filters -- Added better route groups - -## [2.0.9](https://github.com/MasoniteFramework/core/releases/tag/v2.0.9) - 2018-07-06 - -### Added - -- Added a possible default value to the request input -- Added a way to do multiple values in the request.has() method -- Added request route #203 -- Added a pop method to request to remove inputs -- Added a url_from_controller method to request -- Added a contain method to request to request -- Added a is named route method to request - -## [2.0.8](https://github.com/MasoniteFramework/core/releases/tag/v2.0.8) - 2018-06-26 - -### Added -- Added craft info command -- Added the ability to add environments to the container and View Class - -### Changed -- Moved the route middleware to the top of the container so it can be appended onto by packages. - -### Fixed -- Fixed what errors the status code provider is executed on (500 and 404) - -## [2.0.7](https://github.com/MasoniteFramework/core/releases/tag/v2.0.7) - 2018-06-24 - -### Added -- Added warning message to craft serve command if applications are not correctly patched for 2.0 - -## [2.0.6](https://github.com/MasoniteFramework/core/releases/tag/v2.0.6) - 2018-06-22 - -### Fixed -- Fixed windows throwing bad exceptions in the exception view - -## [2.0.5](https://github.com/MasoniteFramework/core/releases/tag/v2.0.5) - 2018-06-22 - -### Added -- Added better exception handling for Masonite encrypted key signing - -## [2.0.4](https://github.com/MasoniteFramework/core/releases/tag/v2.0.4) - 2018-06-16 - -### Fixed -- Fixed circular cleo version. - -## [2.0.3](https://github.com/MasoniteFramework/core/releases/tag/v2.0.3) - 2018-06-16 - -### Fixed -- Fixed controller constructors not being resolved by the container - -## [2.0.2](https://github.com/MasoniteFramework/core/releases/tag/v2.0.2) - 2018-06-15 - -### Changed -- Bumped requests version - -## [2.0.1](https://github.com/MasoniteFramework/core/releases/tag/v2.0.1) - 2018-06-14 - -### Added -- Added Tinker Command #116 -- Added Show Routes Command #117 -- Added Automatic Code Reloading to Serve Command #119 -- Added autoloading support #146 -- Adds a new get_request_method method to request class -- Adds a new parameter to the all() method to get all the inputs without the framework internals -- Added Masontite Scheduler -- Added Database Seeding Support #168 -- Added static file helper #167 -- Added Password helper -- Added dot notation to upload drivers -- Added Status Code provider and support #165 -- Added support for making location dictionaries to upload drivers -- Adds better .env environment support #172 -- Added activate subdomain #173 -- Added class based drivers -- Added collect method the he autoload class and changes the return type of instance and collect as well as added an instantiate to the load method #178 - -### Changed - -- Controller constructors are resolved by the container -- Updated all dependencies to latest version. -- Providers now need to be imported into a provider.py file and removed from the application.py file. #177 -- Renamed Request.redirectTo to Request.redirect_to #152 -- Changed the csrf middleware accordingly. - -### Removed -- Removed all duplicated import class names -- Removed need for providers list to also have duplicated class names -- Removed redirection provider completely -- Removed database specific dependencies - -### Notes -- Need documentation for the new Request.only() method. - -## [Older Releases](https://github.com/MasoniteFramework/core/releases?after=v2.0.1) - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7442dadf..822a3912a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,141 +1,8 @@ # Contributing Guide -## Introduction +Contributing to Masonite is simple: -When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners or contributors of this repository before making a change. - -Please note we have a code of conduct, please follow it in all your interactions with the project. - -## Getting Started - -The framework has three main parts. - -This `MasoniteFramework/masonite` repository is the main repository that will install when creating new projects using the `craft new` command. Not much development will be done in this repository and won't be changed unless new releases of Masonite require changes in the default installation project. - -The `MasoniteFramework/core` repository where the main `masonite` pip package lives. This is where the `from masonite ...` module lives. - -The `MasoniteFramework/craft` repository where the `craft` command lives - -### Getting the Masonite repository up and running to be edited - -[You can read about how the framework flows, works and architectural concepts here](https://masoniteframework.gitbooks.io/docs/content/request-lifecycle.html) - -This repo is simple and will be able to be installed following the installation instruction in the README. - -* Fork the `MasoniteFramework/masonite` repo. -* Clone that repo into your computer: - * `git clone http://github.com/your-username/masonite.git` -* Checkout the current release branch \(example: `develop`\) -* You should now be on a `develop` local branch. -* Run `git pull origin develop` to get the current release version. -* From there simply create your feature branches \(`change-default-orm`\) and make your desired changes. -* Push to your origin repository: - * `git push origin change-default-orm` -* Open a pull request and follow the PR process below - -### Editing the Masonite core repository - -The trick to this is that we need it to be pip installed and then quickly editable until we like it, and then pushed back to the repo for a PR. Do this only if you want to make changes to the core Masonite package - -To do this just: - -* Fork the `MasoniteFramework/core` repo, -* Clone that repo into your computer: - * `git clone http://github.com/your-username/core.git` -* Activate your masonite virtual environment \(optional\) - * Go to where you installed masonite and activate the environment -* While inside the virtual environment, cd into the directory you installed core. -* Run `pip install .` from inside the masonite-core directory. This will install masonite as a pip package. -* Any changes you make to this package just push it to your feature branch on your fork and follow the PR process below. - -{% hint style="warning" %} -This repository has a barebones skeleton of a sample project in order to aid in testing all the features of Masonite against a real project. If you install this as editable by passing the `--editable` flag then this may break your project because it will override the modules in this package with your application modules. -{% endhint %} - -### Editing the craft repository \(`craft` commands\) - -Craft commands make up a large part of the workflow for Masonite. Follow these instructions to get the masonite-cli package on your computer and editable. - -* Fork the `MasoniteFramework/craft` repo, -* Clone that repo into your computer: - * `git clone http://github.com/your-username/craft.git` -* Activate your masonite virtual environment \(optional\) - * Go to where you installed masonite and activate the environment -* While inside the virtual environment, cd into the directory you installed cli -* Run `pip install --editable .` from inside the masonite-cli directory. This will install craft \(which contains the craft commands\) as a pip package but also keep a reference to the folder so you can make changes freely to craft commands while not having to worry about continuously reinstalling it. -* Any changes you make to this package just push it to your feature branch on your fork and follow the PR process below. - -### Comments - -Comments are a vital part of any repository and should be used where needed. It is important not to overcomment something. If you find you need to constantly add comments, you're code may be too complex. Code should be self documenting \(with clearly defined variable and method names\) - -#### Types of comments to use - -There are 3 main type of comments you should use when developing for Masonite: - -**Module Docstrings** - -All modules should have a docstring at the top of every module file and should look something like: - -```python -""" This is a module to add support for Billing users """ -from masonite.request import Request -... -``` - -**Method and Function Docstrings** - -All methods and functions should also contain a docstring with a brief description of what the module does - -For example: - -```python -def some_function(self): - """ - This is a function that does x action. - Then give an exmaple of when to use it - """ - ... code ... -``` - -**Code Comments** - -If you're code MUST be complex enough that future developers will not understand it, add a `#` comment above it - -For normal code this will look something like: - -```python -# This code performs a complex task that may not be understood later on -# You can add a second line like this -complex_code = 'value' - -perform_some_complex_task() -``` - -**Flagpole Comments** - -Flag pole comments are a fantastic way to give developers an inside to what is really happening and for now should only be reserved for configuration files. A flag pole comment gets its name from how the comment looks - -```text -''' -|-------------------------------------------------------------------------- -| A Heading of The Setting Being Set -|-------------------------------------------------------------------------- -| -| A quick description -| -''' - -SETTING = 'some value' -``` - -It's important to note that there should have exactly 75 `-` above and below the header and have a trailing `|` at the bottom of the comment. - -### Pull Request Process - -1. You should open an issue before making any pull requests. Not all features will be added to the framework and some may be better off as a third party package. It wouldn't be good if you worked on a feature for several days and the pull request gets rejected for reasons that could have been discussed in an issue for several minutes. -2. Ensure any changes are well commented and any configuration files that are added have a flagpole comment on the variables it's setting. -3. Update the README.md and `MasoniteFramework/docs` repo with details of changes to the interface, this includes new environment variables, new file locations, container parameters etc. -4. You must add unit testing for any changes made. Of the three repositories listed above, only the `craft` and `core` repos require unit testing. -5. Increase the version numbers in any example files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/) for both `core` and `craft` or [RomVer](http://blog.legacyteam.info/2015/12/romver-romantic-versioning/) for the main Masonite repo. -6. The PR must pass the Travis CI build. The Pull Request can be merged in once you have a successful review from two other collaborators, or the feature maintainer for your specific feature improvement or the repo owner. +- Hop on [Masonite Slack Community](http://slack.masoniteproject.com/) to ask any questions you need! +- Read the [How To Contribute](https://docs.masoniteproject.com/prologue/how-to-contribute) documentation to see ways to contribute to the project. +- Read the [Contributing Guide](https://docs.masoniteproject.com/prologue/contributing-guide) to learn how to contribute to the core source code development of the project. +- [Follow Masonite Framework on Twitter](https://twitter.com/masoniteproject) to get updates about tips and tricks, announcement and releases. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7224ebed6..000000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017-present Joseph Mancuso - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index fcd49e68b..67681ce23 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,15 +1,9 @@ -include src/masonite/snippets/exceptions/css/* -include src/masonite/snippets/exceptions/* -include src/masonite/snippets/* -include src/masonite/snippets/migrations/* -include src/masonite/snippets/scaffold/* -include src/masonite/snippets/auth/controllers/* -include src/masonite/snippets/auth/templates/auth/* - -include src/masonite/commands/presets/bootstrap-stubs/* -include src/masonite/commands/presets/react-stubs/* -include src/masonite/commands/presets/remove-stubs/* -include src/masonite/commands/presets/vue-stubs/* -include src/masonite/commands/presets/vue3-stubs/* -include src/masonite/commands/presets/tailwind-stubs/* -include src/masonite/commands/presets/shared-stubs/* +include src/masonite/stubs/queue/* +include src/masonite/stubs/controllers/* +include src/masonite/stubs/controllers/auth/* +include src/masonite/stubs/jobs/* +include src/masonite/templates/* +include src/masonite/stubs/mailable/* +include src/masonite/stubs/scheduling/* +include src/masonite/stubs/scheduling/templates/auth/* +include src/masonite/validation/snippets/* \ No newline at end of file diff --git a/Makefile b/Makefile index de322709a..3bcf3e523 100644 --- a/Makefile +++ b/Makefile @@ -1,31 +1,39 @@ init: - python -m pip install --upgrade pip - pip install -r requirements.txt --user - pip install -e . - pip install pytest + pip install -r requirements.txt + pip install '.[test]' + # Create MySQL Database + # Create Postgres Database test: python -m pytest tests ci: - make test - make lint + python -m pytest tests -m "not integrations" lint: - python -m flake8 src/masonite/ --ignore=E501,F401,E128,E402,E731,F821,E712,W503 + python -m flake8 src/masonite/ --ignore=E501,F401,E203,E128,E402,E731,F821,E712,W503,F811 format: black src/masonite -deepsource: - curl https://deepsource.io/cli | sh - ./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml + black tests/ + make lint +sort: + isort tests + isort src/masonite coverage: python -m pytest --cov-report term --cov-report xml --cov=src/masonite tests/ python -m coveralls +show: + python -m pytest --cov-report term --cov-report html --cov=src/masonite tests/ +cov: + python -m pytest --cov-report term --cov-report xml --cov=src/masonite tests/ publish: + pip install twine make test - pip install 'twine>=1.5.0' - python setup.py sdist bdist_wheel + python setup.py sdist twine upload dist/* rm -fr build dist .egg masonite.egg-info + rm -rf dist/* pub: - pip install 'twine>=1.5.0' - python setup.py sdist bdist_wheel + python setup.py sdist twine upload dist/* - rm -fr build dist .egg masonite.egg-info \ No newline at end of file + rm -fr build dist .egg masonite.egg-info + rm -rf dist/* +pypirc: + cp .pypirc ~/.pypirc \ No newline at end of file diff --git a/README.md b/README.md index 8397e347f..d68ae407d 100644 --- a/README.md +++ b/README.md @@ -1,347 +1,76 @@ -

- + +

Masonite

-

- -Python Version License License -License -All Contributors - + GitHub Workflow Status + License + PyPI + Python Version + GitHub release (latest by date including pre-releases) + License + Code style: black

-**NOTE: Masonite 2.3 is no longer compatible with the `masonite-cli` tool. Please uninstall that by running `pip uninstall masonite-cli`. If you do not uninstall `masonite-cli` you will have command clashes** - ## About Masonite -The modern and developer centric Python web framework that strives for an actual batteries included developer tool with a lot of out of the box functionality with an extremely extendable architecture. Masonite is perfect for beginner developers getting into their first web applications as well as experienced devs that need to utilize the full potential of Masonite to get their applications done. +> Note: This repository contains the core code of the Masonite framework. If you want to see a Masonite project template please go to [MasoniteFramework/cookie-cutter](https://github.com/MasoniteFramework/cookie-cutter) +The modern and developer centric Python web framework that strives for an actual batteries included developer tool with a lot of out of the box functionality with an extremely extendable architecture. Masonite is perfect for beginner developers getting into their first web applications as well as experienced devs that need to utilize the full potential of Masonite to get their applications done. Masonite works hard to be fast and easy from install to deployment so developers can go from concept to creation in as quick and efficiently as possible. Use it for your next SaaS! Try it once and you’ll fall in love. -* Having a simple and expressive routing engine -* Extremely powerful command line helpers called `craft` commands -* A simple migration system, removing the "magic" and finger crossing of migrations -* A great Active Record style ORM called Orator -* A great filesystem architecture for navigating and expanding your project -* An extremely powerful Service Container (IOC Container) -* Service Providers which makes Masonite extremely extendable +- Easily send emails with the Mail Provider and the SMTP and Mailgun drivers +- Send websocket requests from your server with the Broadcast Provider and Pusher, Ably and PubNub drivers +- IOC container and auto resolving dependency injection +- Service Providers to easily add functionality to the framework +- Extremely simple static files configured and ready to go +- Active Record style ORM called [Masonite ORM](https://github.com/MasoniteFramework/orm) +- An extremely useful command line tool to assist in your development tasks +- Extremely extendable ## Learning Masonite -Masonite strives to have extremely comprehensive documentation. All documentation can be [Found Here](https://docs.masoniteproject.com/) and would be wise to go through the tutorials there. If you find any discrepencies or anything that doesn't make sense, be sure to comment directly on the documentation to start a discussion! - -If you are a visual learner you can find tutorials here: [MasoniteCasts](https://masonitecasts.com) - -Also be sure to join the [Slack channel](http://slack.masoniteproject.com/)! - -## Contributing - -Contributing to Masonite is simple: -* Hop on [Slack Channel](http://slack.masoniteproject.com/)! to ask any questions you need. -* Read the [How To Contribute](https://masoniteframework.gitbook.io/docs/prologue/how-to-contribute) documentation to see ways to contribute to the project. -* Read the [Contributing Guide](https://masoniteframework.gitbook.io/docs/prologue/contributing-guide) to learn how to contribute to the core source code development of the project. -* Read the [Installation](https://docs.masoniteproject.com/#installation) documentation on how to get started creating a Masonite project. -* Check the [open issues and milestones](https://github.com/MasoniteFramework/core/issues). -* If you have any questions just [open up an issue](https://github.com/MasoniteFramework/core/issues/new/choose) to discuss with the core maintainers. -* [Follow Masonite Framework on Twitter](https://twitter.com/masoniteproject) to get updates about tips and tricks, announcement and releases. - -## Requirements - -In order to use Masonite, you’ll need: - -* Python 3.5+ -* Latest version of OpenSSL -* Pip3 - -> All commands of python and pip in this documentation is assuming they are pointing to the correct Python 3 versions. For example, anywhere you see the `python` command ran it is assuming that is a Python 3.5+ Python installation. If you are having issues with any installation steps just be sure the commands are for Python 3.5+ and not 2.7 or below. - -## Linux - -If you are running on a Linux flavor, you’ll need the Python dev package and the libssl package. You can download these packages by running: +New to Masonite ? Please first read the [Official Documentation](https://docs.masoniteproject.com/). +Masonite strives to have extremely comprehensive documentation 😃. It would be wise to go through the tutorials there. +If you find any discrepencies or anything that doesn't make sense, be sure to comment directly on the documentation to start a discussion! -### Debian and Ubuntu based Linux distributions +If you are more of a visual learner you can watch Masonite related tutorial videos at [masonitecasts.com](https://masonitecasts.com) -```text -$ sudo apt-get install python-dev libssl-dev python3-pip -``` - -Or you may need to specify your `python3.x-dev` version: - -```text -$ sudo apt-get install python3.6-dev libssl-dev python3-pip -``` - -### Enterprise Linux based distributions \(Fedora, CentOS, RHEL, ...\) - -```text -# dnf install python-devel openssl-devel -``` - -## Windows - -With windows you MAY need to have the latest OpenSSL version. Install [OpenSSL 32-bit or 64-bit](https://slproweb.com/products/Win32OpenSSL.html). +Also be sure to join the [Masonite Slack Community](http://slack.masoniteproject.com/)! -## Mac +## Getting Started Quickly -If you do not have the latest version of OpenSSL you will encounter some installation issues with creating new applications since we need to download a zip of the application via GitHub. - -With Mac you can install OpenSSL through `brew`. - -``` -brew install openssl -``` - -Python 3.6 does not come preinstalled with certificates so you may need to install certificates with this command: +If you have a working Python 3.6+ getting started is as quick as typing +```bash +pip install masonite +start project my_project +python craft serve ``` -/Applications/Python\ 3.6/Install\ Certificates.command -``` - -You should now be good to install new Masonite application of Mac :) - -### Python 3.7 and Windows - -If you are using [Python 3.7](https://www.python.org/downloads/windows/), add it to your PATH Environment variable. - -Open Windows PowerShell and run: `pip install masonite-cli` - -Add `C:\Users\%USERNAME%\.AppData\Programs\Python\Python37\Scripts\` to PATH Environment variable. - -Note: PATH variables depend on your installation folder - -## Quick Install: - -Here is the quick and dirty of what you need to run. More step by step instructions are found below. - -``` - $ python3 -m venv venv - $ source venv/bin/activate - $ pip install masonite - $ craft new - $ craft serve -``` - -Go to `http://localhost:8000/` -**** - -

-* * * * -

- -Not all computers are made the same so you may have some trouble installing Masonite depending on your machine. If you have any issues be sure to read the [Known Installation Issues](https://docs.masoniteproject.com/prologue/known-installation-issues) Documentation. - -

-* * * * -

- -**** ## Contributing -Please read the [Contributing Documentation](https://masoniteframework.gitbook.io/docs/prologue/contributing-guide) here. Development will be on the current releasing branch of the [Core Repository](https://github.com/MasoniteFramework/core) (typically the `develop` branch) so check open issues, the current Milestone and the releases in that repository. Ask any questions you like in the issues so we can have an open discussion about the framework, design decisions and future of the project. - -## Contributors - - - - - - - - - - - - - - - - - - -
Joseph Mancuso
Joseph Mancuso

💻 🐛 💬 🤔
Vaibhav Mule
Vaibhav Mule

💻 🐛 💬 🤔
Martín Peveri
Martín Peveri

💻 🐛 💬 🤔
Tony Hammack
Tony Hammack

💻 🐛 💬 🤔
Abram C. Isola
Abram C. Isola

💻 🐛 💬 🤔
Mitch Dennett
Mitch Dennett

💻 🐛 💬 🤔
Marlysson Silva
Marlysson Silva

💻 🐛 💬 🤔
Christopher Byrd
Christopher Byrd

💻 🐛 💬 🤔
Björn Theart
Björn Theart

💻 🐛 💬 🤔
Junior Gantin
Junior Gantin

💻 🐛 💬 🤔
- - -Thank you for those who have contributed to Masonite! - - -## License - -The Masonite framework is open-sourced software licensed under the MIT license. - -## Hello World - -Getting started is very easy. Below is how you can get a simple Hello World application up and running. - -## Installation - -> Be sure to join the [Slack Channel](http://slack.masoniteproject.com) for help or guidance. - -Masonite excels at being simple to install and get going. If you are coming from previous versions of Masonite, the order of some of the installation steps have changed a bit. - -Firstly, open a terminal and head to a directory you want to create your application in. You might want to create it in a programming directory for example: - -``` -$ cd ~/programming -$ mkdir myapp -$ cd myapp -``` - -If you are on windows you can just create a directory and open the directory in the Powershell. - -## Activating Our Virtual Environment \(optional\) - -Although this step is technically optional, it is highly recommended. You can create a virtual environment if you don't want to install all of masonite's dependencies on your systems Python. If you use virtual environments then create your virtual environment by running: - -```text -$ python -m venv venv -$ source venv/bin/activate -``` - -or if you are on Windows: - -```text -$ python -m venv venv -$ ./venv/Scripts/activate -``` - -> The `python`command here is utilizing Python 3. Your machine may run Python 2 \(typically 2.7\) by default for UNIX machines. You may set an alias on your machine for Python 3 or simply run `python3`anytime you see the `python`command. - -> For example, you would run `python3 -m venv venv` instead of `python -m venv venv` - -## Installing Masonite - -Now we can install Masonite. This will give us access to a craft command we can use to finish the install steps for us: - -``` -$ pip install masonite -``` - -Once Masonite installs you will now have access to the `craft` command line tool. Craft will become your best friend during your development. You will learn to love it very quickly :). - -You can ensure Masonite and craft installed correctly by running: - -``` -$ craft -``` - -You should see a list of a few commands like `install` and `new` - -## Creating Our Project - -Great! We are now ready to create our first project. We should have the new `craft` command. We can check this by running: - -```text -$ craft -``` - -We are currently only interested in the `craft new` command. To create a new project just run: - -```text -$ craft new -``` - -This command will also run `craft install` which will install our dependencies. - -This will get the latest Masonite project template and unzip it for you. We just need to go into our new project directory and install the dependencies in our `requirements.txt` file. - - -## Additional Commands - -Now that Masonite installed fully we can check all the new commands we have available. There are many :). - -``` -$ craft -``` - -We should see many more commands now. - -## Running The Server - -After it’s done we can just run the server by using another `craft` command: - -```text -$ craft serve -``` - -Congratulations! You’ve setup your first Masonite project! Keep going to learn more about how to use Masonite to build your applications. - -{% hint style="success" %} -You can learn more about craft by reading [The Craft Command](https://github.com/MasoniteFramework/docs/tree/ba9d9f8ac3e41d58b9d92d951f92c898fb16a2a4/the-craft-command.md) documentation or continue on to learning about how to create web application by first reading the [Routing ](the-basics/routing.md)documentation -{% endhint %} - -{% hint style="info" %} -Masonite uses romantic versioning instead of semantic versioning. Because of this, all minor releases \(2.0.x\) will contain bug fixes and fully backwards compatible feature releases. Be sure to always keep your application up to date with the latest minor release to get the full benefit of Masonite's romantic versioning. -{% endhint %} - -## Hello World - -All web routes are in `routes/web.py`. In this file is already the route to the welcome controller. To start your hello world example just add something like: - -```python -Get('/hello/world', 'HelloWorldController@show'), -``` - -our routes constant file should now look something like: - -```python -ROUTES = [ - Get('/', 'WelcomeController@show'), - Get('/hello/world', 'HelloWorldController@show'), -] -``` - -**** - -

-* * * * -

- -NOTE: Notice this new interesting string syntax in our route. This will grant our route access to a controller (which we will create below) - -

-* * * * -

- -**** - -Since we used a string controller we don't have to import our controller into this file. All imports are done through Masonite on the backend. - -You'll notice that we have a reference to the `HelloWorldController` class which we do not have yet. This framework uses controllers in order to separate the application logic. Controllers can be looked at as the views.py in a Django application. The architectural standard here is 1 controller per file. - -In order to make the `HelloWorldController` we can use a `craft` command: - - $ craft controller HelloWorld - -This will scaffold the controller for you and put it in `app/http/controllers/HelloWorldController.py`. This new file will have all the imports for us. - -Inside the `HelloWorldController` we can make our `show` method like this: - -```python -def show(self, view: View): - """ Show Hello World Template """ - return view.render('helloworld') -``` - -As you see above, we are returning a `helloworld` template but we do not have that yet. All templates are in `resources/templates`. We can simply make a file called `helloworld.html` or run the `craft` command: +Contributing to Masonite is simple: - $ craft view helloworld +- Hop on [Masonite Slack Community](http://slack.masoniteproject.com/) to ask any questions you need! +- Read the [How To Contribute](https://docs.masoniteproject.com/prologue/how-to-contribute) documentation to see ways to contribute to the project. +- Read the [Contributing Guide](https://docs.masoniteproject.com/prologue/contributing-guide) to learn how to contribute to the core source code development of the project. +- [Follow Masonite Framework on Twitter](https://twitter.com/masoniteproject) to get updates about tips and tricks, announcement and releases. -Which will create the `resources/templates/helloworld.html` template for us. +## Core Maintainers -Lastly all templates run through the Jinja2 rendering engine so we can use any Jinja2 code inside our template like: +- [Joseph Mancuso](https://github.com/josephmancuso) (Author) +- [Samuel Girardin](https://github.com/girardinsamuel) +- [Marlysson Silva](https://github.com/Marlysson) -inside the `resources/views/helloworld.html` +## Sponsors -``` -{{ 'Hello World' }} -``` +To become a sponsor head to our [GitHub Sponsors page](https://github.com/sponsors/MasoniteFramework)! -Now just run the server: +## Security Vulnerabilities - $ craft serve +If you discover a security vulnerability within Masonite please read the [Security Policy](./SECURITY.md). All security vulnerabilities will be promptly addressed. -And navigate to `localhost:8000/hello/world` and you will see `Hello World` in your browser. +## License -Happy Crafting! +The Masonite framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/SECURITY.md b/SECURITY.md index 6a8084993..cb014540d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ The best approach to reporting a vulnerability with Masonite is to join the [Slack Channel](http://slack.masoniteproject.com) and messaged Joseph Mancuso and tell him about the issue. He will communicate this to his maintainers and get a patch ready and shipped ASAP. -You may also send an email to joe@masoniteproject.com +You may also send an email to joe@masoniteproject.com. ## LTS Versions diff --git a/WHITEPAPER.md b/WHITEPAPER.md new file mode 100644 index 000000000..2b058379b --- /dev/null +++ b/WHITEPAPER.md @@ -0,0 +1,333 @@ +# Masonite 4 White Paper + +This white paper is created to explain, in depth, how Masonite features, classes, and concepts should work. + +**Concepts and code snippets in this white paper are subject to change at anytime. This is a living document and explains how Masonite 4 currently works in its current state.** + +## Why This Paper Exists + +This white paper is intended to be a full explanation of how major parts of the system work. We will use this white paper to bring on new contributors or people interested in learning how Masonite works. + +We will use this document to stay on track and as a guide when creating new features for Masonite to stay consistent and efficient. + +Masonite 4 is the successor to Masonite 3. Masonite 4 is a complete, from the ground up rewrite of Masonite. The reason for this rewrite is plentiful: + +- Masonite was started in December of 2017 as a learning project for me to learn how frameworks work. It has since become a passion project for me and many others but there are still major parts of the framework that still have code from those beginning months. This code has obviously become legacy at this point and needs to be removed and rewritten. +- Masonite has gone through plenty of design changes over the course of 4 years and has relics in the codebase as such. We changed how routes work, added service providers, we changed how authentication works, we added web guards and many other changes. These were all sort of built around the same concepts and I think those decisions in the past have seriously stunted the growth of Masonite. Masonite at one point was one giant python file and only until version 2 did it even have service providers. +- When building a framework, one of the important concepts is making it expandable. This is done simply via new features but also done as packages and as a community. There really is no easy way to write a package for Masonite, theres no standard, theres some nice ways to plug it in but package development is not really there yet. This is why i think there are not many packages currently for Masonite. When first building Masonite I obviously didn't know what I know now. So now I am taking everything i learned over 4 years, plus everything i learned after a successful ORM project rewrite and building a Masonite framework i know will survive the test of time. +- Masonite ORM was developed recently and I am so proud of that library and how we built it that I want to apply those same principals to Masonite. Since Masonite codebase is so tightly coupled to everything it's hard to maintain it and build new features. Refactors are hard because it always leaves small remnants of technical debt left behind that eventually need to be cleaned up with a rewrite anyway. It's difficult sometimes to know which tests need to be fixed, which tests no longer apply and which tests need to be written. When you are dealing with nearly 1000 tests it gets time consuming to check them. They might be failing but how do we make them pass? Do we fix them? Do we fix the code? Do we delete the test? And even when all the tests pass again we are left with a mix of new code, refactored code and code thats there just to make the test pass. This type of time management and technical debt needed for new features is costly for open source projects. + +## Table Of Contents + +- Foundation +- Features + +## Foundation + +In M4, The foundation is completely redone. + +These improvements allows the directory structure to be anything we need it to be. All features are fully encapsulated and modular and Masonite does not need to be in specific directory structure order anymore. + +There is a new concept in M4 called the Application class. This class has is a mix between a new "Application" concept class and the container. So now everything is bound and made from the application class. It is also a callable so wsgi servers actually call this class to. Its very adaptable. + +The application class is an IOC container. So we can bind anything to these classes from key value strings to lists and dicts, to classes. Later we can make these values back out, swap them out with other implementations and other cool things. This keeps the entire framework extremely modular and really revolves around this IOC container. + +### Kernel Classes + +Kernel classes is really just a service provider that only registers things to the application class that is crucial to making the framework work. For example, we need to know where the config directories are, the view directories, controller directories, bind middleware, etc. These are booted before the service providers are even imported. These classes should not need to be developed on but will come with new applications and will be located inside those new applications. These can be tweaked per application. For example if you want your views directory to be located in `app/views` then you can do just that. + +## Providers + +Providers are a concept in Masonite in which they are simply wrappers around binding things to the container as well as logic that runs during a request. Everything will be bound to the container through a provider from mail and sessions features to fetching controller responses and showing the exception handling page. + +Providers will run 2 times. First when they are first added to the container. This runs a `register` method. The register method will bind things into the container. There is a second time it runs which is during the request which will run the `boot` method. + +Let's take the example of the route provider which contains both a `register` and `boot` method: + +```python +class RouteProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + # Register the routes + Route.set_controller_locations( + self.application.make("controllers.location") + ) + + def boot(self): + router = self.application.make("router") + request = self.application.make("request") + response = self.application.make("response") + + route = router.find(request.get_path(), request.get_request_method()) + + # Run before middleware + + if route: + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="before", + ) + Pipeline(request, response).through( + self.application.make("middleware").get_route_middleware(["web"]), + handler="before", + ) + + response.view(route.get_response(self.application)) + + Pipeline(request, response).through( + self.application.make("middleware").get_route_middleware(["web"]), + handler="after", + ) + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="after", + ) + else: + raise Exception(f"NO route found for {request.get_path()}") + +``` + +Notice that this provider is a `RouteProvider` so it registers routes as well as handles getting the response from the controller and attaching it to the response. + +So a provider can do many different things and its really not limited by anything. + +### Pipeline + +Pipeline classes run logic that needs to happen in a specific order and then cancel out if anything fails. This is perfect for request and response in the form of middleware. + +The concept is simple: we pass in 2 things into the pipe and if you want to continue, you return the first object, if you want to stop you return the second object. + +Here is an example of middleware doing this: + +```python +class VerifyCsrfToken(Middleware): + + exempt = [] + + def before(self, request, response): + + if not self.verify_token(request, self.get_token(request)): + return response.status(403) + + token = self.create_token(request) + + request.app.make("view").share( + { + "csrf_field": Markup( + f"" + ), + "csrf_token": token, + } + ) + + return request +``` + +So if the token doesn't pass we return the response which stops and exists the pipeline. Else we will continue down through to the controller. + +## Features + +Features of Masonite should be written in a very specific way. This way features are written in Masonite allow features to be: + +- expanded +- fixed +- tweaked +- provides the maximum effeciency for maintenance +- standardized features so anybody can improve features with a common understanding. + +This is a guide on how Masonite features are developed but also apply to packages as well. Packaging will be in another white paper which will be linked here: Link TBD. + +There are several moving peices to each feature. I'll explain them briefly here and then will go into detail. + +- A manager style class. This is a class that will likely be the front facing class that people use in controllers. This class will have all the drivers registered to it and be responsible for handling switching drivers, wrapping some logic, the front facing API. This is also the class that will be type hinting and "made" from the container. + - (see src/masonite/mail/Mail.py) +- driver class(es). Could be 1 or more + - (see src/masonite/drivers/mail/MailgunDriver.py) +- A service provider to register to the framework + - (see src/masonite/providers/MailProvider.py). +- Components classes when applicable. These are helper classes that wrap logic. In the ORM, think of those expression classes that wrap some logic like if a query is Raw. These classes are small encapsulated peices of functionality designed to write cleaner code in other parts of the system. Because then i would just need to do something like. So component classes help to write clean code somewhere else. very Handy. + - (see src/masonite/mail/MessageAttachment.py & src/masonite/drivers/mail/Recipient.py) +- Bindings should also be extremely simple. for mail it should be `application.make('mail')`. For sessions it should be `application.make('session')`, etc etc. +- Registers drivers to the manager in the same exact way. + (see https://github.com/MasoniteFramework/masonite4/pull/28/files#diff-221cd9f78ee5571e49f930cfd66a2229a784701de1076f132c379c81794e0ff1R18-R20) + +**I'll be using the example of building a mail feature to demonstrate how each part works together.** + +### Managers + +There are 3 parts to a manager class: + +- The manager class itself +- Drivers. +- Optional component classes. + +Managers are wrappers around your feature. Its a single entry point for your app. This is typically the class you will be type hinting. If your API looks like this: + +```python +def show(self, mail: Mail): + mail.mailable(Welcome()).send(driver="smtp") +``` + +Then this manager will have both the `mailable` and `send` methods. This is the front facing class. + +The manager is called a manager class because it manages smaller classes. These smaller classes are called drivers. + +An example manager looks something like this: + +```python +class Mail: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config[self.driver_config.get("default")] + + return self.driver_config.get(driver, {}) + + def mailable(self, mailable): + self.options = mailable.set_application(self.application).build().get_options() + return self + + def send(self, driver=None): + self.options.update(self.get_config_options(driver)) + return self.get_driver(driver).set_options(self.options).send() +``` + +The first 4 methods are really the manager boiler plates and the last 2 are the front facing methods needed to make this specific mail feature work. + +### Drivers + +Driver classes are small classes that do 1 thing and 1 thing only: do the driver logic. If this is an SMTP mail driver then the driver will be responsible for sending an email using SMTP. These driver classes **should not be responsible for anything else**. It should not be responsible for building an actual email, calling a view class to render a template, switching drivers to another driver, nothing. It should do nothing but send an email using SMTP from some kind of data structure like a dictionary of options. + +Here is an example of a driver class: + +```python +import requests +from .Recipient import Recipient + + +class MailgunDriver: + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_mime_message(self): + data = { + "from": self.options.get("from"), + "to": Recipient(self.options.get("to")).header(), + "subject": self.options.get("subject"), + "h:Reply-To": self.options.get("reply_to"), + "html": self.options.get("html_content"), + "text": self.options.get("text_content"), + } + + if self.options.get("cc"): + data.update({"cc", self.options.get("cc")}) + if self.options.get("bcc"): + data.update({"bcc", self.options.get("bcc")}) + + return data + + def get_attachments(self): + files = [] + for attachment in self.options.get("attachments", []): + files.append(("attachment", open(attachment.path, "rb"))) + + return files + + def send(self): + domain = self.options["domain"] + secret = self.options["secret"] + attachments = self.get_attachments() + + return requests.post( + f"https://api.mailgun.net/v3/{domain}/messages", + auth=("api", secret), + data=self.get_mime_message(), + files=attachments, + ) + +``` + +Notice that theres nothing in the class above that isn't related to sending an email with mailgun. + +### Component Classes + +Component classes are another part of this relationship. A component is also a class that does 1 thing and 1 thing only. This class should have a small peice of logic **that is designed to clean up other parts of the code later down the line**. For example, if we want to compile an email from `joe@masoniteproject.com, idmann509@gmail.com` to `, ` we could loop through the emails and append them inside the SMTP driver, right? + +This would look kind of messy and with some psuedo code would look like this: + +```python +# Inside the SMTP mail driver +to_emails = [] +for email in emails.split(','): + to_emails.append(f"<{email.trim()}>") +``` + +This is a problem for a few reasons. + +**Problem number 1** is that we now can no longer add cool features to this part of the feature. For example maybe now we want to support something like `Joseph Mancuso idmann509@gmail.com` and compile it down to `Joseph Mancuso `. We can't really do this and if we do we have to add the logic in each mail driver to support this. It also needs to be tested in each mail driver. + +**Problem Number 2** is we need to do this for each email address. In a normal email we have things like to, from, cc and bcc. so you can see can _can_ make it work but it will get quite messy. Especially if we have to support more features down the road. + +```python +# Inside the SMTP mail driver +to_emails = [] +for email in emails.split(','): + to_emails.append(f"<{email.trim()}>") + +cc_emails = [] +for email in cc_emails.split(','): + cc_emails.append(f"<{email.trim()}>") +``` + +You can see how this can get messy. + +**Solution**: better way to do this is to create a component class to do this for us. Final usage of this component class looks something like: + +```python +# Inside the SMTP mail driver +to_emails = Recipient(emails).header() +cc_emails = Recipient(cc_emails).header() +``` + +The same rules apply to an email attachment. + +So these component classes should be used where applicable so we can add features at a central location and it be propogated throughout other drivers. They should also do 1 thing and 1 thing only to not break anything with side effects + +### Registering Drivers + +Registering drivers are simple too. All drivers need to be registered and can be done in the providers. It should simply pass into an `add_driver()` method and look like this: + +```python + def register(self): + mail = Mail(self.application).set_configuration(config("mail.drivers")) + mail.add_driver("smtp", SMTPDriver(self.application)) + mail.add_driver("mailgun", MailgunDriver(self.application)) + mail.add_driver("terminal", TerminalDriver(self.application)) + self.application.bind("mail", mail) +``` + +Notice we just create the manager, bind the drivers to the manager and then bind the manager to the container. diff --git a/app/User.py b/app/User.py deleted file mode 100644 index b89f8a3ed..000000000 --- a/app/User.py +++ /dev/null @@ -1,11 +0,0 @@ -"""User Model.""" - -from masoniteorm.models import Model - - -class User(Model): - """User Model.""" - - __fillable__ = ['name', 'email', 'password'] - - __auth__ = 'email' diff --git a/app/http/controllers/ConfirmController.py b/app/http/controllers/ConfirmController.py deleted file mode 100644 index 6b7eadb34..000000000 --- a/app/http/controllers/ConfirmController.py +++ /dev/null @@ -1,68 +0,0 @@ -"""The ConfirmController Module.""" -import datetime - -from src.masonite.auth import Auth, MustVerifyEmail -from src.masonite.auth.Sign import Sign -from src.masonite.managers import MailManager -from src.masonite.request import Request -from src.masonite.view import View -from src.masonite.helpers import config - - -class ConfirmController: - """The ConfirmController class.""" - - def __init__(self): - """The ConfirmController Constructor.""" - pass - - def verify_show(self, view: View, auth: Auth): - """Show the Verify Email page for unverified users. - - Arguments: - request {masonite.request.request} -- The Masonite request class. - request {masonite.view.view} -- The Masonite view class. - request {masonite.auth.auth} -- The Masonite Auth class. - - Returns: - [type] -- [description] - """ - return view.render('auth/verify', {'app': config('application'), 'Auth': auth}) - - def confirm_email(self, request: Request, view: View, auth: Auth): - """Confirm User email and show the correct response. - - Arguments: - request {masonite.request.request} -- The Masonite request class. - request {masonite.view.view} -- The Masonite view class. - request {masonite.auth.auth} -- The Masonite Auth class. - - Returns: - [type] -- [description] - """ - sign = Sign() - token = sign.unsign(request.param('id')) - if token is not None: - tokenParts = token.split("::") - if len(tokenParts) > 1: - user = auth.auth_model.find(tokenParts[0]) - if user.verified_at or user.verified_at is None: - timestamp = datetime.datetime.fromtimestamp(float(tokenParts[1])) - now = datetime.datetime.now() - timestamp_plus_10 = timestamp + datetime.timedelta(minutes=10) - - if now < timestamp_plus_10: - user.verified_at = datetime.datetime.now() - user.save() - - return view.render('auth/confirm', {'app': config('application'), 'Auth': auth}) - - return view.render('auth/error', {'app': config('application'), 'Auth': auth}) - - def send_verify_email(self, manager: MailManager, request: Request): - user = request.user() - - if isinstance(user, MustVerifyEmail): - user.verify_email(manager, request) - - return request.redirect('/home') diff --git a/app/http/controllers/ControllerTest.py b/app/http/controllers/ControllerTest.py deleted file mode 100644 index b1176f7d3..000000000 --- a/app/http/controllers/ControllerTest.py +++ /dev/null @@ -1,32 +0,0 @@ -from src.masonite.request import Request -from src.masonite.view import View - - -class ControllerTest: - - def __init__(self, request: Request): - self.request = request - - def show(self): - return self.request - - def test(self): - return 'test' - - def returns_a_view(self, view: View): - return view.render('index') - - def returns_a_dict(self): - return {'id': 1} - - def param(self): - return self.request.param('id') - - def get_param(self, first): - self.request.first = first - return self.request - - def get_param_and_object(self, first, view: View): - self.request.first = first - self.request.view = view - return self.request diff --git a/app/http/controllers/TestController.py b/app/http/controllers/TestController.py deleted file mode 100644 index bd3fdd8d1..000000000 --- a/app/http/controllers/TestController.py +++ /dev/null @@ -1,59 +0,0 @@ -from app.jobs.TestJob import TestJob -from src.masonite import Queue, Mail -from src.masonite.request import Request -from src.masonite.response import Response -from src.masonite.view import View - -class TestController: - - def __init__(self): - self.test = True - - def show(self): - return 'show' - - def v(self, view: View): - return view.render('test') - - def change_header(self, response: Response): - response.header('Content-Type', 'application/xml') - return 'test' - - def change_status(self, response: Response): - response.status(203) - return 'test' - - def change_404(self, response: Response): - response.status(404) - return 'test' - - def testing(self): - return 'test' - - def json_response(self): - return {'id': 2} - - def post_test(self): - return 'post_test' - - def json(self): - return 'success' - - def bad(self): - return 5 / 0 - - def keyerror(self): - x = {'hello': 'world'} - return x['test'] - - def session(self, request: Request): - request.session.set('test', 'value') - return 'session set' - - def queue(self, queue: Queue): - # queue.driver('amqp').push(self.bad) - queue.driver('amqp').push(TestJob, channel='default') - return 'queued' - - def mail(self, mail: Mail): - return mail.to('idmann509@gmail.com').template('test', {'test': 'mail'}) diff --git a/app/http/controllers/UnitTestController.py b/app/http/controllers/UnitTestController.py deleted file mode 100644 index 1d81bd0e4..000000000 --- a/app/http/controllers/UnitTestController.py +++ /dev/null @@ -1,63 +0,0 @@ -"""A UnitTestController Module.""" - -from src.masonite.request import Request -from src.masonite.controllers import Controller -from src.masonite.view import View - - -class UnitTestController(Controller): - """UnitTestController Controller Class.""" - - def __init__(self, request: Request): - """UnitTestController Initializer - - Arguments: - request {masonite.request.Request} -- The Masonite Request class. - """ - self.request = request - - def show(self): - return 'got' - - def store(self): - return 'posted' - - def params(self): - return self.request.input('test') - - def get_params(self): - return self.request.input('test') - - def user(self): - return self.request.user().name - - def json(self): - return self.request.input('test') - - def response(self): - return { - 'count': 5, - 'iterable': [1, 2, 3] - } - - def multi(self): - return { - 'author': { - 'name': 'Joe' - } - } - - def multi_count(self): - return {"count": 5, "iterable": [1, 2, 3]} - - def patch(self): - return self.request.input('test') - - def param(self): - return self.request.param('post_id') - - def view(self, view: View): - return view.render("test", {"count": 1, "users": ["John", "Joe"]}) - - def redirect_view(self): - return self.request.redirect_to("v") diff --git a/app/http/controllers/UserResourceController.py b/app/http/controllers/UserResourceController.py deleted file mode 100644 index 444106355..000000000 --- a/app/http/controllers/UserResourceController.py +++ /dev/null @@ -1,61 +0,0 @@ -""" A UserResourceController Module """ - -from src.masonite.controllers import Controller - - -class UserResourceController(Controller): - """Class Docstring Description - """ - - def show(self): - """Show a single resource listing - ex. Model.find('id') - Get().route("/show", UserResourceController) - """ - - return '' - - def index(self): - """Show several resource listings - ex. Model.all() - Get().route("/index", UserResourceController) - """ - - return '' - - def create(self): - """Show form to create new resource listings - ex. Get().route("/create", UserResourceController) - """ - - return '' - - def store(self): - """Create a new resource listing - ex. Post target to create new Model - Post().route("/store", UserResourceController) - """ - - return '' - - def edit(self): - """Show form to edit an existing resource listing - ex. Get().route("/edit", UserResourceController) - """ - - return '' - - def update(self): - """Edit an existing resource listing - ex. Post target to update new Model - Post().route("/update", UserResourceController) - """ - - return '' - - def destroy(self): - """Delete an existing resource listing - ex. Delete().route("/destroy", UserResourceController) - """ - - return '' \ No newline at end of file diff --git a/app/http/controllers/WelcomeController.py b/app/http/controllers/WelcomeController.py deleted file mode 100644 index c98aaae2a..000000000 --- a/app/http/controllers/WelcomeController.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Welcome The User To Masonite.""" - -from src.masonite.view import View -from src.masonite.controllers import Controller -from app.jobs.TestJob import TestJob -from src.masonite import Queue - - -class WelcomeController(Controller): - """Controller For Welcoming The User.""" - - def show(self, view: View, queue: Queue): - """Show the welcome page. - - Arguments: - view {masonite.view.View} -- The Masonite view class. - request {masonite.request.Request} -- The Masonite request class. - - Returns: - masonite.view.View -- The Masonite view class. - """ - queue.push(TestJob) - return view.render('welcome') diff --git a/app/http/controllers/subdirectory/SubController.py b/app/http/controllers/subdirectory/SubController.py deleted file mode 100644 index f2ba4d2b6..000000000 --- a/app/http/controllers/subdirectory/SubController.py +++ /dev/null @@ -1,5 +0,0 @@ - -class SubController: - - def show(self): - return 'test' diff --git a/app/http/controllers/subdirectory/deep/DeepController.py b/app/http/controllers/subdirectory/deep/DeepController.py deleted file mode 100644 index f86a5cec0..000000000 --- a/app/http/controllers/subdirectory/deep/DeepController.py +++ /dev/null @@ -1,5 +0,0 @@ - -class DeepController: - - def show(self): - return 'test' diff --git a/app/http/middleware/AddAttributeMiddleware.py b/app/http/middleware/AddAttributeMiddleware.py deleted file mode 100644 index 5893e6e69..000000000 --- a/app/http/middleware/AddAttributeMiddleware.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Add Attribute Middleware.""" - -from src.masonite.request import Request - - -class AddAttributeMiddleware: - """Middleware class which loads the current user into the request.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container.""" - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - self.request.attribute = True - - def after(self): - """Run This Middleware After The Route Executes.""" - pass diff --git a/app/http/middleware/AuthenticationMiddleware.py b/app/http/middleware/AuthenticationMiddleware.py deleted file mode 100644 index d6c38eeb6..000000000 --- a/app/http/middleware/AuthenticationMiddleware.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Authentication Middleware.""" - -from src.masonite.request import Request - - -class AuthenticationMiddleware: - """Middleware To Check If The User Is Logged In.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - """ - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - if not self.request.user(): - self.request.redirect_to('login') - - def after(self): - """Run This Middleware After The Route Executes.""" - pass diff --git a/app/http/middleware/CsrfMiddleware.py b/app/http/middleware/CsrfMiddleware.py deleted file mode 100644 index 439bae5dc..000000000 --- a/app/http/middleware/CsrfMiddleware.py +++ /dev/null @@ -1,18 +0,0 @@ -"""CSRF Middleware.""" - -from src.masonite.middleware import CsrfMiddleware as Middleware - - -class CsrfMiddleware(Middleware): - """Verify CSRF Token Middleware.""" - - """Which routes should be exempt from CSRF protection.""" - exempt = [ - # - ] - - """Whether or not the CSRF token should be changed on every request.""" - every_request = False - - """The length of the token to generate.""" - token_length = 30 diff --git a/app/http/middleware/LoadUserMiddleware.py b/app/http/middleware/LoadUserMiddleware.py deleted file mode 100644 index 4ca1c1368..000000000 --- a/app/http/middleware/LoadUserMiddleware.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Load User Middleware.""" - -from src.masonite.auth import Auth -from src.masonite.request import Request - - -class LoadUserMiddleware: - """Middleware class which loads the current user into the request.""" - - def __init__(self, request: Request, auth: Auth): - """Inject Any Dependencies From The Service Container. - - Arguments: - request {masonite.request.Request} -- The Masonite request object. - auth {masonite.auth.Auth} -- The Masonite authentication object. - """ - self.request = request - self.auth = auth - - def before(self): - """Run This Middleware Before The Route Executes.""" - self.load_user() - return self.request - - def after(self): - """Run This Middleware After The Route Executes.""" - pass - - def load_user(self): - """Load user into the request. - - Arguments: - request {masonite.request.Request} -- The Masonite request object. - """ - self.request.set_user(self.auth.user()) diff --git a/app/http/middleware/MiddlewareTest.py b/app/http/middleware/MiddlewareTest.py deleted file mode 100644 index f36c55c8e..000000000 --- a/app/http/middleware/MiddlewareTest.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Middleware Test.""" - -from src.masonite.request import Request - - -class MiddlewareTest: - """Middleware class which loads the current user into the request.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container.""" - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - self.request.path = 'test/middleware/before/ran' - - def after(self): - """Run This Middleware After The Route Executes.""" - pass diff --git a/app/http/middleware/TestHttpMiddleware.py b/app/http/middleware/TestHttpMiddleware.py deleted file mode 100644 index 168fcda76..000000000 --- a/app/http/middleware/TestHttpMiddleware.py +++ /dev/null @@ -1,13 +0,0 @@ -from src.masonite.request import Request - - -class TestHttpMiddleware: - """Test Middleware.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container.""" - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - self.request.environ['HTTP_TEST'] = 'test' diff --git a/app/http/middleware/TestMiddleware.py b/app/http/middleware/TestMiddleware.py deleted file mode 100644 index e8ef6efdf..000000000 --- a/app/http/middleware/TestMiddleware.py +++ /dev/null @@ -1,13 +0,0 @@ -from src.masonite.request import Request - - -class TestMiddleware: - """Test Middleware""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container.""" - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - self.request.path = '/test/middleware' diff --git a/app/http/middleware/VerifyEmailMiddleware.py b/app/http/middleware/VerifyEmailMiddleware.py deleted file mode 100644 index 33dde5517..000000000 --- a/app/http/middleware/VerifyEmailMiddleware.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Verify Email Middleware.""" - -from src.masonite.request import Request - - -class VerifyEmailMiddleware: - """Middleware To Check If The User Has Verified Their Email.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - """ - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - user = self.request.user() - - if user and user.verified_at is None: - self.request.redirect('/email/verify') - - def after(self): - """Run This Middleware After The Route Executes.""" - pass diff --git a/app/http/test_controllers/Request.py b/app/http/test_controllers/Request.py deleted file mode 100644 index e45b52bcb..000000000 --- a/app/http/test_controllers/Request.py +++ /dev/null @@ -1,2 +0,0 @@ -class Request: - pass diff --git a/app/http/test_controllers/TestController.py b/app/http/test_controllers/TestController.py deleted file mode 100644 index bdd408e09..000000000 --- a/app/http/test_controllers/TestController.py +++ /dev/null @@ -1,6 +0,0 @@ -class TestController: - - test = True - - def show(self): - pass diff --git a/app/jobs/TestJob.py b/app/jobs/TestJob.py deleted file mode 100644 index 1c8ef8947..000000000 --- a/app/jobs/TestJob.py +++ /dev/null @@ -1,21 +0,0 @@ -""" A TestJob Queue Job """ - -from src.masonite.queues import Queueable - - -class TestJob(Queueable): - """A TestJob Job - """ - - def __init__(self): - """A TestJob Constructor - """ - - pass - - def handle(self): - """Logic to handle the job.""" - return 2 / 0 - - def failed(self, payload, error): - print('running a failed job hook') diff --git a/config/application.py b/config/application.py deleted file mode 100644 index f17c7a073..000000000 --- a/config/application.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Application Settings.""" - -import os - -from src.masonite import env - -"""Application Name -This value is the name of your application. This value is used when the -framework needs to place the application's name in a notification or -any other location as required by the application or its packages. -""" - -NAME = env('APP_NAME', 'Masonite 2.1') - -"""Application Debug Mode -When your application is in debug mode, detailed error messages with -stack traces will be shown on every error that occurs within your -application. If disabled, a simple generic error page is shown -""" - -DEBUG = env('APP_DEBUG', True) - -"""Secret Key -This key is used to encrypt and decrypt various values. Out of the box -Masonite uses this key to encrypt or decrypt cookies so you can use -it to encrypt and decrypt various values using the Masonite Sign -class. Read the documentation on Encryption to find out how. -""" - -KEY = env('KEY', 'mQhQtBZP-WmaJl5FpW2dC0vpnYs2ms1u5AIVqDM8s6w=') - -"""Application URL -Sets the root URL of the application. This is primarily used for testing -""" - -URL = env('APP_URL', 'http://localhost:8000') - -"""Base Directory -Sets the root path of your project -""" - -BASE_DIRECTORY = os.getcwd() - -"""Static Root -Set the static root of your application that you wil use to store assets -""" - -STATIC_ROOT = os.path.join(BASE_DIRECTORY, 'storage') - -"""Autoload Directories -List of directories that are used to find classes and autoload them into -the Service Container. This is initially used to find models and load -them in but feel free to autoload any directories -""" - -AUTOLOAD = [ - 'app', -] - -FALSY = False - -TEMPLATES={ - 'statuscode': '/src/masonite/snippets/statuscode', - 'exceptions': '/src/masonite/snippets/exception' -} diff --git a/config/auth.py b/config/auth.py deleted file mode 100644 index bb1440ac0..000000000 --- a/config/auth.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Authentication Settings.""" - -from app.User import User - -"""Authentication Model -Put the model here that will be used to authenticate users to your site. -Currently the model must contain a password field. In the model should -be an auth_column = 'column' in the Meta class. This column will be -used to verify credentials in the Auth facade or any other auth -classes. The auth_column will be used to change auth things -like 'email' to 'user' to easily switch which column will -be authenticated. - -@see masonite.auth.Auth -""" - -AUTH = { - 'defaults': { - 'guard': 'web' - }, - 'guards': { - 'web': { - 'driver': 'cookie', - 'model': User, - 'drivers': { # 'cookie', 'jwt' - 'jwt': { - 'reauthentication': True, - 'lifetime': '5 minutes' - } - } - }, - } -} - -DRIVERS = { - -} diff --git a/config/broadcast.py b/config/broadcast.py deleted file mode 100644 index 99925d835..000000000 --- a/config/broadcast.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Broadcast Settings.""" - -from src.masonite import env - -"""Broadcast Driver -Realtime support is critical for any modern web application. Broadcast -drivers allow you to push data from your server to all your clients -to show data updates to your clients in real time without having -to constantly refresh the page or send constant ajax requests - -Supported: 'pusher', 'ably', 'pubnub' -""" - -DRIVER = env('BROADCAST_DRIVER', 'pusher') - -"""Broadcast Drivers -Below is a dictionary of all your driver configurations. Each key in the -dictionary should be the name of a driver. -""" - -DRIVERS = { - 'pusher': { - 'app_id': env('PUSHER_APP_ID', '29382xx..'), - 'client': env('PUSHER_CLIENT', 'shS8dxx..'), - 'secret': env('PUSHER_SECRET', 'HDGdjss..'), - 'cluster': env('PUSHER_CLUSTER', 'eu'), - }, - 'ably': { - 'secret': env('ABLY_SECRET', 'api:key') - }, - 'pubnub': { - 'secret': env('PUBNUB_SECRET', ''), - 'publish_key': env('PUBNUB_PUBLISH_KEY', ''), - 'subscribe_key': env('PUBNUB_SUBSCRIBE_KEY', '') - } -} diff --git a/config/cache.py b/config/cache.py deleted file mode 100644 index 589e04edf..000000000 --- a/config/cache.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Cache Settings.""" - -from src.masonite import env - -"""Cache Driver -Caching is a great way to gain an instant speed boost to your application. -Very often templates will not change and you can utilize caching to the -best by caching your templates forever, monthly or every few seconds - -Supported: 'disk' -""" - -DRIVER = env('CACHE_DRIVER', 'disk') - -"""Cache Drivers -Place all your caching coniguration as a dictionary here. The keys here -should correspond to the driver types supported above. - -Supported: 'disk' -""" - -DRIVERS = { - 'disk': { - 'location': 'bootstrap/cache' - } -} diff --git a/config/database.py b/config/database.py deleted file mode 100644 index 1bc7f5b4c..000000000 --- a/config/database.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Database Settings """ -from masonite import env -from masoniteorm.query import QueryBuilder -from masoniteorm.connections import ConnectionResolver -from masonite.environment import LoadEnvironment - - -LoadEnvironment() - -""" -|-------------------------------------------------------------------------- -| Databases connectors details -|-------------------------------------------------------------------------- -| -| Setup details of the database connectors you want to use. -| -""" - -DATABASES = { - 'default': env('DB_CONNECTION', 'sqlite'), - 'sqlite': { - 'driver': 'sqlite', - 'database': env('SQLITE_DB_DATABASE', 'masonite.sqlite3'), - 'prefix': '' - }, - "mysql": { - "driver": "mysql", - "host": env('DB_HOST'), - "user": env("DB_USERNAME"), - "password": env("DB_PASSWORD"), - "database": env("DB_DATABASE"), - "port": env('DB_PORT'), - "prefix": "", - "grammar": "mysql", - "options": { - "charset": "utf8mb4", - }, - }, - "postgres": { - "driver": "postgres", - "host": env('DB_HOST'), - "user": env("DB_USERNAME"), - "password": env("DB_PASSWORD"), - "database": env("DB_DATABASE"), - "port": env('DB_PORT'), - "prefix": "", - "grammar": "postgres", - }, - 'mssql': { - 'driver': 'mssql', - 'host': env('MSSQL_DATABASE_HOST'), - 'user': env('MSSQL_DATABASE_USER'), - 'password': env('MSSQL_DATABASE_PASSWORD'), - 'database': env('MSSQL_DATABASE_DATABASE'), - 'port': env('MSSQL_DATABASE_PORT'), - 'prefix': '' - }, -} - -DB = ConnectionResolver().set_connection_details(DATABASES) diff --git a/config/factories.py b/config/factories.py deleted file mode 100644 index b60b33357..000000000 --- a/config/factories.py +++ /dev/null @@ -1,15 +0,0 @@ -from masoniteorm.factories import Factory -from app.User import User - -factory = Factory - - -def users_factory(faker): - return { - 'name': faker.name(), - 'email': faker.email(), - 'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu', # == 'secret' - } - - -factory.register(User, users_factory) diff --git a/config/mail.py b/config/mail.py deleted file mode 100644 index 376e811ea..000000000 --- a/config/mail.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Mail Settings.""" - -from src.masonite import env - -"""From Address -This value will be used for the default address when sending emails from -your application. -""" - -FROM = { - 'address': env('MAIL_FROM_ADDRESS', 'hello@example.com'), - 'name': env('MAIL_FROM_NAME', 'Masonite') -} - -"""Mail Driver -The default driver you will like to use for sending emails. You may add -additional drivers as you need or pip install additional drivers. - -Supported: 'smtp', 'mailgun' -""" - -DRIVER = env('MAIL_DRIVER', 'smtp') - -"""Mail Drivers -Different drivers you can use for sending email. -""" - -DRIVERS = { - 'smtp': { - 'host': env('MAIL_HOST', 'smtp.mailtrap.io'), - 'port': env('MAIL_PORT', '465'), - 'username': env('MAIL_USERNAME', 'username'), - 'password': env('MAIL_PASSWORD', 'password'), - 'ssl': env('MAIL_SSL', False), - 'login': env('MAIL_LOGIN_REQUIRED', True), - 'tls': env('MAIL_TLS', False) - }, - 'mailgun': { - 'secret': env('MAILGUN_SECRET', 'key-XX'), - 'domain': env('MAILGUN_DOMAIN', 'sandboxXX.mailgun.org') - } -} diff --git a/config/middleware.py b/config/middleware.py deleted file mode 100644 index 125c6b3cc..000000000 --- a/config/middleware.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Middleware Configuration Settings.""" - -from src.masonite.middleware import (CorsMiddleware, ResponseMiddleware, - SecureHeadersMiddleware, - MaintenanceModeMiddleware) - -from app.http.middleware.AddAttributeMiddleware import AddAttributeMiddleware -from app.http.middleware.AuthenticationMiddleware import AuthenticationMiddleware -from app.http.middleware.CsrfMiddleware import CsrfMiddleware -from app.http.middleware.LoadUserMiddleware import LoadUserMiddleware -from app.http.middleware.MiddlewareTest import MiddlewareTest -from app.http.middleware.VerifyEmailMiddleware import VerifyEmailMiddleware - - -"""HTTP Middleware -HTTP middleware is middleware that will be ran on every request. Middleware -is only ran when a HTTP call is successful (a 200 response). This list -should contain a simple aggregate of middleware classes. -""" - -HTTP_MIDDLEWARE = [ - CsrfMiddleware, - LoadUserMiddleware, - MaintenanceModeMiddleware, - ResponseMiddleware, - SecureHeadersMiddleware, -] - -"""Route Middleware -Specify a dictionary of middleware to be used on a per route basis here. The key will -be the alias to use on routes and the value can be any middleware class or a list -of middleware (middleware stacks). -""" - -ROUTE_MIDDLEWARE = { - 'auth': AuthenticationMiddleware, - 'cors': CorsMiddleware, - 'middleware.test': [ - MiddlewareTest, - AddAttributeMiddleware, - ], - 'test': MiddlewareTest, - 'verified': VerifyEmailMiddleware, -} - -"""Secure Headers to use in masonite.middlware.SecureHeadersMiddleware""" - -SECURE_HEADERS = { - 'Strict-Transport-Security': 'max-age=63072000; includeSubdomains', - 'X-Frame-Options': 'SAMEORIGIN', - 'X-XSS-Protection': '1; mode=block', - 'X-Content-Type-Options': 'sniff-test', - 'Referrer-Policy': 'no-referrer, strict-origin-when-cross-origin', - 'Cache-control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', -} - -CORS = { - 'Access-Control-Allow-Origin': "*", - "Access-Control-Allow-Methods": "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT", - "Access-Control-Allow-Headers": "Content-Type, Accept, X-Requested-With", - "Access-Control-Max-Age": "3600", - "Access-Control-Allow-Credentials": "true" -} diff --git a/config/packages.py b/config/packages.py deleted file mode 100644 index 1b28feb0e..000000000 --- a/config/packages.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Packages Configuration Settings.""" - -"""Site Packages -Although not used often, you may have to add several additional package -directories while building third party packages. Put the path of the -package here and Masonite will pick it up. - ----------- -@example -SITE_PACKAGES = [ - 'venv/lib/python3.6/site-packages' -] ----------- -""" - -SITE_PACKAGES = [ - # -] diff --git a/config/providers.py b/config/providers.py deleted file mode 100644 index 1bcccf66e..000000000 --- a/config/providers.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Providers Configuration File.""" - -from src.masonite.providers import (AppProvider, AuthenticationProvider, BroadcastProvider, CacheProvider, - CsrfProvider, HelpersProvider, MailProvider, - QueueProvider, RouteProvider, - SessionProvider, StatusCodeProvider, - UploadProvider, ViewProvider, - WhitenoiseProvider) - -from masoniteorm.providers.ORMProvider import ORMProvider - -"""Providers List -Providers are a simple way to remove or add functionality for Masonite -The providers in this list are either ran on server start or when a -request is made depending on the provider. Take some time to can -learn more more about Service Providers in our documentation -""" - -PROVIDERS = [ - # Framework Providers - AppProvider, - CsrfProvider, - SessionProvider, - RouteProvider, - StatusCodeProvider, - WhitenoiseProvider, - ViewProvider, - AuthenticationProvider, - ORMProvider, - - # Optional Framework Providers - MailProvider, - UploadProvider, - QueueProvider, - CacheProvider, - BroadcastProvider, - CacheProvider, - HelpersProvider, - - # Third Party Providers - - # Application Providers - -] diff --git a/config/queue.py b/config/queue.py deleted file mode 100644 index fcd586948..000000000 --- a/config/queue.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Queue Settings.""" - -from src.masonite import env - -"""Queue Driver -Queues are an excellent way to send intensive and time consuming tasks -into the background to improve performance of your application. - -Supported: 'async', 'amqp' -""" - -DRIVER = env('QUEUE_DRIVER', 'async') - -"""Queue Drivers -Put any configuration settings for your drivers in this configuration setting. -""" - -DRIVERS = { - 'async': { - 'mode': 'threading' - }, - 'amqp': { - 'username': env('QUEUE_USERNAME', 'guest'), - 'vhost': env('QUEUE_VHOST', ''), - 'password': env('QUEUE_PASSWORD', 'guest'), - 'host': env('QUEUE_HOST', 'localhost'), - 'port': env('QUEUE_PORT', '5672'), - 'channel': env('QUEUE_CHANNEL', 'default'), - } -} diff --git a/config/session.py b/config/session.py deleted file mode 100644 index e185c90bc..000000000 --- a/config/session.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Session Settings.""" - -from src.masonite import env - -"""Session Driver -Sessions are able to be linked to an individual user and carry data from -request to request. The memory driver will store all the session data -inside memory which will delete when the server stops running. - -Supported: 'memory', 'cookie' -""" - -DRIVER = env('SESSION_DRIVER', 'cookie') - -DRIVERS = { - 'cookie': { - 'flash_expires': '2 seconds' - } -} diff --git a/config/storage.py b/config/storage.py deleted file mode 100644 index 8d30ec820..000000000 --- a/config/storage.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Storage Settings.""" - -from src.masonite import env - -"""Storage Driver -The default driver you will like to use for storing uploads. You may add -additional drivers as you need or pip install additional drivers. - -Supported: 'disk', 's3', 'rackspace', 'googlecloud', 'azure' -""" - -DRIVER = env('STORAGE_DRIVER', 'disk') - -"""Storage Drivers -Different drivers you can use for storing file uploads. -""" - -DRIVERS = { - 'disk': { - 'location': { - 'uploading': 'uploads/' - } - }, - 's3': { - 'client': env('S3_CLIENT', 'AxJz...'), - 'secret': env('S3_SECRET', 'HkZj...'), - 'bucket': env('S3_BUCKET', 's3bucket'), - 'location': 'http://s3.amazon.com/bucket', - 'test_locations': { - 'test': 'value' - } - }, - 'rackspace': { - 'username': env('RACKSPACE_USERNAME', 'username'), - 'secret': env('RACKSPACE_SECRET', '3cd5b0e8...'), - 'container': env('RACKSPACE_CONTAINER', 'masonite'), - 'region': env('RACKSPACE_REGION', 'IAD'), - 'location': 'http://03c8...rackcdn.com/' - }, - 'azure': { - 'name': env('AZURE_NAME', 'masonite'), - 'secret': env('AZURE_SECRET', 'RykG8qsa4kTOddF=='), - 'connection': env( - 'AZURE_CONNECTION', - 'DefaultEndpointsProtocol=https;AccountName=...' - ), - 'container': env('AZURE_CONTAINER', 'masonite'), - 'location': 'https://masonite.blob.core.windows.net/container/' - }, -} - - -"""Static Files -Put anywhere you keep your static assets in a key, value dictionary here -The key will be the folder you put your assets in relative to the root -and the value will be the alias you wish to have in your templates. -You may have multiple aliases of the same name - -Example will be the static assets folder at /storage/static -and an alias of -""" - -STATICFILES = { - # folder # template alias - 'storage/static': 'static/', - 'storage/compiled': 'static/', - 'storage/uploads': 'static/', - 'storage/public': '/', -} - -"""SASS Settings -These settings is what Masonite will use to compile SASS into CSS. - -importFrom should contain a list of all folders where your main SASS -files live. Masonite will search in this folder for any .scss files -that do not start with an underscore and compile them. - -includePaths should contain a list of directories of any .scss files -that you plan to @import. - -compileTo should contain a string with the directory you want your sass -compiled to. -""" - -SASSFILES = { - 'importFrom': [ - 'storage/static' - ], - 'includePaths': [ - 'storage/static/sass' - ], - 'compileTo': 'storage/compiled' -} diff --git a/craft b/craft index a5b036bfe..5db753dde 100644 --- a/craft +++ b/craft @@ -5,16 +5,7 @@ This can be used by running "python craft". This module is not ran when the CLI successfully import commands for you. """ -from cleo import Application -from src.masonite import __version__ - -from wsgi import container - -application = Application('Masonite Version:', __version__) - -for key, value in container.providers.items(): - if isinstance(key, str) and key.endswith('Command'): - application.add(container.make('{0}'.format(key))) +from wsgi import application if __name__ == '__main__': - application.run() + application.make('commands').run() diff --git a/database.sqlite3 b/database.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..d0c08a0539a1a9dfa34125997bb3cde66fb42956 GIT binary patch literal 348160 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 diff --git a/package.json b/package.json deleted file mode 100644 index 1c9da95b5..000000000 --- a/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "private": true, - "scripts": { - "dev": "npx mix", - "development": "npx mix", - "watch": "npx mix watch", - "hot": "npx mix hot", - "prod": "npx mix --production", - "production": "npx mix --production" - }, - "devDependencies": { - "axios": "^0.19", - "cross-env": "^5.1", - "laravel-mix": "^4.0.7", - "lodash": "^4.17.13", - "resolve-url-loader": "^2.3.1", - "sass": "^1.15.2", - "sass-loader": "^7.1.0" - } -} diff --git a/pytest.ini b/pytest.ini index b0e5a945f..0b8b97aad 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -filterwarnings = - ignore::DeprecationWarning \ No newline at end of file +markers = + integrations: only run integration tests (deselect with '-m "not integations"') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7873dcd86..400900e18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,29 @@ -ably==1.0.1 -bcrypt>=3.1,<3.2 -boto3==1.5.24 -cleo>=0.8,<0.9 -coveralls -cryptography>=3.4<3.5 -flake8 -hupper>=1.0,<2.0 -Jinja2>=2,<3 -masonite-events>=1.0,<2 -masonite-scheduler>=1.0.0,<=1.0.99 -https://github.com/masoniteframework/orm/archive/0.9.zip -passlib>=1.7,<1.8 -pendulum>=2.1 -pika -psutil>=5.4,<5.5 -pusher==1.7.4 -pyjwt>=1.7.1 -pypugjs==5.9.4 -python-dotenv>=0.8,<0.9 -pytest==4.6.2 -pytest-cov -requests>=2.0,<2.99 -tabulate>=0.8,<0.9 -tldextract>=2.2,<2.3 -whitenoise>=3.3 +pytest +black +cryptography +https://github.com/MasoniteFramework/orm/archive/2.0.zip +python-dotenv +waitress +responses +slackblocks +hashids +pwnedapi +argon2-cffi exceptionite -pubnub>=4.5.1 -responses==0.12.0 -mock>=3.0.5 +dotty_dict +tldextract +hupper +whitenoise +hfilesize +pusher +pytz +coverage +pytest +redis +boto3 +pusher +pymemcache +vonage +slackblocks +argon2-cffi +pwnedapi \ No newline at end of file diff --git a/resources/__init__.py b/resources/__init__.py deleted file mode 100644 index ea18e76b0..000000000 --- a/resources/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Resources directory. - -This directory is responsible for storing any resource based files such as -templates and SASS files -""" diff --git a/resources/templates/__init__.py b/resources/templates/__init__.py deleted file mode 100644 index 2e9156187..000000000 --- a/resources/templates/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Templates directory. - -This directory is responsible for storing all HTML templates for your project. -You are not required to store them here but you will need to add the environment -to the View class in a service provider if you want to use different template -environments. -""" diff --git a/resources/templates/admin_test.html b/resources/templates/admin_test.html deleted file mode 100644 index 8331c6bf2..000000000 --- a/resources/templates/admin_test.html +++ /dev/null @@ -1 +0,0 @@ -{% if user is admin %}True{% else %}False{% endif %} \ No newline at end of file diff --git a/resources/templates/auth/confirm.html b/resources/templates/auth/confirm.html deleted file mode 100644 index 9b29cca86..000000000 --- a/resources/templates/auth/confirm.html +++ /dev/null @@ -1 +0,0 @@ -confirm \ No newline at end of file diff --git a/resources/templates/auth/error.html b/resources/templates/auth/error.html deleted file mode 100644 index 760589cb5..000000000 --- a/resources/templates/auth/error.html +++ /dev/null @@ -1 +0,0 @@ -error \ No newline at end of file diff --git a/resources/templates/base.html b/resources/templates/base.html deleted file mode 100644 index aeb56db11..000000000 --- a/resources/templates/base.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - {% block title %} {{ config('application.name', 'Masonite') }} {% endblock %} - - - - - {% block css %}{% endblock %} - - - - -
- -
- {% block content %} - - {% endblock %} -
-
- - - diff --git a/resources/templates/csrf_field.html b/resources/templates/csrf_field.html deleted file mode 100644 index a093ad723..000000000 --- a/resources/templates/csrf_field.html +++ /dev/null @@ -1 +0,0 @@ -{{ csrf_field }} \ No newline at end of file diff --git a/resources/templates/emails/test.html b/resources/templates/emails/test.html deleted file mode 100644 index d734c5b0a..000000000 --- a/resources/templates/emails/test.html +++ /dev/null @@ -1 +0,0 @@ -testing email \ No newline at end of file diff --git a/resources/templates/filter.html b/resources/templates/filter.html deleted file mode 100644 index 4ade808f5..000000000 --- a/resources/templates/filter.html +++ /dev/null @@ -1 +0,0 @@ -{{ test|slug }} \ No newline at end of file diff --git a/resources/templates/index.html b/resources/templates/index.html deleted file mode 100644 index 2b31011cf..000000000 --- a/resources/templates/index.html +++ /dev/null @@ -1 +0,0 @@ -hey \ No newline at end of file diff --git a/resources/templates/line-statements.html b/resources/templates/line-statements.html deleted file mode 100644 index 744bd1d8d..000000000 --- a/resources/templates/line-statements.html +++ /dev/null @@ -1,3 +0,0 @@ -@if test: - {{ test }} -@endif \ No newline at end of file diff --git a/resources/templates/mail/composers.html b/resources/templates/mail/composers.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/mail/composers.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/mail/share.html b/resources/templates/mail/share.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/mail/share.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/mail/welcome.html b/resources/templates/mail/welcome.html deleted file mode 100644 index b7d69f714..000000000 --- a/resources/templates/mail/welcome.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - to: {{ to }} - - - - Document - - - -
- - -
- - diff --git a/resources/templates/mail/welcome.txt b/resources/templates/mail/welcome.txt deleted file mode 100644 index a3ba88e77..000000000 --- a/resources/templates/mail/welcome.txt +++ /dev/null @@ -1,2 +0,0 @@ -Hi {{to}} -Welcome to MasoniteFramework! diff --git a/resources/templates/pug/hello.pug b/resources/templates/pug/hello.pug deleted file mode 100644 index c06b171e4..000000000 --- a/resources/templates/pug/hello.pug +++ /dev/null @@ -1 +0,0 @@ -p hello {{ name }} \ No newline at end of file diff --git a/resources/templates/test_cache.html b/resources/templates/test_cache.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/test_cache.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/test_exception.html b/resources/templates/test_exception.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/test_exception.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/welcome.html b/resources/templates/welcome.html deleted file mode 100644 index 0d85f10a8..000000000 --- a/resources/templates/welcome.html +++ /dev/null @@ -1,36 +0,0 @@ -{% if exists('auth/base') %} - {% extends 'auth/base.html' %} -{% else %} - {% extends 'base.html' %} -{% endif %} - -{% block css %} - - -{% endblock %} - -{% block title %} - Welcome To {{ config('application.name', 'Masonite none') }} -{% endblock %} - -{% block content %} -
-
- {{ config('application.name') }} -
- - -

- - -
-{% endblock %} diff --git a/routes/web.py b/routes/web.py deleted file mode 100644 index 3ced912e3..000000000 --- a/routes/web.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Web Routes.""" - -from src.masonite.routes import Get, Post, Redirect, RouteGroup, Patch, Options - - -ROUTES = [ - Get().route('/test', None).middleware('auth'), - Get('/bad', 'TestController@bad'), - Get('/welcome', 'WelcomeController@show'), - Get('/keyerror', 'TestController@keyerror'), - Get().route('/queue', 'TestController@queue'), - Options('options', 'TestController@show'), - Redirect('/redirect', 'test'), - Get().domain('test').route('/test', None).middleware('auth'), - Get().domain('test').route('/unit/test', 'TestController@testing').middleware('auth'), - Get().domain('test').route('/test/route', 'TestController@testing'), - Get('/json_response', 'TestController@json_response'), - Post('/test/post/route', 'TestController@post_test'), - Get('/login', 'TestController@testing').name('login'), - Get('/v', 'TestController@v').name('v'), - Get('/', 'TestController@v').name('v'), - Get('/test/param/@id', 'TestController@testing'), - Post('/test/json/response/@id', 'TestController@json'), - Get('/test/set/test/session', 'TestController@session'), - Get('/test/mail', 'TestController@mail'), - Get('/test/view', 'UnitTestController@view'), - Get('/test/redirect', 'UnitTestController@redirect_view'), - RouteGroup([ - Get('/test/1', 'TestController@show'), - Get('/test/2', 'TestController@show') - ], prefix='/example'), - RouteGroup([ - Get('/deep/1', 'DeepController@show'), - ], prefix='/example', namespace='subdirectory.deep.'), - RouteGroup([ - Get('/test/get', 'UnitTestController@show'), - Get('/test/param/@post_id', 'UnitTestController@param'), - Post('/test/post', 'UnitTestController@store').middleware('test'), - Get('/test/get/params', 'UnitTestController@get_params').name('get.params'), - Post('/test/params', 'UnitTestController@params'), - Post('/test/user', 'UnitTestController@user'), - Post('/test/json', 'UnitTestController@json'), - Get('/test/json/response', 'UnitTestController@response'), - Post('/test/json/validate', 'UnitTestController@validate'), - Get('/test/json/multi', 'UnitTestController@multi'), - Get('/test/json/multi_count', 'UnitTestController@multi_count'), - Patch('/test/patch', 'UnitTestController@patch'), - ], prefix="/unit") -] - -from src.masonite.auth import Auth -ROUTES += Auth.routes() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ee8526d3f..000000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[metadata] -description-file = README.md -[flake8] -ignore=E501,F401,E128,E402,E731,F821,E712 -include=masonite diff --git a/setup.py b/setup.py index 61fbc18b3..37cb4c7c6 100644 --- a/setup.py +++ b/setup.py @@ -1,99 +1,188 @@ -import os from setuptools import setup -here = os.path.abspath(os.path.dirname(__file__)) - -meta = {} -with open(os.path.join(here, "src/masonite", "__version__.py"), "r") as f: - exec(f.read(), meta) - -try: - with open("README.md", "r", encoding="utf-8") as f: - readme = f.read() -except FileNotFoundError: - readme = "" +with open("README.md", "r") as fh: + long_description = fh.read() setup( - name=meta["__title__"], + name="masonite", + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version="4.0.0b1.post6", + package_dir={"": "src"}, + description="The Masonite Framework", + long_description=long_description, + long_description_content_type="text/markdown", + # The project's main homepage. + url="https://github.com/masoniteframework/masonite", + # Author details + author="Joe Mancuso", + author_email="joe@masoniteproject.com", + # Choose your license + license="MIT", + # If your package should include things you specify in your MANIFEST.in file + # Use this option if your package needs to include files that are not python files + # like html templates or css files + include_package_data=True, + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=[ + "inflection>=0.3<0.4", + "exceptionite>=1.0<1.1", + "pendulum>=2,<3", + "jinja2>=3.0.0<3.1", + "cleo>=0.8.1,<0.9", + "hupper>=1.10,<1.11", + "waitress>=1.4,<1.5", + "bcrypt>=3.2,<3.3", + "whitenoise>=5.2,<5.3", + "python-dotenv>=0.15,<0.16", + "hashids>=1.3,<1.4", + "cryptography>=3.3.1,<4.0", + "tldextract>=2.2,<2.3", + "hfilesize>=0.1", + "dotty_dict>=1.3.0<1.40", + ], + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 4 - Beta", + # Indicate who your project is intended for + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "Environment :: Web Environment", + # Pick your license as you wish (should match "license" above) + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + # What does your project relate to? + keywords="Masonite, MasoniteFramework, Python, ORM", + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). packages=[ - "masonite.auth.guards", + "masonite", "masonite.auth", - "masonite.commands.presets", + "masonite.authentication.guards", + "masonite.authentication.models", + "masonite.authentication", + "masonite.authorization.models", + "masonite.authorization", + "masonite.broadcasting.controllers", + "masonite.broadcasting.drivers", + "masonite.broadcasting.providers", + "masonite.broadcasting", + "masonite.cache.drivers", + "masonite.cache", "masonite.commands", - "masonite.contracts.managers", - "masonite.contracts", + "masonite.configuration.providers", + "masonite.configuration", + "masonite.container", "masonite.controllers", "masonite.cookies", - "masonite.drivers.authentication", - "masonite.drivers.broadcast", - "masonite.drivers.cache", - "masonite.drivers.mail", "masonite.drivers.queue", "masonite.drivers.session", - "masonite.drivers.storage", - "masonite.drivers.upload", "masonite.drivers", + "masonite.environment", + "masonite.essentials.helpers", + "masonite.essentials.middleware", + "masonite.essentials.providers", + "masonite.essentials", + "masonite.events.commands", + "masonite.events.providers", + "masonite.events", + "masonite.exceptions", + "masonite.facades", + "masonite.filesystem.drivers", + "masonite.filesystem.providers", + "masonite.filesystem", + "masonite.foundation", + "masonite.hashing.drivers", + "masonite.hashing", "masonite.headers", "masonite.helpers", - "masonite.listeners", - "masonite.managers", + "masonite.input", + "masonite.loader", + "masonite.mail.drivers", + "masonite.mail", + "masonite.middleware.route", "masonite.middleware", + "masonite.notification.commands", + "masonite.notification.drivers.vonage", + "masonite.notification.drivers", + "masonite.notification.providers", + "masonite.notification", + "masonite.packages.providers", + "masonite.packages", + "masonite.pipeline.tasks", + "masonite.pipeline", "masonite.providers", "masonite.queues", - "masonite.testing", - "masonite", - ], - version=meta["__version__"], - install_requires=[ - "bcrypt>=3.1,<3.2", - "cleo>=0.8,<0.9", - "cryptography>=2.3<3.5", - "hupper<1.10", - "Jinja2>=2,<3", - "masonite-orm>=1.0,<1.1", - "passlib>=1.7,<1.8", - "pendulum>=2.1,<2.2", - "psutil>=5.4,<5.7", - "python-dotenv>=0.8,<0.11", - "requests>=2.0,<2.99", - "tabulate>=0.8,<0.9", - "tldextract>=2.2,<2.3", - "whitenoise>=3.3,<5", - "exceptionite>=1.0,<2", - ], - description=meta["__description__"], - long_description_content_type="text/markdown", - long_description=readme, - author=meta["__author__"], - author_email=meta["__author_email__"], - package_dir={"": "src"}, - url=meta["__url__"], - keywords=["masonite", "python web framework", "python3"], - license=meta["__licence__"], - python_requires=">=3.5", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Masonite", - "Intended Audience :: Developers", - "Topic :: Software Development :: Build Tools", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Internet :: WWW/HTTP :: WSGI", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: Software Development :: Libraries :: Python Modules", + "masonite.request", + "masonite.response", + "masonite.routes", + "masonite.scheduling.commands", + "masonite.scheduling.providers", + "masonite.scheduling", + "masonite.sessions", + "masonite.storage", + "masonite.templates", + "masonite.tests", + "masonite.utils", + "masonite.validation.commands", + "masonite.validation.providers", + "masonite.validation.resources", + "masonite.validation", + "masonite.views", ], - include_package_data=True, + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + # $ pip install your-package[dev,test] + extras_require={ + "test": [ + "coverage", + "pytest", + "redis", + "boto3", + "pusher", + "pymemcache", + "vonage", + "slackblocks", + "argon2-cffi", + "pwnedapi", + ], + }, + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + ## package_data={ + ## 'sample': [], + ## }, + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '/my_data' + ## data_files=[('my_data', ['data/data_file.txt'])], + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. entry_points={ "console_scripts": [ - "craft = masonite.cli:application.run", + "start = masonite.commands.Entry:application.run", ], }, ) diff --git a/src/masonite/__init__.py b/src/masonite/__init__.py index 876130766..e69de29bb 100644 --- a/src/masonite/__init__.py +++ b/src/masonite/__init__.py @@ -1,24 +0,0 @@ -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) - - -from .managers.BroadcastManager import Broadcast -from .managers.CacheManager import Cache -from .managers.MailManager import Mail -from .managers.QueueManager import Queue -from .managers.SessionManager import Session -from .managers.UploadManager import Upload -from .environment import env -from .__version__ import ( - __title__, - __description__, - __url__, - __version__, - __author__, - __author_email__, - __licence__, - __cookie_cutter_version__, -) - -_file_source = "masonite" diff --git a/src/masonite/__version__.py b/src/masonite/__version__.py deleted file mode 100644 index f5ef6f956..000000000 --- a/src/masonite/__version__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Metadata for Masonite package.""" -__title__ = "masonite" -__description__ = "The core for the Masonite framework" -__url__ = "https://github.com/MasoniteFramework/masonite" -__version__ = "3.0.12" -__author__ = "Joseph Mancuso" -__author_email__ = "joe@masoniteproject.com" -__licence__ = "MIT" - -__cookie_cutter_version__ = "3.0" diff --git a/src/masonite/auth/Auth.py b/src/masonite/auth/Auth.py deleted file mode 100644 index ce6511491..000000000 --- a/src/masonite/auth/Auth.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Authentication Class.""" - - -class Auth: - """Facade class for the Guard class""" - - @staticmethod - def routes(): - from ..routes import Get, Post - - return [ - Get("/login", "auth.LoginController@show").name("login"), - Get("/logout", "auth.LoginController@logout").name("logout"), - Post("/login", "auth.LoginController@store"), - Get("/register", "auth.RegisterController@show").name("register"), - Post("/register", "auth.RegisterController@store"), - Get("/home", "auth.HomeController@show").name("home"), - Get("/email/verify", "auth.ConfirmController@verify_show").name("verify"), - Get("/email/verify/send", "auth.ConfirmController@send_verify_email"), - Get("/email/verify/@id:signed", "auth.ConfirmController@confirm_email"), - Get("/password", "auth.PasswordController@forget").name("forgot.password"), - Post("/password", "auth.PasswordController@send"), - Get("/password/@token/reset", "auth.PasswordController@reset").name( - "password.reset" - ), - Post("/password/@token/reset", "auth.PasswordController@update"), - ] diff --git a/src/masonite/auth/Csrf.py b/src/masonite/auth/Csrf.py deleted file mode 100644 index 98820f274..000000000 --- a/src/masonite/auth/Csrf.py +++ /dev/null @@ -1,54 +0,0 @@ -"""CSRF Protection Module.""" -import binascii -import os -from hmac import compare_digest -from .Sign import Sign -from ..exceptions import InvalidCSRFToken -from cryptography.fernet import InvalidToken - - -class Csrf: - """CSRF Protection Class.""" - - def __init__(self, request): - """CSRF constructor. - - Arguments: - request {masonite.request.Request} -- Request object - """ - self.request = request - - def generate_csrf_token(self, length=30): - """Generate CRSRF token. - - The // you see below is integer division. Since the token will be twice - the size of the length passed to. A length of 30 passed below will generate - a string length of 60 so we integer divide by 2 - - Returns: - string -- Returns token generated. - """ - token = self.request.get_cookie("MSESSID") - self.request.cookie("csrf_token", token) - return token - - def verify_csrf_token(self, token): - """Verify if csrf token is valid from the cookie set. - - Arguments: - token {string} -- The token that was generated. - - Returns: - bool - """ - try: - token = Sign().unsign(token) - except (InvalidToken, TypeError): - pass - - if self.request.get_cookie("csrf_token") and compare_digest( - self.request.get_cookie("csrf_token"), token - ): - return True - else: - return False diff --git a/src/masonite/auth/Sign.py b/src/masonite/auth/Sign.py index db442d3df..3ad3e3e8f 100644 --- a/src/masonite/auth/Sign.py +++ b/src/masonite/auth/Sign.py @@ -1,9 +1,9 @@ """Cryptographic Signing Module.""" import binascii -from cryptography.fernet import Fernet +from cryptography.fernet import Fernet, InvalidToken as CryptographyInvalidToken -from ..exceptions import InvalidSecretKey +from ..exceptions import InvalidSecretKey, InvalidToken class Sign: @@ -22,9 +22,9 @@ def __init__(self, key=None): if key: self.key = key else: - from config import application + from wsgi import application - self.key = application.KEY + self.key = application.make("key") if not self.key: raise InvalidSecretKey( @@ -70,4 +70,7 @@ def unsign(self, value=None): if not value: return f.decrypt(self.encryption).decode("utf-8") - return f.decrypt(bytes(str(value), "utf-8")).decode("utf-8") + try: + return f.decrypt(bytes(str(value), "utf-8")).decode("utf-8") + except CryptographyInvalidToken as e: + raise InvalidToken("Invalid Cryptographic Token") from e diff --git a/src/masonite/auth/__init__.py b/src/masonite/auth/__init__.py index 21f3ca23f..95f902948 100644 --- a/src/masonite/auth/__init__.py +++ b/src/masonite/auth/__init__.py @@ -1,4 +1,2 @@ -from .Auth import Auth -from .Csrf import Csrf from .Sign import Sign from .MustVerifyEmail import MustVerifyEmail diff --git a/src/masonite/auth/guards/AuthenticationGuard.py b/src/masonite/auth/guards/AuthenticationGuard.py deleted file mode 100644 index 977f94ac0..000000000 --- a/src/masonite/auth/guards/AuthenticationGuard.py +++ /dev/null @@ -1,56 +0,0 @@ -class AuthenticationGuard: - def guard(self, guard): - """Specify the guard you want to use - - Arguments: - guard {[type]} -- [description] - """ - from .Guard import Guard - - return Guard(self.app).make(guard) - - def register_guard(self, key, cls=None): - """Registers a new guard class. - - Arguments: - key {string|dict} -- The key to name the guard to a dictionary of key: values - - Keyword Arguments: - cls {object} -- A guard class. (default: {None}) - - Returns: - None - """ - from .Guard import Guard - - if isinstance(key, dict): - return Guard.guards.update(key) - - return Guard.guards.update({key: cls}) - - def register_driver(self, key, cls): - """Registers a new driver with the current guard class. - - Arguments: - key {string} -- The key to register the driver to. - cls {class} -- A guard class. - """ - self.drivers.update({key: cls}) - - def make(self, key): - """Makes a new driver from the current guard class. - - Arguments: - key {string} -- The key to for the driver to make. - - Raises: - DriverNotFound: Thrown when the driver is not registered. - - Returns: - object -- Returns a guard driver object. - """ - if key in self.drivers: - self.driver = self.app.resolve(self.drivers[key]) - return self.driver - - raise DriverNotFound("Could not find the driver {}".format(key)) diff --git a/src/masonite/auth/guards/Guard.py b/src/masonite/auth/guards/Guard.py deleted file mode 100644 index e0d1ba3bd..000000000 --- a/src/masonite/auth/guards/Guard.py +++ /dev/null @@ -1,124 +0,0 @@ -"""A Guard Class Module.""" -from ...app import App -from ...exceptions import DriverNotFound - - -class Guard: - - guards = {} - - def __init__(self, app: App): - """Guard Initializer - - Arguments: - app {masonite.app.App} -- The Masonite container - """ - self.app = app - - def make(self, key): - """Makes a guard that has been previously registered - - Arguments: - key {string} -- The key of the guard to fetch. - - Raises: - DriverNotFound: Raised when trying to fetch a guard that has not been registered yet. - - Returns: - [type] -- [description] - """ - if key in self.guards: - self._guard = self.app.resolve(self.guards[key]) - return self._guard - - raise DriverNotFound("Could not find the guard: '{}'".format(key)) - - def guard(self, key): - """Alias for the make method. - - Arguments: - key {string} -- The key of the guard to fetch. - - Returns: - masonite.guards.* -- An instance of a guard class. - """ - return self.make(key) - - def set(self, key): - """Sets the specified guard as the default guard to use. - - Arguments: - key {string} -- The key of the guard to set. - - Returns: - masonite.guards.* -- An instance of guard class. - """ - return self.make(key) - - def get(self): - """Gets the guard current class. - - Returns: - masonite.guards.* -- An instance of guard class. - """ - return self._guard - - def driver(self, key): - """Gets the driver for the currently set guard class. - - Arguments: - key {string} -- The key of the driver for the guard to get. - - Returns: - masonite.drivers.auth.* -- An auth driver class. - """ - return self._guard.make(key) - - def register_guard(self, key, cls=None): - """Registers a new guard class. - - Arguments: - key {string|dict} -- The key to name the guard to a dictionary of key: values - - Keyword Arguments: - cls {object} -- A guard class. (default: {None}) - - Returns: - None - """ - if isinstance(key, dict): - return self.guards.update(key) - - return self.guards.update({key: cls}) - - def login(self, *args, **kwargs): - """Wrapper method to call the guard class method. - - Returns: - * -- Returns what the guard class method returns. - """ - return self._guard.login(*args, **kwargs) - - def user(self, *args, **kwargs): - """Wrapper method to call the guard class method. - - Returns: - * -- Returns what the guard class method returns. - """ - return self._guard.user(*args, **kwargs) - - def register(self, *args, **kwargs): - """Wrapper method to call the guard class method. - - Returns: - * -- Returns what the guard class method returns. - """ - return self._guard.register(*args, **kwargs) - - def __getattr__(self, key, *args, **kwargs): - """Wrapper method to call the guard class methods. - - Returns: - * -- Returns what the guard class methods returns. - """ - return getattr(self._guard, key) diff --git a/src/masonite/auth/guards/WebGuard.py b/src/masonite/auth/guards/WebGuard.py deleted file mode 100644 index fe2f6b2fa..000000000 --- a/src/masonite/auth/guards/WebGuard.py +++ /dev/null @@ -1,157 +0,0 @@ -import uuid - -import bcrypt - -from ...app import App -from ...request import Request -from ...drivers import AuthCookieDriver, AuthJwtDriver -from ...helpers import config -from ...helpers import password as bcrypt_password -from .AuthenticationGuard import AuthenticationGuard - - -class WebGuard(AuthenticationGuard): - - drivers = {"cookie": AuthCookieDriver, "jwt": AuthJwtDriver} - - def __init__(self, app: App, driver=None, auth_model=None): - self.app = app - self._once = False - self.auth_model = auth_model or config("auth.auth.guards.web.model") - self.driver = self.make(driver or config("auth.auth.guards.web.driver")) - - def user(self): - """Get the currently logged in user. - - Raises: - exception -- Raised when there has been an error handling the user model. - - Returns: - object|bool -- Returns the current authenticated user object or False or None if there is none. - """ - try: - return self.driver.user(self.auth_model) - except Exception as exception: - raise exception - - return None - - def login(self, name, password): - """Login the user based on the parameters provided. - - Arguments: - name {string} -- The field to authenticate. This could be a username or email address. - password {string} -- The password to authenticate with. - - Raises: - exception -- Raised when there has been an error handling the user model. - - Returns: - object|bool -- Returns the current authenticated user object or False or None if there is none. - """ - - if not isinstance(password, str): - raise TypeError( - "Cannot login with password '{}' of type: {}".format( - password, type(password) - ) - ) - - auth_column = self.auth_model.__auth__ - - try: - # Try to login multiple or statements if given an auth list - if isinstance(auth_column, list): - model = self.auth_model.where(auth_column[0], name) - - for authentication_column in auth_column[1:]: - model.or_where(authentication_column, name) - - model = model.first() - else: - model = self.auth_model.where(auth_column, name).first() - - # MariaDB/MySQL can store the password as string - # while PostgreSQL can store it as bytes - # This is to prevent to double encode the password as bytes - password_as_bytes = self._get_password_column(model) - if not isinstance(password_as_bytes, bytes): - password_as_bytes = bytes(password_as_bytes or "", "utf-8") - - if model and bcrypt.checkpw(bytes(password, "utf-8"), password_as_bytes): - if not self._once: - remember_token = str(uuid.uuid4()) - model.remember_token = remember_token - model.save() - self.driver.save(remember_token, model=model) - self.app.make("Request").set_user(model) - return model - - except Exception as exception: - raise exception - - return False - - def logout(self): - """Logout the current authenticated user. - - Returns: - self - """ - self.driver.logout() - return self - - def login_by_id(self, user_id): - """Login a user by the user ID. - - Arguments: - user_id {string|int} -- The ID of the user model record. - - Returns: - object|False -- Returns the current authenticated user object or False or None if there is none. - """ - model = self.auth_model.find(user_id) - - if model: - if not self._once: - remember_token = str(uuid.uuid4()) - model.remember_token = remember_token - model.save() - self.driver.save(remember_token, model=model) - self.app.make("Request").set_user(model) - return model - - return False - - def once(self): - """Log in the user without saving a cookie. - - Returns: - self - """ - self._once = True - return self - - def _get_password_column(self, model): - """Gets the password column to use. - - Arguments: - model {orator.orm.Model} -- An Orator type model. - - Returns: - string - """ - if hasattr(model, "__password__"): - return getattr(model, model.__password__) - - if hasattr(model, "password"): - return getattr(model, "password") - - def register(self, user): - """Register the user. - - Arguments: - user {dict} -- A dictionary of user data information. - """ - user["password"] = bcrypt_password(user["password"]) - return self.auth_model.create(**user) diff --git a/src/masonite/auth/guards/__init__.py b/src/masonite/auth/guards/__init__.py deleted file mode 100644 index 11d537565..000000000 --- a/src/masonite/auth/guards/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .Guard import Guard -from .AuthenticationGuard import AuthenticationGuard -from .WebGuard import WebGuard diff --git a/src/masonite/authentication/Auth.py b/src/masonite/authentication/Auth.py new file mode 100644 index 000000000..e0d444aef --- /dev/null +++ b/src/masonite/authentication/Auth.py @@ -0,0 +1,155 @@ +import pendulum +import uuid +from ..routes import Route + + +class Auth: + def __init__(self, application, guard_config=None): + self.application = application + self.guards = {} + self._guard = None + self.guard_config = guard_config or {} + self.options = {} + + def add_guard(self, name, guard): + self.guards.update({name: guard}) + + def set_configuration(self, config): + self.guard_config = config + return self + + def guard(self, guard): + self._guard = guard + return self + + def get_guard(self, name=None): + if name is None and self._guard is None: + return self.guards[self.guard_config.get("default")] + + return self.guards[self._guard] + + def get_config_options(self, guard=None): + if guard is None: + options = self.guard_config.get(self.guard_config.get("default"), {}) + options.update(self.options) + return options + + options = self.guard_config.get(guard, {}) + options.update(self.options) + return options + + def attempt(self, email, password, once=False): + auth_config = self.get_config_options() + auth_config.update({"once": once}) + return self.get_guard().set_options(auth_config).attempt(email, password) + + def attempt_by_id(self, user_id, once=False): + auth_config = self.get_config_options() + auth_config.update({"once": once}) + return self.get_guard().set_options(auth_config).attempt_by_id(user_id) + + def logout(self): + """Logout the current authenticated user. + + Returns: + self + """ + self.application.make("request").remove_user() + return self.application.make("request").delete_cookie("token") + + def user(self): + """Logout the current authenticated user. + + Returns: + self + """ + auth_config = self.get_config_options() + return self.get_guard().set_options(auth_config).user() + + def register(self, dictionary): + """Logout the current authenticated user. + + Returns: + self + """ + auth_config = self.get_config_options() + return self.get_guard().set_options(auth_config).register(dictionary) + + def password_reset(self, email): + """Logout the current authenticated user. + + Returns: + self + """ + token = str(uuid.uuid4()) + try: + self.application.make("builder").new().table( + self.guard_config.get("password_reset_table") + ).create( + { + "email": email, + "token": token, + "expires_at": pendulum.now() + .add(minutes=self.guard_config.get("password_reset_expiration")) + .to_datetime_string() + if self.guard_config.get("password_reset_expiration") + else None, + "created_at": pendulum.now().to_datetime_string(), + } + ) + except Exception: + return False + + self.application.make("event").fire("auth.password_reset", email, token) + return token + + def reset_password(self, password, token): + """Logout the current authenticated user. + + Returns: + self + """ + + reset_record = ( + self.application.make("builder") + .new() + .table(self.guard_config.get("password_reset_table")) + .where("token", token) + .first() + ) + auth_config = self.get_config_options() + ( + self.get_guard() + .set_options(auth_config) + .reset_password(reset_record.get("email"), password) + ) + + ( + self.application.make("builder") + .new() + .table(self.guard_config.get("password_reset_table")) + .where("token", token) + .delete() + ) + + @classmethod + def routes(self): + return [ + Route.get("/login", "auth.LoginController@show").name("login"), + Route.get("/home", "auth.HomeController@show").name("auth.home"), + Route.get("/register", "auth.RegisterController@show").name("register"), + Route.post("/register", "auth.RegisterController@store").name("register.store"), + Route.get("/password_reset", "auth.PasswordResetController@show").name( + "password_reset" + ), + Route.post("/password_reset", "auth.PasswordResetController@store").name( + "password_reset.store" + ), + Route.get( + "/change_password", "auth.PasswordResetController@change_password" + ).name("change_password"), + Route.post( + "/change_password", "auth.PasswordResetController@store_changed_password" + ).name("change_password.store"), + Route.post("/login", "auth.LoginController@store").name("login.store") + ] diff --git a/src/masonite/authentication/__init__.py b/src/masonite/authentication/__init__.py new file mode 100644 index 000000000..14ff79940 --- /dev/null +++ b/src/masonite/authentication/__init__.py @@ -0,0 +1,2 @@ +from .Auth import Auth +from .models.authenticates import Authenticates diff --git a/src/masonite/authentication/guards/WebGuard.py b/src/masonite/authentication/guards/WebGuard.py new file mode 100644 index 000000000..fe293c361 --- /dev/null +++ b/src/masonite/authentication/guards/WebGuard.py @@ -0,0 +1,80 @@ +class WebGuard: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def attempt(self, username, password): + attempt = self.options.get("model")().attempt(username, password) + if attempt and not self.options.get("once"): + self.application.make("response").cookie("token", attempt.remember_token) + self.application.make("request").set_user(attempt) + return attempt + + def register(self, dictionary): + try: + register = self.options.get("model")().register(dictionary) + except Exception: + return False + return self.attempt_by_id(register.get_id()) + + def user(self): + """Get the currently logged in user. + + Returns: + object|bool -- Returns the current authenticated user object or False or None if there is none. + """ + token = self.application.make("request").cookie("token") + if token and self.options.get("model")(): + return ( + self.options.get("model")().where("remember_token", token).first() + or False + ) + + return False + + def attempt_by_id(self, user_id): + """Login a user by the user ID. + + Arguments: + user_id {string|int} -- The ID of the user model record. + + Returns: + object|False -- Returns the current authenticated user object or False or None if there is none. + """ + attempt = self.options.get("model")().attempt_by_id(user_id) + + if attempt and not self.options.get("once"): + self.application.make("request").cookie("token", attempt.remember_token) + self.application.make("request").set_user(attempt) + return attempt + + return False + + def reset_password(self, username, new_password): + """Login a user by the user ID. + + Arguments: + user_id {string|int} -- The ID of the user model record. + + Returns: + object|False -- Returns the current authenticated user object or False or None if there is none. + """ + attempt = self.options.get("model")().reset_password(username, new_password) + + if attempt: + return attempt + + return False + + def once(self): + """Log in the user without saving a cookie. + + Returns: + self + """ + self._once = True + return self diff --git a/src/masonite/authentication/guards/__init__.py b/src/masonite/authentication/guards/__init__.py new file mode 100644 index 000000000..47ec641de --- /dev/null +++ b/src/masonite/authentication/guards/__init__.py @@ -0,0 +1 @@ +from .WebGuard import WebGuard diff --git a/src/masonite/authentication/models/__init__.py b/src/masonite/authentication/models/__init__.py new file mode 100644 index 000000000..1fc5cebcb --- /dev/null +++ b/src/masonite/authentication/models/__init__.py @@ -0,0 +1 @@ +from .authenticates import Authenticates diff --git a/src/masonite/authentication/models/authenticates.py b/src/masonite/authentication/models/authenticates.py new file mode 100644 index 000000000..66763a232 --- /dev/null +++ b/src/masonite/authentication/models/authenticates.py @@ -0,0 +1,66 @@ +"""Password Helper Module.""" +import uuid +from ...facades import Hash + + +class Authenticates: + + __auth__ = "email" + __password__ = "password" + + def attempt(self, username, password): + """Attempts to login using a username and password""" + record = self.where(self.get_username_column(), username).first() + if not record: + return False + + record_password = getattr(record, self.get_password_column()) + if not isinstance(record_password, bytes): + record_password = bytes(record_password or "", "utf-8") + if Hash.check(password, record_password): + record.set_remember_token().save() + return record + + return False + + def register(self, dictionary): + dictionary.update( + {self.get_password_column(): Hash.make(dictionary.get("password", ""))} + ) + return self.create(dictionary) + + def get_id(self): + return self.get_primary_key_value() + + def attempt_by_id(self, user_id): + """Attempts to login using a username and password""" + record = self.find(user_id) + if not record: + return False + + record.set_remember_token().save() + return record + + def get_remember_token(self): + """Attempts to login using a username and password""" + return self.remember_token + + def set_remember_token(self, token=None): + """Attempts to login using a username and password""" + self.remember_token = str(token) if token else str(uuid.uuid4()) + return self + + def reset_password(self, username, password): + """Attempts to login using a username and password""" + self.where(self.get_username_column(), username).update( + {self.get_password_column(): Hash.make(password)} + ) + return self + + def get_password_column(self): + """Attempts to login using a username and password""" + return self.__password__ + + def get_username_column(self): + """Attempts to login using a username and password""" + return self.__auth__ diff --git a/src/masonite/authorization/AuthorizationResponse.py b/src/masonite/authorization/AuthorizationResponse.py new file mode 100644 index 000000000..cfa30e478 --- /dev/null +++ b/src/masonite/authorization/AuthorizationResponse.py @@ -0,0 +1,30 @@ +from ..exceptions.exceptions import AuthorizationException + + +class AuthorizationResponse: + def __init__(self, allowed, message="", status=None): + self._allowed = allowed + self.status = status + self._message = message + + @classmethod + def allow(cls, message="", status=None): + return cls(True, message, status) + + @classmethod + def deny(cls, message="", status=None): + return cls(False, message, status) + + def allowed(self): + return self._allowed + + def authorize(self): + if not self._allowed: + raise AuthorizationException(self._message, self.status) + return self + + def get_response(self): + return self._message, self.status + + def message(self): + return self._message diff --git a/src/masonite/authorization/AuthorizesRequest.py b/src/masonite/authorization/AuthorizesRequest.py new file mode 100644 index 000000000..ef5a3bc03 --- /dev/null +++ b/src/masonite/authorization/AuthorizesRequest.py @@ -0,0 +1,7 @@ +from ..facades import Gate + + +class AuthorizesRequest: + def authorize(self, permission, *args): + + return Gate.authorize(permission, *args) diff --git a/src/masonite/authorization/Gate.py b/src/masonite/authorization/Gate.py new file mode 100644 index 000000000..46b5298d4 --- /dev/null +++ b/src/masonite/authorization/Gate.py @@ -0,0 +1,170 @@ +from inspect import isclass, signature +from masoniteorm import Model + +from .AuthorizationResponse import AuthorizationResponse +from ..exceptions.exceptions import GateDoesNotExist, PolicyDoesNotExist + + +class Gate: + def __init__( + self, + application, + user_callback=None, + policies={}, + permissions={}, + before_callbacks=[], + after_callbacks=[], + ): + self.application = application + self.user_callback = user_callback + + self.policies = policies + self.permissions = permissions + self.before_callbacks = before_callbacks + self.after_callbacks = after_callbacks + + def define(self, permission, condition): + if not callable(condition): + raise Exception(f"Permission {permission} should be given a callable.") + + self.permissions.update({permission: condition}) + + def register_policies(self, policies): + for model_class, policy_class in policies: + self.policies[model_class] = policy_class + return self + + def get_policy_for(self, instance): + if isinstance(instance, Model): + policy = self.policies.get(instance.__class__, None) + elif isclass(instance): + policy = self.policies.get(instance, None) + elif isinstance(instance, str): + # TODO: load model from str, get class and get policies + policy = None + if policy: + return policy() + else: + return None + + def before(self, before_callback): + if not callable(before_callback): + raise Exception("before() should be given a callable.") + self.before_callbacks.append(before_callback) + + def after(self, after_callback): + if not callable(after_callback): + raise Exception("before() should be given a callable.") + self.after_callbacks.append(after_callback) + + def allows(self, permission, *args): + return self.inspect(permission, *args).allowed() + + def denies(self, permission, *args): + return not self.inspect(permission, *args).allowed() + + def has(self, permission): + return permission in self.permissions + + def for_user(self, user): + return Gate( + self.application, + lambda: user, + self.policies, + self.permissions, + self.before_callbacks, + self.after_callbacks, + ) + + def any(self, permissions, *args): + """Check that every of those permissions are allowed.""" + for permission in permissions: + if self.denies(permission, *args): + return False + return True + + def none(self, permissions, *args): + """Check that none of those permissions are allowed.""" + for permission in permissions: + if self.allows(permission, *args): + return False + return True + + def authorize(self, permission, *args): + return self.inspect(permission, *args).authorize() + + def inspect(self, permission, *args): + """Get permission checks results for the given user then builds and returns an + authorization response.""" + boolean_result = self.check(permission, *args) + if isinstance(boolean_result, AuthorizationResponse): + return boolean_result + if boolean_result: + return AuthorizationResponse.allow() + else: + return AuthorizationResponse.deny() + + def check(self, permission, *args): + """The core of the authorization class. Run before() checks, permission check and then + after() checks.""" + user = self._get_user() + + # run before checks and returns immediately if non null response + result = None + for callback in self.before_callbacks: + result = callback(user, permission) + if result: + break + + # run permission checks if nothing returned previously + if result is None: + # first check in policy + permission_method = None + if len(args) > 0: + policy = self.get_policy_for(args[0]) + if policy: + try: + permission_method = getattr(policy, permission) + except AttributeError: + raise PolicyDoesNotExist( + f"Policy method {permission} not found in {policy.__class__.__name__}." + ) + + if not permission_method: + # else check in gates + try: + permission_method = self.permissions[permission] + except KeyError: + raise GateDoesNotExist( + f"Gate {permission} has not been found. Did you declare it ?" + ) + + params = signature(permission_method).parameters + # check if user parameter is optional (meaning that guests users are allowed) + if ( + permission_method.__defaults__ + and permission_method.__defaults__[0] is None + and not user + ): + result = True + elif not user: + result = False + elif len(params) == 1: + result = permission_method(user) + else: + result = permission_method(user, *args) + + # run after checks + for callback in self.after_callbacks: + after_result = callback(user, permission, result) + result = after_result if after_result is not None else result + + return result + + def _get_user(self): + from ..facades import Request + + if self.user_callback: + return self.user_callback() + else: + return Request.user() diff --git a/src/masonite/authorization/Policy.py b/src/masonite/authorization/Policy.py new file mode 100644 index 000000000..1d66a3fc5 --- /dev/null +++ b/src/masonite/authorization/Policy.py @@ -0,0 +1,9 @@ +from .AuthorizationResponse import AuthorizationResponse + + +class Policy: + def allow(self, message="", code=None): + return AuthorizationResponse.allow(message, code) + + def deny(self, message="", code=None): + return AuthorizationResponse.deny(message, code) diff --git a/src/masonite/authorization/__init__.py b/src/masonite/authorization/__init__.py new file mode 100644 index 000000000..c24f184f3 --- /dev/null +++ b/src/masonite/authorization/__init__.py @@ -0,0 +1,5 @@ +from .AuthorizationResponse import AuthorizationResponse +from .Gate import Gate +from .Policy import Policy +from .models.authorizes import Authorizes +from .AuthorizesRequest import AuthorizesRequest diff --git a/src/masonite/authorization/models/__init__.py b/src/masonite/authorization/models/__init__.py new file mode 100644 index 000000000..0b6a76d85 --- /dev/null +++ b/src/masonite/authorization/models/__init__.py @@ -0,0 +1 @@ +from .authorizes import Authorizes diff --git a/src/masonite/authorization/models/authorizes.py b/src/masonite/authorization/models/authorizes.py new file mode 100644 index 000000000..2d5ea2243 --- /dev/null +++ b/src/masonite/authorization/models/authorizes.py @@ -0,0 +1,12 @@ +from ...facades import Gate + + +class Authorizes: + def can(self, permission, *args): + return Gate.for_user(self).allows(permission, *args) + + def cannot(self, permission, *args): + return Gate.for_user(self).denies(permission, *args) + + def can_any(self, permissions, *args): + return Gate.for_user(self).any(permissions, *args) diff --git a/src/masonite/autoload.py b/src/masonite/autoload.py deleted file mode 100644 index a4ef08ac1..000000000 --- a/src/masonite/autoload.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Autoloader Module. - -This contains the class for autoloading classes from directories. -This class is simply used to point at a directory and retrieve all classes in that directory. -""" - -import inspect -import os -import pkgutil -from pydoc import importlib - -from .exceptions import AutoloadContainerOverwrite, ContainerError, InvalidAutoloadPath - - -class Autoload: - """Autoload class. Used to retrieve all classes from any set of directories.""" - - classes = {} - - def __init__(self, app=None): - """Autoload Constructor. - - Keyword Arguments: - app {masonite.app.App} -- Container class (default: {None}) - """ - self.app = app - - def load(self, directories, instantiate=False): - """Load all classes found in a list of directories into the container. - - Arguments: - directories {list} -- List of directories to search. - - Keyword Arguments: - instantiate {bool} -- Whether or not to instantiate the class (default: {False}) - - Raises: - ContainerError -- Thrown when the container is not loaded into the class. - AutoloadContainerOverwrite -- Thrown when the container already has the key binding. - """ - self.instantiate = instantiate - if not self.app: - raise ContainerError( - "Container not specified. Pass the container into the constructor" - ) - - for (module_loader, name, _) in pkgutil.iter_modules(directories): - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - for obj in inspect.getmembers( - self._get_module_members(module_loader, name) - ): - - # If the object is a class and the objects module starts with the search path - if inspect.isclass(obj[1]) and obj[1].__module__.split(".")[ - :-1 - ] == search_path.split("/"): - if ( - self.app.has(obj[1].__name__) - and self.app.make(obj[1].__name__) - and not self.app.make(obj[1].__name__).__module__.startswith( - search_path - ) - ): - raise AutoloadContainerOverwrite( - "Container already has the key: {}. Cannot overwrite a container key that exists outside of your application.".format( - obj[1].__name__ - ) - ) - self.app.bind(obj[1].__name__, self._can_instantiate(obj)) - - def instances(self, directories, instance, only_app=True, instantiate=False): - """Use to autoload all instances of a specific object. - - Arguments: - directories {list} -- List of directories to search. - instance {object} -- Object to search for instances of. - - Keyword Arguments: - only_app {bool} -- Only search in the current application namespace. This will not found other classes - that are imported through third party packages. (default: {True}) - instantiate {bool} -- Whether or not to instantiate the classes it finds. (default: {False}) - - Returns: - dict -- Returns a dictionary of classes it found. - """ - self.instantiate = instantiate - - for (module_loader, name, _) in pkgutil.iter_modules(directories): - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - for obj in inspect.getmembers( - self._get_module_members(module_loader, name) - ): - if inspect.isclass(obj[1]) and issubclass(obj[1], instance): - if only_app and obj[1].__module__.startswith( - search_path.replace("/", ".") - ): - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - elif not only_app: - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - - return self.classes - - def collect(self, directories, only_app=True, instantiate=False): - """Collect all classes from a specific list of directories. - - Arguments: - directories {list} -- List of directories to search. - - Keyword Arguments: - only_app {bool} -- Only search in the current application namespace. This will not found other classes - that are imported through third party packages. (default: {True}) - instantiate {bool} -- Whether or not to instantiate the classes it finds. (default: {False}) - - Returns: - dict -- Returns a dictionary of objects found and their key bindings. - """ - self.instantiate = instantiate - - for (module_loader, name, _) in pkgutil.iter_modules(directories): - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - - for obj in inspect.getmembers( - self._get_module_members(module_loader, name) - ): - if inspect.isclass(obj[1]): - if only_app and obj[1].__module__.startswith( - search_path.replace("/", ".") - ): - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - elif not only_app: - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - - return self.classes - - def _can_instantiate(self, obj): - """Instantiate the class or not depending on the property set. - - Arguments: - obj {object} -- Object to check for instantiation. - - Returns: - object -- Returns the object being instantiated. - """ - if self.instantiate: - return obj[1]() - - return obj[1] - - def _get_module_members(self, module_loader, name): - """Get the module members. - - Arguments: - module_loader {pkgutil.ModuleLoader} -- Module Loader from the pkgutil library - name {string} -- Name of the module - - Raises: - InvalidAutoloadPath -- Thrown when the search path ends with a forward - - Returns: - module -- returns the imported module. - """ - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - if module_loader.path.endswith("/"): - raise InvalidAutoloadPath("Autoload path cannot have a trailing slash") - return importlib.import_module(search_path.replace("/", ".") + "." + name) diff --git a/src/masonite/broadcasting/Broadcast.py b/src/masonite/broadcasting/Broadcast.py new file mode 100644 index 000000000..cbda301bb --- /dev/null +++ b/src/masonite/broadcasting/Broadcast.py @@ -0,0 +1,74 @@ +from ..routes import Route + + +class Broadcast: + def __init__(self, application, store_config=None): + self.application = application + self.drivers = {} + self.store_config = store_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.store_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.store_config.get("default")] + return self.drivers[name] + + def driver(self, name=None): + store_config = self.get_config_options() + driver = self.get_driver(None) + return driver.set_options(store_config) + + def get_store_config(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def get_config_options(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def channel(self, channels, event=None, value=None, driver=None): + store_config = self.get_config_options() + driver = self.get_driver(driver) + if not isinstance(event, str): + if event is None: + event = channels + channels = event.broadcast_on() + + value = event.broadcast_with() + if not isinstance(channels, list): + channels = [channels] + + for channel in channels: + if not channel.authorized(self.application): + continue + event_class = event.broadcast_as() + + driver.set_options(store_config).channel( + channel.name, event_class, value + ) + else: + if not isinstance(channels, list): + channels = [channels] + for channel in channels: + driver.set_options(store_config).channel(channel, event, value) + + @classmethod + def routes(self, auth_route="/broadcasting/authorize"): + from .controllers import BroadcastingController + + return [ + Route.post(auth_route, BroadcastingController.authorize).name( + "broadcasting.authorize" + ) + ] diff --git a/src/masonite/broadcasting/CanBroadcast.py b/src/masonite/broadcasting/CanBroadcast.py new file mode 100644 index 000000000..39b15215c --- /dev/null +++ b/src/masonite/broadcasting/CanBroadcast.py @@ -0,0 +1,9 @@ +class CanBroadcast: + def broadcast_on(self): + return None + + def broadcast_with(self): + return vars(self) + + def broadcast_as(self): + return self.__class__.__name__ diff --git a/src/masonite/broadcasting/Channel.py b/src/masonite/broadcasting/Channel.py new file mode 100644 index 000000000..67e1ff5d3 --- /dev/null +++ b/src/masonite/broadcasting/Channel.py @@ -0,0 +1,6 @@ +class Channel: + def __init__(self, name): + self.name = name + + def authorized(self, application): + return True diff --git a/src/masonite/broadcasting/PresenceChannel.py b/src/masonite/broadcasting/PresenceChannel.py new file mode 100644 index 000000000..c16c8cf53 --- /dev/null +++ b/src/masonite/broadcasting/PresenceChannel.py @@ -0,0 +1,9 @@ +class PresenceChannel: + def __init__(self, name): + if not name.startswith("presence-"): + name = "presence-" + name + + self.name = name + + def authorized(self, application): + return bool(application.make("request").user()) diff --git a/src/masonite/broadcasting/PrivateChannel.py b/src/masonite/broadcasting/PrivateChannel.py new file mode 100644 index 000000000..74ecbf44f --- /dev/null +++ b/src/masonite/broadcasting/PrivateChannel.py @@ -0,0 +1,9 @@ +class PrivateChannel: + def __init__(self, name): + if not name.startswith("private-"): + name = "private-" + name + + self.name = name + + def authorized(self, application): + return bool(application.make("request").user()) diff --git a/src/masonite/broadcasting/__init__.py b/src/masonite/broadcasting/__init__.py new file mode 100644 index 000000000..4d7bf438c --- /dev/null +++ b/src/masonite/broadcasting/__init__.py @@ -0,0 +1,5 @@ +from .Channel import Channel +from .PrivateChannel import PrivateChannel +from .PresenceChannel import PresenceChannel +from .Broadcast import Broadcast +from .CanBroadcast import CanBroadcast diff --git a/src/masonite/broadcasting/controllers/BroadcastingController.py b/src/masonite/broadcasting/controllers/BroadcastingController.py new file mode 100644 index 000000000..f78cdc382 --- /dev/null +++ b/src/masonite/broadcasting/controllers/BroadcastingController.py @@ -0,0 +1,10 @@ +from ..Broadcast import Broadcast +from ...request import Request +from ...controllers import Controller + + +class BroadcastingController(Controller): + def authorize(self, request: Request, broadcast: Broadcast): + return broadcast.driver("pusher").authorize( + request.input("channel_name"), request.input("socket_id") + ) diff --git a/src/masonite/broadcasting/controllers/__init__.py b/src/masonite/broadcasting/controllers/__init__.py new file mode 100644 index 000000000..c09848264 --- /dev/null +++ b/src/masonite/broadcasting/controllers/__init__.py @@ -0,0 +1 @@ +from .BroadcastingController import BroadcastingController diff --git a/src/masonite/broadcasting/drivers/PusherDriver.py b/src/masonite/broadcasting/drivers/PusherDriver.py new file mode 100644 index 000000000..e80516cce --- /dev/null +++ b/src/masonite/broadcasting/drivers/PusherDriver.py @@ -0,0 +1,35 @@ +class PusherDriver: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + import pusher + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pusher' library. Run 'pip install pusher' to fix this." + ) + + if self.connection: + return self.connection + + self.connection = pusher.Pusher( + app_id=str(self.options.get("app_id")), + key=self.options.get("client"), + secret=self.options.get("secret"), + cluster=self.options.get("cluster"), + ssl=self.options.get("ssl"), + ) + + return self.connection + + def channel(self, channel, event, value): + return self.get_connection().trigger(channel, event, value) + + def authorize(self, channel, socket_id): + return self.get_connection().authenticate(channel=channel, socket_id=socket_id) diff --git a/src/masonite/broadcasting/drivers/__init__.py b/src/masonite/broadcasting/drivers/__init__.py new file mode 100644 index 000000000..37e675f0c --- /dev/null +++ b/src/masonite/broadcasting/drivers/__init__.py @@ -0,0 +1 @@ +from .PusherDriver import PusherDriver diff --git a/src/masonite/broadcasting/providers/BroadcastProvider.py b/src/masonite/broadcasting/providers/BroadcastProvider.py new file mode 100644 index 000000000..10f2737dd --- /dev/null +++ b/src/masonite/broadcasting/providers/BroadcastProvider.py @@ -0,0 +1,20 @@ +from ...providers import Provider +from ..Broadcast import Broadcast +from ..drivers import PusherDriver +from ...configuration import config + + +class BroadcastProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + broadcast = Broadcast(self.application).set_configuration( + config("broadcast.broadcasts") + ) + broadcast.add_driver("pusher", PusherDriver(self.application)) + + self.application.bind("broadcast", broadcast) + + def boot(self): + pass diff --git a/src/masonite/broadcasting/providers/__init__.py b/src/masonite/broadcasting/providers/__init__.py new file mode 100644 index 000000000..6ac8f9ca0 --- /dev/null +++ b/src/masonite/broadcasting/providers/__init__.py @@ -0,0 +1 @@ +from .BroadcastProvider import BroadcastProvider diff --git a/src/masonite/cache/Cache.py b/src/masonite/cache/Cache.py new file mode 100644 index 000000000..d2ebe8699 --- /dev/null +++ b/src/masonite/cache/Cache.py @@ -0,0 +1,59 @@ +class Cache: + def __init__(self, application, store_config=None): + self.application = application + self.drivers = {} + self.store_config = store_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.store_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.store_config.get("default")] + return self.drivers[name] + + def get_store_config(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def get_config_options(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def store(self, name="default"): + store_config = self.get_config_options(name) + driver = self.get_driver(self.get_config_options(name).get("driver")) + return driver.set_options(store_config) + + def add(self, *args, store=None, **kwargs): + return self.store(name=store).add(*args, **kwargs) + + def get(self, *args, store=None, **kwargs): + return self.store(name=store).get(*args, **kwargs) + + def put(self, *args, store=None, **kwargs): + return self.store(name=store).put(*args, **kwargs) + + def has(self, *args, store=None, **kwargs): + return self.store(name=store).has(*args, **kwargs) + + def forget(self, *args, store=None, **kwargs): + return self.store(name=store).forget(*args, **kwargs) + + def increment(self, *args, store=None, **kwargs): + return self.store(name=store).increment(*args, **kwargs) + + def decrement(self, *args, store=None, **kwargs): + return self.store(name=store).decrement(*args, **kwargs) + + def flush(self, *args, store=None, **kwargs): + return self.store(name=store).flush(*args, **kwargs) diff --git a/src/masonite/cache/__init__.py b/src/masonite/cache/__init__.py new file mode 100644 index 000000000..cf73978d4 --- /dev/null +++ b/src/masonite/cache/__init__.py @@ -0,0 +1 @@ +from .Cache import Cache diff --git a/src/masonite/cache/drivers/FileDriver.py b/src/masonite/cache/drivers/FileDriver.py new file mode 100644 index 000000000..fe061f80a --- /dev/null +++ b/src/masonite/cache/drivers/FileDriver.py @@ -0,0 +1,106 @@ +import os +from ...utils.filesystem import make_full_directory, modified_date +from pathlib import Path +import pendulum +import json +import glob + + +class FileDriver: + def __init__(self, application): + self.application = application + + def set_options(self, options): + self.options = options + if options.get("location"): + make_full_directory(options.get("location")) + return self + + def add(self, key, value, seconds=None): + exists = self.get(key) + if exists: + return exists + + return self.put(key, str(value), seconds=seconds) + + def get(self, key, default=None, **options): + if not self.has(key): + return None + + modified_at = self.get_modified_at(os.path.join(self._get_directory(), key)) + + with open(os.path.join(self._get_directory(), key), "r") as f: + value = f.read() + + if modified_at.add(seconds=self.get_cache_expiration(value)).is_past(): + self.forget(key) + return default + + value = self.get_value(value) + + return value + + def put(self, key, value, seconds=None, **options): + + time = self.get_expiration_time(seconds) + + if isinstance(value, (dict,)): + value = json.dumps(value) + + with open(os.path.join(self._get_directory(), key), "w") as f: + f.write(f"{time}:{value}") + + return value + + def has(self, key): + return Path(os.path.join(self._get_directory(), key)).exists() + + def increment(self, key, amount=1): + return self.put(key, str(int(self.get(key)) + amount)) + + def decrement(self, key, amount=1): + return self.put(key, str(int(self.get(key)) - amount)) + + def remember(self, key, callable): + value = self.get(key) + + if value: + return value + + callable(self) + + def forget(self, key): + try: + os.remove(os.path.join(self._get_directory(), key)) + return True + except FileNotFoundError: + return False + + def flush(self): + files = glob.glob(f"{self._get_directory()}/*") + for f in files: + os.remove(f) + + def _get_directory(self): + return self.options.get("location") + + def get_modified_at(self, filename): + return pendulum.from_timestamp(modified_date(filename)) + + def get_expiration_time(self, seconds): + if seconds is None: + seconds = 31557600 * 10 + + return seconds + + def get_value(self, value): + value = str(value.split(":", 1)[1]) + if value.isdigit(): + return str(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + return value + + def get_cache_expiration(self, value): + return int(value.split(":", 1)[0]) diff --git a/src/masonite/cache/drivers/MemcacheDriver.py b/src/masonite/cache/drivers/MemcacheDriver.py new file mode 100644 index 000000000..ce21e2349 --- /dev/null +++ b/src/masonite/cache/drivers/MemcacheDriver.py @@ -0,0 +1,90 @@ +import json + + +class MemcacheDriver: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + from pymemcache.client.base import Client + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pymemcache' library. Run 'pip install pymemcache' to fix this." + ) + + if not self.connection: + if str(self.options.get("port")) != "0": + self.connection = Client( + f"{self.options.get('host')}:{self.options.get('port')}" + ) + else: + self.connection = Client(f"{self.options.get('host')}") + + return self.connection + + def add(self, key, value): + if self.has(key): + return self.get(key) + + self.put(key, value) + return value + + def get(self, key, default=None, **options): + if not self.has(key): + return default + + return self.get_value( + self.get_connection().get(f"{self.get_name()}_cache_{key}") + ) + + def put(self, key, value, seconds=0, **options): + if isinstance(value, (dict, list)): + value = json.dumps(value) + + return self.get_connection().set( + f"{self.get_name()}_cache_{key}", value, expire=seconds + ) + + def has(self, key): + return self.get_connection().get(f"{self.get_name()}_cache_{key}") + + def increment(self, key, amount=1): + return self.put(key, str(int(self.get(key)) + amount)) + + def decrement(self, key, amount=1): + return self.put(key, str(int(self.get(key)) - amount)) + + def remember(self, key, callable): + value = self.get(key) + + if value: + return value + + callable(self) + + def forget(self, key): + return self.get_connection().delete(f"{self.get_name()}_cache_{key}") + + def flush(self): + return self.get_connection().flush_all() + + def get_name(self): + return self.options.get("name") + + def get_value(self, value): + if isinstance(value, bytes): + value = value.decode("utf-8") + + value = str(value) + if value.isdigit(): + return str(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + return value diff --git a/src/masonite/cache/drivers/RedisDriver.py b/src/masonite/cache/drivers/RedisDriver.py new file mode 100644 index 000000000..eab2e7c29 --- /dev/null +++ b/src/masonite/cache/drivers/RedisDriver.py @@ -0,0 +1,95 @@ +import json + + +class RedisDriver: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + import redis + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'redis' library. Run 'pip install redis' to fix this." + ) + + if not self.connection: + self.connection = redis.StrictRedis( + host=self.options.get("host"), + port=self.options.get("port"), + password=self.options.get("password"), + decode_responses=True, + ) + + return self.connection + + def add(self, key, value): + if self.has(key): + return self.get(key) + + self.put(key, value) + return value + + def get(self, key, default=None, **options): + if not self.has(key): + return default + return self.get_value( + self.get_connection().get(f"{self.get_name()}_cache_{key}") + ) + + def put(self, key, value, seconds=None, **options): + + time = self.get_expiration_time(seconds) + + if isinstance(value, (dict, list)): + value = json.dumps(value) + + return self.get_connection().set( + f"{self.get_name()}_cache_{key}", value, ex=time + ) + + def has(self, key): + return self.get_connection().get(f"{self.get_name()}_cache_{key}") + + def increment(self, key, amount=1): + return self.put(key, str(int(self.get(key)) + amount)) + + def decrement(self, key, amount=1): + return self.put(key, str(int(self.get(key)) - amount)) + + def remember(self, key, callable): + value = self.get(key) + + if value: + return value + + callable(self) + + def forget(self, key): + return self.get_connection().delete(f"{self.get_name()}_cache_{key}") + + def flush(self): + return self.get_connection().flushall() + + def get_name(self): + return self.options.get("name") + + def get_expiration_time(self, seconds): + if seconds is None: + seconds = 31557600 * 10 + + return seconds + + def get_value(self, value): + value = str(value) + if value.isdigit(): + return str(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + return value diff --git a/src/masonite/cache/drivers/__init__.py b/src/masonite/cache/drivers/__init__.py new file mode 100644 index 000000000..5c796ed0a --- /dev/null +++ b/src/masonite/cache/drivers/__init__.py @@ -0,0 +1,3 @@ +from .FileDriver import FileDriver +from .RedisDriver import RedisDriver +from .MemcacheDriver import MemcacheDriver diff --git a/src/masonite/cli.py b/src/masonite/cli.py deleted file mode 100644 index 81e966aa1..000000000 --- a/src/masonite/cli.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import sys -from pydoc import ErrorDuringImport -from cleo import Application -from .commands import NewCommand, InstallCommand -from . import __version__ - -sys.path.append(os.getcwd()) - -application = Application("Masonite Version:", __version__) -application.add(NewCommand()) -application.add(InstallCommand()) - - -try: - from wsgi import container - from cleo import Command - - for key, value in container.collect(Command).items(): - application.add(value) -except ErrorDuringImport as e: - print(e) -except ImportError: - pass - -if __name__ == "__main__": - application.run() diff --git a/src/masonite/commands/AuthCommand.py b/src/masonite/commands/AuthCommand.py index 7fc838337..1b48f9873 100644 --- a/src/masonite/commands/AuthCommand.py +++ b/src/masonite/commands/AuthCommand.py @@ -1,59 +1,34 @@ -"""New Authentication System Command.""" +"""Scaffold Auth Command.""" +from cleo import Command +from distutils.dir_util import copy_tree import os -import shutil -from cleo import Command -from ..helpers.filesystem import make_directory +from ..utils.location import controllers_path, views_path +from ..utils.filesystem import get_module_dir class AuthCommand(Command): """ - Creates an authentication system. + Creates a new authentication scaffold. auth """ - def handle(self): - self.info("Scaffolding Application ...") - module_path = os.path.dirname(os.path.realpath(__file__)) - - with open("routes/web.py", "a") as f: - # add all the routes - f.write("\nfrom masonite.auth import Auth \n") - f.write("ROUTES += Auth.routes()") - f.write("\n") + def __init__(self, application): + super().__init__() + self.app = application - make_directory( - os.path.join(os.getcwd(), "app/http/controllers/auth/LoginController.py") + def handle(self): + copy_tree( + self.get_template_path(), + views_path("auth"), ) + copy_tree(self.get_controllers_path(), controllers_path("auth")) - # move controllers - shutil.copyfile( - module_path + "/../snippets/auth/controllers/LoginController.py", - os.getcwd() + "/app/http/controllers/auth/LoginController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/RegisterController.py", - os.getcwd() + "/app/http/controllers/auth/RegisterController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/HomeController.py", - os.getcwd() + "/app/http/controllers/auth/HomeController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/ConfirmController.py", - os.getcwd() + "/app/http/controllers/auth/ConfirmController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/PasswordController.py", - os.getcwd() + "/app/http/controllers/auth/PasswordController.py", - ) - # move templates - shutil.copytree( - module_path + "/../snippets/auth/templates/auth", - os.getcwd() + "/resources/templates/auth", - ) + self.info("Auth scaffolded successfully!") - self.info( - "Project Scaffolded. You now have 5 new controllers, 7 new templates and 9 new routes" - ) + def get_template_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/templates/auth") + + def get_controllers_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/controllers/auth") diff --git a/src/masonite/commands/BaseScaffoldCommand.py b/src/masonite/commands/BaseScaffoldCommand.py deleted file mode 100644 index efde70275..000000000 --- a/src/masonite/commands/BaseScaffoldCommand.py +++ /dev/null @@ -1,54 +0,0 @@ -from cleo import Command - -from ..app import App -from ..helpers.filesystem import make_directory -from ..view import View - - -class BaseScaffoldCommand(Command): - """ - Creates a model. - - model - {name : Name of the model} - """ - - scaffold_name = "Example" - suffix = "" - postfix = "" - prefix = "" - file_extension = ".py" - base_directory = "app/example/" - file_to_lower = False - - template = "/masonite/snippets/scaffold/model" - - def handle(self): - # If postfix already exists as part of the name, don't add it again - if self.postfix and self.argument("name").lower().endswith( - self.postfix.lower() - ): - class_name = self.argument("name") - else: - class_name = self.argument("name") + self.postfix - - view = View(App()) - class_directory = "{}{}{}{}".format( - self.base_directory, class_name, self.suffix, self.file_extension - ) - - if self.file_to_lower: - class_directory = class_directory.lower() - - if not make_directory(class_directory): - return self.line_error("{0} Already Exists!".format(self.scaffold_name)) - - with open(class_directory, "w+") as f: - if view.exists(self.template): - f.write( - view.render( - self.template, - {"class": self.prefix + class_name.split("/")[-1]}, - ).rendered_template - ) - self.info("{} Created Successfully!".format(self.scaffold_name)) diff --git a/src/masonite/commands/CommandCapsule.py b/src/masonite/commands/CommandCapsule.py new file mode 100644 index 000000000..6d700cac7 --- /dev/null +++ b/src/masonite/commands/CommandCapsule.py @@ -0,0 +1,12 @@ +class CommandCapsule: + def __init__(self, command_application): + self.command_application = command_application + self.commands = [] + + def add(self, *commands): + self.commands.append(commands) + self.command_application.add_commands(*commands) + return self + + def run(self): + return self.command_application.run() diff --git a/src/masonite/commands/CommandCommand.py b/src/masonite/commands/CommandCommand.py deleted file mode 100644 index d2079fdf6..000000000 --- a/src/masonite/commands/CommandCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Creates New Command Command.""" -from ..commands import BaseScaffoldCommand - - -class CommandCommand(BaseScaffoldCommand): - """ - Creates a new command. - - command - {name : Name of the command you would like to create} - """ - - scaffold_name = "Command" - postfix = "Command" - template = "/masonite/snippets/scaffold/command" - base_directory = "app/commands/" diff --git a/src/masonite/commands/ControllerCommand.py b/src/masonite/commands/ControllerCommand.py deleted file mode 100644 index ec46b25a2..000000000 --- a/src/masonite/commands/ControllerCommand.py +++ /dev/null @@ -1,42 +0,0 @@ -"""New Controller Command.""" -from ..view import View -from ..app import App -from ..helpers.filesystem import make_directory - -from cleo import Command - - -class ControllerCommand(Command): - """ - Creates a controller. - - controller - {name : Name of the controller you would like to create} - {--r|--resource : Create a controller as a resource} - {--e|--exact : For add the name controller without `Controller` text} - """ - - def handle(self): - controller = self.argument("name") - view = View(App()) - - if not self.option("exact"): - controller = controller + "Controller" - - if not make_directory("app/http/controllers/{0}.py".format(controller)): - return self.line_error("{0} Controller Exists!".format(controller)) - - with open("app/http/controllers/{0}.py".format(controller), "w+") as f: - if view.exists("/masonite/snippets/scaffold/controller"): - if self.option("resource"): - template = "/masonite/snippets/scaffold/controller_resource" - else: - template = "/masonite/snippets/scaffold/controller" - - f.write( - view.render( - template, {"class": controller.split("/")[-1]} - ).rendered_template - ) - - self.info("Controller Created Successfully!") diff --git a/src/masonite/commands/DownCommand.py b/src/masonite/commands/DownCommand.py deleted file mode 100644 index 7512ccaab..000000000 --- a/src/masonite/commands/DownCommand.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Down Command.""" - -from cleo import Command - - -class DownCommand(Command): - """ - Puts the server in a maintenance state. - - down - """ - - def handle(self): - with open("bootstrap/down", "w+"): - pass diff --git a/src/masonite/commands/Entry.py b/src/masonite/commands/Entry.py new file mode 100644 index 000000000..c25347fe0 --- /dev/null +++ b/src/masonite/commands/Entry.py @@ -0,0 +1,23 @@ +"""Craft Command. + +This module is really used for backup only if the masonite CLI cannot import this for you. +This can be used by running "python craft". This module is not ran when the CLI can +successfully import commands for you. +""" + +from cleo import Application +from .ProjectCommand import ( + ProjectCommand, +) + +from .KeyCommand import KeyCommand +from .InstallCommand import InstallCommand + +application = Application("Masonite Starter Version:", 4.0) + +application.add(ProjectCommand()) +application.add(KeyCommand()) +application.add(InstallCommand()) + +if __name__ == "__main__": + application.run() diff --git a/src/masonite/commands/InfoCommand.py b/src/masonite/commands/InfoCommand.py deleted file mode 100644 index 9b382059b..000000000 --- a/src/masonite/commands/InfoCommand.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Displays Information Command.""" -import math -import os -import platform -import sys -import psutil - -from cleo import Command -from tabulate import tabulate - -from ..__version__ import __version__, __cookie_cutter_version__ - - -class InfoCommand(Command): - """ - Displays environment info for debugging. - - info - """ - - def handle(self): - from ..cli import application - - rows = [] - - rows.append(["System Information", self._get_system_info()]) - mem = math.ceil(psutil.virtual_memory().total / 1024 / 1024 / 1024.0) - rows.append(["System Memory", str(mem) + " GB"]) - rows.append(["Python Version", self._get_python_info()]) - rows.append(["Virtual Environment", self._check_virtual_environment()]) - rows.append(["Masonite Version", __version__]) - rows.append(["Cookie Cutter Version", __cookie_cutter_version__]) - - if "APP_ENV" in os.environ: - rows.append(["APP_ENV", os.environ.get("APP_ENV")]) - - if "APP_DEBUG" in os.environ: - rows.append(["APP_DEBUG", os.environ.get("APP_DEBUG")]) - - self.info("") - self.info(tabulate(rows, headers=["Environment Information", ""])) - self.info("") - - def _get_python_info(self): - py_version = platform.python_version() - py_implementation = platform.python_implementation() - return py_implementation + " " + py_version - - def _check_virtual_environment(self): - if hasattr(sys, "real_prefix") or "VIRTUAL_ENV" in os.environ: - return u"\u2713" # currently running in virtual env - return "X" - - def _get_system_info(self): - bits, _ = platform.architecture() - operating_system, _, _, _, arch, _ = platform.uname() - - if operating_system.lower() == "darwin": - operating_system = "MacOS" - return "{} {} {}".format(operating_system, arch, bits) diff --git a/src/masonite/commands/InstallCommand.py b/src/masonite/commands/InstallCommand.py index be6dca486..728a17cc0 100644 --- a/src/masonite/commands/InstallCommand.py +++ b/src/masonite/commands/InstallCommand.py @@ -28,7 +28,7 @@ def handle(self): call(["pipenv", "install"]) if not self.option("no-key"): - call(["pipenv", "shell", "craft", "key", "--store"]) + call(["pipenv", "shell", "new", "key", "--store"]) return except Exception: @@ -42,10 +42,10 @@ def handle(self): raise OSError("Could not find a Pipfile or a requirements.txt file") if not self.option("no-key"): try: - call(["craft", "key", "--store"]) + self.call("key", "--store") except Exception: self.line_error( - "Could not successfully install Masonite. This could happen for several reasons but likely because of how craft is installed on your system and you could be hitting permission issues when craft is fetching required modules." + "Could not successfully install Masonite. This could happen for several reasons but likely because of how Masonite is installed on your system and you could be hitting permission issues when Masonite is fetching required modules." " If you have correctly followed the installation instructions then you should try everything again but start inside an virtual environment first to avoid any permission issues. If that does not work then seek help in" " the Masonite Slack channel. Links can be found on GitHub in the main Masonite repo." ) diff --git a/src/masonite/commands/JobCommand.py b/src/masonite/commands/JobCommand.py deleted file mode 100644 index ef46e8baf..000000000 --- a/src/masonite/commands/JobCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Job Command.""" -from ..commands import BaseScaffoldCommand - - -class JobCommand(BaseScaffoldCommand): - """ - Creates a new Job. - - job - {name : Name of the job you want to create} - """ - - scaffold_name = "Job" - template = "/masonite/snippets/scaffold/job" - base_directory = "app/jobs/" - postfix = "Job" diff --git a/src/masonite/commands/KeyCommand.py b/src/masonite/commands/KeyCommand.py index 964f7ad2a..b3cdccf12 100644 --- a/src/masonite/commands/KeyCommand.py +++ b/src/masonite/commands/KeyCommand.py @@ -22,8 +22,8 @@ def handle(self): data = file.readlines() for line_number, line in enumerate(data): - if line.startswith("KEY="): - data[line_number] = "KEY={}\n".format(key) + if line.startswith("APP_KEY="): + data[line_number] = "APP_KEY={}\n".format(key) break with open(".env", "w") as file: diff --git a/src/masonite/commands/MailableCommand.py b/src/masonite/commands/MailableCommand.py deleted file mode 100644 index 4cd12c581..000000000 --- a/src/masonite/commands/MailableCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Job Command.""" -from ..commands import BaseScaffoldCommand - - -class MailableCommand(BaseScaffoldCommand): - """ - Creates a new Mailable. - - mailable - {name : Name of the job you want to create} - """ - - scaffold_name = "Mailable" - template = "/masonite/snippets/scaffold/mailable" - base_directory = "app/mailable/" - postfix = "Mailable" diff --git a/src/masonite/commands/MakeControllerCommand.py b/src/masonite/commands/MakeControllerCommand.py new file mode 100644 index 000000000..6a173d2ff --- /dev/null +++ b/src/masonite/commands/MakeControllerCommand.py @@ -0,0 +1,41 @@ +"""New Controller Command.""" +from cleo import Command +import inflection +import os + +from ..utils.location import controllers_path +from ..utils.filesystem import get_module_dir, render_stub_file + + +class MakeControllerCommand(Command): + """ + Creates a new controller class. + + controller + {name : Name of the controller} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + if not name.endswith("Controller"): + name += "Controller" + + content = render_stub_file(self.get_controllers_path(), name) + + filename = f"{name}.py" + with open(controllers_path(filename), "w") as f: + f.write(content) + + self.info(f"Controller Created ({controllers_path(filename, absolute=False)})") + + def get_template_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/templates/") + + def get_controllers_path(self): + return os.path.join( + get_module_dir(__file__), "../stubs/controllers/Controller.py" + ) diff --git a/src/masonite/commands/MakeJobCommand.py b/src/masonite/commands/MakeJobCommand.py new file mode 100644 index 000000000..98a842ea6 --- /dev/null +++ b/src/masonite/commands/MakeJobCommand.py @@ -0,0 +1,38 @@ +"""New Key Command.""" +from cleo import Command +import inflection +import os + +from ..utils.filesystem import make_directory, render_stub_file, get_module_dir +from ..utils.location import jobs_path + + +class MakeJobCommand(Command): + """ + Creates a new job class. + + job + {name : Name of the job} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_jobs_path(), name) + + filename = f"{name}.py" + filepath = jobs_path(filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Job Created ({jobs_path(filename, absolute=False)})") + + def get_template_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/templates/") + + def get_jobs_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/jobs/Job.py") diff --git a/src/masonite/commands/MakeMailableCommand.py b/src/masonite/commands/MakeMailableCommand.py new file mode 100644 index 000000000..d76db4d63 --- /dev/null +++ b/src/masonite/commands/MakeMailableCommand.py @@ -0,0 +1,39 @@ +"""New Mailable Command.""" +from cleo import Command +import inflection +import os + +from ..utils.filesystem import make_directory, render_stub_file, get_module_dir +from ..utils.str import as_filepath +from ..utils.location import base_path + + +class MakeMailableCommand(Command): + """ + Creates a new mailable class. + + mailable + {name : Name of the mailable} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_mailables_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("mailables.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Mailable Created ({relative_filename})") + + def get_mailables_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/mailable/Mailable.py") diff --git a/src/masonite/commands/MakePolicyCommand.py b/src/masonite/commands/MakePolicyCommand.py new file mode 100644 index 000000000..7880d7aee --- /dev/null +++ b/src/masonite/commands/MakePolicyCommand.py @@ -0,0 +1,57 @@ +"""New Policy Command.""" +import inflection +import os +from cleo import Command + +from ..utils.filesystem import make_directory + + +class MakePolicyCommand(Command): + """ + Creates a new policy class. + + policy + {name : Name of the policy} + {--model=? : Create a policy for a model with a set of predefined methods} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + if not name.endswith("Policy"): + name += "Policy" + + if self.option("model"): + with open(self.get_model_policy_path(), "r") as f: + content = f.read() + content = content.replace("__class__", name) + else: + with open(self.get_base_policy_path(), "r") as f: + content = f.read() + content = content.replace("__class__", name) + + file_name = os.path.join( + self.app.make("policies.location").replace(".", "/"), name + ".py" + ) + + make_directory(file_name) + + with open(file_name, "w") as f: + f.write(content) + self.info(f"Policy Created ({file_name})") + + def get_template_path(self): + current_path = os.path.dirname(os.path.realpath(__file__)) + + return os.path.join(current_path, "../stubs/templates/") + + def get_base_policy_path(self): + current_path = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(current_path, "../stubs/policies/Policy.py") + + def get_model_policy_path(self): + current_path = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(current_path, "../stubs/policies/ModelPolicy.py") diff --git a/src/masonite/commands/MakeProviderCommand.py b/src/masonite/commands/MakeProviderCommand.py new file mode 100644 index 000000000..720decefe --- /dev/null +++ b/src/masonite/commands/MakeProviderCommand.py @@ -0,0 +1,39 @@ +"""New Provider Command.""" +from cleo import Command +import inflection +import os + +from ..utils.filesystem import make_directory, render_stub_file, get_module_dir +from ..utils.str import as_filepath +from ..utils.location import base_path + + +class MakeProviderCommand(Command): + """ + Creates a new mailable class. + + provider + {name : Name of the mailable} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_providers_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("providers.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Provider Created ({relative_filename})") + + def get_providers_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/providers/Provider.py") diff --git a/src/masonite/commands/MiddlewareCommand.py b/src/masonite/commands/MiddlewareCommand.py deleted file mode 100644 index c8b7b2c5c..000000000 --- a/src/masonite/commands/MiddlewareCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Middleware Command.""" -from ..commands import BaseScaffoldCommand - - -class MiddlewareCommand(BaseScaffoldCommand): - """ - Creates a middleware. - - middleware - {name : Name of the middleware} - """ - - scaffold_name = "Middleware" - suffix = "Middleware" - template = "/masonite/snippets/scaffold/middleware" - base_directory = "app/http/middleware/" diff --git a/src/masonite/commands/ModelCommand.py b/src/masonite/commands/ModelCommand.py deleted file mode 100644 index 419479fc4..000000000 --- a/src/masonite/commands/ModelCommand.py +++ /dev/null @@ -1,51 +0,0 @@ -"""New Model Command.""" -from ..app import App -from ..helpers.filesystem import make_directory -from ..view import View - -from cleo import Command - - -class ModelCommand(Command): - """ - Creates a model. - - model - {name : Name of the model} - {--m|migration : Create a migration for specified model} - {--s|seed=? : Create a database seed} - """ - - scaffold_name = "Model" - template = "/masonite/snippets/scaffold/model" - base_directory = "app/" - - def handle(self): - class_name = self.argument("name") - view = View(App()) - class_directory = "{}{}.py".format(self.base_directory, class_name) - - if not make_directory(class_directory): - return self.line_error("{0} Already Exists!".format(self.scaffold_name)) - - with open(class_directory, "w+") as f: - if view.exists(self.template): - f.write( - view.render( - self.template, {"class": class_name.split("/")[-1]} - ).rendered_template - ) - self.info("{} Created Successfully!".format(self.scaffold_name)) - - if self.option("migration"): - model_name = class_name.lower() + "s" - self.call( - "migration", - [("name", "create_{}_table".format(model_name)), ("-c", model_name)], - ) - - if self.option("seed"): - seed_file = model_name - seed_file = self.option("seed") - - self.call("seed", [("table", seed_file)]) diff --git a/src/masonite/commands/ModelDocstringCommand.py b/src/masonite/commands/ModelDocstringCommand.py deleted file mode 100644 index a5f5d4d6e..000000000 --- a/src/masonite/commands/ModelDocstringCommand.py +++ /dev/null @@ -1,34 +0,0 @@ -"""A ModelDocstringCommand Command.""" - -from cleo import Command - - -class ModelDocstringCommand(Command): - """ - Generate a model docstring based on a table definition - - model:docstring - {table : Name of the table to generate the docstring for} - {--c|connection=default : The connection to use} - """ - - def handle(self): - from config.database import DB - - if self.option("connection") == "default": - conn = DB.get_schema_manager().list_table_columns(self.argument("table")) - else: - conn = ( - DB.connection(self.option("connection")) - .get_schema_manager() - .list_table_columns(self.argument("table")) - ) - - docstring = '"""Model Definition (generated with love by Masonite) \n\n' - for name, column in conn.items(): - length = "({})".format(column._length) if column._length else "" - docstring += "{}: {}{} default: {}\n".format( - name, column.get_type(), length, column.get_default() - ) - - print(docstring + '"""') diff --git a/src/masonite/commands/PresetCommand.py b/src/masonite/commands/PresetCommand.py deleted file mode 100644 index 326e0060d..000000000 --- a/src/masonite/commands/PresetCommand.py +++ /dev/null @@ -1,71 +0,0 @@ -"""New Preset System Command.""" -from cleo import Command -from ..commands.presets.React import React -from ..commands.presets.Vue import Vue -from ..commands.presets.Vue3 import Vue3 -from ..commands.presets.Bootstrap import Bootstrap -from ..commands.presets.Remove import Remove -from ..commands.presets.Tailwind import Tailwind - - -class PresetCommand(Command): - """ - Swap the front-end scaffolding for the application - - preset - {name : Name of the preset} - """ - - def handle(self): - self.info("Scaffolding Application ...") - preset_name = self.argument("name") - presets_list = ["react", "vue", "vue3", "remove", "bootstrap", "tailwind2"] - if preset_name not in presets_list: - raise ValueError("Invalid preset. Choices are: {0}".format(presets_list)) - return getattr(self, preset_name)() - - def remove(self): - """Removes frontend scaffolding""" - Remove().install() - self.info("Frontend scaffolding removed successfully.") - - def react(self): - """Add React frontend while also removing Vue (if it was previously selected)""" - React().install() - self.info("React scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - - def vue(self): - """Add Vue frontend while also removing React (if it was previously selected)""" - Vue().install() - self.info("Vue scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - - def vue3(self): - """Add Vue 3.0 frontend while also removing React (if it was previously selected)""" - Vue3().install() - self.info("Vue 3.0 scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - self.comment("Then you can use the view app_vue3 as demo.") - - def bootstrap(self): - """Add Bootstrap Sass scafolding""" - Bootstrap().install() - self.info("Bootstrap scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - - def tailwind2(self): - """Add Tailwind CSS 2.X.""" - Tailwind().install() - self.info("Tailwind CSS 2 scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) diff --git a/src/masonite/commands/NewCommand.py b/src/masonite/commands/ProjectCommand.py similarity index 87% rename from src/masonite/commands/NewCommand.py rename to src/masonite/commands/ProjectCommand.py index 19ff709d6..66f8860bc 100644 --- a/src/masonite/commands/NewCommand.py +++ b/src/masonite/commands/ProjectCommand.py @@ -2,6 +2,7 @@ import os import shutil import zipfile +import tempfile import requests from io import BytesIO @@ -11,14 +12,13 @@ ProjectProviderHttpError, ProjectTargetNotEmpty, ) -from .. import __cookie_cutter_version__ -class NewCommand(Command): +class ProjectCommand(Command): """ Creates a new Masonite project - new + project {target? : Path of you Masonite project} {--b|--branch=False : Specify which branch from the Masonite repo you would like to install} {--r|--release=False : Specify which version of Masonite you would like to install} @@ -29,6 +29,7 @@ class NewCommand(Command): providers = ["github", "gitlab"] # timeout in seconds for requests made to providers TIMEOUT = 20 + BRANCH = 4.0 def __init__(self, *args, **kwargs): super().__init__() @@ -51,11 +52,6 @@ def handle(self): to_dir = os.path.join(os.getcwd(), target) self.check_target_does_not_exist(to_dir) - for directory in os.listdir(os.getcwd()): - if directory.startswith("masonite-"): - return self.comment( - 'There is a folder that starts with "masonite-" and therefore craft cannot create a new project' - ) try: if repo and provider not in self.providers: return self.line_error( @@ -71,7 +67,7 @@ def handle(self): and branch == "False" and version == "False" ): - branch = __cookie_cutter_version__ + branch = self.BRANCH if branch != "False": branch_data = self.get_branch_provider_data(provider, branch) @@ -141,47 +137,46 @@ def handle(self): self.info("Crafting Application ...") + # create a tmp directory to extract project template + tmp_dir = tempfile.TemporaryDirectory() try: - # Python 3 request = requests.get(zipurl) with zipfile.ZipFile(BytesIO(request.content)) as zfile: - extracted_name = zfile.infolist()[0].filename - zfile.extractall(os.getcwd()) - success = True - except ImportError: - # Python 2 - import urllib - - r = urllib.urlopen(zipurl) - with zipfile.ZipFile(BytesIO(r.read())) as z: - extracted_name = z.infolist()[0].filename - z.extractall(os.getcwd()) - + zfile.extractall(tmp_dir.name) + extracted_path = os.path.join( + tmp_dir.name, zfile.infolist()[0].filename + ) success = True except Exception as e: self.line_error("An error occured when downloading {0}".format(zipurl)) raise e if success: - from_dir = os.path.join(os.getcwd(), extracted_name) if target == ".": - for file in os.listdir(from_dir): - shutil.move(os.path.join(from_dir, file), to_dir) - os.rmdir(from_dir) + shutil.move(extracted_path, os.getcwd()) else: - os.rename(from_dir, to_dir) + shutil.move(extracted_path, to_dir) + + # remove tmp directory + tmp_dir.cleanup() + + if target == ".": + from_dir = os.path.join(os.getcwd(), zfile.infolist()[0].filename) + + for file in os.listdir(zfile.infolist()[0].filename): + shutil.move(os.path.join(from_dir, file), os.getcwd()) + os.rmdir(from_dir) self.info("Application Created Successfully!") - self.info("Installing Dependencies ") if target == ".": + self.info("Installing Dependencies...") self.call("install") - self.info( - "Installed Successfully. Just Run `craft serve` To Start Your Application." + "Installed Successfully. Just Run `python craft serve` To Start Your Application." ) else: self.info( - "Project Created Successfully. You now will have to CD into your new '{}' directory and run `craft install` to complete the installation".format( + "You now will have to go into your new '{}' directory and run `start install` to complete the installation".format( target ) ) @@ -199,7 +194,7 @@ def check_target_does_not_exist(self, target): if os.path.isdir(target): raise ProjectTargetNotEmpty( - "{} already exists. You must craft a project in a not existing directory.".format( + "{} already exists. You must craft a project in a new directory.".format( target ) ) diff --git a/src/masonite/commands/ProviderCommand.py b/src/masonite/commands/ProviderCommand.py deleted file mode 100644 index ad0e3bd7c..000000000 --- a/src/masonite/commands/ProviderCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Providers Command.""" -from ..commands import BaseScaffoldCommand - - -class ProviderCommand(BaseScaffoldCommand): - """ - Creates a new Service Provider. - - provider - {name : Name of the Service Provider you want to create} - """ - - scaffold_name = "Service Provider" - base_directory = "app/providers/" - template = "/masonite/snippets/scaffold/provider" - postfix = "Provider" diff --git a/src/masonite/commands/PublishCommand.py b/src/masonite/commands/PublishCommand.py deleted file mode 100644 index e87bbf992..000000000 --- a/src/masonite/commands/PublishCommand.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Publish Service Providers""" -from cleo import Command - - -class PublishCommand(Command): - """ - Publishes a Service Provider - - publish - {name : Name of the Service Provider you want to publish} - {--t|tag=None : The tag of the provider you want to publish} - """ - - def handle(self): - from wsgi import container - - for provider in container.make("Providers"): - if provider.__class__.__name__ == self.argument("name"): - if self.option("tag") != "None": - provider.publish(tag=self.option("tag")) - provider.publish_migrations(tag=self.option("tag")) - - provider.publish() - provider.publish_migrations() - - return - - raise ValueError("Could not find the {} provider".format(self.argument("name"))) diff --git a/src/masonite/commands/PublishPackageCommand.py b/src/masonite/commands/PublishPackageCommand.py new file mode 100644 index 000000000..acc16e4e7 --- /dev/null +++ b/src/masonite/commands/PublishPackageCommand.py @@ -0,0 +1,46 @@ +from cleo import Command + + +class PublishPackageCommand(Command): + """ + Publish package files to your project + + package:publish + {name : Name of the package} + {--r|--resources=? : Resources to publish in you project (config, views, migrations...)} + {--d|--dry=? : Just show a preview of what will be published into your project} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + from ..packages.providers import PackageProvider + + name = self.argument("name") + selected_provider = None + for provider in self.app.get_providers(): + if isinstance(provider, PackageProvider) and provider.package.name == name: + selected_provider = provider + if not selected_provider: + self.line_error( + f"No package has been registered under the name {name}.", style="error" + ) + return + + if self.option("resources"): + resources = self.option("resources").split(",") + else: + resources = None + dry = self.option("dry") + published_resources = selected_provider.publish(resources, dry) + if dry: + self.info("The following files would be published:") + else: + self.info("The following files have been published:") + for resource, files in published_resources.items(): + self.info("\n") + self.info(f"{resource.capitalize()}:") + for f in files: + self.info(f" - {f}") diff --git a/src/masonite/commands/QueueFailedCommand.py b/src/masonite/commands/QueueFailedCommand.py new file mode 100644 index 000000000..34da72d05 --- /dev/null +++ b/src/masonite/commands/QueueFailedCommand.py @@ -0,0 +1,33 @@ +"""New Key Command.""" +from cleo import Command +import os + +from ..utils.filesystem import make_directory, get_module_dir +from ..utils.time import migration_timestamp +from ..utils.location import base_path + + +class QueueFailedCommand(Command): + """ + Creates a failed jobs table + + queue:failed + {--d|--directory=databases/migrations : Specifies the directory to create the migration in} + """ + + def handle(self): + with open( + os.path.join( + get_module_dir(__file__), "stubs/queue/create_failed_jobs_table.py" + ) + ) as fp: + output = fp.read() + + filename = f"{migration_timestamp()}_create_failed_jobs_table.py" + path = os.path.join(base_path(self.option("directory")), filename) + make_directory(path) + + with open(path, "w") as fp: + fp.write(output) + + self.info(f"Migration file created: {filename}") diff --git a/src/masonite/commands/QueueRetryCommand.py b/src/masonite/commands/QueueRetryCommand.py new file mode 100644 index 000000000..5c6d0de3c --- /dev/null +++ b/src/masonite/commands/QueueRetryCommand.py @@ -0,0 +1,28 @@ +"""New Key Command.""" +from cleo import Command + + +class QueueRetryCommand(Command): + """ + Puts all failed queue jobs back onto the queue. + + queue:retry + {--c|--connection=default : Specifies the database connection if using database driver.} + {--queue=default : The queue to listen to} + {--d|driver=None : Specify the driver you would like to connect to} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + driver = None if self.option("driver") == "None" else self.option("driver") + + return self.app.make("queue").retry( + { + "driver": driver, + "connection": self.option("connection"), + "queue": self.option("queue"), + } + ) diff --git a/src/masonite/commands/QueueTableCommand.py b/src/masonite/commands/QueueTableCommand.py index 59155e365..11b9ca92f 100644 --- a/src/masonite/commands/QueueTableCommand.py +++ b/src/masonite/commands/QueueTableCommand.py @@ -1,26 +1,36 @@ -"""A QueueTableCommand Command""" - - +"""New Queue Table Command.""" from cleo import Command +import os -from ..helpers.filesystem import copy_migration +from ..utils.filesystem import make_directory, get_module_dir +from ..utils.time import migration_timestamp +from ..utils.location import base_path class QueueTableCommand(Command): """ - Create migration files for the queue feature + Creates the jobs table queue:table - {--failed : Created the queue failed table} - {--jobs : Created the queue failed table} + {--d|--directory=databases/migrations : Specifies the directory to create the migration in} """ def handle(self): - if self.option("failed"): - copy_migration("masonite/snippets/migrations/create_failed_jobs_table.py") - self.info("Failed queue table migration created successfully") - if self.option("jobs"): - copy_migration("masonite/snippets/migrations/create_queue_jobs_table.py") - self.info("Jobs queue table migration created successfully") - - self.line("Please specify the --failed or --jobs flags") + with open( + os.path.join( + get_module_dir(__file__), "../stubs/queue/create_queue_jobs_table.py" + ) + ) as fp: + output = fp.read() + + relative_filename = os.path.join( + self.option("directory"), + f"{migration_timestamp()}_create_queue_jobs_table.py", + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as fp: + fp.write(output) + + self.info(f"Migration file created: {relative_filename}") diff --git a/src/masonite/commands/QueueWorkCommand.py b/src/masonite/commands/QueueWorkCommand.py index ecb5af2f3..66332ac99 100644 --- a/src/masonite/commands/QueueWorkCommand.py +++ b/src/masonite/commands/QueueWorkCommand.py @@ -1,38 +1,31 @@ -"""A QueueWorkCommand Command.""" - +"""Queue Work Command.""" from cleo import Command -from .. import Queue - class QueueWorkCommand(Command): """ - Start the queue worker + Creates a new queue worker to consume queue jobs queue:work - {--c|channel=default : The channel to listen on the queue} - {--queue=default : The queue to listen to} - {--d|driver=default : Specify the driver you would like to connect to} - {--f|fair : Send jobs to queues that have no jobs instead of randomly selecting a queue} - {--p|poll=0 : Specify the amount of time a worker should poll} - {--failed : Run only the failed jobs} + {--c|--connection : Specifies the database connection if using database driver.} + {--queue=? : The queue to listen to} + {--d|driver=? : Specify the driver you would like to use} + {--p|poll=? : Specify the seconds a worker should wait before fetching new jobs} + {--attempts=? : Specify the number of times a job should be retried before it fails} """ - def handle(self): - from wsgi import container + def __init__(self, application): + super().__init__() + self.app = application - if self.option("driver") == "default": - queue = container.make(Queue) - else: - queue = container.make(Queue).driver(self.option("driver")) + def handle(self): + options = {} + options.update({"driver": self.option("driver")}) + options.update({"poll": self.option("poll") or "1"}) + options.update({"attempts": self.option("attempts") or "3"}) + options.update({"queue": self.option("queue") or "default"}) - if self.option("failed"): - queue.run_failed_jobs() - return + if self.option("verbose"): + options.update({"verbosity": "v" + self.option("verbose")}) - queue.connect().consume( - self.option("channel"), - fair=self.option("fair"), - poll=self.option("poll"), - queue=self.option("queue"), - ) + return self.app.make("queue").consume(options) diff --git a/src/masonite/commands/RoutesCommand.py b/src/masonite/commands/RoutesCommand.py deleted file mode 100644 index 093a3228f..000000000 --- a/src/masonite/commands/RoutesCommand.py +++ /dev/null @@ -1,31 +0,0 @@ -"""List Routes Command.""" -from cleo import Command -from tabulate import tabulate - - -class RoutesCommand(Command): - """ - List out all routes of the application. - - show:routes - """ - - def handle(self): - from wsgi import container - - web_routes = container.make("WebRoutes") - - routes = [["Method", "Path", "Name", "Domain", "Middleware"]] - - for route in web_routes: - routes.append( - [ - route.method_type, - route.route_url, - route.named_route, - route.required_domain, - ",".join(route.list_middleware), - ] - ) - - print(tabulate(routes, headers="firstrow", tablefmt="rst")) diff --git a/src/masonite/commands/SeedCommand.py b/src/masonite/commands/SeedCommand.py deleted file mode 100644 index 99e93ac8e..000000000 --- a/src/masonite/commands/SeedCommand.py +++ /dev/null @@ -1,31 +0,0 @@ -"""New Seeder Command.""" -import subprocess -import os - -from cleo import Command - - -class SeedCommand(Command): - """ - Create a seeder to seed a database. - - seed - {table : Name of the table to seed} - """ - - def handle(self): - table = self.argument("table").lower() - subprocess.call( - ["orator make:seed {}_table_seeder -p databases/seeds".format(table)], - shell=True, - ) - - self.check_init_file() - - def check_init_file(self): - os.makedirs(os.path.dirname("databases/seeds/__init__.py"), exist_ok=True) - - with open("databases/seeds/__init__.py") as f: - if "sys.path.append(os.getcwd())" not in f.read(): - with open("databases/seeds/__init__.py", "w+") as fp: - fp.write("import os\nimport sys\nsys.path.append(os.getcwd())\n") diff --git a/src/masonite/commands/SeedRunCommand.py b/src/masonite/commands/SeedRunCommand.py deleted file mode 100644 index 119add642..000000000 --- a/src/masonite/commands/SeedRunCommand.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Run Seed Command.""" -import subprocess - -from cleo import Command - - -class SeedRunCommand(Command): - """ - Run seed for database. - - seed:run - {table=None : Name of the table to seed} - """ - - def handle(self): - - table = self.argument("table").lower() - if not table == "none": - seeder = "--seeder {}_table_seeder".format(table.lower()) - else: - seeder = "" - - subprocess.call( - [ - "orator db:seed -p databases/seeds -c config/database.py -f {}".format( - seeder - ), - ], - shell=True, - ) diff --git a/src/masonite/commands/ServeCommand.py b/src/masonite/commands/ServeCommand.py index 41fd11c52..03f977b70 100644 --- a/src/masonite/commands/ServeCommand.py +++ b/src/masonite/commands/ServeCommand.py @@ -1,11 +1,8 @@ -import time -import os +import sys -from hupper.logger import DefaultLogger, LogLevel -from hupper.reloader import Reloader, find_default_monitor_factory +import hupper +import waitress from cleo import Command -from ..exceptions import DriverLibraryNotFound -from ..helpers import has_unmigrated_migrations class ServeCommand(Command): @@ -21,25 +18,22 @@ class ServeCommand(Command): {--l|live-reload : Make the server automatically refresh your web browser} """ - def handle(self): - if has_unmigrated_migrations(): - self.comment( - "\nYou have unmigrated migrations. Run 'craft migrate' to migrate them\n" - ) + def __init__(self, application): + super().__init__() + self.app = application + def handle(self): if self.option("live-reload"): try: from livereload import Server except ImportError: - raise DriverLibraryNotFound( + raise ImportError( "Could not find the livereload library. Install it by running 'pip install livereload==2.5.1'" ) - from wsgi import container - from config import application import glob - server = Server(container.make("WSGI")) + server = Server(self.app) for filepath in glob.glob("resources/templates/**/*/"): server.watch(filepath) @@ -49,52 +43,33 @@ def handle(self): "This will only work for templates. Changes to Python files may require a browser refresh." ) self.line("") - application = server.serve( + server.serve( port=self.option("port"), restart_delay=self.option("reload-interval"), liveport=5500, - root=application.BASE_DIRECTORY, + root=self.app.base_path, debug=True, ) return - if not self.option("dont-reload"): - logger = DefaultLogger(LogLevel.INFO) + reloader = hupper.start_reloader(self.app.make("server.runner")) - # worker args are pickled and then passed to the new process - worker_args = [ - self.option("host"), - self.option("port"), - "wsgi:application", - ] - - reloader = Reloader( - "masonite.commands._devserver.run", - find_default_monitor_factory(logger), - logger, - worker_args=worker_args, - ) + # monitor an extra file + reloader.watch_files([".env", self.app.get_storage_path()]) - self._run_reloader(reloader, extra_files=[".env", "storage/"]) - else: - from wsgi import application - from ._devserver import run +def main(args=sys.argv[1:]): + from wsgi import application - run(self.option("host"), self.option("port"), application) + host = "127.0.0.1" + port = "8000" + if "--host" in args: + host = args[args.index("--host") + 1] + if "--port" in args: + port = args[args.index("--host") + 1] + if "-p" in args: + port = args[args.index("-p") + 1] - def _run_reloader(self, reloader, extra_files=[]): - reloader._capture_signals() - reloader._start_monitor() - for blob in extra_files: - reloader.monitor.add_path(os.path.join(os.getcwd(), blob)) - try: - while True: - if not reloader._run_worker(): - reloader._wait_for_changes() - time.sleep(float(self.option("reload-interval"))) - except KeyboardInterrupt: - pass - finally: - reloader._stop_monitor() - reloader._restore_signals() + waitress.serve( + application, host=host, port=port, clear_untrusted_proxy_headers=False + ) diff --git a/src/masonite/commands/TestCommand.py b/src/masonite/commands/TestCommand.py deleted file mode 100644 index f712a2164..000000000 --- a/src/masonite/commands/TestCommand.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Creates New Test Command.""" -from ..commands import BaseScaffoldCommand - - -class TestCommand(BaseScaffoldCommand): - """ - Creates a new test case. - - test - {name : Name of the test you would like to create} - """ - - scaffold_name = "Test" - postfix = "" - prefix = "Test" - template = "/masonite/snippets/scaffold/test" - base_directory = "tests/test_" - file_to_lower = True diff --git a/src/masonite/commands/TinkerCommand.py b/src/masonite/commands/TinkerCommand.py index 412ef8b46..cabc83a83 100644 --- a/src/masonite/commands/TinkerCommand.py +++ b/src/masonite/commands/TinkerCommand.py @@ -1,12 +1,21 @@ """Starts Interactive Console Command.""" import code import sys - from cleo import Command -BANNER = """Masonite Python {} Console +from ..environment import env +from ..utils.collections import collect +from ..utils.structures import load, data_get +from ..utils.location import base_path, config_path +from ..helpers import optional, url +from ..facades import Loader + + +BANNER = """Masonite Python \033[92m {} \033[0m Console This interactive console has the following things imported: - container as 'app' + -\033[92m app(container), \033[0m + - Utils:\033[92m {}, \033[0m + - Models:\033[92m {}, \033[0m Type `exit()` to exit.""" @@ -16,14 +25,48 @@ class TinkerCommand(Command): Run a python shell with the container pre-loaded. tinker + {--i|ipython : Run a IPython shell} """ def handle(self): - from wsgi import container + from wsgi import application + from masoniteorm.models import Model version = "{}.{}.{}".format( sys.version_info.major, sys.version_info.minor, sys.version_info.micro ) - banner = BANNER.format(version) + models = Loader.find_all(Model, "tests/integrations/app") + banner = BANNER.format( + version, + "env, optional, load, collect, url, asset, route, load, data_get, base_path, config_path", + ",".join(models.keys()), + ) + helpers = { + "app": application, + "env": env, + "optional": optional, + "collect": collect, + "url": url.url, + "asset": url.asset, + "route": url.route, + "load": load, + "data_get": data_get, + "base_path": base_path, + "config_path": config_path, + } + context = {**helpers, **models} + + if self.option("ipython"): + try: + import IPython + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'IPython' library. Run 'pip install ipython' to fix this." + ) + from traitlets.config import Config - code.interact(banner=banner, local={"app": container}) + c = Config() + c.TerminalInteractiveShell.banner1 = banner + IPython.start_ipython(argv=[], user_ns=context, config=c) + else: + code.interact(banner=banner, local=context) diff --git a/src/masonite/commands/UpCommand.py b/src/masonite/commands/UpCommand.py deleted file mode 100644 index 5377ee8fd..000000000 --- a/src/masonite/commands/UpCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Up Command.""" - -import os - -from cleo import Command - - -class UpCommand(Command): - """ - Brings the server out of maintenance state. - - up - """ - - def handle(self): - os.remove("bootstrap/down") diff --git a/src/masonite/commands/ViewCommand.py b/src/masonite/commands/ViewCommand.py deleted file mode 100644 index 186e7cf27..000000000 --- a/src/masonite/commands/ViewCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New View Command.""" -from ..commands import BaseScaffoldCommand - - -class ViewCommand(BaseScaffoldCommand): - """ - Creates a view. - - view - {name : Name of the view you would like to create} - """ - - scaffold_name = "View" - template = "/masonite/snippets/scaffold/view" - file_extension = ".html" - base_directory = "resources/templates/" diff --git a/src/masonite/commands/__init__.py b/src/masonite/commands/__init__.py index a3d034327..6c1fd110a 100644 --- a/src/masonite/commands/__init__.py +++ b/src/masonite/commands/__init__.py @@ -1,27 +1,15 @@ +from .CommandCapsule import CommandCapsule from .AuthCommand import AuthCommand -from .BaseScaffoldCommand import BaseScaffoldCommand -from .CommandCommand import CommandCommand -from .ControllerCommand import ControllerCommand -from .DownCommand import DownCommand -from .InfoCommand import InfoCommand -from .InstallCommand import InstallCommand -from .JobCommand import JobCommand +from .TinkerCommand import TinkerCommand from .KeyCommand import KeyCommand -from .MailableCommand import MailableCommand -from .MiddlewareCommand import MiddlewareCommand -from .ModelCommand import ModelCommand -from .ModelDocstringCommand import ModelDocstringCommand -from .NewCommand import NewCommand -from .PresetCommand import PresetCommand -from .ProviderCommand import ProviderCommand -from .PublishCommand import PublishCommand +from .ServeCommand import ServeCommand from .QueueWorkCommand import QueueWorkCommand +from .QueueRetryCommand import QueueRetryCommand from .QueueTableCommand import QueueTableCommand -from .ServeCommand import ServeCommand -from .ViewCommand import ViewCommand -from .RoutesCommand import RoutesCommand -from .SeedCommand import SeedCommand -from .SeedRunCommand import SeedRunCommand -from .TestCommand import TestCommand -from .TinkerCommand import TinkerCommand -from .UpCommand import UpCommand +from .QueueFailedCommand import QueueFailedCommand +from .MakeControllerCommand import MakeControllerCommand +from .MakeJobCommand import MakeJobCommand +from .MakeMailableCommand import MakeMailableCommand +from .MakeProviderCommand import MakeProviderCommand +from .PublishPackageCommand import PublishPackageCommand +from .MakePolicyCommand import MakePolicyCommand diff --git a/src/masonite/commands/_devserver.py b/src/masonite/commands/_devserver.py deleted file mode 100644 index ef7430515..000000000 --- a/src/masonite/commands/_devserver.py +++ /dev/null @@ -1,140 +0,0 @@ -# Pure-python development wsgi server. -# Parts are borrowed from Adrian Holovaty and the Django Project -# (https://www.djangoproject.com/). - -import logging -import socket -import sys -from wsgiref import simple_server - -logging.basicConfig(level=logging.INFO) - - -def is_broken_pipe_error(): - exc_type, exc_value = sys.exc_info()[:2] - return issubclass(exc_type, socket.error) and exc_value.args[0] == 32 - - -class WSGIServer(simple_server.WSGIServer): - """BaseHTTPServer that implements the Python WSGI protocol""" - - request_queue_size = 10 - - def __init__(self, *args, ipv6=False, allow_reuse_address=True, **kwargs): - if ipv6: - self.address_family = socket.AF_INET6 - self.allow_reuse_address = allow_reuse_address - super().__init__(*args, **kwargs) - - def handle_error(self, request, client_address): - if is_broken_pipe_error(): - logging.info("- Broken pipe from %s\n", client_address) - else: - super().handle_error(request, client_address) - - -class ServerHandler(simple_server.ServerHandler): - http_version = "1.1" - - def handle_error(self): - # Ignore broken pipe errors, otherwise pass on - if not is_broken_pipe_error(): - super().handle_error() - - -class WSGIRequestHandler(simple_server.WSGIRequestHandler): - protocol_version = "HTTP/1.1" - - def address_string(self): - # Short-circuit parent method to not call socket.getfqdn - return self.client_address[0] - - def log_message(self, message_format, *args): - extra = { - "request": self.request, - "server_time": self.log_date_time_string(), - } - if args[1][0] == "4": - # 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x - if args[0].startswith("\x16\x03"): - extra["status_code"] = 500 - logging.error( - "You're accessing the development server over HTTPS, but " - "it only supports HTTP.\n", - extra=extra, - ) - return - - if args[1].isdigit() and len(args[1]) == 3: - status_code = int(args[1]) - extra["status_code"] = status_code - - if status_code >= 500: - level = logging.error - elif status_code >= 400: - level = logging.warning - else: - level = logging.info - else: - level = logging.info - - level(message_format, *args, extra=extra) - - def get_environ(self): - # Strip all headers with underscores in the name before constructing - # the WSGI environ. This prevents header-spoofing based on ambiguity - # between underscores and dashes both normalized to underscores in WSGI - # env vars. Nginx and Apache 2.4+ both do this as well. - for k in self.headers: - if "_" in k: - del self.headers[k] - - return super().get_environ() - - def handle(self): - """Copy of WSGIRequestHandler.handle() but with different ServerHandler""" - self.raw_requestline = self.rfile.readline(65537) - if len(self.raw_requestline) > 65536: - self.requestline = "" - self.request_version = "" - self.command = "" - self.send_error(414) - return - - if not self.parse_request(): # An error code has been sent, just exit - return - - handler = ServerHandler( - self.rfile, self.wfile, self.get_stderr(), self.get_environ() - ) - handler.request_handler = self # backpointer for logging - handler.run(self.server.get_app()) - - -def _split_module_and_app(moduleapp): - if ":" in moduleapp: - parts = moduleapp.split(":") - return parts[0], parts[1] - return moduleapp, "application" - - -def _import_application(module_name, app_name): - from importlib import import_module - - module = import_module(module_name) - return getattr(module, app_name) - - -def run(host, port, wsgi_handler, ipv6=False, httpd_cls=WSGIServer): - if isinstance(wsgi_handler, str): - module_name, app_name = _split_module_and_app(wsgi_handler) - wsgi_handler = _import_application(module_name, app_name) - - server_address = (host, int(port)) - httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) - httpd.set_app(wsgi_handler) - try: - print("Serving at: http://{}:{}".format(host, port)) - httpd.serve_forever() - except KeyboardInterrupt: - pass diff --git a/src/masonite/commands/presets/Bootstrap.py b/src/masonite/commands/presets/Bootstrap.py deleted file mode 100644 index b061bcf9a..000000000 --- a/src/masonite/commands/presets/Bootstrap.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Bootstrap Preset""" -import os -import shutil -from ..presets import Preset - - -class Bootstrap(Preset): - """Configure the front-end scaffolding for the application to use Bootstrap""" - - def install(self): - """Install the preset""" - self.update_packages() - self.update_sass() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """Updates the packages array to include Bootstrap specific packages""" - packages["bootstrap"] = "^4.0.0" - packages["jquery"] = "^3.2" - packages["popper.js"] = "^1.12" - return packages - - def update_sass(self): - """Copies Bootstrap scss files into application""" - directory = "resources/sass" - if not os.path.exists(os.path.realpath(directory)): - os.makedirs(os.path.realpath(directory)) - shutil.copyfile( - os.path.dirname(__file__) + "/bootstrap-stubs/_variables.scss", - "resources/sass/_variables.scss", - ) - shutil.copyfile( - os.path.dirname(__file__) + "/bootstrap-stubs/app.scss", - "resources/sass/app.scss", - ) diff --git a/src/masonite/commands/presets/Preset.py b/src/masonite/commands/presets/Preset.py deleted file mode 100644 index 2c21e8b16..000000000 --- a/src/masonite/commands/presets/Preset.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import shutil -import json - - -class Preset: - def ensure_component_directory_exists(self): - """Ensure the component directories we need exist.""" - directory = "resources/js/components" - if not os.path.exists(os.path.realpath(directory)): - os.makedirs(os.path.realpath(directory)) - - def update_packages(self, dev=True): - """Update the "package.json" file.""" - if not os.path.exists(os.path.realpath("package.json")): - return - - configuration_key = "devDependencies" if dev else "dependencies" - - packages = {} - with open(os.path.realpath("package.json"), "r+") as f: - packages = json.load(f) - packages[configuration_key] = self.update_package_array( - packages[configuration_key] if configuration_key in packages else {} - ) - f.seek(0) # Rewind to beginning of file - f.truncate() - f.write(json.dumps(packages, sort_keys=True, indent=4)) - - def remove_node_modules(self): - """Remove the installed Node modules.""" - for filename in ["package-lock.json", "yarn.lock"]: - if os.path.exists(os.path.realpath(filename)): - os.remove(filename) - shutil.rmtree("node_modules", ignore_errors=True) - - def create_scss_file(self): - """Create an empty app.scss file""" - os.makedirs(os.path.realpath("resources/sass")) - with open(os.path.realpath("resources/sass/app.scss"), "w") as f: - f.write("// Add your Sass here\n") - f.write("// For Tailwind CSS\n") - f.write("// @import 'tailwindcss/base';\n") - f.write("// @import 'tailwindcss/components';\n") - f.write("// @import 'tailwindcss/utilities';\n") diff --git a/src/masonite/commands/presets/React.py b/src/masonite/commands/presets/React.py deleted file mode 100644 index 4a794ebdc..000000000 --- a/src/masonite/commands/presets/React.py +++ /dev/null @@ -1,64 +0,0 @@ -"""React Preset""" -import os -import shutil -from ..presets import Preset - - -class React(Preset): - """ - Configure the front-end scaffolding for the application to use React - - Will also remove VueJS as React and Vue are a bit mutally exclusive - """ - - def install(self): - """Install the preset""" - self.ensure_component_directory_exists() - self.update_packages() - self.update_webpack_configuration() - self.update_bootstrapping() - self.update_component() - self.create_scss_file() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include React specific packages - but also remove VueJS ones - """ - for package in ["vue", "vue-template-compiler"]: - packages.pop(package, None) - - packages["@babel/preset-react"] = "^7.0.0" - packages["react"] = "^16.2.0" - packages["react-dom"] = "^16.2.0" - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/react-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_component(self): - """ - Copy example React component into application - (delete example Vue component if it exists) - """ - vue_component = "resources/js/components/ExampleComponent.vue" - if os.path.exists(os.path.realpath(vue_component)): - os.remove(vue_component) - shutil.copyfile( - os.path.dirname(__file__) + "/react-stubs/Example.js", - "resources/js/components/Example.js", - ) - - def update_bootstrapping(self): - """Copies template app.js and bootstrap.js into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/react-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/shared-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) diff --git a/src/masonite/commands/presets/Remove.py b/src/masonite/commands/presets/Remove.py deleted file mode 100644 index 61309b6f0..000000000 --- a/src/masonite/commands/presets/Remove.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Remove Preset""" -import os -import shutil -from ..presets import Preset - - -class Remove(Preset): - """Removes any defined Preset""" - - def install(self): - """Install the preset""" - self.update_packages() - self.update_bootstrapping() - self.update_webpack_configuration() - self.remove_node_modules() - if os.path.exists(os.path.realpath("resources/sass/_variables.scss")): - os.remove("resources/sass/_variables.scss") - shutil.rmtree("resources/js/components", ignore_errors=True) - shutil.rmtree("public/css", ignore_errors=True) - shutil.rmtree("public/js", ignore_errors=True) - - def update_package_array(self, packages={}): - """Updates the packages array to remove React, VueJS, and Bootstrap packages""" - packages_to_remove = [ - "bootstrap", - "jquery", - "popper.js", - "vue", - "vue-template-compiler", - "@babel/preset-react", - "react", - "react-dom", - ] - for package in packages_to_remove: - packages.pop(package, None) - - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/remove-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_bootstrapping(self): - """Copies template app.js file into application""" - for directory in ["resources/sass", "resources/js"]: - if not os.path.exists(os.path.realpath(directory)): - os.makedirs(os.path.realpath(directory)) - with open("resources/sass/app.scss", "w") as f: - f.write("") - shutil.copyfile( - os.path.dirname(__file__) + "/remove-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/remove-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) diff --git a/src/masonite/commands/presets/Tailwind.py b/src/masonite/commands/presets/Tailwind.py deleted file mode 100644 index 5df85affa..000000000 --- a/src/masonite/commands/presets/Tailwind.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Tailwind Preset""" -import os -import shutil -from ..presets import Preset - - -class Tailwind(Preset): - """ - Configure the front-end scaffolding for the application to use Tailwind - """ - - def install(self): - """Install the preset""" - self.update_packages() - self.update_webpack_configuration() - self.create_tailwind_config() - self.update_scss_file() - self.update_base_views() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include VueJS specific packages - """ - packages["autoprefixer"] = "^10.2.1" - packages["tailwindcss"] = "^2.0.2" - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/webpack.mix.js", - "webpack.mix.js", - ) - - def create_tailwind_config(self): - """ - Copy example Tailwind configuration into application - """ - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/tailwind.config.js", - "tailwind.config.js", - ) - - def update_scss_file(self): - """Create a app.scss file configured for Tailwind.""" - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/style.scss", - "storage/static/sass/style.scss", - ) - - def update_base_views(self): - """Update base views""" - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/base.html", - "resources/templates/base.html", - ) - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/welcome.html", - "resources/templates/welcome.html", - ) diff --git a/src/masonite/commands/presets/Vue.py b/src/masonite/commands/presets/Vue.py deleted file mode 100644 index fdf999837..000000000 --- a/src/masonite/commands/presets/Vue.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Vue Preset""" -import os -import shutil -from ..presets import Preset - - -class Vue(Preset): - """ - Configure the front-end scaffolding for the application to use VueJS - - Will also remove React as React and Vue are a bit mutally exclusive - """ - - def install(self): - """Install the preset""" - self.ensure_component_directory_exists() - self.update_packages() - self.update_webpack_configuration() - self.update_bootstrapping() - self.update_component() - self.create_scss_file() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include VueJS specific packages - but also remove React ones - """ - for package in ["@babel/preset-react", "react", "react-dom"]: - packages.pop(package, None) - - packages["vue"] = "^2.6.12" - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_component(self): - """ - Copy example VueJS component into application - (delete example React component if it exists) - """ - vue_component = "resources/js/components/Example.js" - if os.path.exists(os.path.realpath(vue_component)): - os.remove(vue_component) - shutil.copyfile( - os.path.dirname(__file__) + "/vue-stubs/ExampleComponent.vue", - "resources/js/components/ExampleComponent.vue", - ) - - def update_bootstrapping(self): - """Copies template app.js and bootstrap.js into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/shared-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) diff --git a/src/masonite/commands/presets/Vue3.py b/src/masonite/commands/presets/Vue3.py deleted file mode 100644 index a7a0463a1..000000000 --- a/src/masonite/commands/presets/Vue3.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Vue 3 Preset""" -import os -import shutil -from ..presets import Preset - - -class Vue3(Preset): - """ - Configure the front-end scaffolding for the application to use VueJS 3.0 - - Will also remove React as React and Vue are a bit mutally exclusive - """ - - def install(self): - """Install the preset""" - self.ensure_component_directory_exists() - self.update_packages() - self.update_webpack_configuration() - self.update_bootstrapping() - self.update_component() - self.create_scss_file() - self.create_view() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include VueJS specific packages - but also remove React ones - """ - for package in ["@babel/preset-react", "react", "react-dom"]: - packages.pop(package, None) - - packages["vue"] = "^3.0.4" - packages["@vue/compiler-sfc"] = "^3.0.4" - packages["vue-loader"] = "^16.1.2" - - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_component(self): - """ - Copy example VueJS component into application - (delete example React component if it exists) - """ - # delete React component if exists - vue_component = "resources/js/components/Example.js" - if os.path.exists(os.path.realpath(vue_component)): - os.remove(vue_component) - - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/HelloWorld.vue", - os.path.join("resources/js/components/", "HelloWorld.vue"), - ) - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/App.vue", - os.path.join("resources/js/", "App.vue"), - ) - - def update_bootstrapping(self): - """Copies template app.js and bootstrap.js into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/shared-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) - - def create_view(self): - """Copy an example app view with assets included""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/app.html", - "resources/templates/app_vue3.html", - ) diff --git a/src/masonite/commands/presets/__init__.py b/src/masonite/commands/presets/__init__.py deleted file mode 100644 index 95141ed8e..000000000 --- a/src/masonite/commands/presets/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .Preset import Preset -from .React import React -from .Vue import Vue -from .Tailwind import Tailwind -from .Remove import Remove -from .Bootstrap import Bootstrap diff --git a/src/masonite/commands/presets/bootstrap-stubs/_variables.scss b/src/masonite/commands/presets/bootstrap-stubs/_variables.scss deleted file mode 100644 index 0407ab577..000000000 --- a/src/masonite/commands/presets/bootstrap-stubs/_variables.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Body -$body-bg: #f8fafc; - -// Typography -$font-family-sans-serif: 'Nunito', sans-serif; -$font-size-base: 0.9rem; -$line-height-base: 1.6; - -// Colors -$blue: #3490dc; -$indigo: #6574cd; -$purple: #9561e2; -$pink: #f66d9b; -$red: #e3342f; -$orange: #f6993f; -$yellow: #ffed4a; -$green: #38c172; -$teal: #4dc0b5; -$cyan: #6cb2eb; diff --git a/src/masonite/commands/presets/bootstrap-stubs/app.scss b/src/masonite/commands/presets/bootstrap-stubs/app.scss deleted file mode 100644 index eb9482212..000000000 --- a/src/masonite/commands/presets/bootstrap-stubs/app.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Fonts -@import url('https://fonts.googleapis.com/css?family=Nunito'); - -// Variables -@import 'variables'; - -// Bootstrap -@import '~bootstrap/scss/bootstrap'; - -.navbar { - background-color: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); -} diff --git a/src/masonite/commands/presets/react-stubs/Example.js b/src/masonite/commands/presets/react-stubs/Example.js deleted file mode 100644 index f18b589a1..000000000 --- a/src/masonite/commands/presets/react-stubs/Example.js +++ /dev/null @@ -1,22 +0,0 @@ -import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; - -export default class Example extends Component { - render() { - return ( -
-
-
-
-
Example Component
-
I'm an example component!
-
-
-
-
- ); - } -} -if (document.getElementById('example')) { - ReactDOM.render(, document.getElementById('example')); -} \ No newline at end of file diff --git a/src/masonite/commands/presets/react-stubs/app.js b/src/masonite/commands/presets/react-stubs/app.js deleted file mode 100644 index bd05c4fb0..000000000 --- a/src/masonite/commands/presets/react-stubs/app.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes React and other helpers. It's a great starting point while - * building robust, powerful web applications using React + Masonite. - */ - -require('./bootstrap'); - -/** - * Next, we will create a fresh React component instance and attach it to - * the page. Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ - -require('./components/Example'); \ No newline at end of file diff --git a/src/masonite/commands/presets/react-stubs/webpack.mix.js b/src/masonite/commands/presets/react-stubs/webpack.mix.js deleted file mode 100644 index d6d6725d4..000000000 --- a/src/masonite/commands/presets/react-stubs/webpack.mix.js +++ /dev/null @@ -1,15 +0,0 @@ -const mix = require('laravel-mix'); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix.js('resources/js/app.js', 'public/js').react() - .sass('resources/sass/app.scss', 'public/css'); \ No newline at end of file diff --git a/src/masonite/commands/presets/remove-stubs/app.js b/src/masonite/commands/presets/remove-stubs/app.js deleted file mode 100644 index ab7e693da..000000000 --- a/src/masonite/commands/presets/remove-stubs/app.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * First, we will load all of this project's Javascript utilities and other - * dependencies. Then, we will be ready to develop a robust and powerful - * application frontend using useful Masonite and JavaScript libraries. - */ - -require('./bootstrap'); \ No newline at end of file diff --git a/src/masonite/commands/presets/remove-stubs/bootstrap.js b/src/masonite/commands/presets/remove-stubs/bootstrap.js deleted file mode 100644 index 25620bbdf..000000000 --- a/src/masonite/commands/presets/remove-stubs/bootstrap.js +++ /dev/null @@ -1,25 +0,0 @@ -window._ = require('lodash'); - -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Masonite back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -window.axios = require('axios'); - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - -/** - * Next we will register the CSRF Token as a common header with Axios so that - * all outgoing HTTP requests automatically have it attached. This is just - * a simple convenience so we don't have to attach every token manually. - */ - -let token = document.head.querySelector('meta[name="csrf-token"]'); - -if (token) { - window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; -} else { - console.error('CSRF token not found: https://docs.masoniteproject.com/security/csrf-protection#ajax-vue-axios'); -} diff --git a/src/masonite/commands/presets/remove-stubs/webpack.mix.js b/src/masonite/commands/presets/remove-stubs/webpack.mix.js deleted file mode 100644 index cd6b2cb4d..000000000 --- a/src/masonite/commands/presets/remove-stubs/webpack.mix.js +++ /dev/null @@ -1,15 +0,0 @@ -const mix = require('laravel-mix'); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix.js('resources/js/app.js', 'public/js') - .sass('resources/sass/app.scss', 'public/css'); \ No newline at end of file diff --git a/src/masonite/commands/presets/shared-stubs/bootstrap.js b/src/masonite/commands/presets/shared-stubs/bootstrap.js deleted file mode 100644 index 25620bbdf..000000000 --- a/src/masonite/commands/presets/shared-stubs/bootstrap.js +++ /dev/null @@ -1,25 +0,0 @@ -window._ = require('lodash'); - -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Masonite back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -window.axios = require('axios'); - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - -/** - * Next we will register the CSRF Token as a common header with Axios so that - * all outgoing HTTP requests automatically have it attached. This is just - * a simple convenience so we don't have to attach every token manually. - */ - -let token = document.head.querySelector('meta[name="csrf-token"]'); - -if (token) { - window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; -} else { - console.error('CSRF token not found: https://docs.masoniteproject.com/security/csrf-protection#ajax-vue-axios'); -} diff --git a/src/masonite/commands/presets/tailwind-stubs/base.html b/src/masonite/commands/presets/tailwind-stubs/base.html deleted file mode 100644 index 929143a34..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/base.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - {% block title %} {{ config('application.name', 'Masonite') }} {% endblock %} - - - - - {% block css %}{% endblock %} - - - -
-
- {% block content %} - - {% endblock %} -
-
- - - \ No newline at end of file diff --git a/src/masonite/commands/presets/tailwind-stubs/style.scss b/src/masonite/commands/presets/tailwind-stubs/style.scss deleted file mode 100644 index 8fa4b3c5d..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/style.scss +++ /dev/null @@ -1,7 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -.links > a { - @apply uppercase px-6 font-bold text-xs tracking-widest; -} \ No newline at end of file diff --git a/src/masonite/commands/presets/tailwind-stubs/tailwind.config.js b/src/masonite/commands/presets/tailwind-stubs/tailwind.config.js deleted file mode 100644 index d6d3ca2df..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/tailwind.config.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - purge: [], - darkMode: false, // or 'media' or 'class' - theme: { - fontFamily: { - "sans": "Raleway, sans-serif", // set as sans-serif font - }, - extend: {}, - }, - variants: { - extend: {}, - }, - plugins: [], -} diff --git a/src/masonite/commands/presets/tailwind-stubs/webpack.mix.js b/src/masonite/commands/presets/tailwind-stubs/webpack.mix.js deleted file mode 100644 index d7ab81c99..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/webpack.mix.js +++ /dev/null @@ -1,22 +0,0 @@ -const mix = require("laravel-mix"); -const path = require("path"); -const tailwindcss = require("tailwindcss") - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix - .js("storage/static/js/app.js", "storage/compiled/js") - .sass("storage/static/sass/style.scss", "storage/compiled/") - .options({ - processCssUrls: false, - postCss: [tailwindcss("tailwind.config.js")], - }) diff --git a/src/masonite/commands/presets/tailwind-stubs/welcome.html b/src/masonite/commands/presets/tailwind-stubs/welcome.html deleted file mode 100644 index 0514de6a9..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/welcome.html +++ /dev/null @@ -1,39 +0,0 @@ -{% if exists('auth/base') %} - {% extends 'auth/base.html' %} -{% else %} - {% extends 'base.html' %} -{% endif %} - -{% block css %} - - -{% endblock %} - -{% block title %} - Welcome To {{ config('application.name', 'Masonite') }} -{% endblock %} - -{% block content %} -
-
- {{ config('application.name') }} -
-
- - - -
- -

- - -
-{% endblock %} \ No newline at end of file diff --git a/src/masonite/commands/presets/vue-stubs/ExampleComponent.vue b/src/masonite/commands/presets/vue-stubs/ExampleComponent.vue deleted file mode 100644 index 6b9652464..000000000 --- a/src/masonite/commands/presets/vue-stubs/ExampleComponent.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/src/masonite/commands/presets/vue-stubs/app.js b/src/masonite/commands/presets/vue-stubs/app.js deleted file mode 100644 index b224c5b77..000000000 --- a/src/masonite/commands/presets/vue-stubs/app.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes Vue and other libraries. It is a great starting point when - * building robust, powerful web applications using Vue and Masonite. - */ - -require("./bootstrap"); - -import Vue from "vue"; - -/** - * The following block of code may be used to automatically register your - * Vue components. It will recursively scan this directory for the Vue - * components and automatically register them with their "basename". - * - * Eg. ./components/ExampleComponent.vue -> - */ - -// const files = require.context('./', true, /\.vue$/i) -// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default)) - -Vue.component( - "example-component", - require("./components/ExampleComponent.vue").default -); - -/** - * Next, we will create a fresh Vue application instance and attach it to - * the page. Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ - -const app = new Vue({ - el: "#app", -}); diff --git a/src/masonite/commands/presets/vue-stubs/webpack.mix.js b/src/masonite/commands/presets/vue-stubs/webpack.mix.js deleted file mode 100644 index c0ff8d71a..000000000 --- a/src/masonite/commands/presets/vue-stubs/webpack.mix.js +++ /dev/null @@ -1,17 +0,0 @@ -const mix = require("laravel-mix"); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix - .js("resources/js/app.js", "public/js") - .vue({ version: 2 }) - .sass("resources/sass/app.scss", "public/css"); diff --git a/src/masonite/commands/presets/vue3-stubs/App.vue b/src/masonite/commands/presets/vue3-stubs/App.vue deleted file mode 100644 index 9a646ed6f..000000000 --- a/src/masonite/commands/presets/vue3-stubs/App.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/src/masonite/commands/presets/vue3-stubs/HelloWorld.vue b/src/masonite/commands/presets/vue3-stubs/HelloWorld.vue deleted file mode 100644 index ae0fc9408..000000000 --- a/src/masonite/commands/presets/vue3-stubs/HelloWorld.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/src/masonite/commands/presets/vue3-stubs/app.html b/src/masonite/commands/presets/vue3-stubs/app.html deleted file mode 100644 index 996e36221..000000000 --- a/src/masonite/commands/presets/vue3-stubs/app.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - {% block title %}{{ config('application.name', 'Masonite') }}{% endblock%} - - {% block css %}{% endblock %} - - - -
- - - diff --git a/src/masonite/commands/presets/vue3-stubs/app.js b/src/masonite/commands/presets/vue3-stubs/app.js deleted file mode 100644 index e53bf97b2..000000000 --- a/src/masonite/commands/presets/vue3-stubs/app.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes Vue and other libraries. It is a great starting point when - * building robust, powerful web applications using Vue and Masonite. - */ - -require("./bootstrap"); - -/** - * Next, we will create a fresh Vue application instance - */ -import { createApp } from "vue/dist/vue.esm-bundler.js"; -import App from "./App.vue"; -const app = createApp(App); - -/** You can also create an application without the App.vue - -const app = createApp({}); - -*/ - -/** - * The following block of code may be used to automatically register your - * Vue components. It will recursively scan this directory for the Vue - * components and automatically register them with their "basename". - * - * Eg. ./components/ExampleComponent.vue -> - -const files = require.context("./", true, /\.vue$/i); -files - .keys() - .map((key) => - app.component(key.split("/").pop().split(".")[0], files(key).default) - ); - */ - -/** -Or you can register components manually - -import ExampleComponent from './components/ExampleComponent.vue' -app.component("example-component", ExampleComponent) - - */ - -/** Finally we attach the Vue instance to the page. - * Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ - -app.mount("#app"); diff --git a/src/masonite/commands/presets/vue3-stubs/webpack.mix.js b/src/masonite/commands/presets/vue3-stubs/webpack.mix.js deleted file mode 100644 index f5b2affc4..000000000 --- a/src/masonite/commands/presets/vue3-stubs/webpack.mix.js +++ /dev/null @@ -1,31 +0,0 @@ -const mix = require("laravel-mix"); -const path = require("path"); - -// For Tailwind CSS -// const tailwindcss = require("tailwindcss") -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix - .js("resources/js/app.js", "storage/compiled/js") - .vue({ version: 3 }) - .sass("resources/sass/app.scss", "storage/compiled/css"); - -// For Tailwind CSS, append -// .options({ -// processCssUrls: false, -// postCss: [ tailwindcss('tailwind.config.js') ], -// }) - -// New Alias plugin -mix.alias({ - "@": path.resolve("resources/js"), -}); diff --git a/src/masonite/configuration/Configuration.py b/src/masonite/configuration/Configuration.py new file mode 100644 index 000000000..a476f791d --- /dev/null +++ b/src/masonite/configuration/Configuration.py @@ -0,0 +1,74 @@ +from ..facades import Loader +from ..utils.structures import data +from ..exceptions import InvalidConfigurationLocation, InvalidConfigurationSetup + + +class Configuration: + # Foundation configuration keys + reserved_keys = [ + "application", + "auth", + "broadcast", + "cache", + "database", + "filesystem", + "mail", + "notification", + "providers", + "queue", + "session", + ] + + def __init__(self, application): + self.application = application + self._config = data() + + def load(self): + """At boot load configuration from all files and store them in here.""" + config_root = self.application.make("config.location") + for module_name, module in Loader.get_modules( + config_root, raise_exception=True + ).items(): + params = Loader.get_parameters(module) + for name, value in params.items(): + self._config[f"{module_name}.{name.lower()}"] = value + + # check loaded configuration + if not self._config.get("application"): + raise InvalidConfigurationLocation( + f"Config directory {config_root} does not contain required configuration files." + ) + + def merge_with(self, path, external_config): + """Merge external config at key with project config at same key. It's especially + useful in Masonite packages in order to merge the configuration default package with + the package configuration which can be published in project. + + This functions disallow merging configuration using foundation configuration keys + (such as 'application'). + """ + if path in self.reserved_keys: + raise InvalidConfigurationSetup( + f"{path} is a reserved configuration key name. Please use an other key." + ) + if isinstance(external_config, str): + # config is a path and should be loaded + params = Loader.get_parameters(external_config) + else: + params = external_config + base_config = {name.lower(): value for name, value in params.items()} + merged_config = {**base_config, **self.get(path, {})} + self.set(path, merged_config) + + def set(self, path, value): + self._config[path] = value + + def get(self, path, default=None): + try: + config_at_path = self._config[path] + if isinstance(config_at_path, dict): + return data(config_at_path) + else: + return config_at_path + except KeyError: + return default diff --git a/src/masonite/configuration/__init__.py b/src/masonite/configuration/__init__.py new file mode 100644 index 000000000..2847ba4f2 --- /dev/null +++ b/src/masonite/configuration/__init__.py @@ -0,0 +1,2 @@ +from .helpers import config +from .Configuration import Configuration diff --git a/src/masonite/configuration/helpers.py b/src/masonite/configuration/helpers.py new file mode 100644 index 000000000..1e8f0d5af --- /dev/null +++ b/src/masonite/configuration/helpers.py @@ -0,0 +1,5 @@ +from ..facades import Config + + +def config(key, default=None): + return Config.get(key, default) diff --git a/src/masonite/configuration/providers/ConfigurationProvider.py b/src/masonite/configuration/providers/ConfigurationProvider.py new file mode 100644 index 000000000..52c54aaa9 --- /dev/null +++ b/src/masonite/configuration/providers/ConfigurationProvider.py @@ -0,0 +1,16 @@ +from ...providers import Provider + +from ..Configuration import Configuration + + +class ConfigurationProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + config = Configuration(self.application) + config.load() + self.application.bind("config", config) + + def boot(self): + pass diff --git a/src/masonite/configuration/providers/__init__.py b/src/masonite/configuration/providers/__init__.py new file mode 100644 index 000000000..6e0364154 --- /dev/null +++ b/src/masonite/configuration/providers/__init__.py @@ -0,0 +1 @@ +from .ConfigurationProvider import ConfigurationProvider diff --git a/src/masonite/container/__init__.py b/src/masonite/container/__init__.py new file mode 100644 index 000000000..0aca8bbb1 --- /dev/null +++ b/src/masonite/container/__init__.py @@ -0,0 +1 @@ +from .container import Container diff --git a/src/masonite/app.py b/src/masonite/container/container.py similarity index 89% rename from src/masonite/app.py rename to src/masonite/container/container.py index ca72ebe2c..7d1525e16 100644 --- a/src/masonite/app.py +++ b/src/masonite/container/container.py @@ -2,36 +2,32 @@ import inspect -from .exceptions import ( +from ..exceptions import ( ContainerError, MissingContainerBindingNotFound, StrictContainerException, ) -class App: +class Container: """Core of the Service Container. Performs bindings and resolving of objects to and from the container. """ - def __init__( - self, strict=False, override=True, resolve_parameters=False, remember=False - ): - """App class constructor.""" - self.providers = {} - self.strict = strict - self.override = override - self.resolve_parameters = resolve_parameters - self.remember = remember - self._hooks = { - "make": {}, - "bind": {}, - "resolve": {}, - } - - self.swaps = {} - self._remembered = {} + objects = {} + strict = False + override = True + resolve_parameters = {} + remember = False + _hooks = { + "make": {}, + "bind": {}, + "resolve": {}, + } + + swaps = {} + _remembered = {} def bind(self, name, class_obj): """Bind classes into the container with a key value pair. @@ -49,14 +45,14 @@ def bind(self, name, class_obj): class_obj, name ) ) - if self.strict and name in self.providers: + if self.strict and name in self.objects: raise StrictContainerException( "You cannot override a key inside a strict container" ) - if self.override or name not in self.providers: + if self.override or name not in self.objects: self.fire_hook("bind", name, class_obj) - self.providers.update({name: class_obj}) + self.objects.update({name: class_obj}) return self @@ -91,8 +87,8 @@ def make(self, name, *arguments): object -- Returns the object that is fetched. """ - if name in self.providers: - obj = self.providers[name] + if name in self.objects: + obj = self.objects[name] self.fire_hook("make", name, obj) if inspect.isclass(obj): obj = self.resolve(obj, *arguments) @@ -120,7 +116,7 @@ def has(self, name): bool """ if isinstance(name, str): - return name in self.providers + return name in self.objects else: try: self._find_obj(name) @@ -215,21 +211,16 @@ def resolve(self, obj, *resolving_arguments): "This container is not set to resolve parameters. You can set this in the container" " constructor using the 'resolve_parameters=True' keyword argument." ) - try: - if self.remember: - if not inspect.ismethod(obj): - self._remembered[obj] = objects - else: - signature = "{}.{}.{}".format( - obj.__module__, obj.__self__.__class__.__name__, obj.__name__ - ) - self._remembered[signature] = objects - return obj(*objects) - except (TypeError,) as e: - exception = ContainerError - exception.from_obj = obj - raise exception(str(e) + " while calling {}".format(obj)) from e + if self.remember: + if not inspect.ismethod(obj): + self._remembered[obj] = objects + else: + signature = "{}.{}.{}".format( + obj.__module__, obj.__self__.__class__.__name__, obj.__name__ + ) + self._remembered[signature] = objects + return obj(*objects) def collect(self, search): """Fetch a dictionary of objects using a search query. @@ -249,7 +240,7 @@ def collect(self, search): # '*ExceptionHook' # 'Sentry*' # 'Sentry*Hook' - for key, value in self.providers.items(): + for key, value in self.objects.items(): if isinstance(key, str): if search.startswith("*"): if key.endswith(search.split("*")[1]): @@ -268,7 +259,7 @@ def collect(self, search): "There is no '*' in your collection search" ) else: - for provider_key, provider_class in self.providers.items(): + for provider_key, provider_class in self.objects.items(): if ( inspect.isclass(provider_class) and issubclass(provider_class, search) @@ -295,7 +286,7 @@ def _find_annotated_parameter(self, parameter): return self.swaps[parameter.annotation](parameter.annotation, self) return obj - for _, provider_class in self.providers.items(): + for _, provider_class in self.objects.items(): if ( parameter.annotation == provider_class @@ -337,8 +328,8 @@ def _find_parameter(self, keyword): """ parameter = str(keyword) - if parameter != "self" and parameter in self.providers: - obj = self.providers[parameter] + if parameter != "self" and parameter in self.objects: + obj = self.objects[parameter] self.fire_hook("resolve", parameter, obj) return obj elif "=" in parameter: @@ -442,7 +433,7 @@ def _find_obj(self, obj): Returns: object -- Returns the object in the container """ - for _, provider_class in self.providers.items(): + for _, provider_class in self.objects.items(): if obj in (provider_class, provider_class.__class__): return_obj = provider_class self.fire_hook("resolve", obj, return_obj) diff --git a/src/masonite/contracts/AuthContract.py b/src/masonite/contracts/AuthContract.py deleted file mode 100644 index 8546b13cd..000000000 --- a/src/masonite/contracts/AuthContract.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC as Contract, abstractmethod - - -class AuthContract(Contract): - @abstractmethod - def user(self): - pass - - @abstractmethod - def save(self): - pass - - @abstractmethod - def delete(self): - pass diff --git a/src/masonite/contracts/BroadcastContract.py b/src/masonite/contracts/BroadcastContract.py deleted file mode 100644 index 82118974f..000000000 --- a/src/masonite/contracts/BroadcastContract.py +++ /dev/null @@ -1,11 +0,0 @@ -from abc import ABC, abstractmethod - - -class BroadcastContract(ABC): - @abstractmethod - def ssl(self): - pass - - @abstractmethod - def channel(self): - pass diff --git a/src/masonite/contracts/CacheContract.py b/src/masonite/contracts/CacheContract.py deleted file mode 100644 index 3a3fb45c6..000000000 --- a/src/masonite/contracts/CacheContract.py +++ /dev/null @@ -1,23 +0,0 @@ -from abc import ABC, abstractmethod - - -class CacheContract(ABC): - @abstractmethod - def store(self): - pass - - @abstractmethod - def store_for(self): - pass - - @abstractmethod - def get(self): - pass - - @abstractmethod - def is_valid(self): - pass - - @abstractmethod - def exists(self): - pass diff --git a/src/masonite/contracts/MailContract.py b/src/masonite/contracts/MailContract.py deleted file mode 100644 index ca15657d1..000000000 --- a/src/masonite/contracts/MailContract.py +++ /dev/null @@ -1,19 +0,0 @@ -from abc import ABC, abstractmethod - - -class MailContract(ABC): - @abstractmethod - def to(self): - pass - - @abstractmethod - def template(self): - pass - - @abstractmethod - def send_from(self): - pass - - @abstractmethod - def subject(self): - pass diff --git a/src/masonite/contracts/QueueContract.py b/src/masonite/contracts/QueueContract.py deleted file mode 100644 index 1e940911f..000000000 --- a/src/masonite/contracts/QueueContract.py +++ /dev/null @@ -1,27 +0,0 @@ -from abc import ABC, abstractmethod - - -class QueueContract(ABC): - @abstractmethod - def push(self, *objects, args=(), callback="handle", ran=1, channel=None): - pass - - @abstractmethod - def connect(self): - pass - - @abstractmethod - def consume(self, channel, fair=False): - pass - - @abstractmethod - def work(self): - pass - - @abstractmethod - def run_failed_jobs(self): - pass - - @abstractmethod - def add_to_failed_queue_table(self): - pass diff --git a/src/masonite/contracts/SessionContract.py b/src/masonite/contracts/SessionContract.py deleted file mode 100644 index 0e6510b84..000000000 --- a/src/masonite/contracts/SessionContract.py +++ /dev/null @@ -1,35 +0,0 @@ -from abc import ABC, abstractmethod - - -class SessionContract(ABC): - @abstractmethod - def get(self): - pass - - @abstractmethod - def set(self): - pass - - @abstractmethod - def has(self): - pass - - @abstractmethod - def all(self): - pass - - @abstractmethod - def delete(self): - pass - - @abstractmethod - def flash(self): - pass - - @abstractmethod - def reset(self): - pass - - @abstractmethod - def helper(self): - pass diff --git a/src/masonite/contracts/StorageContract.py b/src/masonite/contracts/StorageContract.py deleted file mode 100644 index b8a236970..000000000 --- a/src/masonite/contracts/StorageContract.py +++ /dev/null @@ -1,140 +0,0 @@ -from abc import ABC as Contract -from abc import abstractmethod - - -class StorageContract(Contract): - @abstractmethod - def put(self, location, contents): - """Puts a file into the correct directory - - Arguments: - location {string} -- The location of the file - contents {string|object|file-like object} -- The file object to add. - """ - pass - - @abstractmethod - def get(self, location): - """Get the file contents - - Arguments: - location {string} -- The location of the file - """ - pass - - @abstractmethod - def append(self, location, contents): - """Get the file contents - - Arguments: - location {string} -- The location of the file. - contents {string|object|file-like object} -- The file object to add. - """ - pass - - @abstractmethod - def delete(self, location): - """Deletes the file. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def exists(self, location): - """Checks if a file exists. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def driver(self): - pass - - @abstractmethod - def url(self, location): - """Gets the full URL of the file to be served. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def size(self, location): - """Gets the size of the file. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def extension(self, location): - """Gets the extension of the file. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def upload(self, *args, **kwargs): - """Passes all arguments to the upload version of this storage driver. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def all(self, location): - """Gets all files in a specific directory - - Arguments: - location {string} -- The location of the directory. - """ - pass - - @abstractmethod - def make_directory(self, directory): - """Make an empty directory - - Arguments: - directory {string} -- The location of the directory. - """ - pass - - @abstractmethod - def delete_directory(self, directory, force=False): - """Delete a directory. - - Arguments: - directory {string} -- The location of the directory - - Keyword Arguments: - force {bool} -- Whether or not a directory with contents should be deleted. (default: {False}) - """ - pass - - @abstractmethod - def move(self, old, new): - """Move a file from 1 location to another. - - Arguments: - old {string} -- The file of the file object to be moved. - new {string} -- The location where the file object should be moved to. - """ - pass - - @abstractmethod - def name(self, location): - """Gets the name of the file with the extension - - Arguments: - location {string} -- The location of the file - """ - pass diff --git a/src/masonite/contracts/UploadContract.py b/src/masonite/contracts/UploadContract.py deleted file mode 100644 index 20efc0a1a..000000000 --- a/src/masonite/contracts/UploadContract.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC, abstractmethod - - -class UploadContract(ABC): - @abstractmethod - def accept(self): - pass - - @abstractmethod - def validate_extension(self): - pass - - @abstractmethod - def store(self): - pass diff --git a/src/masonite/contracts/__init__.py b/src/masonite/contracts/__init__.py deleted file mode 100644 index 6d05f5e9e..000000000 --- a/src/masonite/contracts/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .AuthContract import AuthContract -from .BroadcastContract import BroadcastContract -from .CacheContract import CacheContract -from .MailContract import MailContract -from .QueueContract import QueueContract -from .SessionContract import SessionContract -from .StorageContract import StorageContract -from .UploadContract import UploadContract - -from .managers.BroadcastManagerContract import BroadcastManagerContract -from .managers.CacheManagerContract import CacheManagerContract -from .managers.MailManagerContract import MailManagerContract -from .managers.QueueManagerContract import QueueManagerContract -from .managers.SessionManagerContract import SessionManagerContract -from .managers.StorageManagerContract import StorageManagerContract -from .managers.UploadManagerContract import UploadManagerContract diff --git a/src/masonite/contracts/managers/BroadcastManagerContract.py b/src/masonite/contracts/managers/BroadcastManagerContract.py deleted file mode 100644 index 0ffb517a3..000000000 --- a/src/masonite/contracts/managers/BroadcastManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class BroadcastManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/CacheManagerContract.py b/src/masonite/contracts/managers/CacheManagerContract.py deleted file mode 100644 index cea45a79f..000000000 --- a/src/masonite/contracts/managers/CacheManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class CacheManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/MailManagerContract.py b/src/masonite/contracts/managers/MailManagerContract.py deleted file mode 100644 index 89cc95f3e..000000000 --- a/src/masonite/contracts/managers/MailManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class MailManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/QueueManagerContract.py b/src/masonite/contracts/managers/QueueManagerContract.py deleted file mode 100644 index 73c04a23c..000000000 --- a/src/masonite/contracts/managers/QueueManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class QueueManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/SessionManagerContract.py b/src/masonite/contracts/managers/SessionManagerContract.py deleted file mode 100644 index 67dbb328e..000000000 --- a/src/masonite/contracts/managers/SessionManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class SessionManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/StorageManagerContract.py b/src/masonite/contracts/managers/StorageManagerContract.py deleted file mode 100644 index 90d75de43..000000000 --- a/src/masonite/contracts/managers/StorageManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class StorageManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/UploadManagerContract.py b/src/masonite/contracts/managers/UploadManagerContract.py deleted file mode 100644 index f0620bfb5..000000000 --- a/src/masonite/contracts/managers/UploadManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class UploadManagerContract(ABC): - pass diff --git a/src/masonite/controllers/RedirectController.py b/src/masonite/controllers/RedirectController.py new file mode 100644 index 000000000..3b1b5c7a9 --- /dev/null +++ b/src/masonite/controllers/RedirectController.py @@ -0,0 +1,10 @@ +from ..response import Response + + +class RedirectController: + def __init__(self, url, status): + self.url = url + self.status = status + + def redirect(self, response: Response): + return response.redirect(self.url, status=self.status) diff --git a/src/masonite/controllers/__init__.py b/src/masonite/controllers/__init__.py index fd9c5d18f..b933ec388 100644 --- a/src/masonite/controllers/__init__.py +++ b/src/masonite/controllers/__init__.py @@ -1 +1,2 @@ from .Controller import Controller +from .RedirectController import RedirectController diff --git a/src/masonite/cookies/Cookie.py b/src/masonite/cookies/Cookie.py index 4ae956192..0b5fe55cd 100644 --- a/src/masonite/cookies/Cookie.py +++ b/src/masonite/cookies/Cookie.py @@ -5,7 +5,7 @@ def __init__( value, expires=None, http_only=True, - path=None, + path="/", timezone=None, secure=False, ): diff --git a/src/masonite/cookies/CookieJar.py b/src/masonite/cookies/CookieJar.py index d6ee5fc47..d2e925a4a 100644 --- a/src/masonite/cookies/CookieJar.py +++ b/src/masonite/cookies/CookieJar.py @@ -1,5 +1,7 @@ +import pendulum from .Cookie import Cookie -from ..helpers import cookie_expire_time + +from ..utils.time import cookie_expire_time class CookieJar: @@ -12,26 +14,37 @@ def add(self, name, value, **options): self.cookies.update({name: Cookie(name, value, **options)}) def all(self): + cookies = self.loaded_cookies + cookies.update(self.cookies) + return cookies + + def all_added(self): return self.cookies def get(self, name): - aggregate = self.loaded_cookies - aggregate.update(self.cookies) + aggregate = self.all() return aggregate.get(name) def exists(self, name): return name in self.cookies or name in self.loaded_cookies + def is_expired(self, name): + cookie = self.get(name) + return cookie.expires < pendulum.now() + def delete(self, name): self.deleted_cookies.update( { name: Cookie( - name, "", expires=cookie_expire_time("2 months"), timezone="GMT" + name, "", expires=cookie_expire_time("expired"), timezone="GMT" ) } ) if name in self.cookies: - return self.cookies.pop(name) + self.cookies.pop(name) + + if name in self.loaded_cookies: + self.loaded_cookies.pop(name) def load_cookie(self, key, value): self.loaded_cookies.update({key: Cookie(key, value)}) @@ -47,13 +60,14 @@ def to_dict(self): def load(self, cookie_string): for compound_value in cookie_string.split("; "): - key, value = compound_value.split("=", 1) - self.load_cookie(key, value) + if "=" in compound_value: + key, value = compound_value.split("=", 1) + self.load_cookie(key, value) return self def render_response(self): cookies = [] - for name, cookie in self.all().items(): + for name, cookie in {**self.deleted_cookies, **self.all_added()}.items(): cookies.append(("Set-Cookie", cookie.render())) return cookies diff --git a/src/masonite/drivers/BaseDriver.py b/src/masonite/drivers/BaseDriver.py deleted file mode 100644 index 80426f4b0..000000000 --- a/src/masonite/drivers/BaseDriver.py +++ /dev/null @@ -1,31 +0,0 @@ -"""The base class that all drivers inherit from.""" - - -class BaseDriver: - """Base driver class.""" - - _manager = None - - def driver(self, driver): - """Return an instance of the driver specified. - - Arguments: - driver {string} -- String representation of the driver to be resolved from the container. - This can be values like "s3" or "disk" - - Returns: - masonite.drivers -- Returns an instance of the driver. - """ - return self._manager.driver(driver) - - def load_manager(self, manager): - """Load the manager into the driver. - - Arguments: - manager {masonite.managers} -- Needs to be a Manager class. - - Returns: - self - """ - self._manager = manager - return self diff --git a/src/masonite/drivers/__init__.py b/src/masonite/drivers/__init__.py deleted file mode 100644 index 8f14e1b17..000000000 --- a/src/masonite/drivers/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from .BaseDriver import BaseDriver -from .mail.BaseMailDriver import BaseMailDriver -from .mail.Mailable import Mailable -from .authentication.AuthCookieDriver import AuthCookieDriver -from .authentication.AuthJwtDriver import AuthJwtDriver -from .upload.BaseUploadDriver import BaseUploadDriver -from .queue.BaseQueueDriver import BaseQueueDriver -from .cache.BaseCacheDriver import BaseCacheDriver -from .broadcast.BroadcastAblyDriver import BroadcastAblyDriver -from .broadcast.BroadcastPusherDriver import BroadcastPusherDriver -from .broadcast.BroadcastPubNubDriver import BroadcastPubNubDriver -from .cache.CacheDiskDriver import CacheDiskDriver -from .cache.CacheRedisDriver import CacheRedisDriver -from .mail.MailMailgunDriver import MailMailgunDriver -from .mail.MailSmtpDriver import MailSmtpDriver -from .mail.MailLogDriver import MailLogDriver -from .mail.MailTerminalDriver import MailTerminalDriver -from .queue.QueueAsyncDriver import QueueAsyncDriver -from .queue.QueueAmqpDriver import QueueAmqpDriver -from .queue.QueueDatabaseDriver import QueueDatabaseDriver -from .session.SessionCookieDriver import SessionCookieDriver -from .session.SessionMemoryDriver import SessionMemoryDriver -from .storage.StorageDiskDriver import StorageDiskDriver -from .upload.UploadDiskDriver import UploadDiskDriver -from .upload.UploadS3Driver import UploadS3Driver diff --git a/src/masonite/drivers/authentication/AuthCookieDriver.py b/src/masonite/drivers/authentication/AuthCookieDriver.py deleted file mode 100644 index 961d365cd..000000000 --- a/src/masonite/drivers/authentication/AuthCookieDriver.py +++ /dev/null @@ -1,64 +0,0 @@ -"""AuthCookieDriver Module.""" - -from ...contracts import AuthContract -from ...drivers import BaseDriver -from ...app import App - - -class AuthCookieDriver(BaseDriver, AuthContract): - def __init__(self, app: App): - """AuthCookieDriver initializer. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - """ - self.app = app - - def user(self, auth_model): - """Gets the user based on this driver implementation - - Arguments: - auth_model {orator.orm.Model} -- An Orator ORM type object. - - Returns: - Model|bool - """ - if self.app.make("Request").get_cookie("token") and auth_model: - return ( - auth_model.where( - "remember_token", self.app.make("Request").get_cookie("token") - ).first() - or False - ) - - return False - - def save(self, remember_token, **_): - """Saves the cookie to some state. - - In this case the state is saving to a cookie. - - Arguments: - remember_token {string} -- A token containing the state. - - Returns: - bool - """ - return self.app.make("Request").cookie("token", remember_token) - - def delete(self): - """Deletes the state depending on the implementation of this driver. - - Returns: - bool - """ - return self.app.make("Request").delete_cookie("token") - - def logout(self): - """Deletes the state depending on the implementation of this driver. - - Returns: - bool - """ - self.delete() - self.app.make("Request").reset_user() diff --git a/src/masonite/drivers/authentication/AuthJwtDriver.py b/src/masonite/drivers/authentication/AuthJwtDriver.py deleted file mode 100644 index 5bd3dacd8..000000000 --- a/src/masonite/drivers/authentication/AuthJwtDriver.py +++ /dev/null @@ -1,105 +0,0 @@ -"""AuthJWTDriver Module.""" - -import pendulum -from ...auth import Auth -from ...contracts import AuthContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config, cookie_expire_time -from ...request import Request - - -class AuthJwtDriver(BaseDriver, AuthContract): - def __init__(self, request: Request): - """AuthCookieDriver initializer. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - """ - self.request = request - try: - import jwt - - self.jwt = jwt - except ImportError: - raise DriverLibraryNotFound( - "Please install pyjwt by running 'pip install pyjwt'" - ) - - def user(self, auth_model): - """Gets the user based on this driver implementation - - Arguments: - auth_model {orator.orm.Model} -- An Orator ORM type object. - - Returns: - Model|bool - """ - from config.application import KEY - - if self.request.get_cookie("token"): - - try: - token = self.jwt.decode( - self.request.get_cookie("token"), KEY, algorithms=["HS256"] - ) - except self.jwt.exceptions.DecodeError: - self.delete() - return False - - expired = token["expired"] - token.pop("expired") - if not pendulum.from_format(expired, "ddd, DD MMM YYYY H:mm:ss").is_past(): - auth_model = auth_model() - return auth_model.hydrate(token) - - if config("auth.drivers.jwt.reauthentication", True): - auth_model = Auth(self.request).login_by_id( - token[auth_model.__primary_key__] - ) - else: - auth_model.hydrate(token) - - token.update( - { - "expired": cookie_expire_time( - config("auth.drivers.jwt.lifetime", "5 minutes") - ) - } - ) - self.request.cookie("token", token) - return auth_model - return False - - def save(self, _, **kwargs): - """Saves the state of authentication. - - In this case the state is serializing the user model and saving to a token cookie. - - Arguments: - remember_token {string} -- A token containing the state. - - Returns: - bool - """ - from config.application import KEY - - model = kwargs.get("model", False) - serialized_dictionary = model.serialize() - serialized_dictionary.update({"expired": cookie_expire_time("5 minutes")}) - token = self.jwt.encode(serialized_dictionary, KEY, algorithm="HS256") - if isinstance(token, bytes): - token = bytes(token).decode("utf-8") - self.request.cookie("token", token) - - def delete(self): - """Deletes the state depending on the implementation of this driver. - - Returns: - bool - """ - self.request.delete_cookie("token") - - def logout(self): - self.delete() - self.request.reset_user() diff --git a/src/masonite/drivers/broadcast/BroadcastAblyDriver.py b/src/masonite/drivers/broadcast/BroadcastAblyDriver.py deleted file mode 100644 index 8ec9f5783..000000000 --- a/src/masonite/drivers/broadcast/BroadcastAblyDriver.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Module for using the Ably websocket driver.""" - -from ...contracts import BroadcastContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config - - -class BroadcastAblyDriver(BroadcastContract, BaseDriver): - """Class for the Ably Driver.""" - - def __init__(self): - """Ably driver constructor. - - Arguments: - BroadcastConfig {config.broadcast} -- Broadcast configuration setting. - """ - self.ssl_message = True - - def ssl(self, boolean): - """Set whether to send data with SSL enabled. - - Arguments: - boolean {bool} -- Boolean on whether to set SSL. - - Returns: - self - """ - self.ssl_message = boolean - return self - - def channel(self, channels, message, event="base-event"): - """Specify which channel(s) you want to send information to. - - Arguments: - channels {string|list} -- Can be a string for the channel or a list of strings for the channels. - message {string} -- The message you want to send to the channel(s) - - Keyword Arguments: - event {string} -- The event you want broadcasted along with your data. (default: {'base-event'}) - - Raises: - DriverLibraryNotFound -- Thrown when ably is not installed. - - Returns: - string -- Returns the message sent. - """ - try: - from ably import AblyRest - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "ably" library. Please pip install this library running "pip install ably"' - ) - - configuration = config("broadcast.drivers.ably") - if not configuration: - raise Exception("Could not find ably broadcast configuration") - - client = AblyRest("{0}".format(configuration["secret"])) - - if isinstance(channels, list): - for channel in channels: - ably_channel = client.channels.get(channel) - ably_channel.publish(event, message) - else: - channel = client.channels.get(channels) - channel.publish(event, message) - - return message diff --git a/src/masonite/drivers/broadcast/BroadcastPubNubDriver.py b/src/masonite/drivers/broadcast/BroadcastPubNubDriver.py deleted file mode 100644 index 3d543425f..000000000 --- a/src/masonite/drivers/broadcast/BroadcastPubNubDriver.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Module for the PubNub websocket driver.""" - -from ...contracts import BroadcastContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config - - -class BroadcastPubNubDriver(BroadcastContract, BaseDriver): - """Class for the PubNub websocket driver.""" - - def __init__(self): - """PubNub driver constructor. - - Arguments: - BroadcastConfig {config.broadcast} -- Broadcast configuration. - """ - self.ssl_message = True - - def ssl(self, boolean): - """Set whether to send data with SSL enabled. - - Arguments: - boolean {bool} -- Boolean on whether to set SSL. - - Returns: - self - """ - self.ssl_message = boolean - return self - - def channel(self, channels, message, event="base-event"): - """Specify which channel(s) you want to send information to. - - Arguments: - channels {string|list} -- Can be a string for the channel or a list of strings for the channels. - message {string} -- The message you want to send to the channel(s) - - Keyword Arguments: - event {string} -- The event you want broadcasted along with your data. (default: {'base-event'}) - - Raises: - DriverLibraryNotFound -- Thrown when pubnub is not installed. - - Returns: - string -- Returns the message sent. - """ - try: - from pubnub.pnconfiguration import PNConfiguration - from pubnub.pubnub import PubNub - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "pubnub" library. Please pip install this library running "pip install pubnub"' - ) - - configuration = config("broadcast.drivers.pubnub") - pnconfig = PNConfiguration() - pnconfig.publish_key = configuration["publish_key"] - pnconfig.subscribe_key = configuration["subscribe_key"] - pnconfig.secret = configuration["secret"] - pnconfig.ssl = self.ssl_message - pnconfig.uuid = config("application.name") - - pubnub = PubNub(pnconfig) - - if isinstance(message, str): - message = {"message": message} - - if isinstance(channels, str): - channels = [channels] - - for channel in channels: - envelope = pubnub.publish().channel(channel).message(message).sync() - if envelope.status.is_error(): - print( - "PubNub Broadcast: error sending message to channel {0}.".format( - channel - ) - ) - return message diff --git a/src/masonite/drivers/broadcast/BroadcastPusherDriver.py b/src/masonite/drivers/broadcast/BroadcastPusherDriver.py deleted file mode 100644 index 1128b56f9..000000000 --- a/src/masonite/drivers/broadcast/BroadcastPusherDriver.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Module for the Pusher websocket driver.""" - -from ...contracts import BroadcastContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config - - -class BroadcastPusherDriver(BroadcastContract, BaseDriver): - """Class for the Pusher websocket driver.""" - - def __init__(self): - """Pusher driver constructor. - - Arguments: - BroadcastConfig {config.broadcast} -- Broadcast configuration. - """ - self.ssl_message = True - - def ssl(self, boolean): - """Set whether to send data with SSL enabled. - - Arguments: - boolean {bool} -- Boolean on whether to set SSL. - - Returns: - self - """ - self.ssl_message = boolean - return self - - def channel(self, channels, message, event="base-event"): - """Specify which channel(s) you want to send information to. - - Arguments: - channels {string|list} -- Can be a string for the channel or a list of strings for the channels. - message {string} -- The message you want to send to the channel(s) - - Keyword Arguments: - event {string} -- The event you want broadcasted along with your data. (default: {'base-event'}) - - Raises: - DriverLibraryNotFound -- Thrown when pusher is not installed. - - Returns: - string -- Returns the message sent. - """ - try: - import pusher - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "pusher" library. Please pip install this library running "pip install pusher"' - ) - - configuration = config("broadcast.drivers.pusher") - - pusher_client = pusher.Pusher( - app_id=str(configuration["app_id"]), - key=configuration["client"], - secret=configuration["secret"], - cluster=configuration["cluster"], - ssl=self.ssl_message, - ) - - if isinstance(message, str): - message = {"message": message} - - if isinstance(channels, list): - for channel in channels: - pusher_client.trigger(channel, event, message) - else: - pusher_client.trigger(channels, event, message) - - return message diff --git a/src/masonite/drivers/cache/BaseCacheDriver.py b/src/masonite/drivers/cache/BaseCacheDriver.py deleted file mode 100644 index 2430bdd5c..000000000 --- a/src/masonite/drivers/cache/BaseCacheDriver.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Base cache driver module.""" - -from ...drivers import BaseDriver - - -class BaseCacheDriver(BaseDriver): - """Base class that all cache drivers inherit from.""" - - def calculate_time(self, cache_type, cache_time): - """Convert time to required unit - Returns: - self - """ - - cache_type = cache_type.lower() - calc = 0 - - if cache_type in ("second", "seconds"): - # Set time now for - calc = 1 - elif cache_type in ("minute", "minutes"): - calc = 60 - elif cache_type in ("hour", "hours"): - calc = 60 ** 2 - elif cache_type in ("day", "days"): - calc = 60 ** 3 - elif cache_type in ("month", "months"): - calc = 60 ** 4 - elif cache_type in ("year", "years"): - calc = 60 ** 5 - else: - raise ValueError("{0} is not a valid caching type.".format(cache_type)) - - return cache_time * calc diff --git a/src/masonite/drivers/cache/CacheDiskDriver.py b/src/masonite/drivers/cache/CacheDiskDriver.py deleted file mode 100644 index 508f35766..000000000 --- a/src/masonite/drivers/cache/CacheDiskDriver.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Module for the ache disk driver.""" - -import glob -import os -import time - -from ...contracts import CacheContract -from ...drivers import BaseCacheDriver -from ...helpers import config - - -class CacheDiskDriver(CacheContract, BaseCacheDriver): - """Class for the cache disk driver.""" - - def __init__(self): - """Cache disk driver constructor. - - Arguments: - CacheConfig {config.cache} -- Cache configuration module. - Application {config.application} -- Application configuration module. - """ - self.config = config("cache") - self.appconfig = config("application") - self.cache_forever = None - - def store(self, key, value, extension=".txt", location=None): - """Store content in cache file. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Returns: - string -- Returns the key - """ - self.cache_forever = True - if not location: - location = self.config.DRIVERS["disk"]["location"] - - location += "/" - path = os.path.join(location, key + extension) - if not os.path.exists(path): - self._create_directory(path) - - with open(path, "w") as file: - file.write(value) - - return key - - def store_for( - self, key, value, cache_time, cache_type, extension=".txt", location=None - ): - """Store the cache for a specific amount of time. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - cache_time {int|string} -- The time as a string or an integer (1, 2, 5, 100, etc) - cache_type {string} -- The type of time to store for (minute, minutes, hours, seconds, etc) - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Raises: - ValueError -- Thrown if an invalid cache type was caught (like houes instead of hours). - - Returns: - string -- Returns the key - """ - self.cache_forever = False - - cache_for_time = self.calculate_time(cache_type, cache_time) - - cache_for_time = cache_for_time + time.time() - - key = self.store(key + ":" + str(cache_for_time), value, extension, location) - - return key - - def get(self, key): - """Get the data from a key in the cache.""" - if not self.is_valid(key): - return None - - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - content = "" - - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - try: - with open(glob.glob(glob_path)[0], "r") as file: - content = file.read() - except IndexError: - pass - - return content - - def delete(self, key): - """Delete file cache.""" - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - for template in glob.glob(glob_path): - os.remove(template) - - def update(self, key, value, location=None): - """Update a specific cache by key.""" - if not location: - location = self.config.DRIVERS["disk"]["location"] + "/" - - location = os.path.join(location, key) - cache = glob.glob(location + ":*")[0] - - with open(cache, "w") as file: - file.write(str(value)) - - return key - - def exists(self, key): - """Check if the cache exists.""" - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - find_template = glob.glob(glob_path) - if find_template: - return True - return False - - def is_valid(self, key): - """Check if a valid cache.""" - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - cache_file = glob.glob(glob_path) - if cache_file: - try: - cache_timestamp = float( - os.path.splitext(cache_file[0])[0].split(":")[1] - ) - except IndexError: - if self.cache_forever: - return True - - return False - - if cache_timestamp > time.time(): - return True - - self.delete(key) - return False - - def _create_directory(self, directory): - """Creates a new dir. - - Arguments: - directory {string} -- name of directory to create. - - Returns: - bool - Returns a boolean if the directory was created. - """ - if not os.path.exists(os.path.dirname(directory)): - # Create the path to the model if it does not exist - os.makedirs(os.path.dirname(directory)) - return True - return False diff --git a/src/masonite/drivers/cache/CacheRedisDriver.py b/src/masonite/drivers/cache/CacheRedisDriver.py deleted file mode 100644 index 537308434..000000000 --- a/src/masonite/drivers/cache/CacheRedisDriver.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Module for the ache disk driver.""" - -import os - -from ...contracts import CacheContract -from ...drivers import BaseCacheDriver -from ...exceptions import DriverLibraryNotFound - - -class CacheRedisDriver(CacheContract, BaseCacheDriver): - """Class for the cache redis driver.""" - - def __init__(self): - """Cache redis driver constructor. - - Arguments: - CacheConfig {config.cache} -- Cache configuration module. - Application {config.application} -- Application configuration module. - """ - from config import application, cache - - self.appconfig = application - self.cache_forever = None - self.app_name = os.getenv("APP_NAME", "masonite") - - config = cache.DRIVERS["redis"] - - try: - import redis - - self.redis = redis - except ImportError: - raise DriverLibraryNotFound( - "Could not find the 'redis' library. Run pip install redis to fix this." - ) - - self.connection = redis.StrictRedis( - host=config["host"], - port=config["port"], - password=config["password"], - decode_responses=True, - ) - - def store(self, key, value): - """Stores content in cache file. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Returns: - string -- Returns the key - """ - - self.cache_forever = True - - self.connection.set("{0}_cache_{1}".format(self.app_name, key), value) - - return key - - def store_for(self, key, value, cache_time, cache_type): - """Store the cache for a specific amount of time. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - cache_time {int|string} -- The time as a string or an integer (1, 2, 5, 100, etc) - cache_type {string} -- The type of time to store for (minute, minutes, hours, seconds, etc) - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Raises: - ValueError -- Thrown if an invalid cache type was caught (like houes instead of hours). - - Returns: - string -- Returns the key - """ - - self.cache_forever = False - cache_for_time = self.calculate_time(cache_type, cache_time) - - self.connection.set( - "{0}_cache_{1}".format(self.app_name, key), value, ex=cache_for_time - ) - - return key - - def get(self, key): - """Get the data from a key in the cache.""" - return self.connection.get("{0}_cache_{1}".format(self.app_name, key)) - - def delete(self, key): - """Delete file cache.""" - self.connection.delete("{0}_cache_{1}".format(self.app_name, key)) - - def update(self, key, value): - """Updates a specific cache by key.""" - time_to_expire = self.connection.ttl("{0}_cache_{1}".format(self.app_name, key)) - - if time_to_expire > 0: - self.connection.set( - "{0}_cache_{1}".format(self.app_name, key), value, ex=time_to_expire - ) - else: - self.connection.set("{0}_cache_{1}".format(self.app_name, key), value) - - return key - - def exists(self, key): - """Check if the cache exists.""" - return self.connection.exists("{0}_cache_{1}".format(self.app_name, key)) - - def is_valid(self, key): - """Check if a valid cache.""" - return self.exists(key) diff --git a/src/masonite/drivers/mail/BaseMailDriver.py b/src/masonite/drivers/mail/BaseMailDriver.py deleted file mode 100644 index 82d4fcae7..000000000 --- a/src/masonite/drivers/mail/BaseMailDriver.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Base mail driver module.""" - -import copy -import re - - -from ...app import App -from ...helpers import config, deprecated -from ...response import Responsable -from .. import BaseDriver - - -MAIL_FROM_RE = re.compile(r'(?:"?([^"]*)"?\s)?(?:]+)>?)') - - -class BaseMailDriver(BaseDriver, Responsable): - """Base mail driver class. This class is inherited by all mail drivers.""" - - def __init__(self, app: App): - """Base mail driver constructor. - - Arguments: - app {masonite.app.App} -- The Masonite container class. - view {object} -- This is the masonite.view.View class. - """ - self.config = config("mail") - self.app = app - self.to_addresses = [] - self.message_subject = "Subject" - self.message_reply_to = None - self.from_name = self.config.FROM["name"] - self.from_address = self.config.FROM["address"] - self._queue = False - self.html_content = None - self.text_content = None - self._message = None - - def _get_message_for_send_deprecated(self, message_contents): - """Helper method for backwards compatibility to generate a message from .send() - - Args: - message_contents: String - - Returns: - message - """ - # we used to not override self.message_body, so save it and set it back... - old_text, old_html = self.text_content, self.html_content - self.text_content, self.html_content = None, message_contents - data = self.message() - self.text_content, self.html_content = old_text, old_html - return data - - def message(self): - """Creates a message object for the underlying driver. - - Returns: - message - """ - raise NotImplementedError - - @property - def mail_from_header(self): - return '"{0}" <{1}>'.format(self.from_name, self.from_address) - - @property - def mail_to_header(self): - return ",".join(self.to_addresses) - - def text(self, content): - """Set the text content of the email. - - Arguments: - content {string} -- The email text content. - - Returns: - self - """ - self.text_content = content - return self - - def html(self, content): - """Set the html content of the email. - - Arguments: - content {string} -- The email html content. - - Returns: - self - """ - self.html_content = content - return self - - @property - def message_body(self): - """Returns the body of the message.""" - return self.html_content or self.text_content - - @message_body.setter - @deprecated("Please use `.text()` and `.html()` methods instead.") - def message_body(self, value): - self.html_content = value - - def to(self, user_email): - """Set the user email address who you want to send mail to. - - Arguments: - user_email {string} -- The user email address. - - Returns: - self - """ - if callable(user_email): - user_email = user_email.email - - if isinstance(user_email, (list, tuple)): - self.to_addresses = user_email - else: - self.to_addresses = [user_email] - return self - - def queue(self): - """Whether the email should be queued or not when sending. - - Returns: - self - """ - self._queue = True - return self - - def template(self, template_name, dictionary={}, mimetype="html"): - """Create an email from a normal Jinja template. - - Arguments: - template_name {string} -- The name of the template. - - Keyword Arguments: - dictionary {dict} -- The data to be passed to the template. (default: {{}}) - mimetype {string} -- whether it is html or text content. (default: {html}) - - Returns: - self - """ - view = copy.copy(self.app.make("ViewClass")) - content = view.render(template_name, dictionary).rendered_template - if mimetype == "html": - self.html(content) - else: - self.text(content) - return self - - def send_from(self, address, name=None): - """Set the from address of who the sender should be. - - Arguments: - address {string} -- A email address used as the From field in an email. - "John S" - John S - john@example.com - name {string} -- A name used as the From field in an email. - - Returns: - self - """ - match = MAIL_FROM_RE.match(address) - if not match: - raise ValueError("Invalid address specified") - - match_name, match_address = match.groups() - self.from_address = match_address - if name is None and match_name: - self.from_name = match_name - - return self - - def mailable(self, mailable): - """Set the from address of who the sender should be. - - Arguments: - address {string} -- A name used as the From field in an email. - - Returns: - self - """ - mailable = self.app.resolve(mailable.build) - ( - self.to(mailable._to) - .send_from(mailable._from) - .subject(mailable._subject) - .template(mailable.template, mailable.variables) - .reply_to(mailable._reply_to) - ) - return self - - def subject(self, subject): - """Set the subject of an email. - - Arguments: - subject {string} -- The subject of the email - - Returns: - self - """ - self.message_subject = subject - return self - - def get_response(self): - return self.message_body - - def reply_to(self, reply_to): - """Set the Reply-To of an email. - - Arguments: - reply_to {string} -- The reply-to of the email - - Returns: - self - """ - self.message_reply_to = reply_to - return self diff --git a/src/masonite/drivers/mail/MailLogDriver.py b/src/masonite/drivers/mail/MailLogDriver.py deleted file mode 100644 index b59a7de77..000000000 --- a/src/masonite/drivers/mail/MailLogDriver.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Log Driver Module.""" - -import logging -import os - -from ...app import App -from ...contracts import MailContract -from ...drivers import BaseMailDriver - - -class MailLogDriver(BaseMailDriver, MailContract): - """Mail log driver.""" - - def __init__(self, app: App): - super().__init__(app) - - if "log" in self.config.DRIVERS and "location" in self.config.DRIVERS["log"]: - log_location = self.config.DRIVERS["log"]["location"] - else: - log_location = "bootstrap/mail" - - if not os.path.exists(log_location): - # Create the path to the model if it does not exist - os.makedirs(log_location) - - handler = logging.FileHandler( - "{0}/{1}".format(log_location, os.getenv("MAIL_LOGFILE", "mail.log")), - delay=True, - ) - self.logger = logging.getLogger(__name__) - self.logger.handlers = [] - self.logger.propagate = False - self.logger.addHandler(handler) - self.logger.setLevel(logging.INFO) - - def send(self, message=None): - """Prints the message in a log. - - Keyword Arguments: - message {string} -- The message to be printed. (default: { None }) - - Returns: - None - """ - - if not message: - message = self.message_body - - self.logger.info("***************************************") - - self.logger.info("To: {}".format(self.mail_to_header)) - self.logger.info("From: {}".format(self.mail_from_header)) - self.logger.info("Subject: {}".format(self.message_subject)) - self.logger.info("Reply-To: {}".format(self.message_reply_to)) - self.logger.info("Message: ") - self.logger.info(message) - - self.logger.info("***************************************") diff --git a/src/masonite/drivers/mail/MailMailgunDriver.py b/src/masonite/drivers/mail/MailMailgunDriver.py deleted file mode 100644 index d67b035f2..000000000 --- a/src/masonite/drivers/mail/MailMailgunDriver.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Mailgun Driver Module.""" -import warnings - -import requests - -from ...contracts.MailContract import MailContract -from ...drivers import BaseMailDriver - - -class MailMailgunDriver(BaseMailDriver, MailContract): - """Mailgun driver.""" - - def message(self): - data = { - "from": self.mail_from_header, - "to": self.to_addresses, - "subject": self.message_subject, - "h:Reply-To": self.message_reply_to, - } - - # Attach both mimetypes if they exist. - if self.text_content: - data["text"] = self.text_content - if self.html_content: - data["html"] = self.html_content - - return data - - def send(self, message=None): - """Send the message through the Mailgun service. - - Keyword Arguments: - message {string} -- The message to be sent to Mailgun. (default: {None}) - - Returns: - requests.post -- Returns the response as a requests object. - """ - if message and isinstance(message, str): - warnings.warn( - "Passing message to .send() is deprecated. Please use .text() and .html().", - category=DeprecationWarning, - stacklevel=2, - ) - data = self._get_message_for_send_deprecated(message) - - # The above should be removed once deprecation time period passed. - elif not message: - data = self.message() - else: - data = message - - if self._queue: - from wsgi import container - from ... import Queue - - return container.make(Queue).push(self._send_mail, args=(data,)) - - return self._send_mail(data) - - def _send_mail(self, data): - """Wrapper around sending mail so it can also be used with queues. - - Arguments: - data {dict} -- The data for mailgun post request. - - Returns: - requests.post - """ - - domain = self.config.DRIVERS["mailgun"]["domain"] - secret = self.config.DRIVERS["mailgun"]["secret"] - - return requests.post( - "https://api.mailgun.net/v3/{0}/messages".format(domain), - auth=("api", secret), - data=data, - ) diff --git a/src/masonite/drivers/mail/MailSmtpDriver.py b/src/masonite/drivers/mail/MailSmtpDriver.py deleted file mode 100644 index 6da4e05e4..000000000 --- a/src/masonite/drivers/mail/MailSmtpDriver.py +++ /dev/null @@ -1,111 +0,0 @@ -"""SMTP Driver Module.""" - -import smtplib -import ssl -import warnings -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - -from ...contracts.MailContract import MailContract -from ...drivers import BaseMailDriver - - -class MailSmtpDriver(BaseMailDriver, MailContract): - """Mail smtp driver.""" - - def message(self): - """Creates a message object for the underlying driver. - - Returns: - email.mime.multipart.MIMEMultipart - """ - message = MIMEMultipart("alternative") - message["Subject"] = self.message_subject - message["From"] = self.mail_from_header - message["To"] = self.mail_to_header - message["Reply-To"] = self.message_reply_to - - # Attach both mimetypes if they exist. - if self.html_content: - message.attach(MIMEText(self.html_content, "html")) - - if self.text_content: - message.attach(MIMEText(self.text_content, "plain")) - - return message - - def send(self, message=None, message_contents=None): - """Send the message through SMTP. - - Keyword Arguments: - message {string} -- The HTML message to be sent to SMTP. (default: {None}) - - Returns: - None - """ - # The old argument name was `message_contents`. users might have used this as keyword argument or as arg. - assert ( - message is None or message_contents is None - ), 'using deprecated argument "message_contents" together with the new arg "message" ??' - message_contents = message or message_contents - if message_contents and isinstance(message_contents, str): - warnings.warn( - "Passing message_contents to .send() is a deprecated. Please use .text() and .html().", - category=DeprecationWarning, - stacklevel=2, - ) - message = self._get_message_for_send_deprecated(message_contents) - - # The above should be removed once deprecation time period passed. - elif not message: - message = self.message() - - self._smtp_connect() - - if self._queue: - from wsgi import container - from ... import Queue - - container.make(Queue).push( - self._send_mail, - args=(self.mail_from_header, self.to_addresses, message), - ) - return - - return self._send_mail(self.mail_from_header, self.to_addresses, message) - - def _smtp_connect(self): - """Sets self.smtp to an instance of `smtplib.SMTP` - and connects using configuration in config.DRIVERS.smtp - Returns: - None - """ - config = self.config.DRIVERS["smtp"] - if "ssl" in config and config["ssl"] is True: - self.smtp = smtplib.SMTP_SSL( - "{0}:{1}".format(config["host"], config["port"]) - ) - else: - self.smtp = smtplib.SMTP("{0}:{1}".format(config["host"], config["port"])) - - # Check if TLS enabled - if "tls" in config and config["tls"] is True: - # Define secure TLS connection - context = ssl.create_default_context() - context.check_hostname = False - - # Check if correct response code for starttls is received from the server - if self.smtp.starttls(context=context)[0] != 220: - raise smtplib.SMTPNotSupportedError( - "Server is using untrusted protocol." - ) - - if config.get("login", True): - self.smtp.login(config["username"], config["password"]) - - def _send_mail(self, *args): - """Wrapper around sending mail so it can also be used for queues.""" - mail_from_header, to_addresses, message = args - response = self.smtp.send_message(message) - self.smtp.quit() - return response diff --git a/src/masonite/drivers/mail/MailTerminalDriver.py b/src/masonite/drivers/mail/MailTerminalDriver.py deleted file mode 100644 index 7645a7cd3..000000000 --- a/src/masonite/drivers/mail/MailTerminalDriver.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Terminal Driver Module.""" - -import logging - -from ...app import App -from ...contracts.MailContract import MailContract -from ...drivers import BaseMailDriver - - -class MailTerminalDriver(BaseMailDriver, MailContract): - """Mail terminal driver.""" - - def __init__(self, app: App): - super().__init__(app) - self.logger = logging.getLogger(__name__) - self.logger.handlers = [] - handler = logging.StreamHandler() - self.logger.setLevel(logging.INFO) - self.logger.addHandler(handler) - self.logger.propagate = False - - def send(self, message=None): - """Prints the message to the terminal. - - Keyword Arguments: - message {string} -- The message to be printed. (default: { None }) - - Returns: - None - """ - - if not message: - message = self.message_body - - self.logger.info("***************************************") - - self.logger.info("To: {}".format(self.mail_to_header)) - self.logger.info("From: {}".format(self.mail_from_header)) - self.logger.info("Subject: {}".format(self.message_subject)) - self.logger.info("Reply-To: {}".format(self.message_reply_to)) - self.logger.info("Message: ") - self.logger.info(message) - - self.logger.info("***************************************") diff --git a/src/masonite/drivers/mail/Mailable.py b/src/masonite/drivers/mail/Mailable.py deleted file mode 100644 index ce76f13d0..000000000 --- a/src/masonite/drivers/mail/Mailable.py +++ /dev/null @@ -1,26 +0,0 @@ -class Mailable: - - _to = "" - _from = "" - _subject = "" - - def view(self, template, variables={}): - self.template = template - self.variables = variables - return self - - def to(self, to): - self._to = to - return self - - def reply_to(self, reply_to): - self._reply_to = reply_to - return self - - def send_from(self, send_from): - self._from = send_from - return self - - def subject(self, subject): - self._subject = subject - return self diff --git a/src/masonite/drivers/queue/AMQPDriver.py b/src/masonite/drivers/queue/AMQPDriver.py new file mode 100644 index 000000000..a5076eae9 --- /dev/null +++ b/src/masonite/drivers/queue/AMQPDriver.py @@ -0,0 +1,185 @@ +import pickle +import pendulum +import inspect +from ...utils.console import HasColoredOutput + + +class AMQPDriver(HasColoredOutput): + def __init__(self, application): + self.application = application + self.connection = None + self.publishing_channel = None + + def set_options(self, options): + self.options = options + return self + + def push(self, *jobs, args=(), **kwargs): + for job in jobs: + payload = { + "obj": job, + "args": args, + "callback": self.options.get("callback", "handle"), + "created": pendulum.now(tz=self.options.get("tz", "UTC")), + } + + try: + self.connect().publish(payload) + except (self.get_connection_exceptions()): + self.connect().publish(payload) + + def get_connection_exceptions(self): + pika = self.get_package_library() + return ( + pika.exceptions.ConnectionClosed, + pika.exceptions.ChannelClosed, + pika.exceptions.ConnectionWrongStateError, + pika.exceptions.ChannelWrongStateError, + ) + + def publish(self, payload): + pika = self.get_package_library() + self.publishing_channel.basic_publish( + exchange="", + routing_key=self.options.get("queue"), + body=pickle.dumps(payload), + properties=pika.BasicProperties( + delivery_mode=2, # make message persistent + ), + ) + self.publishing_channel.close() + self.connection.close() + + def get_package_library(self): + try: + import pika + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pika' library. Run 'pip install pika' to fix this." + ) + + return pika + + def connect(self): + try: + import pika + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pika' library. Run 'pip install pika' to fix this." + ) + + self.connection = pika.BlockingConnection( + pika.URLParameters( + "amqp://{}:{}@{}{}/{}".format( + self.options.get("username"), + self.options.get("password"), + self.options.get("host"), + ":" + str(self.options.get("port")) + if self.options.get("port") + else "", + self.options.get("vhost", "%2F"), + ) + ) + ) + + self.publishing_channel = self.connection.channel() + + self.publishing_channel.queue_declare(self.options.get("queue"), durable=True) + + return self + + def consume(self): + self.success( + '[*] Waiting to process jobs on the "{}" queue. To exit press CTRL+C'.format( + self.options.get("queue") + ) + ) + + self.connect() + + self.publishing_channel.basic_qos(prefetch_count=1) + + self.publishing_channel.basic_consume(self.options.get("queue"), self.work) + + try: + self.publishing_channel.start_consuming() + finally: + self.publishing_channel.stop_consuming() + self.publishing_channel.close() + self.connection.close() + + def retry(self): + builder = ( + self.application.make("builder") + .new() + .on(self.options.get("connection")) + .table(self.options.get("failed_table", "failed_jobs")) + ) + + jobs = builder.get() + + if len(jobs) == 0: + self.success("No failed jobs found.") + return + + for job in jobs: + try: + self.connect().publish(pickle.loads(job["payload"])) + except (self.get_connection_exceptions()): + self.connect().publish(pickle.loads(job["payload"])) + + self.success(f"Added {len(jobs)} failed jobs back to the queue") + builder.table(self.options.get("failed_table", "failed_jobs")).where_in( + "id", [x["id"] for x in jobs] + ).delete() + + def work(self, ch, method, _, body): + + job = pickle.loads(body) + obj = job["obj"] + args = job["args"] + callback = job["callback"] + + try: + try: + if inspect.isclass(obj): + obj = self.application.resolve(obj) + + getattr(obj, callback)(*args) + + except AttributeError: + obj(*args) + + self.success( + f"[{method.delivery_tag}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Successfully Processed" + ) + except Exception as e: + self.danger( + f"[{method.delivery_tag}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Failed" + ) + + getattr(obj, "failed")(job, str(e)) + + self.add_to_failed_queue_table( + self.application.make("builder").new(), str(job["obj"]), body, str(e) + ) + + ch.basic_ack(delivery_tag=method.delivery_tag) + + def add_to_failed_queue_table(self, builder, name, payload, exception): + builder.table(self.options.get("failed_table", "failed_jobs")).create( + { + "driver": "amqp", + "queue": self.options.get("queue", "default"), + "name": name, + "connection": self.options.get("connection"), + "created_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + "exception": exception, + "payload": payload, + "failed_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + } + ) diff --git a/src/masonite/drivers/queue/QueueAsyncDriver.py b/src/masonite/drivers/queue/AsyncDriver.py similarity index 59% rename from src/masonite/drivers/queue/QueueAsyncDriver.py rename to src/masonite/drivers/queue/AsyncDriver.py index 16ede633f..8678bc86d 100644 --- a/src/masonite/drivers/queue/QueueAsyncDriver.py +++ b/src/masonite/drivers/queue/AsyncDriver.py @@ -1,26 +1,57 @@ -"""Async Driver Method.""" - import inspect import os from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed -from ...app import App -from ...contracts import QueueContract -from ...drivers import BaseQueueDriver from ...exceptions import QueueException -from ...helpers import HasColoredCommands, config -class QueueAsyncDriver(BaseQueueDriver, HasColoredCommands, QueueContract): - """Queue Aysnc Driver.""" +class AsyncDriver: + def __init__(self, application): + self.application = application + + def set_options(self, options): + self.options = options + return self - def __init__(self, app: App): - """Queue Async Driver. + def push(self, *jobs, args=(), **kwargs): + """Push objects onto the async stack. Arguments: - Container {masonite.app.App} -- The application container. + objects {*args of objects} - This can be several objects as parameters into this method. + options {**kwargs of options} - Additional options for async driver """ - self.container = app + + # Initialize Extra Options + options = self.options + callback = options.get("callback", "handle") + mode = options.get("mode", "threading") + workers = options.get("workers", None) + + # Set processor to either use threads or processes + processor = self._get_processor(mode=mode, max_workers=workers) + is_blocking = options.get("blocking", False) + + ran = {} + for obj in jobs: + obj = self.application.resolve(obj) if inspect.isclass(obj) else obj + try: + future = processor.submit(getattr(obj, callback), *args, **kwargs) + except AttributeError: + # Could be wanting to call only a method asynchronously + future = processor.submit(obj, *args, **kwargs) + ran.update({future: obj}) + + if is_blocking: + for job in as_completed(ran.keys()): + if job.exception(): + ran[job].failed(ran[job], job.exception()) + print(f"Job Ran: {job}") + + def consume(self, **options): + pass + + def retry(self, **options): + pass def _get_processor(self, mode, max_workers): """Set processor to use either threads or multiprocesses @@ -30,13 +61,10 @@ def _get_processor(self, mode, max_workers): max_workers {int} - number of threads/processes to use """ - # Necessary for Python 3.4, can be removed in 3.5+ if max_workers is None: # Use this number because ThreadPoolExecutor is often # used to overlap I/O instead of CPU work. max_workers = (os.cpu_count() or 1) * 5 - if max_workers <= 0: - raise QueueException("max_workers must be greater than 0") # Determine Mode for Processing if mode == "threading": @@ -46,35 +74,3 @@ def _get_processor(self, mode, max_workers): else: raise QueueException("Queue mode {} not recognized".format(mode)) return processor - - def push(self, *objects, args=(), kwargs={}, **options): - """Push objects onto the async stack. - - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - options {**kwargs of options} - Additional options for async driver - """ - - # Initialize Extra Options - callback = options.get("callback", "handle") - mode = options.get("mode", config("queue.drivers.async.mode", "threading")) - workers = options.get("workers", None) - - # Set processor to either use threads or processes - processor = self._get_processor(mode=mode, max_workers=workers) - is_blocking = config("queue.drivers.async.blocking", False) - - ran = [] - for obj in objects: - obj = self.container.resolve(obj) if inspect.isclass(obj) else obj - try: - future = processor.submit(getattr(obj, callback), *args, **kwargs) - except AttributeError: - # Could be wanting to call only a method asyncronously - future = processor.submit(obj, *args, **kwargs) - - ran.append(future) - - if is_blocking: - for job in as_completed(ran): - self.info("Job Ran: {}".format(job)) diff --git a/src/masonite/drivers/queue/BaseQueueDriver.py b/src/masonite/drivers/queue/BaseQueueDriver.py deleted file mode 100644 index 2365eacdb..000000000 --- a/src/masonite/drivers/queue/BaseQueueDriver.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Base queue driver.""" - -import pickle - -import pendulum - -from ...drivers import BaseDriver -from ...helpers import HasColoredCommands - - -class BaseQueueDriver(BaseDriver, HasColoredCommands): - def add_to_failed_queue_table(self, payload, channel=None, driver="amqp"): - from config.database import DB - from config import queue - - schema = DB.get_schema_builder() - - if schema.has_table("failed_jobs"): - DB.get_query_builder().table("failed_jobs").create( - { - "driver": driver, - "queue": channel, - "channel": channel, - "payload": pickle.dumps(payload), - "failed_at": pendulum.now().to_datetime_string(), - } - ) - - def run_failed_jobs(self): - from config.database import DB - - try: - self.success("Attempting to send failed jobs back to the queue ...") - builder = DB.get_query_builder() - jobs = builder.table("failed_jobs").get() - if not jobs: - return self.success("No failed jobs found") - - self.success(f"Found {len(jobs)} jobs") - for job in builder.table("failed_jobs").get(): - payload = pickle.loads(job["payload"]) - - builder.table("failed_jobs").where("payload", job["payload"]).delete() - self.push( - payload["obj"], args=payload["args"], callback=payload["callback"] - ) - self.success("Jobs successfully added back to the queue.") - except Exception as e: - raise e - self.danger("Could not get the failed_jobs table") - - def push(self, *objects, args=(), callback="handle", ran=1, channel=None): - raise NotImplementedError - - def connect(self): - return self - - def consume(self, channel, **options): - raise NotImplementedError( - "The {} driver does not implement consume".format(self.__class__.__name__) - ) - - def work(self): - raise NotImplementedError( - "The {} driver does not implement work".format(self.__class__.__name__) - ) diff --git a/src/masonite/drivers/queue/DatabaseDriver.py b/src/masonite/drivers/queue/DatabaseDriver.py new file mode 100644 index 000000000..0c7abb34f --- /dev/null +++ b/src/masonite/drivers/queue/DatabaseDriver.py @@ -0,0 +1,205 @@ +import pickle +import pendulum +from ...utils.console import HasColoredOutput +from ...utils.time import parse_human_time +import time + + +class DatabaseDriver(HasColoredOutput): + def __init__(self, application): + self.application = application + + def set_options(self, options): + self.options = options + return self + + def push(self, *jobs, args=(), **kwargs): + builder = self.get_builder() + + available_at = parse_human_time(kwargs.get("delay", "now")) + + for job in jobs: + payload = pickle.dumps( + { + "obj": job, + "args": args, + "kwargs": kwargs, + "callback": self.options.get("callback", "handle"), + } + ) + + builder.create( + { + "name": str(job), + "payload": payload, + "available_at": available_at.to_datetime_string(), + "attempts": 0, + "queue": self.options.get("queue", "default"), + } + ) + + def consume(self): + print("Listening for jobs on queue: " + self.options.get("queue", "default")) + builder = self.get_builder() + + while True: + time.sleep(int(self.options.get("poll", 1))) + if self.options.get("verbosity") == "vv": + print("Checking for available jobs .. ") + builder = builder.new().table(self.options.get("table")) + jobs = ( + builder.where("queue", self.options.get("queue", "default")) + .where( + "available_at", + "<=", + pendulum.now(tz=self.options.get("tz", "UTC")).to_datetime_string(), + ) + .limit(10) + .order_by("id") + .get() + ) + + if self.options.get("verbosity") == "vv": + print(f"Found {len(jobs)} job(s) ") + + builder.where_in("id", [x["id"] for x in jobs]).update( + { + "reserved_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string() + } + ) + + for job in jobs: + builder.where("id", job["id"]).table(self.options.get("table")).update( + { + "ran_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + } + ) + payload = job["payload"] + unserialized = pickle.loads(job["payload"]) + obj = unserialized["obj"] + args = unserialized["args"] + callback = unserialized["callback"] + + try: + try: + getattr(obj, callback)(*args) + + except AttributeError: + obj(*args) + + self.success( + f"[{job['id']}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Successfully Processed" + ) + if self.options.get("verbosity") == "vv": + print(f"Successful. Deleting Job ID: {job['id']}") + builder.where("id", job["id"]).delete() + except Exception as e: # skipcq + self.danger( + f"[{job['id']}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Failed" + ) + + if job["attempts"] + 1 < self.options.get("attempts", 1): + builder.where("id", job["id"]).table( + self.options.get("table") + ).update( + { + "attempts": job["attempts"] + 1, + } + ) + elif job["attempts"] + 1 >= self.options.get( + "attempts", 1 + ) and not self.options.get("failed_table"): + # Delete the jobs + builder.where("id", job["id"]).table( + self.options.get("table") + ).update( + { + "attempts": job["attempts"] + 1, + } + ) + + if hasattr(obj, "failed"): + getattr(obj, "failed")(unserialized, str(e)) + + builder.where("id", job["id"]).table( + self.options.get("table") + ).delete() + elif self.options.get("failed_table"): + self.add_to_failed_queue_table( + builder, job["name"], payload, str(e) + ) + + if hasattr(obj, "failed"): + getattr(obj, "failed")(unserialized, str(e)) + + builder.where("id", job["id"]).table( + self.options.get("table") + ).delete() + else: + builder.where("id", job["id"]).table( + self.options.get("table") + ).update( + { + "attempts": job["attempts"] + 1, + } + ) + + def retry(self): + builder = self.get_builder() + + jobs = ( + builder.table(self.options.get("failed_table")) + .where("queue", self.options.get("queue", "default")) + .get() + ) + + if len(jobs) == 0: + self.success("No failed jobs found.") + return + + for job in jobs: + builder.table("jobs").create( + { + "name": str(job["name"]), + "payload": job["payload"], + "attempts": 0, + "available_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + "queue": job["queue"], + } + ) + self.success(f"Added {len(jobs)} failed job(s) back to the queue") + builder.table(self.options.get("failed_table", "failed_jobs")).where_in( + "id", [x["id"] for x in jobs] + ).delete() + + def get_builder(self): + return ( + self.application.make("builder") + .new() + .on(self.options.get("connection")) + .table(self.options.get("table")) + ) + + def add_to_failed_queue_table(self, builder, name, payload, exception): + builder.table(self.options.get("failed_table", "failed_jobs")).create( + { + "driver": "database", + "queue": self.options.get("queue", "default"), + "name": name, + "connection": self.options.get("connection"), + "created_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + "exception": exception, + "payload": payload, + "failed_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + } + ) diff --git a/src/masonite/drivers/queue/QueueAmqpDriver.py b/src/masonite/drivers/queue/QueueAmqpDriver.py deleted file mode 100644 index 6453b5fe9..000000000 --- a/src/masonite/drivers/queue/QueueAmqpDriver.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Driver for AMQP support""" - -import inspect -import pickle -import time - -import pendulum -from ...contracts import QueueContract -from ...drivers import BaseQueueDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import HasColoredCommands -from ...queues import Queueable - - -class QueueAmqpDriver(BaseQueueDriver, QueueContract, HasColoredCommands): - def __init__(self): - """Queue AMQP Driver.""" - from config import queue - - self.queue = queue - if "amqp" in self.queue.DRIVERS: - listening_channel = self.queue.DRIVERS["amqp"]["channel"] - else: - listening_channel = "default" - - # Start the connection - self.publishing_channel = listening_channel - self.connect() - - def _publish(self, body): - - self.channel.basic_publish( - exchange="", - routing_key=self.publishing_channel, - body=pickle.dumps(body), - properties=self.pika.BasicProperties( - delivery_mode=2, # make message persistent - ), - ) - self.channel.close() - self.connection.close() - - def push( - self, *objects, args=(), callback="handle", ran=1, channel=None, **options - ): # skipcq PYL-W0613 - """Push objects onto the amqp stack. - - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - """ - if channel: - self.publishing_channel = channel - - for obj in objects: - # Publish to the channel for each object - payload = { - "obj": obj, - "args": args, - "callback": callback, - "created": pendulum.now(), - "ran": ran, - } - try: - additional_exceptions = ( - self.pika.exceptions.ConnectionWrongStateError, - self.pika.exceptions.ChannelWrongStateError, - ) - except AttributeError: - additional_exceptions = () - - try: - self._publish(payload) - except ( - ( - self.pika.exceptions.ConnectionClosed, - self.pika.exceptions.ChannelClosed, - ), - additional_exceptions, - ): - self.connect() - self._publish(payload) - - def connect(self): - try: - import pika - - self.pika = pika - except ImportError: - raise DriverLibraryNotFound( - "Could not find the 'pika' library. Run pip install pika to fix this." - ) - - self.connection = pika.BlockingConnection( - pika.URLParameters( - "amqp://{}:{}@{}{}/{}".format( - self.queue.DRIVERS["amqp"]["username"], - self.queue.DRIVERS["amqp"]["password"], - self.queue.DRIVERS["amqp"]["host"], - ":" + str(self.queue.DRIVERS["amqp"]["port"]) - if "port" in self.queue.DRIVERS["amqp"] - and self.queue.DRIVERS["amqp"]["port"] - else "", - self.queue.DRIVERS["amqp"]["vhost"] - if "vhost" in self.queue.DRIVERS["amqp"] - and self.queue.DRIVERS["amqp"]["vhost"] - else "%2F", - ) - ) - ) - - self.channel = self.connection.channel() - - self.channel.queue_declare(self.publishing_channel, durable=True) - - return self - - def consume(self, channel, fair=False, **options): - self.success( - '[*] Waiting to process jobs on the "{}" channel. To exit press CTRL+C'.format( - channel - ) - ) - - self.queue = channel - - if fair: - self.channel.basic_qos(prefetch_count=1) - - self.basic_consume(self.work, channel) - - try: - self.channel.start_consuming() - finally: - self.channel.stop_consuming() - self.channel.close() - self.connection.close() - - def basic_consume(self, callback, queue_name): - try: - self.channel.basic_consume(callback, queue=queue_name) - except TypeError: - self.channel.basic_consume(queue_name, callback) - - def work(self, ch, method, _, body): - from wsgi import container - - job = pickle.loads(body) - obj = job["obj"] - args = job["args"] - callback = job["callback"] - ran = job["ran"] - - try: - try: - if inspect.isclass(obj): - obj = container.resolve(obj) - - getattr(obj, callback)(*args) - - except AttributeError: - obj(*args) - - try: - self.success("[\u2713] Job Successfully Processed") - except UnicodeEncodeError: - self.success("[Y] Job Successfully Processed") - except Exception as e: - self.danger("Job Failed: {}".format(str(e))) - - if not obj.run_again_on_fail: - ch.basic_ack(delivery_tag=method.delivery_tag) - return - - if ran < obj.run_times and isinstance(obj, Queueable): - time.sleep(1) - self.push(obj.__class__, args=args, callback=callback, ran=ran + 1) - else: - if hasattr(obj, "failed"): - getattr(obj, "failed")(job, str(e)) - - self.add_to_failed_queue_table(job, channel=self.queue) - - ch.basic_ack(delivery_tag=method.delivery_tag) diff --git a/src/masonite/drivers/queue/QueueDatabaseDriver.py b/src/masonite/drivers/queue/QueueDatabaseDriver.py deleted file mode 100644 index feb34cf0f..000000000 --- a/src/masonite/drivers/queue/QueueDatabaseDriver.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Async Driver Method.""" - -import inspect -import pickle -import time - -import pendulum -from ...contracts import QueueContract -from ...drivers import BaseQueueDriver -from ...helpers import HasColoredCommands, parse_human_time -from ...queues import Queueable -from .QueueJobsModel import QueueJobsModel - - -class QueueDatabaseDriver(BaseQueueDriver, HasColoredCommands, QueueContract): - """Queue Aysnc Driver.""" - - def __init__(self): - """Queue Async Driver. - - Arguments: - Container {masonite.app.App} -- The application container. - """ - pass - - def connect(self): - return self - - def push(self, *objects, args=(), kwargs={}, **options): - """Push objects onto the async stack. - - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - options {**kwargs of options} - Additional options for async driver - """ - - from config.database import DB as schema - from masoniteorm.query import QueryBuilder - - callback = options.get("callback", "handle") - wait = options.get("wait", None) - connection = options.get("connection", "default") - queue = options.get("queue", "default") - - if wait: - wait = parse_human_time(wait).to_datetime_string() - - for job in objects: - if schema.get_schema_builder(connection).has_table("queue_jobs"): - payload = pickle.dumps( - {"obj": job, "args": args, "kwargs": kwargs, "callback": callback} - ) - - schema.get_query_builder(connection).table("queue_jobs").create( - { - "name": str(job), - "serialized": payload, - "created_at": pendulum.now().to_datetime_string(), - "attempts": 0, - "ran_at": None, - "queue": queue, - "available_at": wait, - "reserved_at": None, - } - ) - - def consume(self, channel, **options): # skipcq - from config.database import DB, DATABASES - from wsgi import container - - self.info( - '[*] Waiting to process jobs from the "queue_jobs" table on the "{}" connection. To exit press CTRL + C'.format( - channel - ) - ) - - builder = QueueJobsModel - while True: - jobs = ( - builder.where_null("ran_at") - .where_null("reserved_at") - .where("queue", options.get("queue", "default")) - .where( - lambda q: q.where_null("available_at").or_where( - "available_at", "<=", pendulum.now().to_datetime_string() - ) - ) - .limit(5) - .order_by("id") - .get() - ) - - builder.where_in("id", jobs.pluck("id")).update( - {"reserved_at": pendulum.now().to_datetime_string()} - ) - - if not jobs.count(): - time.sleep(int(options.get("poll")) or 1) - continue - - for job in jobs: - builder.where("id", job["id"]).update( - { - "ran_at": pendulum.now().to_datetime_string(), - } - ) - unserialized = pickle.loads(job["serialized"]) - obj = unserialized["obj"] - args = unserialized["args"] - callback = unserialized["callback"] - - try: - try: - if inspect.isclass(obj): - obj = container.resolve(obj) - - getattr(obj, callback)(*args) - - except AttributeError: - obj(*args) - - try: - self.success("[\u2713] Job Successfully Processed") - except UnicodeEncodeError: - self.success("[Y] Job Successfully Processed") - builder.where("id", job["id"]).delete() - except Exception as e: # skipcq - self.danger("Job Failed: {}".format(str(e))) - - # if not obj.run_again_on_fail: - builder.where("id", job["id"]).delete() - - if hasattr(obj, "failed"): - getattr(obj, "failed")(unserialized, str(e)) - self.add_to_failed_queue_table( - unserialized, channel=channel, driver="database" - ) diff --git a/src/masonite/drivers/queue/QueueJobsModel.py b/src/masonite/drivers/queue/QueueJobsModel.py deleted file mode 100644 index 882944aa6..000000000 --- a/src/masonite/drivers/queue/QueueJobsModel.py +++ /dev/null @@ -1,6 +0,0 @@ -from masoniteorm.models import Model - - -class QueueJobsModel(Model): - __table__ = "queue_jobs" - __timestamps__ = None diff --git a/src/masonite/drivers/queue/__init__.py b/src/masonite/drivers/queue/__init__.py new file mode 100644 index 000000000..165c6b4f8 --- /dev/null +++ b/src/masonite/drivers/queue/__init__.py @@ -0,0 +1,3 @@ +from .DatabaseDriver import DatabaseDriver +from .AsyncDriver import AsyncDriver +from .AMQPDriver import AMQPDriver diff --git a/src/masonite/drivers/session/CookieDriver.py b/src/masonite/drivers/session/CookieDriver.py new file mode 100644 index 000000000..71024897f --- /dev/null +++ b/src/masonite/drivers/session/CookieDriver.py @@ -0,0 +1,58 @@ +"""Session Cookie Module.""" + + +class CookieDriver: + """Cookie Session Driver.""" + + def __init__(self, application): + """Cookie Session Constructor. + + Arguments: + application {dict} -- The application class + """ + self.application = application + + def start(self): + request = self.get_request() + data = {} + flashed = {} + for key, value in request.cookie_jar.to_dict().items(): + if key.startswith("s_"): + data.update({key.replace("s_", ""): value}) + elif key.startswith("f_"): + flashed.update({key.replace("f_", ""): value}) + + return {"data": data, "flashed": flashed} + + def save(self, added=None, deleted=None, flashed=None, deleted_flashed=None): + response = self.get_response() + if added is None: + added = {} + if deleted is None: + deleted = [] + if flashed is None: + flashed = {} + if deleted_flashed is None: + deleted_flashed = [] + + for key, value in added.items(): + response.cookie(f"s_{key}", value) + + for key, value in flashed.items(): + response.cookie(f"f_{key}", value) + + for key in deleted: + response.delete_cookie(f"s_{key}") + + for key in deleted_flashed: + response.delete_cookie(f"f_{key}") + + def get_response(self): + return self.application.make("response") + + def get_request(self): + return self.application.make("request") + + def helper(self): + """Use to create builtin helper function.""" + return self diff --git a/src/masonite/drivers/session/SessionCookieDriver.py b/src/masonite/drivers/session/SessionCookieDriver.py deleted file mode 100644 index 33ed530e5..000000000 --- a/src/masonite/drivers/session/SessionCookieDriver.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Session Cookie Module.""" - -import json -from email import message - -from ...contracts import SessionContract -from ...drivers import BaseDriver -from ...helpers import config -from ...request import Request - - -class SessionCookieDriver(SessionContract, BaseDriver): - """Cookie Session Driver.""" - - def __init__(self, request: Request): - """Cookie Session Constructor. - - Arguments: - Environ {dict} -- The WSGI environment - Request {masonite.request.Request} -- The Request class. - """ - self.request = request - - def get(self, key): - """Get a value from the session. - - Arguments: - key {string} -- The key to get from the session. - - Returns: - string|None - Returns None if a value does not exist. - """ - cookie = self.request.get_cookie("s_{0}".format(key)) - if cookie: - return self._get_serialization_value(cookie) - - cookie = self.request.get_cookie("f_{0}".format(key)) - if cookie: - return self._get_serialization_value(cookie) - - return None - - def get_flashed(self, key): - value = self.get(key) - if value: - self.delete_flash(key) - return value - - return None - - def set(self, key, value): - """Set a vlue in the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - if isinstance(value, dict): - value = json.dumps(value) - - self.request.cookie("s_{0}".format(key), value) - - def has(self, key): - """Check if a key exists in the session. - - Arguments: - key {string} -- The key to check for in the session. - - Returns: - bool - """ - if self.get(key): - return True - return False - - def all(self, flash_only=False): - """Get all session data. - - Returns: - dict - """ - return self.__collect_data(flash_only=flash_only) - - def delete(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - self.__collect_data() - - if self.request.get_cookie("s_{}".format(key)): - self.request.delete_cookie("s_{}".format(key)) - return True - - return False - - def delete_flash(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - self.__collect_data() - - if self.request.get_cookie("f_{}".format(key)): - self.request.delete_cookie("f_{}".format(key)) - return True - - return False - - def __collect_data(self, flash_only=False): - """Collect data from session and flash data. - - Returns: - dict - """ - cookies = {} - all_cookies = self.request.get_cookies().to_dict() - for key, value in all_cookies.items(): - if not (key.startswith("f_") or key.startswith("s_")): - continue - - if flash_only and not key.startswith("f_"): - continue - - key = key.replace("f_", "").replace("s_", "") - - cookies.update({key: self.get(key)}) - return cookies - - return cookies - - def flash(self, key, value): - """Add temporary data to the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - if isinstance(value, (dict, list)): - value = json.dumps(value) - - self.request.cookie( - "f_{0}".format(key), - value, - ) - - def get_error_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - only_messages = [] - messages = self.all(flash_only=True).get("errors", {}).items() - for key, messages in messages: - for error_message in messages: - only_messages.append(error_message) - self.reset(flash_only=True) - return only_messages - - def get_flashed_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - messages = self.all(flash_only=True) - self.reset(flash_only=True) - return messages - - def reset(self, flash_only=False): - """Delete all session data. - - Keyword Arguments: - flash_only {bool} -- If only flash data should be deleted. (default: {False}) - """ - cookies = self.__collect_data() - for cookie in cookies: - if flash_only: - self.request.delete_cookie("f_{0}".format(cookie)) - continue - - self.request.delete_cookie("s_{0}".format(cookie)) - - def helper(self): - """Use to create builtin helper function.""" - return self - - def _get_serialization_value(self, value): - try: - return json.loads(value) - except ValueError: - return value diff --git a/src/masonite/drivers/session/SessionMemoryDriver.py b/src/masonite/drivers/session/SessionMemoryDriver.py deleted file mode 100644 index 61e88f915..000000000 --- a/src/masonite/drivers/session/SessionMemoryDriver.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Session Memory Module.""" - -from ...contracts import SessionContract -from ...drivers import BaseDriver -from ...request import Request - - -class SessionMemoryDriver(SessionContract, BaseDriver): - """Memory Session Driver.""" - - _session = {} - _flash = {} - - def __init__(self, request: Request): - """Cookie Session Constructor. - - Arguments: - Environ {dict} -- The WSGI environment - """ - self.request = request - - def get(self, key): - """Get a value from the session. - - Arguments: - key {string} -- The key to get from the session. - - Returns: - string|None - Returns None if a value does not exist. - """ - data = self.__collect_data(key) - if data: - return data - - return None - - def set(self, key, value): - """Set a vlue in the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - ip = self.__get_client_address() - - if ip not in self._session: - self._session[ip] = {} - - self._session[ip][key] = value - - def has(self, key): - """Check if a key exists in the session. - - Arguments: - key {string} -- The key to check for in the session. - - Returns: - bool - """ - data = self.__collect_data() - if data and key in data: - return True - return False - - def all(self): - """Get all session data. - - Returns: - dict - """ - return self.__collect_data() - - def flash(self, key, value): - """Add temporary data to the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - ip = self.__get_client_address() - if ip not in self._flash: - self._flash[ip] = {} - - self._flash[ip][key] = value - - def get_flashed(self, key): - value = self.get(key) - if value: - self.delete_flash(key) - return value - - return None - - def get_error_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - ip = self.__get_client_address() - only_messages = [] - messages = self._flash.get(ip, {}).get("errors", {}).items() - for key, messages in messages: - for message in messages: - only_messages.append(message) - self.reset(flash_only=True) - return only_messages - - def get_flashed_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - messages = self._flash.get(ip, {}) - self.reset(flash_only=True) - return messages - - def reset(self, flash_only=False): - """Delete all session data. - - Keyword Arguments: - flash_only {bool} -- If only flash data should be deleted. (default: {False}) - """ - ip = self.__get_client_address() - - if flash_only: - if ip in self._flash: - self._flash[ip] = {} - else: - if ip in self._session: - self._session[ip] = {} - - def delete(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - data = self.__collect_data() - - if key in data: - del data[key] - return True - - return False - - def delete_flash(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - return self.delete(key) - - def __get_client_address(self): - """Get ip from the client.""" - if "HTTP_X_FORWARDED_FOR" in self.request.environ: - return self.request.environ["HTTP_X_FORWARDED_FOR"].split(",")[-1].strip() - - return self.request.environ["REMOTE_ADDR"] - - def __collect_data(self, key=False): - """Collect data from session and flash data. - - Returns: - dict - """ - ip = self.__get_client_address() - - # Declare a new dictionary - session = {} - - # If the session data has keys - if ip in self._session: - session = self._session[ip] - - # If the session flash has keys - if ip in self._flash: - session.update(self._flash[ip]) - - # If a key is set and it is inside the new declared session, return that key - if key and key in session: - return session[key] - - # If the key is set and is not in the session - if key and key not in session: - return None - - # If the session is still an empty dictionary - if not session: - return None - - # No checks have been hit. Return the new dictionary - return session - - def helper(self): - """Used to create builtin helper function.""" - return self diff --git a/src/masonite/drivers/session/__init__.py b/src/masonite/drivers/session/__init__.py new file mode 100644 index 000000000..234b67c5c --- /dev/null +++ b/src/masonite/drivers/session/__init__.py @@ -0,0 +1 @@ +from .CookieDriver import CookieDriver diff --git a/src/masonite/drivers/storage/StorageDiskDriver.py b/src/masonite/drivers/storage/StorageDiskDriver.py deleted file mode 100644 index 6f2d11ead..000000000 --- a/src/masonite/drivers/storage/StorageDiskDriver.py +++ /dev/null @@ -1,75 +0,0 @@ -import os -import pathlib -import shutil - -from ... import Upload -from ...contracts import StorageContract -from ...drivers import BaseDriver - - -class StorageDiskDriver(BaseDriver, StorageContract): - def put(self, location, contents): - with open(location, "w+") as file: - file.write(contents) - - def append(self, location, contents): - with open(location, "a+") as file: - file.write(contents) - - def get(self, location): - with open(location) as f: - return f.read() - - def delete(self, location): - try: - os.remove(location) - return True - except FileNotFoundError: - return False - - def exists(self, location): - return pathlib.Path(location).exists() - - def size(self, location): - try: - return os.path.getsize(location) - except FileNotFoundError: - return 0 - - def extension(self, location): - return pathlib.Path(location).suffix.replace(".", "") - - def url(self, location): - pass - - def name(self, location): - return pathlib.Path(location).name - - def upload(self, *args, **kwargs): - from wsgi import container - - return container.make(Upload).driver("disk").store(*args, **kwargs) - - def all(self): - pass - - def make_directory(self, location): - location = os.path.join(os.getcwd(), location) - if os.path.isdir(location): - return True - - os.mkdir(location) - return True - - def delete_directory(self, directory, force=False): - if force: - shutil.rmtree(directory) - return True - try: - pathlib.Path(directory).rmdir() - return True - except FileNotFoundError: - return True - - def move(self, old, new): - return shutil.move(old, new) diff --git a/src/masonite/drivers/upload/BaseUploadDriver.py b/src/masonite/drivers/upload/BaseUploadDriver.py deleted file mode 100644 index ba3860b29..000000000 --- a/src/masonite/drivers/upload/BaseUploadDriver.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Base upload driver module.""" - -import _io -from ...drivers import BaseDriver -from ...exceptions import FileTypeException -from ...helpers import random_string -from ...helpers import config - - -class BaseUploadDriver(BaseDriver): - """Base class that all upload drivers inherit from.""" - - accept_file_types = ("jpg", "jpeg", "png", "gif", "bmp") - - def accept(self, *args, **kwargs): - """Set file types to accept before uploading. - - Returns: - self - """ - - extensions = list(args) - - if "*" in extensions and len(extensions) > 1: - raise ValueError("When uses '*' isn't allowed accept other file type.") - - if extensions == ["*"]: - self.accept_file_types = None - else: - self.accept_file_types = args - - return self - - def validate_extension(self, filename): - """Check for valid file extenstions set with the 'accept' method. - - Arguments: - filename {string} -- The filename with file extension to validate. - - Raises: - FileTypeException -- Thrown if the specified file extension is incorrect. - """ - if self.accept_file_types is not None: - if not filename.lower().endswith(self.accept_file_types): - raise FileTypeException("The file extension is not supported.") - - return True - - def get_location(self, location=None): - """Get the location of where to upload. - - Keyword Arguments: - location {string} -- The path to upload to. If none then this will check for configuration settings. (default: {None}) - - Returns: - string -- Returns the location it uploaded to. - """ - if not location: - location = config("storage.drivers.disk.location") - - if "." in location: - location = location.split(".") - return config("storage.drivers")[location[0]]["location"][location[1]] - elif isinstance(location, str): - return location - elif isinstance(location, dict): - return list(location.values())[0] - - return location - - def get_name(self, fileitem): - return "{}.{}".format(random_string(25).lower(), self.get_extension(fileitem)) - - def get_extension(self, fileitem): - if isinstance(fileitem, _io.TextIOWrapper): - return fileitem.name.split(".")[-1] - else: - return fileitem.filename.split(".")[-1] diff --git a/src/masonite/drivers/upload/UploadDiskDriver.py b/src/masonite/drivers/upload/UploadDiskDriver.py deleted file mode 100644 index 881b62bce..000000000 --- a/src/masonite/drivers/upload/UploadDiskDriver.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Upload Disk Driver.""" - -import os -import _io - -from ...contracts import UploadContract -from ...drivers import BaseUploadDriver -from ...helpers.filesystem import make_directory - - -class UploadDiskDriver(BaseUploadDriver, UploadContract): - """Upload to and from the file system.""" - - file_location = None - - def __init__(self): - """Upload Disk Driver Constructor.""" - pass - - def store(self, fileitem, filename=None, location=None): - """Store the file onto a server. - - Arguments: - fileitem {cgi.Storage} -- Storage object. - - Keyword Arguments: - location {string} -- The location on disk you would like to store the file. (default: {None}) - filename {string} -- A new file name you would like to name the file. (default: {None}) - - Returns: - string -- Returns the file name just saved. - """ - - # use the new filename or get it from the fileitem - if filename is None: - filename = self.get_name(fileitem) - - # Check if is a valid extension - self.validate_extension(self.get_name(fileitem)) - - location = self.get_location(location) - - location = os.path.join(location, filename) - - make_directory(location) - - if isinstance(fileitem, _io.TextIOWrapper): - with open(location, "wb") as file: - file.write(bytes(fileitem.read(), "utf-8")) - else: - with open(location, "wb") as file: - file.write(fileitem.file.read()) - - self.file_location = location - - return filename diff --git a/src/masonite/drivers/upload/UploadS3Driver.py b/src/masonite/drivers/upload/UploadS3Driver.py deleted file mode 100644 index 0b4596d26..000000000 --- a/src/masonite/drivers/upload/UploadS3Driver.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Upload S3 Driver.""" - -import os - -from ...contracts import UploadContract -from ...drivers import BaseUploadDriver -from ...exceptions import DriverLibraryNotFound -from ...managers import UploadManager -from ...helpers import config - - -class UploadS3Driver(BaseUploadDriver, UploadContract): - """Amazon S3 Upload driver.""" - - def __init__(self, upload: UploadManager): - """Upload Disk Driver Constructor. - - Arguments: - UploadManager {masonite.managers.UploadManager} -- The Upload Manager object. - StorageConfig {config.storage} -- Storage configuration. - """ - self.upload = upload - self.config = config("storage") - - def store(self, fileitem, filename=None, location=None): - """Store the file into Amazon S3 server. - - Arguments: - fileitem {cgi.Storage} -- Storage object. - - Keyword Arguments: - location {string} -- The location on disk you would like to store the file. (default: {None}) - filename {string} -- A new file name you would like to name the file. (default: {None}) - - Raises: - DriverLibraryNotFound -- Raises when the boto3 library is not installed. - - Returns: - string -- Returns the file name just saved. - """ - try: - import boto3 - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "boto3" library. Please pip install this library by running "pip install boto3"' - ) - - driver = self.upload.driver("disk") - driver.accept_file_types = self.accept_file_types - driver.store(fileitem, filename=filename, location="storage/temp") - file_location = driver.file_location - - # use the new filename or get it from the fileitem - if filename is None: - filename = self.get_name(fileitem) - - # Check if is a valid extension - self.validate_extension(self.get_name(fileitem)) - - session = boto3.Session( - aws_access_key_id=self.config.DRIVERS["s3"]["client"], - aws_secret_access_key=self.config.DRIVERS["s3"]["secret"], - ) - - s3 = session.resource("s3") - - if location: - location = os.path.join(location, filename) - else: - location = os.path.join(filename) - - s3.meta.client.upload_file( - file_location, self.config.DRIVERS["s3"]["bucket"], location - ) - - return filename diff --git a/src/masonite/environment/__init__.py b/src/masonite/environment/__init__.py new file mode 100644 index 000000000..999f0dafb --- /dev/null +++ b/src/masonite/environment/__init__.py @@ -0,0 +1 @@ +from .environment import LoadEnvironment, env diff --git a/src/masonite/environment.py b/src/masonite/environment/environment.py similarity index 92% rename from src/masonite/environment.py rename to src/masonite/environment/environment.py index 4f49f037c..59c7de571 100644 --- a/src/masonite/environment.py +++ b/src/masonite/environment/environment.py @@ -49,6 +49,9 @@ def _load_environment(self, environment, override=False): def env(value, default="", cast=True): + """Helper to retrieve the value of an environment variable or returns + a default value. In addition, if type can be inferred then the value can be casted to the + inferred type.""" env_var = os.getenv(value, default) if not cast: diff --git a/src/masonite/essentials/helpers/__init__.py b/src/masonite/essentials/helpers/__init__.py new file mode 100644 index 000000000..94a710d1c --- /dev/null +++ b/src/masonite/essentials/helpers/__init__.py @@ -0,0 +1 @@ +from .hashid import hashid diff --git a/src/masonite/essentials/helpers/hashid.py b/src/masonite/essentials/helpers/hashid.py new file mode 100644 index 000000000..e9ec83a34 --- /dev/null +++ b/src/masonite/essentials/helpers/hashid.py @@ -0,0 +1,34 @@ +from hashids import Hashids + + +def hashid(*values, decode=False, min_length=7): + hash_class = Hashids(min_length=min_length) + if type(values[0]) == dict and decode: + new_dict = {} + for key, value in values[0].items(): + if hasattr(value, "value"): + value = value.value + + if value and hash_class.decode(value): + value = hash_class.decode(value) + + if type(value) == tuple: + value = value[0] + new_dict.update({key: value}) + return new_dict + + if not decode: + if isinstance(values[0], dict): + new_dic = {} + for key, value in values[0].items(): + if hasattr(value, "value"): + value = value.value + if str(value).isdigit(): + new_dic.update({key: hash_class.encode(int(value))}) + else: + new_dic.update({key: value}) + return new_dic + + return hash_class.encode(*values) + + return Hashids().decode(*values) diff --git a/src/masonite/essentials/middleware/HashIDMiddleware.py b/src/masonite/essentials/middleware/HashIDMiddleware.py new file mode 100644 index 000000000..18d810f6d --- /dev/null +++ b/src/masonite/essentials/middleware/HashIDMiddleware.py @@ -0,0 +1,17 @@ +from ...middleware import Middleware + +from ..helpers import hashid + + +class HashIDMiddleware(Middleware): + def before(self, request, response): + request.input_bag.query_string = hashid( + request.input_bag.query_string, decode=True + ) + request.params = hashid(request.params, decode=True) + request.input_bag.post_data = hashid(request.input_bag.post_data, decode=True) + return request + + def after(self, request, response): + + return request diff --git a/src/masonite/essentials/middleware/__init__.py b/src/masonite/essentials/middleware/__init__.py new file mode 100644 index 000000000..68f9c0f46 --- /dev/null +++ b/src/masonite/essentials/middleware/__init__.py @@ -0,0 +1 @@ +from .HashIDMiddleware import HashIDMiddleware diff --git a/src/masonite/essentials/providers/HashIDProvider.py b/src/masonite/essentials/providers/HashIDProvider.py new file mode 100644 index 000000000..9c8893dd2 --- /dev/null +++ b/src/masonite/essentials/providers/HashIDProvider.py @@ -0,0 +1,13 @@ +from ...providers import Provider +from ..helpers.hashid import hashid + + +class HashIDProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + self.application.make("view").share({"hashid": hashid}) + + def boot(self): + pass diff --git a/src/masonite/events/Event.py b/src/masonite/events/Event.py new file mode 100644 index 000000000..93d39b074 --- /dev/null +++ b/src/masonite/events/Event.py @@ -0,0 +1,81 @@ +""" Event Module """ + +import inspect + + +class Event: + def __init__(self, application): + """Event contructor + Arguments: + application - The Masonite application class + """ + self.application = application + self.events = {} + + def get_events(self): + return self.events + + def listen(self, event, listeners): + + if not isinstance(listeners, list): + listeners = [listeners] + + if event in self.events: + self.events[event] += listeners + else: + self.events.update({event: listeners}) + + return self + + def fire(self, event, *args, **kwargs): + if isinstance(event, str): + collected_events = self.collect_events(event) + for collected_event in collected_events: + for listener in self.events.get(collected_event, []): + listener().handle(event, *args, **kwargs) + return collected_events + else: + if inspect.isclass(event): + lookup = event + event = event() + else: + lookup = event.__class__ + for listener in self.events.get(lookup, []): + listener().handle(event, *args, **kwargs) + + return [event] + + def collect_events(self, fired_event): + collected_events = [] + for stored_event in self.events.keys(): + + if not isinstance(stored_event, str): + continue + + if stored_event == fired_event: + collected_events.append(fired_event) + + elif stored_event.endswith("*") and fired_event.startswith( + stored_event.replace("*", "") + ): + collected_events.append(stored_event) + + elif stored_event.startswith("*") and fired_event.endswith( + stored_event.replace("*", "") + ): + collected_events.append(stored_event) + + elif "*" in stored_event: + starts, end = stored_event.split("*") + if fired_event.startswith(starts) and fired_event.endswith(end): + collected_events.append(stored_event) + + return collected_events + + def subscribe(self, *listeners): + """Subscribe a specific listener object to the events system + Raises: + InvalidSubscriptionType -- raises when the subscribe attribute on the listener object is not a class. + """ + for listener in listeners: + listener.subscribe(self) diff --git a/src/masonite/events/Listener.py b/src/masonite/events/Listener.py new file mode 100644 index 000000000..b63e74ae4 --- /dev/null +++ b/src/masonite/events/Listener.py @@ -0,0 +1,2 @@ +class Listener: + pass diff --git a/src/masonite/events/__init__.py b/src/masonite/events/__init__.py new file mode 100644 index 000000000..d0703322f --- /dev/null +++ b/src/masonite/events/__init__.py @@ -0,0 +1 @@ +from .Event import Event diff --git a/src/masonite/events/commands/MakeEventCommand.py b/src/masonite/events/commands/MakeEventCommand.py new file mode 100644 index 000000000..62b533f53 --- /dev/null +++ b/src/masonite/events/commands/MakeEventCommand.py @@ -0,0 +1,39 @@ +"""New Event Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import make_directory, get_module_dir, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeEventCommand(Command): + """ + Creates a new event class. + + event + {name : Name of the event} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_stub_event_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("events.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Event Created ({relative_filename})") + + def get_stub_event_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/events/Event.py") diff --git a/src/masonite/events/commands/MakeListenerCommand.py b/src/masonite/events/commands/MakeListenerCommand.py new file mode 100644 index 000000000..51cbae89e --- /dev/null +++ b/src/masonite/events/commands/MakeListenerCommand.py @@ -0,0 +1,38 @@ +"""New Listener Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import make_directory, get_module_dir, render_stub_file +from ...utils.str import as_filepath +from ...utils.location import base_path + + +class MakeListenerCommand(Command): + """ + Creates a new listener class. + + listener + {name : Name of the listener} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("listeners.location")), f"{name}.py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Listener Created ({relative_filename})") + + def get_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/events/Listener.py") diff --git a/src/masonite/events/commands/__init__.py b/src/masonite/events/commands/__init__.py new file mode 100644 index 000000000..3a94a3945 --- /dev/null +++ b/src/masonite/events/commands/__init__.py @@ -0,0 +1,2 @@ +from .MakeEventCommand import MakeEventCommand +from .MakeListenerCommand import MakeListenerCommand diff --git a/src/masonite/events/providers/EventProvider.py b/src/masonite/events/providers/EventProvider.py new file mode 100644 index 000000000..429b3491b --- /dev/null +++ b/src/masonite/events/providers/EventProvider.py @@ -0,0 +1,18 @@ +from ...providers import Provider +from ..Event import Event +from ..commands import MakeListenerCommand, MakeEventCommand + + +class EventProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + event = Event(self.application) + self.application.make("commands").add( + MakeListenerCommand(self.application), MakeEventCommand(self.application) + ) + self.application.bind("event", event) + + def boot(self): + pass diff --git a/src/masonite/events/providers/__init__.py b/src/masonite/events/providers/__init__.py new file mode 100644 index 000000000..e2bbb71d2 --- /dev/null +++ b/src/masonite/events/providers/__init__.py @@ -0,0 +1 @@ +from .EventProvider import EventProvider diff --git a/src/masonite/exception_handler.py b/src/masonite/exception_handler.py deleted file mode 100644 index 8af355271..000000000 --- a/src/masonite/exception_handler.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Exception Handler Module. - -A module for controlling exceptions handling when an error occurs doing executing -code in a Masonite application. These errors could are thrown during runtime. -""" - -import inspect -import os -import sys -import traceback - -from exceptionite.errors import Handler, SolutionsIntegration, StackOverflowIntegration - -from .app import App -from .exceptions import DumpException -from .helpers import config -from .listeners import BaseExceptionListener -from .request import Request -from .response import Response -from .view import View - -package_directory = os.path.dirname(os.path.realpath(__file__)) - - -class ExceptionHandler: - """Class for handling exceptions thrown during runtime.""" - - def __init__(self, app): - """ExceptionHandler constructor. Also responsible for loading static files into the container. - - Arguments: - app {masonite.app.App} -- Container object - """ - self._app = app - self.response = self._app.make(Response) - - self._register_static_files() - - def _register_static_files(self): - """Register static files into the container.""" - storage = config("storage") - if storage: - storage.STATICFILES.update( - {os.path.join(package_directory, "snippets/exceptions"): "_exceptions/"} - ) - - def load_exception(self, exception): - """Load the exception thrown into this handler. - - Arguments: - exception {Exception} -- This is the exception object thrown at runtime. - """ - self._exception = exception - - if self._app.has("Exception{}Handler".format(exception.__class__.__name__)): - - return self._app.make( - "Exception{}Handler".format(exception.__class__.__name__) - ).handle(exception) - - self.handle(exception) - - def run_listeners(self, exception, stacktraceback): - for exception_class in self._app.collect(BaseExceptionListener): - if ( - "*" in exception_class.listens - or exception.__class__ in exception_class.listens - ): - file, line = self.get_file_and_line(stacktraceback) - self._app.resolve(exception_class).handle(exception, file, line) - - def get_file_and_line(self, stacktraceback): - for stack in stacktraceback[::-1]: - if "site-packages" not in stack[0]: - return (stack[0], stack[1]) - - return (0, 0) - - def handle(self, exception): - """Render an exception view if the DEBUG configuration is True. Else this should not return anything. - - Returns: - None - """ - - stacktraceback = traceback.extract_tb(sys.exc_info()[2]) - self.run_listeners(exception, stacktraceback) - # Run Any Framework Exception Hooks - self._app.make("HookHandler").fire("*ExceptionHook") - request = self._app.make("Request") - response = self._app.make(Response) - - # Check if DEBUG is False - from config import application - - if not application.DEBUG: - response.status(500) - return - - handler = Handler(exception) - handler.integrate(SolutionsIntegration()) - handler.integrate( - StackOverflowIntegration(), - ) - - if "application/json" in request.header("Content-Type"): - stacktrace = [] - for trace in handler.stacktrace(): - stacktrace.append(trace.file + " line " + str(trace.lineno)) - - return response.json( - { - "Exeption": handler.exception(), - "Message": str(exception), - "traceback": stacktrace, - }, - status=500, - ) - - response.view(handler.render(), status=500) - - -class DD: - def __init__(self, container): - self.app = container - - def dump(self, *args): - dump_list = [] - for i, obj in enumerate(args): - dump_name = "ObjDump{}".format(i) - self.app.bind(dump_name, obj) - dump_list.append(dump_name) - self.app.bind("ObjDumpList", dump_list) - raise DumpException - - -class DumpHandler: - def __init__(self, view: View, request: Request, app: App, response: Response): - self.view = view - self.request = request - self.app = app - self.response = response - - def handle(self, _): - from masoniteorm.models import Model - - self.app.make("HookHandler").fire("*ExceptionHook") - - dump_objs = [] - for dump_name in self.app.make("ObjDumpList"): - obj = self.app.make(dump_name) - dump_objs.append( - { - "obj": obj, - "members": inspect.getmembers(obj, predicate=inspect.ismethod), - "properties": inspect.getmembers(obj), - } - ) - - self.response.view( - self.view.render( - "/masonite/snippets/exceptions/dump", - { - "objs": dump_objs, - "type": type, - "list": list, - "inspect": inspect, - "hasattr": hasattr, - "getattr": getattr, - "Model": Model, - "isinstance": isinstance, - "show_methods": (bool, str, list, dict), - "len": len, - }, - ) - ) diff --git a/src/masonite/exceptions/DD.py b/src/masonite/exceptions/DD.py new file mode 100644 index 000000000..ef40a8e80 --- /dev/null +++ b/src/masonite/exceptions/DD.py @@ -0,0 +1,15 @@ +from .exceptions import DumpException + + +class DD: + def __init__(self, container): + self.app = container + + def dump(self, *args): + dump_list = [] + for i, obj in enumerate(args): + dump_name = "ObjDump{}".format(i) + self.app.bind(dump_name, obj) + dump_list.append(dump_name) + self.app.bind("ObjDumpList", dump_list) + raise DumpException diff --git a/src/masonite/exceptions/DumpExceptionHandler.py b/src/masonite/exceptions/DumpExceptionHandler.py new file mode 100644 index 000000000..5308bc87c --- /dev/null +++ b/src/masonite/exceptions/DumpExceptionHandler.py @@ -0,0 +1,35 @@ +import inspect + + +class DumpExceptionHandler: + def __init__(self, application): + self.application = application + + def handle(self, exception): + dump_objs = [] + for dump_name in self.application.make("ObjDumpList"): + obj = self.application.make(dump_name) + dump_objs.append( + { + "obj": obj, + "members": inspect.getmembers(obj, predicate=inspect.ismethod), + "properties": inspect.getmembers(obj), + } + ) + + return self.application.make("response").view( + self.application.make("view").render( + "/masonite/templates/dump", + { + "objs": dump_objs, + "type": type, + "list": list, + "inspect": inspect, + "hasattr": hasattr, + "getattr": getattr, + "isinstance": isinstance, + "show_methods": (bool, str, list, dict), + "len": len, + }, + ) + ) diff --git a/src/masonite/exceptions/ExceptionHandler.py b/src/masonite/exceptions/ExceptionHandler.py new file mode 100644 index 000000000..7f39edef6 --- /dev/null +++ b/src/masonite/exceptions/ExceptionHandler.py @@ -0,0 +1,65 @@ +from exceptionite.errors import Handler, StackOverflowIntegration, SolutionsIntegration + + +class ExceptionHandler: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config[self.driver_config.get("default")] + + return self.driver_config.get(driver, {}) + + def handle(self, exception): + response = self.application.make("response") + request = self.application.make("request") + self.application.make("event").fire( + f"masonite.exception.{exception.__class__.__name__}", exception + ) + + if self.application.has(f"{exception.__class__.__name__}Handler"): + return self.application.make( + f"{exception.__class__.__name__}Handler" + ).handle(exception) + + if hasattr(exception, "get_response"): + return response.view(exception.get_response(), exception.get_status()) + + handler = Handler(exception) + if self.options.get("handlers.stack_overflow"): + handler.integrate(StackOverflowIntegration()) + if self.options.get("handlers.solutions"): + handler.integrate(SolutionsIntegration()) + handler.context( + { + "WSGI": { + "Path": request.get_path(), + "Input": request.input_bag.all_as_values() or None, + # 'Parameters': request.url_params, + "Request Method": request.get_request_method(), + }, + "Headers": request.header_bag.to_dict(), + } + ) + + return response.view(handler.render(), status=500) diff --git a/src/masonite/exceptions/__init__.py b/src/masonite/exceptions/__init__.py new file mode 100644 index 000000000..2a9ae9f5e --- /dev/null +++ b/src/masonite/exceptions/__init__.py @@ -0,0 +1,32 @@ +from .ExceptionHandler import ExceptionHandler +from .DumpExceptionHandler import DumpExceptionHandler +from .DD import DD +from .exceptions import ( + InvalidRouteCompileException, + RouteMiddlewareNotFound, + ContainerError, + MissingContainerBindingNotFound, + StrictContainerException, + ResponseError, + InvalidHTTPStatusCode, + RequiredContainerBindingNotFound, + ViewException, + RouteNotFoundException, + DumpException, + InvalidSecretKey, + InvalidCSRFToken, + NotificationException, + InvalidToken, + ProjectLimitReached, + ProjectProviderTimeout, + ProjectProviderHttpError, + ProjectTargetNotEmpty, + MixFileNotFound, + MixManifestNotFound, + InvalidConfigurationLocation, + InvalidConfigurationSetup, + InvalidPackageName, + LoaderNotFound, + QueueException, + AmbiguousError, +) diff --git a/src/masonite/exceptions.py b/src/masonite/exceptions/exceptions.py similarity index 62% rename from src/masonite/exceptions.py rename to src/masonite/exceptions/exceptions.py index b70d8dd93..73f42e0a9 100644 --- a/src/masonite/exceptions.py +++ b/src/masonite/exceptions/exceptions.py @@ -54,6 +54,10 @@ class InvalidSecretKey(Exception): pass +class InvalidToken(Exception): + pass + + class StrictContainerException(Exception): pass @@ -104,3 +108,52 @@ class ProjectProviderHttpError(Exception): class ProjectTargetNotEmpty(Exception): pass + + +class NotificationException(Exception): + pass + + +class AuthorizationException(Exception): + def __init__(self, message, status): + super().__init__(self) + self.message = message or "Action not authorized" + self.status = status or 403 + + def get_response(self): + return self.message + + def get_status(self): + return self.status + + +class GateDoesNotExist(Exception): + pass + + +class PolicyDoesNotExist(Exception): + pass + + +class MixManifestNotFound(Exception): + pass + + +class MixFileNotFound(Exception): + pass + + +class InvalidConfigurationLocation(Exception): + pass + + +class InvalidConfigurationSetup(Exception): + pass + + +class InvalidPackageName(Exception): + pass + + +class LoaderNotFound(Exception): + pass diff --git a/src/masonite/facades/Auth.py b/src/masonite/facades/Auth.py new file mode 100644 index 000000000..ddeb848d7 --- /dev/null +++ b/src/masonite/facades/Auth.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Auth(metaclass=Facade): + key = "auth" diff --git a/src/masonite/facades/Config.py b/src/masonite/facades/Config.py new file mode 100644 index 000000000..5b17e2ecd --- /dev/null +++ b/src/masonite/facades/Config.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Config(metaclass=Facade): + key = "config" diff --git a/src/masonite/facades/Facade.py b/src/masonite/facades/Facade.py new file mode 100644 index 000000000..a214d9db0 --- /dev/null +++ b/src/masonite/facades/Facade.py @@ -0,0 +1,5 @@ +class Facade(type): + def __getattr__(self, attribute, *args, **kwargs): + from wsgi import application + + return getattr(application.make(self.key), attribute) diff --git a/src/masonite/facades/Gate.py b/src/masonite/facades/Gate.py new file mode 100644 index 000000000..c3b9fd31c --- /dev/null +++ b/src/masonite/facades/Gate.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Gate(metaclass=Facade): + key = "gate" diff --git a/src/masonite/facades/Hash.py b/src/masonite/facades/Hash.py new file mode 100644 index 000000000..bda5fa0fd --- /dev/null +++ b/src/masonite/facades/Hash.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Hash(metaclass=Facade): + key = "hash" diff --git a/src/masonite/facades/Loader.py b/src/masonite/facades/Loader.py new file mode 100644 index 000000000..2232de98a --- /dev/null +++ b/src/masonite/facades/Loader.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Loader(metaclass=Facade): + key = "loader" diff --git a/src/masonite/facades/Mail.py b/src/masonite/facades/Mail.py new file mode 100644 index 000000000..dd44b13e2 --- /dev/null +++ b/src/masonite/facades/Mail.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Mail(metaclass=Facade): + key = "mail" diff --git a/src/masonite/facades/Notification.py b/src/masonite/facades/Notification.py new file mode 100644 index 000000000..f045ff371 --- /dev/null +++ b/src/masonite/facades/Notification.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Notification(metaclass=Facade): + key = "notification" diff --git a/src/masonite/facades/Request.py b/src/masonite/facades/Request.py new file mode 100644 index 000000000..cea382bbe --- /dev/null +++ b/src/masonite/facades/Request.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Request(metaclass=Facade): + key = "request" diff --git a/src/masonite/facades/Response.py b/src/masonite/facades/Response.py new file mode 100644 index 000000000..1981474bd --- /dev/null +++ b/src/masonite/facades/Response.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Response(metaclass=Facade): + key = "response" diff --git a/src/masonite/facades/Session.py b/src/masonite/facades/Session.py new file mode 100644 index 000000000..b97bafc18 --- /dev/null +++ b/src/masonite/facades/Session.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Session(metaclass=Facade): + key = "session" diff --git a/src/masonite/facades/Url.py b/src/masonite/facades/Url.py new file mode 100644 index 000000000..77ec325a9 --- /dev/null +++ b/src/masonite/facades/Url.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Url(metaclass=Facade): + key = "url" diff --git a/src/masonite/facades/View.py b/src/masonite/facades/View.py new file mode 100644 index 000000000..dcfe7aa74 --- /dev/null +++ b/src/masonite/facades/View.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class View(metaclass=Facade): + key = "view" diff --git a/src/masonite/facades/__init__.py b/src/masonite/facades/__init__.py new file mode 100644 index 000000000..fa76a21a2 --- /dev/null +++ b/src/masonite/facades/__init__.py @@ -0,0 +1,13 @@ +from .Facade import Facade +from .Request import Request +from .Response import Response +from .Mail import Mail +from .Hash import Hash +from .Url import Url +from .Session import Session +from .View import View +from .Gate import Gate +from .Auth import Auth +from .Config import Config +from .Loader import Loader +from .Notification import Notification diff --git a/src/masonite/filesystem/File.py b/src/masonite/filesystem/File.py new file mode 100644 index 000000000..73de6f938 --- /dev/null +++ b/src/masonite/filesystem/File.py @@ -0,0 +1,26 @@ +import hashlib +import os + + +class File: + def __init__(self, content, filename=None): + self.content = content + self.filename = filename + + def path(self): + pass + + def extension(self): + return os.path.splitext(self.filename)[1] + + def name(self): + return self.filename + + def stream(self): + return self.content + + def hash_path_name(self): + return f"{self.hash_name()}{self.extension()}" + + def hash_name(self): + return hashlib.sha1(bytes(self.name(), "utf-8")).hexdigest() diff --git a/src/masonite/filesystem/FileStream.py b/src/masonite/filesystem/FileStream.py new file mode 100644 index 000000000..981bbe368 --- /dev/null +++ b/src/masonite/filesystem/FileStream.py @@ -0,0 +1,16 @@ +import os + + +class FileStream: + def __init__(self, stream, name=None): + self.stream = stream + self._name = name + + def path(self): + return self.stream.name + + def extension(self): + return os.path.splitext(self._name or self.path())[1] + + def name(self): + return self._name or os.path.basename(self.path()) diff --git a/src/masonite/filesystem/Storage.py b/src/masonite/filesystem/Storage.py new file mode 100644 index 000000000..948dc7a14 --- /dev/null +++ b/src/masonite/filesystem/Storage.py @@ -0,0 +1,35 @@ +class Storage: + def __init__(self, application, store_config=None): + self.application = application + self.drivers = {} + self.store_config = store_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.store_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.store_config.get("default")] + return self.drivers[name] + + def get_store_config(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def get_config_options(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def disk(self, name="default"): + store_config = self.get_config_options(name) + driver = self.get_driver(self.get_config_options(name).get("driver")) + return driver.set_options(store_config) diff --git a/src/masonite/filesystem/UploadedFile.py b/src/masonite/filesystem/UploadedFile.py new file mode 100644 index 000000000..44538ea01 --- /dev/null +++ b/src/masonite/filesystem/UploadedFile.py @@ -0,0 +1,30 @@ +import os +import hashlib + + +class UploadedFile: + def __init__(self, filename, content): + self.filename = filename + self.content = content + + def extension(self): + return os.path.splitext(self.filename)[1] + + @property + def name(self): + return self.filename + + def path_name(self): + return f"{self.name()}{self.extension()}" + + def hash_path_name(self): + return f"{self.hash_name()}{self.extension()}" + + def stream(self): + return self.content + + def hash_name(self): + return hashlib.sha1(bytes(self.name(), "utf-8")).hexdigest() + + def get_content(self): + return self.content diff --git a/src/masonite/filesystem/__init__.py b/src/masonite/filesystem/__init__.py new file mode 100644 index 000000000..cc1613b6a --- /dev/null +++ b/src/masonite/filesystem/__init__.py @@ -0,0 +1,3 @@ +from .Storage import Storage +from .File import File +from .UploadedFile import UploadedFile diff --git a/src/masonite/filesystem/drivers/AmazonS3Driver.py b/src/masonite/filesystem/drivers/AmazonS3Driver.py new file mode 100644 index 000000000..566e31d43 --- /dev/null +++ b/src/masonite/filesystem/drivers/AmazonS3Driver.py @@ -0,0 +1,146 @@ +import os +from shutil import copyfile, move +from ..FileStream import FileStream +import uuid + + +class AmazonS3Driver: + def __init__(self, application): + self.application = application + self.options = {} + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + import boto3 + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'boto3' library. Run 'pip install boto3' to fix this." + ) + + if not self.connection: + self.connection = boto3.Session( + aws_access_key_id=self.options.get("client"), + aws_secret_access_key=self.options.get("secret"), + region_name=self.options.get("region"), + ) + + return self.connection + + def get_bucket(self): + return self.options.get("bucket") + + def get_name(self, path, alias): + extension = os.path.splitext(path)[1] + return f"{alias}{extension}" + + def put(self, file_path, content): + self.get_connection().resource("s3").Bucket(self.get_bucket()).put_object( + Key=file_path, Body=content + ) + return content + + def put_file(self, file_path, content, name=None): + file_name = self.get_name(content.name, name or str(uuid.uuid4())) + + if hasattr(content, "get_content"): + content = content.get_content() + + self.get_connection().resource("s3").Bucket(self.get_bucket()).put_object( + Key=os.path.join(file_path, file_name), Body=content + ) + return os.path.join(file_path, file_name) + + def get(self, file_path): + try: + return ( + self.get_connection() + .resource("s3") + .Bucket(self.get_bucket()) + .Object(file_path) + .get() + .get("Body") + .read() + .decode("utf-8") + ) + except self.missing_file_exceptions(): + pass + + def missing_file_exceptions(self): + import boto3 + + return (boto3.exceptions.botocore.errorfactory.ClientError,) + + def exists(self, file_path): + try: + self.get_connection().resource("s3").Bucket(self.get_bucket()).Object( + file_path + ).get().get("Body").read() + return True + except self.missing_file_exceptions(): + return False + + def missing(self, file_path): + return not self.exists(file_path) + + def stream(self, file_path): + return FileStream( + self.get_connection() + .resource("s3") + .Bucket(self.get_bucket()) + .Object(file_path) + .get() + .get("Body") + .read(), + file_path, + ) + + def copy(self, from_file_path, to_file_path): + copy_source = {"Bucket": self.get_bucket(), "Key": from_file_path} + self.get_connection().resource("s3").meta.client.copy( + copy_source, self.get_bucket(), to_file_path + ) + + def move(self, from_file_path, to_file_path): + self.copy(from_file_path, to_file_path) + self.delete(from_file_path) + + def prepend(self, file_path, content): + value = self.get(file_path) + content = content + value + self.put(file_path, content) + return content + + def append(self, file_path, content): + value = self.get(file_path) or "" + value += content + self.put(file_path, content) + + def delete(self, file_path): + return ( + self.get_connection() + .resource("s3") + .Object(self.get_bucket(), file_path) + .delete() + ) + + def store(self, file, name=None): + full_path = name or file.hash_path_name() + self.get_connection().resource("s3").Bucket(self.get_bucket()).put_object( + Key=full_path, Body=file.stream() + ) + return full_path + + def make_file_path_if_not_exists(self, file_path): + if not os.path.isfile(file_path): + if not os.path.exists(os.path.dirname(file_path)): + # Create the path to the model if it does not exist + os.makedirs(os.path.dirname(file_path)) + + return True + + return False diff --git a/src/masonite/filesystem/drivers/LocalDriver.py b/src/masonite/filesystem/drivers/LocalDriver.py new file mode 100644 index 000000000..5a48a10d0 --- /dev/null +++ b/src/masonite/filesystem/drivers/LocalDriver.py @@ -0,0 +1,105 @@ +import os +from shutil import copyfile, move +from ..FileStream import FileStream +import uuid +import os + + +class LocalDriver: + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_path(self, path): + file_path = os.path.join(self.options.get("path"), path) + self.make_file_path_if_not_exists(file_path) + return file_path + + def get_name(self, path, alias): + extension = os.path.splitext(path)[1] + return f"{alias}{extension}" + + def put(self, file_path, content): + with open(self.get_path(os.path.join(file_path)), "w") as f: + f.write(content) + return content + + def put_file(self, file_path, content, name=None): + file_name = self.get_name(content.name, name or str(uuid.uuid4())) + + if hasattr(content, "get_content"): + content = content.get_content() + + if isinstance(content, str): + content = bytes(content, "utf-8") + + with open(self.get_path(os.path.join(file_path, file_name)), "wb") as f: + f.write(content) + + return os.path.join(file_path, file_name) + + def get(self, file_path): + try: + with open(self.get_path(file_path), "r") as f: + content = f.read() + + return content + except FileNotFoundError: + return None + + def exists(self, file_path): + return os.path.exists(self.get_path(file_path)) + + def missing(self, file_path): + return not self.exists(file_path) + + def stream(self, file_path): + with open(self.get_path(file_path), "r") as f: + content = f + return FileStream(content) + + def copy(self, from_file_path, to_file_path): + return copyfile(from_file_path, to_file_path) + + def move(self, from_file_path, to_file_path): + return move(self.get_path(from_file_path), self.get_path(to_file_path)) + + def prepend(self, file_path, content): + value = self.get(file_path) + content = content + value + self.put(file_path, content) + return content + + def append(self, file_path, content): + with open(self.get_path(file_path), "a") as f: + f.write(content) + return content + + def delete(self, file_path): + return os.remove(self.get_path(file_path)) + + def make_directory(self, directory): + pass + + def store(self, file, name=None): + if name: + name = f"{name}{file.extension()}" + full_path = self.get_path(name or file.hash_path_name()) + with open(full_path, "wb") as f: + f.write(file.stream()) + + return full_path + + def make_file_path_if_not_exists(self, file_path): + if not os.path.isfile(file_path): + if not os.path.exists(os.path.dirname(file_path)): + # Create the path to the model if it does not exist + os.makedirs(os.path.dirname(file_path)) + + return True + + return False diff --git a/src/masonite/filesystem/drivers/__init__.py b/src/masonite/filesystem/drivers/__init__.py new file mode 100644 index 000000000..668aac8db --- /dev/null +++ b/src/masonite/filesystem/drivers/__init__.py @@ -0,0 +1,2 @@ +from .LocalDriver import LocalDriver +from .AmazonS3Driver import AmazonS3Driver diff --git a/src/masonite/filesystem/providers/StorageProvider.py b/src/masonite/filesystem/providers/StorageProvider.py new file mode 100644 index 000000000..cb92cdb7f --- /dev/null +++ b/src/masonite/filesystem/providers/StorageProvider.py @@ -0,0 +1,20 @@ +from ...providers import Provider +from ..Storage import Storage +from ...configuration import config +from ..drivers import LocalDriver, AmazonS3Driver + + +class StorageProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + storage = Storage(self.application).set_configuration( + config("filesystem.disks") + ) + storage.add_driver("file", LocalDriver(self.application)) + storage.add_driver("s3", AmazonS3Driver(self.application)) + self.application.bind("storage", storage) + + def boot(self): + pass diff --git a/src/masonite/filesystem/providers/__init__.py b/src/masonite/filesystem/providers/__init__.py new file mode 100644 index 000000000..c788cf890 --- /dev/null +++ b/src/masonite/filesystem/providers/__init__.py @@ -0,0 +1 @@ +from .StorageProvider import StorageProvider diff --git a/src/masonite/foundation/Application.py b/src/masonite/foundation/Application.py new file mode 100644 index 000000000..cb0107f10 --- /dev/null +++ b/src/masonite/foundation/Application.py @@ -0,0 +1,77 @@ +import os +import sys +from ..container import Container + + +class Application(Container): + def __init__(self, base_path=None): + self.base_path = base_path + self.storage_path = None + self.response_handler = None + self.providers = [] + + def set_response_handler(self, response_handler): + self.response_handler = response_handler + + def get_response_handler(self): + return self.response_handler + + def register_providers(self, *providers): + for provider in providers: + provider = provider(self) + provider.register() + return self + + def use_storage_path(self, path): + self.storage_path = path + + def get_storage_path(self): + return self.storage_path + + def add_providers(self, *providers): + for provider in providers: + provider = provider(self) + provider.register() + self.providers.append(provider) + + return self + + def set_controller_locations(self, location): + self._controller_locations = location + + def get_controller_locations(self, location): + return self._controller_locations + + def get_providers(self): + return self.providers + + def __call__(self, *args, **kwargs): + return self.response_handler(*args, **kwargs) + + def is_dev(self): + """Check if app is running in development mode.""" + return os.getenv("APP_ENV") == "development" + + def is_production(self): + """Check if app is running in production mode.""" + return os.getenv("APP_ENV") == "production" + + def is_running_tests(self): + """Check if app is running tests.""" + + return "pytest" in sys.modules + + def is_running_in_console(self): + """Check if application is running in console. This is useful to only run some providers + logic when used in console. We can differenciate if the application is being served or + if an application command is ran in console.""" + if len(sys.argv) > 1: + return sys.argv[1] != "serve" + return True + + def environment(self): + """Helper to get current environment.""" + if self.is_running_tests(): + return "testing" + else: + return os.getenv("APP_ENV") diff --git a/src/masonite/foundation/Kernel.py b/src/masonite/foundation/Kernel.py new file mode 100644 index 000000000..72180b560 --- /dev/null +++ b/src/masonite/foundation/Kernel.py @@ -0,0 +1,79 @@ +import os +from cleo import Application as CommandApplication + +from .response_handler import response_handler +from ..commands import ( + TinkerCommand, + CommandCapsule, + KeyCommand, + ServeCommand, + QueueWorkCommand, + QueueRetryCommand, + QueueTableCommand, + QueueFailedCommand, + AuthCommand, + MakePolicyCommand, + MakeControllerCommand, + MakeJobCommand, + MakeMailableCommand, + MakeProviderCommand, + PublishPackageCommand, +) +from ..environment import LoadEnvironment +from ..middleware import MiddlewareCapsule +from ..routes import Router +from ..loader import Loader + +from ..tests.HttpTestResponse import HttpTestResponse +from ..tests.TestResponseCapsule import TestResponseCapsule + + +class Kernel: + def __init__(self, app): + self.application = app + + def register(self): + self.load_environment() + self.register_framework() + self.register_commands() + self.register_testing() + + def load_environment(self): + LoadEnvironment() + + def register_framework(self): + self.application.set_response_handler(response_handler) + self.application.use_storage_path( + os.path.join(self.application.base_path, "storage") + ) + self.application.bind("middleware", MiddlewareCapsule()) + self.application.bind( + "router", + Router(), + ) + self.application.bind("loader", Loader()) + + def register_commands(self): + self.application.bind( + "commands", + CommandCapsule(CommandApplication("Masonite Version:", "4.0")).add( + TinkerCommand(), + KeyCommand(), + ServeCommand(self.application), + QueueWorkCommand(self.application), + QueueRetryCommand(self.application), + QueueFailedCommand(), + QueueTableCommand(), + AuthCommand(self.application), + MakePolicyCommand(self.application), + MakeControllerCommand(self.application), + MakeJobCommand(self.application), + MakeMailableCommand(self.application), + MakeProviderCommand(self.application), + PublishPackageCommand(self.application), + ), + ) + + def register_testing(self): + test_response = TestResponseCapsule(HttpTestResponse) + self.application.bind("tests.response", test_response) diff --git a/src/masonite/foundation/__init__.py b/src/masonite/foundation/__init__.py new file mode 100644 index 000000000..56b9a796f --- /dev/null +++ b/src/masonite/foundation/__init__.py @@ -0,0 +1,3 @@ +from .Application import Application +from .Kernel import Kernel +from .response_handler import response_handler diff --git a/src/masonite/wsgi.py b/src/masonite/foundation/response_handler.py similarity index 56% rename from src/masonite/wsgi.py rename to src/masonite/foundation/response_handler.py index 84522fd66..433fdb86b 100644 --- a/src/masonite/wsgi.py +++ b/src/masonite/foundation/response_handler.py @@ -8,7 +8,9 @@ def response_handler(environ, start_response): Returns: WSGI Response """ - from wsgi import container + from wsgi import application + + application.bind("environ", environ) """Add Environ To Service Container Add the environ to the service container. The environ is generated by the @@ -16,17 +18,15 @@ def response_handler(environ, start_response): incoming requests """ - container.bind("Environ", environ) - - """Execute All Service Providers That Require The WSGI Server - Run all service provider boot methods if the wsgi attribute is true. - """ + # """Execute All Service Providers That Require The WSGI Server + # Run all service provider boot methods if the wsgi attribute is true. + # """ try: - for provider in container.make("WSGIProviders"): - container.resolve(provider.boot) + for provider in application.get_providers(): + application.resolve(provider.boot) except Exception as e: - container.make("ExceptionHandler").load_exception(e) + application.make("exception_handler").handle(e) """We Are Ready For Launch If we have a solid response and not redirecting then we need to return @@ -35,11 +35,12 @@ def response_handler(environ, start_response): to next. """ - from masonite.response import Response + _, response = application.make("request"), application.make("response") - response = container.make(Response) - - start_response(response.get_status_code(), response.get_and_reset_headers()) + start_response( + response.get_status_code(), + response.get_headers() + response.cookie_jar.render_response(), + ) """Final Step This will take the data variable from the Service Container and return @@ -48,7 +49,7 @@ def response_handler(environ, start_response): return iter([response.get_response_content()]) -def package_response_handler(environ, start_response): +def testcase_handler(application, environ, start_response, exception_handling=True): """The WSGI Application Server. Arguments: @@ -58,7 +59,9 @@ def package_response_handler(environ, start_response): Returns: WSGI Response """ - from wsgi import container + from wsgi import application + + application.bind("environ", environ) """Add Environ To Service Container Add the environ to the service container. The environ is generated by the @@ -66,17 +69,17 @@ def package_response_handler(environ, start_response): incoming requests """ - container.bind("Environ", environ) - - """Execute All Service Providers That Require The WSGI Server - Run all service provider boot methods if the wsgi attribute is true. - """ + # """Execute All Service Providers That Require The WSGI Server + # Run all service provider boot methods if the wsgi attribute is true. + # """ try: - for provider in container.make("WSGIProviders"): - container.resolve(provider.boot) + for provider in application.get_providers(): + application.resolve(provider.boot) except Exception as e: - container.make("ExceptionHandler").load_exception(e) + if not exception_handling: + raise e + application.make("exception_handler").handle(e) """We Are Ready For Launch If we have a solid response and not redirecting then we need to return @@ -85,14 +88,15 @@ def package_response_handler(environ, start_response): to next. """ - from src.masonite.response import Response + request, response = application.make("request"), application.make("response") - response = container.make(Response) - - start_response(response.get_status_code(), response.get_and_reset_headers()) + start_response( + response.get_status_code(), + response.get_headers() + response.cookie_jar.render_response(), + ) """Final Step This will take the data variable from the Service Container and return it to the WSGI server. """ - return iter([response.get_response_content()]) + return (request, response) diff --git a/src/masonite/hashing/Hash.py b/src/masonite/hashing/Hash.py new file mode 100644 index 000000000..68fe04fe6 --- /dev/null +++ b/src/masonite/hashing/Hash.py @@ -0,0 +1,48 @@ +class Hash: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config.get(self.driver_config.get("default"), {}) + return self.driver_config.get(driver, {}) + + def make(self, string, options={}, driver=None): + """Hash a string based on configured hashing protocol.""" + return ( + self.get_driver(driver) + .set_options(options or self.get_config_options(driver)) + .make(string) + ) + + def check(self, plain_string, hashed_string, options={}, driver=None): + """Verify that a given string matches its hashed version (based on configured hashing protocol).""" + return ( + self.get_driver(driver) + .set_options(options or self.get_config_options(driver)) + .check(plain_string, hashed_string) + ) + + def needs_rehash(self, hashed_string, options={}, driver=None): + """Verify that a given hash needs to be hashed again because parameters for generating + the hash have changed.""" + return ( + self.get_driver(driver) + .set_options(options or self.get_config_options(driver)) + .needs_rehash(hashed_string) + ) diff --git a/src/masonite/hashing/__init__.py b/src/masonite/hashing/__init__.py new file mode 100644 index 000000000..a9b94a26c --- /dev/null +++ b/src/masonite/hashing/__init__.py @@ -0,0 +1 @@ +from .Hash import Hash diff --git a/src/masonite/hashing/drivers/Argon2Hasher.py b/src/masonite/hashing/drivers/Argon2Hasher.py new file mode 100644 index 000000000..0e8c33d35 --- /dev/null +++ b/src/masonite/hashing/drivers/Argon2Hasher.py @@ -0,0 +1,34 @@ +class Argon2Hasher: + def __init__(self, options={}): + self.options = options + + def set_options(self, options): + self.options = options + return self + + def _get_password_hasher(self): + try: + import argon2 + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'argon2' library. Run 'pip install argon2-cffi' to fix this." + ) + + memory = self.options.get("memory", argon2.DEFAULT_MEMORY_COST) + threads = self.options.get("threads", argon2.DEFAULT_PARALLELISM) + time = self.options.get("time", argon2.DEFAULT_TIME_COST) + return argon2.PasswordHasher( + memory_cost=memory, parallelism=threads, time_cost=time + ) + + def make(self, string): + ph = self._get_password_hasher() + return str(ph.hash(bytes(string, "utf-8"))) + + def check(self, plain_string, hashed_string): + ph = self._get_password_hasher() + return ph.verify(hashed_string, bytes(plain_string, "utf-8")) + + def needs_rehash(self, hashed_string): + ph = self._get_password_hasher() + return ph.check_needs_rehash(hashed_string) diff --git a/src/masonite/hashing/drivers/BcryptHasher.py b/src/masonite/hashing/drivers/BcryptHasher.py new file mode 100644 index 000000000..dc5d228fc --- /dev/null +++ b/src/masonite/hashing/drivers/BcryptHasher.py @@ -0,0 +1,28 @@ +import bcrypt + + +class BcryptHasher: + def __init__(self, options={}): + self.options = options + + def set_options(self, options): + self.options = options + return self + + def make(self, string): + rounds = self.options.get("rounds", 12) + salt = bcrypt.gensalt(rounds=rounds) + return bcrypt.hashpw(bytes(string, "utf-8"), salt) + + def check(self, plain_string, hashed_string): + if not isinstance(hashed_string, bytes): + hashed_string = bytes(hashed_string or "", "utf-8") + return bcrypt.checkpw(bytes(plain_string, "utf-8"), hashed_string) + + def needs_rehash(self, hashed_string): + # Bcrypt hashes have the format $2b${rounds}${salt}{checksum}. rounds is encoded as + # 2 zero-padded decimal digits. The prefix (2b) is never modified in make() function so we + # can assume that rounds value used when generating the hash is located at [4:6] indexes + # of the hash. + old_rounds_value = int(hashed_string[4:6]) + return old_rounds_value != self.options.get("rounds", 12) diff --git a/src/masonite/hashing/drivers/__init__.py b/src/masonite/hashing/drivers/__init__.py new file mode 100644 index 000000000..694bd9102 --- /dev/null +++ b/src/masonite/hashing/drivers/__init__.py @@ -0,0 +1,2 @@ +from .BcryptHasher import BcryptHasher +from .Argon2Hasher import Argon2Hasher diff --git a/src/masonite/headers/HeaderBag.py b/src/masonite/headers/HeaderBag.py index a33e4c8b8..c21133f97 100644 --- a/src/masonite/headers/HeaderBag.py +++ b/src/masonite/headers/HeaderBag.py @@ -36,5 +36,12 @@ def load(self, environ): if key.startswith("HTTP_"): self.add(Header(key, value)) + def to_dict(self): + dic = {} + for name, header in self.bag.items(): + dic.update({name: header.value}) + + return dic + def __getitem__(self, key): return self.bag[self.convert_name(key)] diff --git a/src/masonite/helpers/Extendable.py b/src/masonite/helpers/Extendable.py deleted file mode 100644 index 43da3835b..000000000 --- a/src/masonite/helpers/Extendable.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Extendable Module.""" - -import inspect - - -class Extendable: - """Add the ability to extend classes on the fly.""" - - def extend(self, key, obj=None): - """Extend the current class with an object. - - This essentially extends a class on the fly. - - Arguments: - key {string} -- The name of the method you want to set - - Keyword Arguments: - obj {object} -- Any function, method or class (default: {None}) - - Returns: - self - """ - # If both key and an object is passed - if obj: - if inspect.ismethod(obj): - obj = obj.__func__ - - setattr(self, key, obj.__get__(self)) - return self - - # Extend all of a classes methods into this class - if inspect.isclass(key): - for method in inspect.getmembers(key, inspect.isfunction): - setattr(self, method[0], method[1].__get__(self)) - - # Extend a function into this class - elif inspect.isfunction(key): - setattr(self, key.__name__, key.__get__(self)) - elif inspect.ismethod(obj): - setattr(self, key.__name__, key.__func__.__get__(self)) - return self diff --git a/src/masonite/helpers/__init__.py b/src/masonite/helpers/__init__.py index af46a560d..96b8bde85 100644 --- a/src/masonite/helpers/__init__.py +++ b/src/masonite/helpers/__init__.py @@ -1,16 +1,5 @@ -from .static import static -from .password import password -from .misc import ( - random_string, - dot, - clean_request_input, - HasColoredCommands, - Compact as compact, - deprecated, -) -from .Extendable import Extendable -from .time import cookie_expire_time, parse_human_time +from ..facades import Url as url from .optional import Optional as optional -from .structures import config, Dot, load -from .migrations import has_unmigrated_migrations -from masoniteorm.collection import Collection as collect +from .mix import MixHelper +from .urls import UrlsHelper +from .compact import Compact as compact diff --git a/src/masonite/helpers/compact.py b/src/masonite/helpers/compact.py new file mode 100644 index 000000000..ec8190285 --- /dev/null +++ b/src/masonite/helpers/compact.py @@ -0,0 +1,29 @@ +from ..exceptions import AmbiguousError +import inspect + + +class Compact: + def __new__(cls, *args): + frame = inspect.currentframe() + + cls.dictionary = {} + for arg in args: + if isinstance(arg, dict): + cls.dictionary.update(arg) + continue + + found = [] + for key, value in frame.f_back.f_locals.items(): + if value == arg: + for f in found: + if value is f and f is not None: + raise AmbiguousError( + "Cannot contain variables with multiple of the same object in scope. " + "Getting {}".format(value) + ) + cls.dictionary.update({key: value}) + found.append(value) + + if len(args) != len(cls.dictionary): + raise ValueError("Could not find all variables in this") + return cls.dictionary diff --git a/src/masonite/helpers/filesystem.py b/src/masonite/helpers/filesystem.py deleted file mode 100644 index 63c792584..000000000 --- a/src/masonite/helpers/filesystem.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -import shutil - - -def make_directory(directory): - if not os.path.isfile(directory): - if not os.path.exists(os.path.dirname(directory)): - # Create the path to the model if it does not exist - os.makedirs(os.path.dirname(directory)) - - return True - - return False - - -def copy_migration(directory_file, to="databases/migrations"): - import datetime - - base_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../") - - file_path = os.path.join(base_path, directory_file) - to_location = os.path.join( - os.getcwd(), - to, - datetime.datetime.utcnow().strftime("%Y_%m_%d_%H%M%S") - + "_" - + os.path.basename(directory_file), - ) - shutil.copyfile(file_path, to_location) - - print("\033[92m {} has been created \033[0m".format(to_location)) diff --git a/src/masonite/helpers/migrations.py b/src/masonite/helpers/migrations.py deleted file mode 100644 index f4fc10426..000000000 --- a/src/masonite/helpers/migrations.py +++ /dev/null @@ -1,43 +0,0 @@ -import subprocess - -from ..helpers import config, HasColoredCommands -from ..packages import add_venv_site_packages -from masoniteorm.migrations import Migration - - -class Migrations(HasColoredCommands): - def __init__(self, connection=None): - self._ran = [] - self._notes = [] - from config import database - - if not connection or connection == "default": - connection = database.DATABASES["default"] - self.migrator = Migration("sqlite") - self.migrator.create_table_if_not_exists() - - def run(self): - self.migrator.migrate() - - return self - - def rollback(self): - self.migrator.rollback() - - return self - - def refresh(self): - self.run() - self.rollback() - - def reset(self): - self.migrator.rollback_all() - - return self - - def ran(self): - return self._ran - - -def has_unmigrated_migrations(): - return False diff --git a/src/masonite/helpers/misc.py b/src/masonite/helpers/misc.py deleted file mode 100644 index 39d90fa42..000000000 --- a/src/masonite/helpers/misc.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Module for miscellaneous helper methods.""" - -import random -import string -import warnings - -from ..exceptions import AmbiguousError - - -def random_string(length=4): - """Generate a random string based on the length given. - - Keyword Arguments: - length {int} -- The amount of the characters to generate (default: {4}) - - Returns: - string - """ - return "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(length) - ) - - -def dot(data, compile_to=None): - notation_list = data.split(".") - - compiling = "" - compiling += notation_list[0] - beginning_string = compile_to.split("{1}")[0] - compiling = beginning_string + compiling - dot_split = compile_to.replace(beginning_string + "{1}", "").split("{.}") - if any(len(x) > 1 for x in dot_split): - raise ValueError("Cannot have multiple values between {1} and {.}") - - for notation in notation_list[1:]: - compiling += dot_split[0] - compiling += notation - compiling += dot_split[1] - return compiling - - -def clean_request_input(value, clean=True, quote=True): - if not clean: - return value - - import html - - try: - if isinstance(value, str): - return html.escape(value, quote=quote) - elif isinstance(value, list): - return [html.escape(x, quote=quote) for x in value] - elif isinstance(value, int): - return value - elif isinstance(value, dict): - return {key: html.escape(val, quote=quote) for (key, val) in value.items()} - except (AttributeError, TypeError): - pass - - return value - - -class HasColoredCommands: - def success(self, message): - print("\033[92m {0} \033[0m".format(message)) - - def warning(self, message): - print("\033[93m {0} \033[0m".format(message)) - - def danger(self, message): - print("\033[91m {0} \033[0m".format(message)) - - def info(self, message): - return self.success(message) - - -class Compact: - def __new__(cls, *args): - import inspect - - frame = inspect.currentframe() - - cls.dictionary = {} - for arg in args: - - if isinstance(arg, dict): - cls.dictionary.update(arg) - continue - - found = [] - for key, value in frame.f_back.f_locals.items(): - if value == arg: - for f in found: - if value is f and f is not None: - raise AmbiguousError( - "Cannot contain variables with multiple of the same object in scope. " - "Getting {}".format(value) - ) - cls.dictionary.update({key: value}) - found.append(value) - - if len(args) != len(cls.dictionary): - raise ValueError("Could not find all variables in this") - return cls.dictionary - - -def deprecated(message): - warnings.simplefilter("default", DeprecationWarning) - - def deprecated_decorator(func): - def deprecated_func(*args, **kwargs): - warnings.warn( - "{} is a deprecated function. {}".format(func.__name__, message), - category=DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return deprecated_func - - return deprecated_decorator diff --git a/src/masonite/helpers/mix.py b/src/masonite/helpers/mix.py new file mode 100644 index 000000000..ce8aba902 --- /dev/null +++ b/src/masonite/helpers/mix.py @@ -0,0 +1,34 @@ +import json +from os.path import join, exists + +from ..configuration import config +from ..utils.location import base_path +from ..exceptions import MixManifestNotFound, MixFileNotFound + + +class MixHelper: + def __init__(self, app): + self.app = app + + def url(self, path, manifest_dir=""): + if not path.startswith("/"): + path = "/" + path + + root_url = config("application.mix_base_url") or config("application.app_url") + + # load manifest file + manifest_file = base_path(join(manifest_dir, "mix-manifest.json")) + if not exists(manifest_file): + raise MixManifestNotFound( + "Mix manifest file mix-manifest.json does not exist." + ) + manifest = {} + with open(manifest_file, "r") as f: + manifest = json.load(f) + + # build asset path + try: + compiled_path = manifest[path] + except KeyError: + raise MixFileNotFound(f"Can't locate mix file: {path}.") + return join(root_url, compiled_path.lstrip("/")) diff --git a/src/masonite/helpers/optional.py b/src/masonite/helpers/optional.py index 4bf2d7e88..a39b29dfa 100644 --- a/src/masonite/helpers/optional.py +++ b/src/masonite/helpers/optional.py @@ -1,25 +1,35 @@ -class NoneType: +class DefaultType: + def __init__(self, value): + self.value = value + def __getattr__(self, attr): - return None + return self.value def __call__(self, *args, **kwargs): - return None + return self.value def __eq__(self, other): - return other is None + if self.value is None: + return other is self.value + else: + return other == self.value class Optional: - def __init__(self, obj): + """Optional helper class that allow evaluting an expression which can be undefined without + raising an expression but returning a default value (None).""" + + def __init__(self, obj, default=None): self.obj = obj + self.default = default def __getattr__(self, attr): if hasattr(self.obj, attr): return getattr(self.obj, attr) - return NoneType() + return DefaultType(self.default) def __call__(self, *args, **kwargs): - return NoneType() + return DefaultType(self.default) def instance(self): return self.obj diff --git a/src/masonite/helpers/password.py b/src/masonite/helpers/password.py deleted file mode 100644 index e361c4047..000000000 --- a/src/masonite/helpers/password.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Password Helper Module.""" - -import bcrypt - - -def password(password_string): - """Bcrypt a string. - - Useful for storing passwords in a database. - - Arguments: - pass {string} -- A string like a users plain text password to be bcrypted. - - Returns: - string -- The encrypted string. - """ - return bytes( - bcrypt.hashpw(bytes(password_string, "utf-8"), bcrypt.gensalt()) - ).decode("utf-8") diff --git a/src/masonite/helpers/routes.py b/src/masonite/helpers/routes.py deleted file mode 100644 index fd84b5e75..000000000 --- a/src/masonite/helpers/routes.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Helper Functions for RouteProvider.""" - -import re -from urllib.parse import parse_qs - -from .misc import deprecated - - -def flatten_routes(routes): - """Flatten the grouped routes into a single list of routes. - - Arguments: - routes {list} -- This can be a multi dementional list which can flatten all lists into a single list. - - Returns: - list -- Returns the flatten list. - """ - route_collection = [] - for route in routes: - if isinstance(route, list): - for r in flatten_routes(route): - route_collection.append(r) - else: - route_collection.append(route) - - return route_collection - - -def compile_route_to_regex(route): - """Compile a route to regex. - - Arguments: - route {masonite.routes.Route} -- The Masonite route object - - Returns: - string -- Returns the regex of the route. - """ - # Split the route - split_given_route = route.split("/") - - # compile the provided url into regex - url_list = [] - regex = "^" - for regex_route in split_given_route: - if "*" in regex_route or "@" in regex_route: - if ":int" in regex_route: - regex += r"(\d+)" - elif ":string" in regex_route: - regex += r"([a-zA-Z]+)" - else: - # default - regex += r"[\w.\-\/]+" - regex += r"\/" - - # append the variable name passed @(variable):int to a list - url_list.append( - regex_route.replace("@", "").replace(":int", "").replace(":string", "") - ) - else: - regex += regex_route + r"\/" - - if regex.endswith("/") and not route.endswith("/"): - regex = regex[:-2] - - regex += "$" - - return regex - - -def create_matchurl(url, route): - """Create a regex string for router.url to be matched against. - - Arguments: - router {masonite.routes.Route} -- The Masonite route object - route {masonite.routes.BaseHttpRoute} -- The current route being executed. - - Returns: - string -- compiled regex string - """ - - if route._compiled_regex is None: - route.compile_route_to_regex() - - if not url.endswith("/"): - return route._compiled_regex - elif url == "/": - return route._compiled_regex - - return route._compiled_regex_end - - -def query_parse(query_string): - d = {} - for key, value in parse_qs(query_string).items(): - regex_match = re.match(r"(?P[^\[]+)\[(?P[^\]]+)\]", key) - if regex_match: - gd = regex_match.groupdict() - d.setdefault(gd["key"], {})[gd["value"]] = value[0] - else: - d.update({key: value[0]}) - - return d diff --git a/src/masonite/helpers/sign.py b/src/masonite/helpers/sign.py deleted file mode 100644 index b44568af2..000000000 --- a/src/masonite/helpers/sign.py +++ /dev/null @@ -1,49 +0,0 @@ -from ..auth import Sign - - -def sign(value): - """Shortcut for Sign class. - - Arguments: - value {string} -- The value that is going to be encrypted - - Returns: - string -- The string value after encryption. - """ - return Sign().sign(value) - - -def encrypt(value): - """Shortcut for Sign class sign method. - - Arguments: - value {string} -- The value that is going to be encrypted - - Returns: - string -- The string value after encryption. - """ - return sign(value) - - -def unsign(value): - """Shortcut for Sign class unsign method. - - Arguments: - value {string} -- The value that is going to be decrypted - - Returns: - string -- The string value after decryption. - """ - return Sign().unsign(value) - - -def decrypt(value): - """Shortcut for Sign class unsign method. - - Arguments: - value {string} -- The value that is going to be decrypted - - Returns: - string -- The string value after decryption. - """ - return unsign(value) diff --git a/src/masonite/helpers/static.py b/src/masonite/helpers/static.py deleted file mode 100644 index fa13abd37..000000000 --- a/src/masonite/helpers/static.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Static Helper Module.""" - - -def static(alias, file_name): - """Get the static file location of an asset. - - Arguments: - alias {string} -- The driver and location to search for. This could be s3.uploads - file_name {string} -- The filename of the file to return. - - Returns: - string -- Returns the file location. - """ - from config.storage import DRIVERS - - if "." in alias: - alias = alias.split(".") - location = DRIVERS[alias[0]]["location"][alias[1]] - if location.endswith("/"): - location = location[:-1] - - return "{}/{}".format(location, file_name) - - location = DRIVERS[alias]["location"] - if isinstance(location, dict): - location = list(location.values())[0] - if location.endswith("/"): - location = location[:-1] - - return "{}/{}".format(location, file_name) diff --git a/src/masonite/helpers/structures.py b/src/masonite/helpers/structures.py deleted file mode 100644 index 548942efb..000000000 --- a/src/masonite/helpers/structures.py +++ /dev/null @@ -1,219 +0,0 @@ -"""A Module For Manipulating Code Structures.""" - -import inspect -import pydoc -from collections.abc import MutableMapping - -from masoniteorm.collection import Collection as collect - - -class Dot: - def dot(self, search, dictionary, default=None): - """The search string in dot notation to look into the dictionary for. - - Arguments: - search {string} -- This should be a string in dot notation - like 'key.key.value'. - dictionary {dict} -- A normal dictionary which will be searched using - the search string in dot notation. - - Keyword Arguments: - default {string} -- The default value if nothing is found - in the dictionary. (default: {None}) - - Returns: - string -- Returns the value found the dictionary or the default - value specified above if nothing is found. - """ - if "." not in search: - if search == "": - return dictionary - try: - return dictionary[search] - except KeyError: - return default - - searching = search.split(".") - possible = None - if "*" not in search: - return self.flatten(dictionary).get(search, default) - - while searching: - dic = dictionary - for value in searching: - if not dic: - if "*" in searching: - return [] - return default - - if isinstance(dic, list): - try: - return collect(dic).pluck(searching[searching.index("*") + 1]) - except KeyError: - return [] - - if not isinstance(dic, dict): - return default - - dic = dic.get(value) - - if isinstance(dic, str) and dic.isnumeric(): - continue - - if ( - dic - and not isinstance(dic, int) - and hasattr(dic, "__len__") - and len(dic) == 1 - and not isinstance(dic[list(dic)[0]], dict) - ): - possible = dic - - if not isinstance(dic, dict): - return dic - - del searching[-1] - return possible - - def flatten(self, d, parent_key="", sep="."): - items = [] - for k, v in d.items(): - new_key = parent_key + sep + k if parent_key else k - if isinstance(v, MutableMapping): - items.append((new_key, v)) - items.extend(self.flatten(v, new_key, sep=sep).items()) - elif isinstance(v, list): - for index, val in enumerate(v): - items.extend( - self.flatten({str(index): val}, new_key, sep=sep).items() - ) - else: - items.append((new_key, v)) - - return dict(items) - - def locate(self, search_path, default=""): - """Locate the object from the given search path - - Arguments: - search_path {string} -- A search path to fetch the object - from like config.application.debug. - - Keyword Arguments: - default {string} -- A default string if the search path is - not found (default: {''}) - - Returns: - any -- Could be a string, object or anything else that is fetched. - """ - value = self.find(search_path, default) - - if isinstance(value, dict): - return self.dict_dot(".".join(search_path.split(".")[3:]), value, default) - - if value is not None: - return value - - return default - - def dict_dot(self, search, dictionary, default=""): - """Takes a dot notation representation of a dictionary and fetches it from the dictionary. - - This will take something like s3.locations and look into the s3 dictionary and fetch the locations - key. - - Arguments: - search {string} -- The string to search for in the dictionary using dot notation. - dictionary {dict} -- The dictionary to search through. - - Returns: - string -- The value of the dictionary element. - """ - return self.dot(search, dictionary, default) - - def find(self, search_path, default=""): - """Used for finding both the uppercase and specified version. - - Arguments: - search_path {string} -- The search path to find the module, - dictionary key, object etc. This is typically - in the form of dot notation 'config.application.debug' - - Keyword Arguments: - default {string} -- The default value to return if the search path - could not be found. (default: {''}) - - Returns: - any -- Could be a string, object or anything else that is fetched. - """ - value = pydoc.locate(search_path) - - if value: - return value - - paths = search_path.split(".") - - value = pydoc.locate(".".join(paths[:-1]) + "." + paths[-1].upper()) - - if value or value is False: - return value - - search_path = -1 - - # Go backwards through the dot notation until a match is found. - ran = 0 - while ran < len(paths): - try: - value = pydoc.locate( - ".".join(paths[:search_path]) + "." + paths[search_path].upper() - ) - except IndexError: - return default - - if value: - break - - value = pydoc.locate( - ".".join(paths[:search_path]) + "." + paths[search_path] - ) - - if value: - break - - search_path -= 1 - ran += 1 - - if not value or inspect.ismodule(value): - return default - - return value - - -def config(path, default=""): - """Used to fetch a value from a configuration file - - Arguments: - path {string} -- The search path using dot notation of the value to get - - Keyword Arguments: - default {str} -- The default value if not value and be found (default: {''}) - - Returns: - mixed - """ - return Dot().locate("config." + path, default) - - -def load(path, default=""): - """Used to fetch a value from a configuration file - - Arguments: - path {string} -- The search path using dot notation of the value to get - - Keyword Arguments: - default {str} -- The default value if not value and be found (default: {''}) - - Returns: - mixed - """ - return Dot().locate(path, default) diff --git a/src/masonite/helpers/time.py b/src/masonite/helpers/time.py deleted file mode 100644 index aeacb4ee8..000000000 --- a/src/masonite/helpers/time.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Time Module.""" - -import pendulum - - -def cookie_expire_time(str_time): - """Take a string like 1 month or 5 minutes and returns a pendulum instance. - - Arguments: - str_time {string} -- Could be values like 1 second or 3 minutes - - Returns: - pendulum -- Returns Pendulum instance - """ - if str_time != "expired": - number = int(str_time.split(" ")[0]) - length = str_time.split(" ")[1] - - if length in ("second", "seconds"): - # Sat, 06 Jun 2020 15:36:16 GMT - return ( - pendulum.now("GMT") - .add(seconds=number) - .format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("minute", "minutes"): - return ( - pendulum.now("GMT") - .add(minutes=number) - .format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("hour", "hours"): - return ( - pendulum.now("GMT").add(hours=number).format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("days", "days"): - return ( - pendulum.now("GMT").add(days=number).format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("week", "weeks"): - return pendulum.now("GMT").add(weeks=1).format("ddd, DD MMM YYYY H:mm:ss") - elif length in ("month", "months"): - return ( - pendulum.now("GMT") - .add(months=number) - .format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("year", "years"): - return ( - pendulum.now("GMT").add(years=number).format("ddd, DD MMM YYYY H:mm:ss") - ) - - return None - else: - return pendulum.now("GMT").subtract(years=20).format("ddd, DD MMM YYYY H:mm:ss") - - -def parse_human_time(str_time): - """Take a string like 1 month or 5 minutes and returns a pendulum instance. - - Arguments: - str_time {string} -- Could be values like 1 second or 3 minutes - - Returns: - pendulum -- Returns Pendulum instance - """ - if str_time != "expired": - number = int(str_time.split(" ")[0]) - length = str_time.split(" ")[1] - - if length in ("second", "seconds"): - return pendulum.now("GMT").add(seconds=number) - elif length in ("minute", "minutes"): - return pendulum.now("GMT").add(minutes=number) - elif length in ("hour", "hours"): - return pendulum.now("GMT").add(hours=number) - elif length in ("days", "days"): - return pendulum.now("GMT").add(days=number) - elif length in ("week", "weeks"): - return pendulum.now("GMT").add(weeks=1) - elif length in ("month", "months"): - return pendulum.now("GMT").add(months=number) - elif length in ("year", "years"): - return pendulum.now("GMT").add(years=number) - - return None - else: - return pendulum.now("GMT").subtract(years=20) diff --git a/src/masonite/helpers/urls.py b/src/masonite/helpers/urls.py new file mode 100644 index 000000000..052f0fdfb --- /dev/null +++ b/src/masonite/helpers/urls.py @@ -0,0 +1,49 @@ +from os.path import join +from ..configuration import config + + +class UrlsHelper: + """URLs helper provide handy functions to build URLs.""" + + def __init__(self, app): + self.app = app + + def url(self, path=""): + """Generates a fully qualified url to the given path. If no path is given this will return + the base url domain.""" + # ensure that no slash is prefixing the relative path + relative_path = path.lstrip("/") + return join(config("application.app_url"), relative_path) + + def asset(self, alias, filename): + """Generates a fully qualified URL for the given asset using the given disk + Example: + asset("local", "avatar.jpg") (take first pat) + asset("s3.private", "doc.pdf") (when multiple paths are specified for the disk) + """ + disks = config("filesystem.disks") + # ensure that no slash is prefixing the relative filename path + filename = filename.lstrip("/") + if "." in alias: + alias = alias.split(".") + location = disks[alias[0]]["path"][alias[1]] + else: + location = disks[alias]["path"] + # take first path if no path specified + if isinstance(location, dict): + location = list(location.values())[0] + return join(location, filename) + + def route(self, name, params={}, absolute=True): + """Generates a fully qualified URL to the given route name. + Example: + route("users.home") : http://masonite.app/dashboard/ + route("users.profile", {"id": 1}) : http://masonite.app/users/1/profile/ + route("users.profile", {"id": 1}, absolute=False) : /users/1/profile/ + """ + + relative_url = self.app.make("router").route(name, params) + if absolute: + return self.url(relative_url) + else: + return relative_url diff --git a/src/masonite/helpers/view_helpers.py b/src/masonite/helpers/view_helpers.py deleted file mode 100644 index 4d8e41b1f..000000000 --- a/src/masonite/helpers/view_helpers.py +++ /dev/null @@ -1,62 +0,0 @@ -"""View Helper Module.""" - -from jinja2 import Markup - - -def set_request_method(method_type): - """Return an input string for use in a view to change the request method of a form. - - Arguments: - method_type {string} -- Can be options like GET, POST, PUT, PATCH, DELETE - - Returns: - string -- An input string. - """ - return Markup( - "".format(method_type) - ) - - -def back(location=None): - """Return an input element for use in telling Masonite which route to redirect back to. - - Arguments: - location {string} -- The route to redirect back to. - - Returns: - string -- An input string. - """ - if location is None: - from wsgi import container - - request = container.make("Request") - intended_route = request.session.get("__intend") - if intended_route: - location = intended_route - else: - location = request.path - - return Markup("".format(location)) - - -def hidden(value, name="hidden-input"): - return Markup("".format(name, value)) - - -def old(session_key, default=""): - """Return the old value submitted by forms validated with validators. - - Arguments: - session_key {string} -- The key flashed to session. - - Returns: - string -- An input string. - """ - - from wsgi import container - - session_container = container.make("Session") - - if session_container.has(session_key): - return session_container.get_flashed(session_key) - return default diff --git a/src/masonite/hook.py b/src/masonite/hook.py deleted file mode 100644 index 5b1ec4ba4..000000000 --- a/src/masonite/hook.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Module for the Hook class.""" - -from .app import App - - -class Hook: - """Hook class is responsible for finding and firing framework hooks.""" - - def __init__(self, app: App): - """Hook constructor. - - Arguments: - app {masonite.app.App} -- Container object. - """ - self._app = app - - def fire(self, search): - """Find all the classes to be fired with the exception hook search string. - - Arguments: - search {string} -- The search string to collect classes with. - """ - for key in self._app.collect(search): - self._app.make(key).load(self._app) diff --git a/src/masonite/input/Input.py b/src/masonite/input/Input.py new file mode 100644 index 000000000..d2aa2e244 --- /dev/null +++ b/src/masonite/input/Input.py @@ -0,0 +1,4 @@ +class Input: + def __init__(self, name, value): + self.name = name + self.value = value diff --git a/src/masonite/input/InputBag.py b/src/masonite/input/InputBag.py index ccc4a54e4..1f036dade 100644 --- a/src/masonite/input/InputBag.py +++ b/src/masonite/input/InputBag.py @@ -1,10 +1,11 @@ from .Input import Input from urllib.parse import parse_qs -import email.parser +import re import json import cgi import re -from ..helpers import Dot as DictDot, clean_request_input +from ..utils.structures import data_get +from ..filesystem import UploadedFile class InputBag: @@ -22,8 +23,8 @@ def load(self, environ): def parse(self, environ): if "QUERY_STRING" in environ: - self.query_string = self.query_parse(environ["QUERY_STRING"]) + if "wsgi.input" in environ: if "application/json" in environ.get("CONTENT_TYPE", ""): try: @@ -32,6 +33,7 @@ def parse(self, environ): request_body_size = 0 request_body = environ["wsgi.input"].read(request_body_size) + if isinstance(request_body, bytes): request_body = request_body.decode("utf-8") @@ -41,6 +43,7 @@ def parse(self, environ): else: for name, value in json.loads(request_body or "{}").items(): self.post_data.update({name: Input(name, value)}) + elif "application/x-www-form-urlencoded" in environ.get("CONTENT_TYPE", ""): try: request_body_size = int(environ.get("CONTENT_LENGTH", 0)) @@ -48,12 +51,10 @@ def parse(self, environ): request_body_size = 0 request_body = environ["wsgi.input"].read(request_body_size) - if isinstance(request_body, bytes): - request_body = request_body.decode("utf-8") + parsed_request_body = parse_qs(bytes(request_body).decode("utf-8")) + + self.post_data = self.parse_dict(parsed_request_body) - for parts in request_body.split("&"): - name, value = parts.split("=", 1) - self.post_data.update({name: Input(name, value)}) elif "multipart/form-data" in environ.get("CONTENT_TYPE", ""): try: request_body_size = int(environ.get("CONTENT_LENGTH", 0)) @@ -67,7 +68,21 @@ def parse(self, environ): ) for name in fields: - self.post_data.update({name: Input(name, fields.getvalue(name))}) + value = fields.getvalue(name) + if isinstance(value, bytes): + self.post_data.update( + { + name: UploadedFile( + fields[name].filename, fields.getvalue(name) + ) + } + ) + else: + self.post_data.update( + {name: Input(name, fields.getvalue(name))} + ) + + self.post_data = self.parse_dict(self.post_data) else: try: request_body_size = int(environ.get("CONTENT_LENGTH", 0)) @@ -75,18 +90,33 @@ def parse(self, environ): request_body_size = 0 request_body = environ["wsgi.input"].read(request_body_size) + if request_body: + self.post_data.update( + json.loads(bytes(request_body).decode("utf-8")) + ) - def get(self, name, default=None): + def get(self, name, default=None, clean=True, quote=True): + input = data_get(self.all(), name, default) - input = DictDot().dot(name, self.all(), default=default) - if isinstance(input, (dict, str)): + if isinstance(input, (str,)): return input + if isinstance(input, list) and len(input) == 1: + return input[0] + elif isinstance(input, (dict,)): + rendered = {} + for key, inp in input.items(): + if hasattr(inp, "value"): + inp = inp.value + rendered.update({key: inp}) + return rendered elif hasattr(input, "value"): + if isinstance(input.value, list) and len(input.value) == 1: + return input.value[0] + elif isinstance(input.value, dict): + return input.value return input.value - else: - return input - return default + return input def has(self, *names): return all((name in self.all()) for name in names) @@ -112,14 +142,54 @@ def all_as_values(self, internal_variables=False): return new + def only(self, *args): + all = self.all() + new = {} + for name, input in all.items(): + if name not in args: + continue + new.update({name: self.get(name)}) + + return new + def query_parse(self, query_string): + return self.parse_dict(parse_qs(query_string)) + + def parse_dict(self, dictionary): d = {} - for name, value in parse_qs(query_string).items(): - regex_match = re.match(r"(?P[^\[]+)\[(?P[^\]]+)\]", name) - if regex_match: - gd = regex_match.groupdict() - d.setdefault(gd["name"], {})[gd["value"]] = Input(name, value[0]) + for name, value in dictionary.items(): + if name.endswith("[]"): + d.update({name: value}) + else: + regex_match = re.match(r"(?P[^\[]+)\[(?P[^\]]+)\]", name) + + if regex_match: + gd = regex_match.groupdict() + if isinstance(value, Input): + d.setdefault(gd["name"], {})[gd["value"]] = value + else: + d.setdefault(gd["name"], {})[gd["value"]] = value[0] + else: + try: + d.update({name: value[0]}) + except TypeError: + d.update({name: value}) + + new_dict = {} + # Further filter the dictionary + for name, value in d.items(): + if "[]" in name: + new_name = name.replace("[]", "") + regex_match = re.match( + r"(?P[^\[]+)*\[(?P[^\]]+)\]", new_name + ) + if regex_match: + new_dict.setdefault(regex_match["name"], []).append( + {regex_match["value"]: value} + ) + else: + new_dict.update({name: value}) else: - d.update({name: Input(name, value[0])}) + new_dict.update({name: value}) - return d + return new_dict diff --git a/src/masonite/input/__init__.py b/src/masonite/input/__init__.py new file mode 100644 index 000000000..fb3f53e79 --- /dev/null +++ b/src/masonite/input/__init__.py @@ -0,0 +1 @@ +from .InputBag import InputBag diff --git a/src/masonite/listeners/BaseExceptionListener.py b/src/masonite/listeners/BaseExceptionListener.py deleted file mode 100644 index 8220c34e1..000000000 --- a/src/masonite/listeners/BaseExceptionListener.py +++ /dev/null @@ -1,2 +0,0 @@ -class BaseExceptionListener: - pass diff --git a/src/masonite/listeners/__init__.py b/src/masonite/listeners/__init__.py deleted file mode 100644 index 9446b2ad1..000000000 --- a/src/masonite/listeners/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .BaseExceptionListener import BaseExceptionListener diff --git a/src/masonite/loader/Loader.py b/src/masonite/loader/Loader.py new file mode 100644 index 000000000..cc1909dbd --- /dev/null +++ b/src/masonite/loader/Loader.py @@ -0,0 +1,78 @@ +"""Loader class to easily list, find or load any object in a given module, or folder.""" +import inspect +import pkgutil +import os + +from ..exceptions import LoaderNotFound +from ..utils.str import as_filepath +from ..utils.structures import load + + +def parameters_filter(obj_name, obj): + return ( + obj_name.isupper() + and not obj_name.startswith("__") + and not obj_name.endswith("__") + ) + + +class Loader: + def get_modules(self, files_or_directories, raise_exception=False): + if not isinstance(files_or_directories, list): + files_or_directories = [files_or_directories] + + _modules = {} + module_paths = list(map(as_filepath, files_or_directories)) + for module_loader, name, _ in pkgutil.iter_modules(module_paths): + module = load( + f"{os.path.relpath(module_loader.path)}.{name}", + raise_exception=raise_exception, + ) + _modules.update({name: module}) + return _modules + + def find(self, class_instance, paths, class_name, raise_exception=False): + _classes = self.find_all(class_instance, paths) + for name, obj in _classes.items(): + if name == class_name: + return obj + if raise_exception: + raise LoaderNotFound( + f"No {class_instance} named {class_name} has been found in {paths}" + ) + return None + + def find_all(self, class_instance, paths, raise_exception=False): + _classes = {} + for module in self.get_modules(paths).values(): + for obj_name, obj in inspect.getmembers(module): + # check if obj is the same class as the given one + if inspect.isclass(obj) and issubclass(obj, class_instance): + # check if the class really belongs to those paths to load internal only + if obj.__module__.startswith(module.__package__): + _classes.update({obj_name: obj}) + if not len(_classes.keys()) and raise_exception: + raise LoaderNotFound(f"No {class_instance} have been found in {paths}") + return _classes + + def get_object(self, path_or_module, object_name, raise_exception=False): + return load(path_or_module, object_name, raise_exception=raise_exception) + + def get_objects(self, path_or_module, filter_method=None, raise_exception=False): + """Returns a dictionary of objects from the given path (file or dotted). The dictionary can + be filtered if a given callable is given.""" + if isinstance(path_or_module, str): + module = load(path_or_module, raise_exception=raise_exception) + else: + module = path_or_module + if not module: + return None + return dict(inspect.getmembers(module, filter_method)) + + def get_parameters(self, module_or_path): + _parameters = {} + for name, obj in self.get_objects(module_or_path).items(): + if parameters_filter(name, obj): + _parameters.update({name: obj}) + + return _parameters diff --git a/src/masonite/loader/__init__.py b/src/masonite/loader/__init__.py new file mode 100644 index 000000000..b66f1da04 --- /dev/null +++ b/src/masonite/loader/__init__.py @@ -0,0 +1 @@ +from .Loader import Loader diff --git a/src/masonite/mail/Mail.py b/src/masonite/mail/Mail.py new file mode 100644 index 000000000..63f459f19 --- /dev/null +++ b/src/masonite/mail/Mail.py @@ -0,0 +1,33 @@ +class Mail: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config.get(self.driver_config.get("default"), {}) + + return self.driver_config.get(driver, {}) + + def mailable(self, mailable): + self.options = mailable.set_application(self.application).build().get_options() + return self + + def send(self, driver=None): + selected_driver = driver or self.options.get("driver", None) + self.options.update(self.get_config_options(selected_driver)) + return self.get_driver(selected_driver).set_options(self.options).send() diff --git a/src/masonite/mail/Mailable.py b/src/masonite/mail/Mailable.py new file mode 100644 index 000000000..d99e44349 --- /dev/null +++ b/src/masonite/mail/Mailable.py @@ -0,0 +1,102 @@ +from .MessageAttachment import MessageAttachment + + +class Mailable: + def __init__(self): + self._to = "" + self._cc = "" + self._bcc = "" + self._from = "" + self._reply_to = "" + self._subject = "" + self._priority = None + self._driver = None + self.text_content = "" + self.html_content = "" + self.attachments = [] + + def to(self, to): + self._to = to + return self + + def cc(self, cc): + self._cc = cc + return self + + def bcc(self, bcc): + self._bcc = bcc + return self + + def set_application(self, application): + self.application = application + return self + + def from_(self, _from): + self._from = _from + return self + + def attach(self, name, path): + self.attachments.append(MessageAttachment(name, path)) + return self + + def reply_to(self, reply_to): + self._reply_to = reply_to + return self + + def subject(self, subject): + self._subject = subject + return self + + def text(self, content): + self.text_content = content + return self + + def html(self, content): + self.html_content = content + return self + + def view(self, view, data={}): + return self.html( + self.application.make("view").render(view, data).rendered_template + ) + + def priority(self, priority): + self._priority = str(priority) + return self + + def high_priority(self): + self.priority(1) + return self + + def low_priority(self): + self.priority(5) + return self + + def driver(self, driver): + self._driver = driver + return self + + def get_response(self): + self.build() + if self.get_options().get("html_content"): + return self.get_options().get("html_content") + if self.get_options().get("text_content"): + return self.get_options().get("text_content") + + def get_options(self): + return { + "to": self._to, + "cc": self._cc, + "bcc": self._bcc, + "from": self._from, + "subject": self._subject, + "text_content": self.text_content, + "html_content": self.html_content, + "reply_to": self._reply_to, + "attachments": self.attachments, + "priority": self._priority, + "driver": self._driver, + } + + def build(self, *args, **kwargs): + return self diff --git a/src/masonite/mail/MessageAttachment.py b/src/masonite/mail/MessageAttachment.py new file mode 100644 index 000000000..0f3cb1cab --- /dev/null +++ b/src/masonite/mail/MessageAttachment.py @@ -0,0 +1,4 @@ +class MessageAttachment: + def __init__(self, alias, path): + self.alias = alias + self.path = path diff --git a/src/masonite/mail/MockMail.py b/src/masonite/mail/MockMail.py new file mode 100644 index 000000000..dcf8ef890 --- /dev/null +++ b/src/masonite/mail/MockMail.py @@ -0,0 +1,101 @@ +from .Mail import Mail + + +class MockMail(Mail): + def __init__(self, application, *args, **kwargs): + super().__init__(application, *args, **kwargs) + self.count = 0 + + def send(self, driver=None): + self.count += 1 + return self + + def seeEmailBcc(self, bcc): + assert bcc == self.options.get( + "bcc" + ), f"BCC of {self.options.get('bcc')} does not match expected {bcc}" + return self + + def seeEmailCc(self, cc): + assert cc == self.options.get( + "cc" + ), f"CC of {self.options.get('cc')} does not match expected {cc}" + return self + + def seeEmailContains(self, contents): + assert contents in self.options.get( + "html_content" + ) or contents in self.options.get( + "text_content" + ), f"Could not find the {contents} in the email" + return self + + def getHtmlContents(self, contents): + return self.options.get("html_content") + + def getTextContents(self, contents): + return self.options.get("text_content") + + def seeEmailCountEquals(self, count): + assert ( + count == self.count + ), f"Email count of {self.count} does not match expected {count}" + return self + + def seeEmailDoesNotContain(self, contents): + assert contents not in self.options.get( + "html_content" + ) and contents not in self.options.get( + "text_content" + ), f"Found {contents} in the email but should not be" + return self + + def seeEmailFrom(self, assertion): + assert assertion == self.options.get( + "from" + ), f"Assertion of from address {self.options.get('from')} does not match expected {assertion}" + return self + + def seeEmailReplyTo(self, assertion): + assert assertion == self.options.get( + "reply_to" + ), f"Assertion of reply-to {self.options.get('reply_to')} does not match expected {assertion}" + return self + + def seeEmailSubjectContains(self, assertion): + assert assertion in self.options.get( + "subject" + ), f"Assertion of subject {self.options.get('subject')} does not contain expected {assertion}" + return self + + def seeEmailSubjectDoesNotContain(self, assertion): + assert assertion not in self.options.get( + "subject" + ), f"Assertion of subject {self.options.get('subject')} does contain expected {assertion}" + return self + + def seeEmailSubjectEquals(self, assertion): + assert assertion == self.options.get( + "subject" + ), f"Assertion of subject address {self.options.get('subject')} does not match expected {assertion}" + return self + + def seeEmailTo(self, assertion): + assert assertion == self.options.get( + "to" + ), f"Assertion of to address {self.options.get('to')} does not match expected {assertion}" + return self + + def seeEmailPriority(self, assertion): + assert assertion == self.options.get( + "priority" + ), f"Assertion of priority {self.options.get('priority')} does not match expected {assertion}" + return self + + def seeEmailWasNotSent(self): + assert self.count == 0, "Expected email was not sent but it was sent" + return self + + def seeEmailWasSent(self): + assert self.count > 0, "Expected email was not sent but it was sent" + return self diff --git a/src/masonite/mail/Recipient.py b/src/masonite/mail/Recipient.py new file mode 100644 index 000000000..29e0dabb7 --- /dev/null +++ b/src/masonite/mail/Recipient.py @@ -0,0 +1,18 @@ +class Recipient: + def __init__(self, recipient): + if isinstance(recipient, (list, tuple)): + recipient = ",".join(recipient) + self.recipient = recipient + + def header(self): + headers = [] + for address in self.recipient.split(","): + + if "<" in address: + headers.append(address) + continue + + if address.strip(): + headers.append(f"<{address.strip()}>") + + return ", ".join(headers) diff --git a/src/masonite/mail/__init__.py b/src/masonite/mail/__init__.py new file mode 100644 index 000000000..ae10d6645 --- /dev/null +++ b/src/masonite/mail/__init__.py @@ -0,0 +1,3 @@ +from .Mail import Mail +from .Mailable import Mailable +from .MockMail import MockMail diff --git a/src/masonite/mail/drivers/MailgunDriver.py b/src/masonite/mail/drivers/MailgunDriver.py new file mode 100644 index 000000000..57d8a1fb6 --- /dev/null +++ b/src/masonite/mail/drivers/MailgunDriver.py @@ -0,0 +1,51 @@ +import requests +from ..Recipient import Recipient + + +class MailgunDriver: + def __init__(self, application): + self.application = application + self.options = {} + self.content_type = None + + def set_options(self, options): + self.options = options + return self + + def get_mime_message(self): + data = { + "from": self.options.get("from"), + "to": Recipient(self.options.get("to")).header(), + "subject": self.options.get("subject"), + "h:Reply-To": self.options.get("reply_to"), + "html": self.options.get("html_content"), + "text": self.options.get("text_content"), + } + + if self.options.get("cc"): + data.update({"cc", self.options.get("cc")}) + if self.options.get("bcc"): + data.update({"bcc", self.options.get("bcc")}) + if self.options.get("priority"): + data.update({"h:X-Priority", self.options.get("priority")}) + + return data + + def get_attachments(self): + files = [] + for attachment in self.options.get("attachments", []): + files.append(("attachment", open(attachment.path, "rb"))) + + return files + + def send(self): + domain = self.options["domain"] + secret = self.options["secret"] + attachments = self.get_attachments() + + return requests.post( + f"https://api.mailgun.net/v3/{domain}/messages", + auth=("api", secret), + data=self.get_mime_message(), + files=attachments, + ) diff --git a/src/masonite/mail/drivers/SMTPDriver.py b/src/masonite/mail/drivers/SMTPDriver.py new file mode 100644 index 000000000..bd6b3052f --- /dev/null +++ b/src/masonite/mail/drivers/SMTPDriver.py @@ -0,0 +1,76 @@ +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.application import MIMEApplication +from email.mime.text import MIMEText +from ..Recipient import Recipient +import ssl + + +class SMTPDriver: + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_mime_message(self): + message = MIMEMultipart("alternative") + + message["Subject"] = self.options.get("subject") + + message["From"] = Recipient(self.options.get("from")).header() + message["To"] = Recipient(self.options.get("to")).header() + if self.options.get("reply_to"): + message["Reply-To"] = Recipient(self.options.get("reply_to")).header() + + if self.options.get("cc"): + message["Cc"] = Recipient(self.options.get("cc")).header() + + if self.options.get("bcc"): + message["Bcc"] = Recipient(self.options.get("bcc")).header() + + if self.options.get("html_content"): + message.attach(MIMEText(self.options.get("html_content"), "html")) + + if self.options.get("text_content"): + message.attach(MIMEText(self.options.get("text_content"), "plain")) + + if self.options.get("priority"): + message["X-Priority"] = self.options.get("priority") + + for attachment in self.options.get("attachments", []): + with open(attachment.path, "rb") as fil: + part = MIMEApplication(fil.read(), Name=attachment.alias) + + part["Content-Disposition"] = f"attachment; filename={attachment.alias}" + message.attach(part) + + return message + + def make_connection(self): + options = self.options + if options.get("ssl"): + smtp = smtplib.SMTP_SSL("{0}:{1}".format(options["host"], options["port"])) + else: + smtp = smtplib.SMTP("{0}:{1}".format(options["host"], int(options["port"]))) + + if options.get("tls"): + context = ssl.create_default_context() + context.check_hostname = False + + # Check if correct response code for starttls is received from the server + if smtp.starttls(context=context)[0] != 220: + raise smtplib.SMTPNotSupportedError( + "Server is using untrusted protocol." + ) + + if options.get("username") and options.get("password"): + smtp.login(options.get("username"), options.get("password")) + + return smtp + + def send(self): + smtp = self.make_connection() + smtp.send_message(self.get_mime_message()) diff --git a/src/masonite/mail/drivers/TerminalDriver.py b/src/masonite/mail/drivers/TerminalDriver.py new file mode 100644 index 000000000..d519e3b7b --- /dev/null +++ b/src/masonite/mail/drivers/TerminalDriver.py @@ -0,0 +1,30 @@ +from ..Recipient import Recipient + + +class TerminalDriver: + def __init__(self, application): + self.application = application + self.options = {} + self.content_type = None + + def set_options(self, options): + self.options = options + return self + + def send(self): + print("-------------------------------------") + print(f"To: {Recipient(self.options.get('to')).header()}") + print(f"From: {Recipient(self.options.get('from')).header()}") + print(f"Cc: {Recipient(self.options.get('cc')).header()}") + print(f"Bcc: {Recipient(self.options.get('bcc')).header()}") + print(f"Subject: {self.options.get('subject')}") + print("-------------------------------------") + print(f"{self.options.get('html_content')}") + if self.options.get("text_content"): + print("-------------------------------------") + print(f"Text Content: {self.options.get('text_content')}") + if self.options.get("attachments"): + print("-------------------------------------") + for index, attachment in enumerate(self.options.get("attachments")): + index += 1 + print(f"Attachment {index}: {attachment.alias} from {attachment.path}") diff --git a/src/masonite/mail/drivers/__init__.py b/src/masonite/mail/drivers/__init__.py new file mode 100644 index 000000000..ac606aeab --- /dev/null +++ b/src/masonite/mail/drivers/__init__.py @@ -0,0 +1,3 @@ +from .SMTPDriver import SMTPDriver +from .MailgunDriver import MailgunDriver +from .TerminalDriver import TerminalDriver diff --git a/src/masonite/managers/AuthManager.py b/src/masonite/managers/AuthManager.py deleted file mode 100644 index 483db9e11..000000000 --- a/src/masonite/managers/AuthManager.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Auth Manager Module.""" - -from .Manager import Manager - - -class AuthManager(Manager): - """Manages all auth drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "auth" - driver_prefix = "Auth" - - -class Auth: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/BroadcastManager.py b/src/masonite/managers/BroadcastManager.py deleted file mode 100644 index 460a82883..000000000 --- a/src/masonite/managers/BroadcastManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Broadcast Manager Module.""" - -from ..contracts import BroadcastManagerContract -from .Manager import Manager - - -class BroadcastManager(Manager, BroadcastManagerContract): - """Manages all broadcast drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "broadcast" - driver_prefix = "Broadcast" - - -class Broadcast: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/CacheManager.py b/src/masonite/managers/CacheManager.py deleted file mode 100644 index 28da99fcc..000000000 --- a/src/masonite/managers/CacheManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Cache Manager.""" - -from ..contracts import CacheManagerContract -from .Manager import Manager - - -class CacheManager(Manager, CacheManagerContract): - """Manages all cache drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "cache" - driver_prefix = "Cache" - - -class Cache: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/MailManager.py b/src/masonite/managers/MailManager.py deleted file mode 100644 index 5382e4402..000000000 --- a/src/masonite/managers/MailManager.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Mail Manager Module.""" - -from ..contracts import MailManagerContract -from .Manager import Manager -from ..helpers import config - - -class MailManager(Manager, MailManagerContract): - """Manages all mail drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "mail" - driver_prefix = "Mail" - - def helper(self): - """Helper Method to work with returning the driver from the MailManager. - - Returns: - Mail Driver - """ - return self.driver(config("mail.driver")) - - -class Mail: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/Manager.py b/src/masonite/managers/Manager.py deleted file mode 100644 index 4d5b01478..000000000 --- a/src/masonite/managers/Manager.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Manager Module.""" - -import inspect - -from ..exceptions import ( - DriverNotFound, - MissingContainerBindingNotFound, - UnacceptableDriverType, -) - -from ..app import App -from ..helpers import config - - -class Manager: - """Base Manager Class.""" - - config = None - driver_prefix = None - - def __init__(self, container: App): - """Manager constructor. - - Keyword Arguments: - container {masonite.app.App} -- The container class (default: {None}) - """ - self.manage_driver = None - self.container = container - - def load_container(self, container): - """Load the container into the class and creates the default driver. - - Arguments: - container {masonite.app.App} -- The container class - - Returns: - self - """ - self.container = container - self.create_driver() - return self - - def driver(self, driver): - """Create the driver specified and returns the driver instance. - - Arguments: - driver {masonite.drivers.Driver} -- An instance of a Driver class. - - Returns: - masonite.drivers.Driver -- Returns a driver which is an instance of the base Driver class. - """ - self.create_driver(driver) - return self.manage_driver.load_manager(self) - - def create_driver(self, driver=None): - """Create the driver to be used. - - This could be used as the default driver when the manager is created or called internally on the fly - to change to a specific driver - - Keyword Arguments: - driver {string} -- The name of the driver to switch to (default: {None}) - - Raises: - UnacceptableDriverType -- Raised when a driver passed in is not a string or a class - DriverNotFound -- Raised when the driver can not be found. - """ - - if driver in (None, "default"): - driver = config("{}.driver".format(self.config)).capitalize() - else: - if isinstance(driver, str): - driver = driver.capitalize() - - try: - if isinstance(driver, str): - self.manage_driver = self.container.make( - "{0}{1}Driver".format(self.driver_prefix, driver) - ) - return - elif inspect.isclass(driver): - self.manage_driver = self.container.resolve(driver) - return - - raise UnacceptableDriverType( - "String or class based driver required. {} driver recieved.".format( - driver - ) - ) - except MissingContainerBindingNotFound: - raise DriverNotFound( - "Could not find the {0}{1}Driver from the service container. Are you missing a service provider?".format( - self.driver_prefix, driver - ) - ) diff --git a/src/masonite/managers/QueueManager.py b/src/masonite/managers/QueueManager.py deleted file mode 100644 index 0dfd11a73..000000000 --- a/src/masonite/managers/QueueManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Queue Manager Module.""" - -from ..contracts import QueueManagerContract -from .Manager import Manager - - -class QueueManager(Manager, QueueManagerContract): - """Manages all queue drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "queue" - driver_prefix = "Queue" - - -class Queue: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/SessionManager.py b/src/masonite/managers/SessionManager.py deleted file mode 100644 index 98a9f55b1..000000000 --- a/src/masonite/managers/SessionManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Session Manager Module.""" - -from ..contracts import SessionManagerContract -from .Manager import Manager - - -class SessionManager(Manager, SessionManagerContract): - """Manages all session drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "session" - driver_prefix = "Session" - - -class Session: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/StorageManager.py b/src/masonite/managers/StorageManager.py deleted file mode 100644 index 52095654b..000000000 --- a/src/masonite/managers/StorageManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Queue Manager Module.""" - -from ..contracts import StorageManagerContract -from .Manager import Manager - - -class StorageManager(Manager, StorageManagerContract): - """Manages all queue drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "storage" - driver_prefix = "Storage" - - -class Storage: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/UploadManager.py b/src/masonite/managers/UploadManager.py deleted file mode 100644 index 39ba1851e..000000000 --- a/src/masonite/managers/UploadManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Upload Manager Module.""" - -from ..contracts import UploadManagerContract -from .Manager import Manager - - -class UploadManager(Manager, UploadManagerContract): - """Manages all upload drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "storage" - driver_prefix = "Upload" - - -class Upload: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/__init__.py b/src/masonite/managers/__init__.py deleted file mode 100644 index ed8d49a29..000000000 --- a/src/masonite/managers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .Manager import Manager -from .AuthManager import AuthManager -from .BroadcastManager import BroadcastManager -from .CacheManager import CacheManager -from .MailManager import MailManager -from .QueueManager import QueueManager -from .SessionManager import SessionManager -from .StorageManager import StorageManager -from .UploadManager import UploadManager diff --git a/src/masonite/middleware/CorsMiddleware.py b/src/masonite/middleware/CorsMiddleware.py deleted file mode 100644 index ddfdf0acd..000000000 --- a/src/masonite/middleware/CorsMiddleware.py +++ /dev/null @@ -1,25 +0,0 @@ -"""CORS Middleware.""" - -from ..helpers import config -from ..response import Response - - -class CorsMiddleware: - """Appends CORS headers to HTTP response. - - Put any CORS middleware you need as a CORS dictionary inside your - middleware config file. - """ - - def __init__(self, response: Response): - """Inject Any Dependencies From The Service Container. - - Arguments: - Request {masonite.request.Request} -- The Masonite request object - """ - self.response = response - - def before(self): - """Run This Middleware After The Route Executes.""" - headers = config("middleware.cors") or {} - self.response.header(headers) diff --git a/src/masonite/middleware/CsrfMiddleware.py b/src/masonite/middleware/CsrfMiddleware.py deleted file mode 100644 index 7bf7cdebc..000000000 --- a/src/masonite/middleware/CsrfMiddleware.py +++ /dev/null @@ -1,95 +0,0 @@ -"""CSRF Middleware.""" - -from jinja2 import Markup - -from ..auth import Csrf -from ..exceptions import InvalidCSRFToken -from ..request import Request -from ..view import View -import binascii -import os - - -class CsrfMiddleware: - """Verify CSRF Token Middleware.""" - - exempt = ["/"] - every_request = True - token_length = 30 - - def __init__(self, request: Request, csrf: Csrf, view: View): - """Initialize the CSRF Middleware - - Arguments: - request {masonite.request.Request} -- The normal Masonite request class. - csrf {masonite.auth.Csrf} -- CSRF auth class. - view {masonite.view.View} -- The normal Masonite view class. - """ - - self.request = request - self.csrf = csrf - self.view = view - - def before(self): - """Execute this method before the controller.""" - if not self.request.get_cookie("MSESSID"): - session_id = bytes( - binascii.b2a_hex(os.urandom(self.token_length // 2)) - ).decode("utf-8") - self.request.cookie("MSESSID", session_id, expires="5 minutes") - token = self.verify_token() - - self.view.share( - { - "csrf_field": Markup( - "".format(token) - ), - "csrf_token": token, - } - ) - - def after(self): - pass - - def in_exempt(self): - """Determine if the request has a URI that should pass through CSRF verification. - - Returns: - bool - """ - for route in self.exempt: - if self.request.contains(route): - return True - - return False - - def generate_token(self): - """Generate a token that will be used for CSRF protection - - Returns: - string -- A random string based on the length given - """ - - return self.csrf.generate_csrf_token(self.token_length) - - def verify_token(self): - """Verify if csrf token in post is valid. - - Raises: - InvalidCSRFToken -- Thrown if the CSRF tokens do not match. - - Returns: - string -- Returns a new token or the current token. - """ - if self.request.is_not_safe() and not self.in_exempt(): - token = ( - self.request.header("X-CSRF-TOKEN") - or self.request.header("X-XSRF-TOKEN") - or self.request.input("__token") - ) - if not self.csrf.verify_csrf_token(token): - raise InvalidCSRFToken("Invalid CSRF token.") - - return token - else: - return self.generate_token() diff --git a/src/masonite/middleware/GuardMiddleware.py b/src/masonite/middleware/GuardMiddleware.py deleted file mode 100644 index 0eb6d9e52..000000000 --- a/src/masonite/middleware/GuardMiddleware.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Guard Middleware.""" - -from ..auth import Auth -from ..helpers import config - - -class GuardMiddleware: - """Middleware to switch the guard""" - - def __init__(self, auth: Auth): - self.auth = auth - - def before(self, guard): - """Sets specified guard for the request. - - Arguments: - guard {string} -- The key of the guard to set. - """ - self.auth.set(guard) - - def after(self, _): - """Sets the default guard back after the request. - - Arguments: - _ {ignored} -- ignored - """ - self.auth.set(config("auth.auth.defaults.guard")) diff --git a/src/masonite/middleware/MaintenanceModeMiddleware.py b/src/masonite/middleware/MaintenanceModeMiddleware.py deleted file mode 100644 index d2e7cc1a9..000000000 --- a/src/masonite/middleware/MaintenanceModeMiddleware.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Maintainance Mode Middleware.""" -import os -from ..response import Response -from config import application - - -class MaintenanceModeMiddleware: - def __init__(self, response: Response): - self.response = response - - def before(self): - down = os.path.exists( - os.path.join(application.BASE_DIRECTORY, "bootstrap/down") - ) - if down is True: - self.response.status(503) diff --git a/src/masonite/middleware/ResponseMiddleware.py b/src/masonite/middleware/ResponseMiddleware.py deleted file mode 100644 index 8a23faefa..000000000 --- a/src/masonite/middleware/ResponseMiddleware.py +++ /dev/null @@ -1,21 +0,0 @@ -from ..app import App -from ..request import Request -from ..response import Response - - -class ResponseMiddleware: - def __init__(self, request: Request, app: App, response: Response): - self.request = request - self.app = app - self.response = response - - def after(self): - if self.request.redirect_url: - self.response.redirect(self.request.redirect_url, status=302) - self.request.reset_redirections() - - if self.app.has("Session") and self.response.is_status(200): - try: - self.app.make("Session").driver("memory").reset(flash_only=True) - except Exception: - pass diff --git a/src/masonite/middleware/SecureHeadersMiddleware.py b/src/masonite/middleware/SecureHeadersMiddleware.py deleted file mode 100644 index fb5125085..000000000 --- a/src/masonite/middleware/SecureHeadersMiddleware.py +++ /dev/null @@ -1,40 +0,0 @@ -"""SecureHeaders Middleware.""" - -from ..response import Response - - -class SecureHeadersMiddleware: - """SecureHeaders Middleware.""" - - def __init__(self, response: Response): - """Inject Any Dependencies From The Service Container. - - Arguments: - Response {masonite.response.Response} -- The Masonite response object - """ - self.response = response - self.headers = { - "Strict-Transport-Security": "max-age=63072000; includeSubdomains", - "X-Frame-Options": "SAMEORIGIN", - "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - "Referrer-Policy": "no-referrer, strict-origin-when-cross-origin", - "Cache-control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - } - - def before(self): - """Run This Middleware Before The Route Executes.""" - pass - - def after(self): - """Run This Middleware After The Route Executes.""" - from config import middleware - - try: - # Try importing secure headers if they exist in the config file - self.headers.update(middleware.SECURE_HEADERS) - except AttributeError: - pass - - self.response.header(self.headers) diff --git a/src/masonite/middleware/__init__.py b/src/masonite/middleware/__init__.py index 87d11f2c0..16f53ce9d 100644 --- a/src/masonite/middleware/__init__.py +++ b/src/masonite/middleware/__init__.py @@ -1,6 +1,6 @@ -from .CsrfMiddleware import CsrfMiddleware -from .GuardMiddleware import GuardMiddleware -from .CorsMiddleware import CorsMiddleware -from .MaintenanceModeMiddleware import MaintenanceModeMiddleware -from .ResponseMiddleware import ResponseMiddleware -from .SecureHeadersMiddleware import SecureHeadersMiddleware +from .middleware_capsule import MiddlewareCapsule +from .middleware import Middleware +from .route.VerifyCsrfToken import VerifyCsrfToken +from .route.SessionMiddleware import SessionMiddleware +from .route.EncryptCookies import EncryptCookies +from .route.LoadUserMiddleware import LoadUserMiddleware diff --git a/src/masonite/middleware/middleware.py b/src/masonite/middleware/middleware.py new file mode 100644 index 000000000..8fffa40aa --- /dev/null +++ b/src/masonite/middleware/middleware.py @@ -0,0 +1,2 @@ +class Middleware: + pass diff --git a/src/masonite/middleware/middleware_capsule.py b/src/masonite/middleware/middleware_capsule.py new file mode 100644 index 000000000..5722be99a --- /dev/null +++ b/src/masonite/middleware/middleware_capsule.py @@ -0,0 +1,64 @@ +class MiddlewareCapsule: + def __init__(self): + self.route_middleware = {} + self.http_middleware = [] + + def add(self, middleware): + if isinstance(middleware, dict): + self.route_middleware.update(middleware) + + if isinstance(middleware, list): + self.http_middleware += middleware + + return self + + def remove(self, middleware): + if middleware in self.route_middleware: + self.route_middleware.pop(middleware) + elif middleware in self.http_middleware: + self.http_middleware.pop(self.http_middleware.index(middleware)) + return self + + def get_route_middleware(self, keys=None): + middlewares = [] + if keys is None: + return self.route_middleware + + if keys is None: + keys = [] + + for key in keys: + found = self.route_middleware[key] + if isinstance(found, list): + middlewares += found + else: + middlewares += [found] + return middlewares + + def get_http_middleware(self): + return self.http_middleware + + def run_route_middleware(self, middlewares, request, response, callback="before"): + for middleware in middlewares: + if ":" in middleware: + # get list of arguments if any + middleware_to_run, raw_arguments = middleware.split(":") + raw_arguments = raw_arguments.split(",") + # try to parse arguments with @ from requests + arguments = [] + for arg in raw_arguments: + if "@" in arg: + arg = arg.replace("@", "") + arg = request.input(arg) + arguments.append(arg) + arguments = tuple(arguments) + else: + middleware_to_run = middleware + arguments = () + route_middlewares = self.get_route_middleware([middleware_to_run]) + for route_middleware in route_middlewares: + middleware_response = getattr(route_middleware(), callback)( + request, response, *arguments + ) + if middleware_response != request: + break diff --git a/src/masonite/middleware/route/EncryptCookies.py b/src/masonite/middleware/route/EncryptCookies.py new file mode 100644 index 000000000..6995d31e7 --- /dev/null +++ b/src/masonite/middleware/route/EncryptCookies.py @@ -0,0 +1,21 @@ +from ...exceptions import InvalidToken + + +class EncryptCookies: + def before(self, request, response): + for _, cookie in request.cookie_jar.all().items(): + try: + cookie.value = request.app.make("sign").unsign(cookie.value) + except InvalidToken: + pass + + return request + + def after(self, request, response): + for _, cookie in response.cookie_jar.all().items(): + try: + cookie.value = request.app.make("sign").sign(cookie.value) + except InvalidToken: + pass + + return request diff --git a/src/masonite/middleware/route/LoadUserMiddleware.py b/src/masonite/middleware/route/LoadUserMiddleware.py new file mode 100644 index 000000000..00669f21e --- /dev/null +++ b/src/masonite/middleware/route/LoadUserMiddleware.py @@ -0,0 +1,11 @@ +from .. import Middleware +from ...facades import Auth + + +class LoadUserMiddleware(Middleware): + def before(self, request, _): + request.set_user(Auth.user()) + return request + + def after(self, request, _): + return request diff --git a/src/masonite/middleware/route/SessionMiddleware.py b/src/masonite/middleware/route/SessionMiddleware.py new file mode 100644 index 000000000..cc91b15b1 --- /dev/null +++ b/src/masonite/middleware/route/SessionMiddleware.py @@ -0,0 +1,29 @@ +from .. import Middleware +from ...utils.str import random_string +from ...facades import Request, Session, Response + + +class SessionMiddleware(Middleware): + def before(self, request, response): + if not request.cookie("SESSID"): + session_code = random_string(10) + response.cookie("SESSID", session_code) + request.cookie("SESSID", session_code) + Session.start() + request.app.make("response").with_input = self.with_input + request.app.make("response").with_errors = self.with_errors + request.app.make("request").session = Session + return request + + def after(self, request, _): + Session.save() + return request + + def with_input(self): + for key, value in Request.all().items(): + Session.flash(key, value) + return Response + + def with_errors(self, errors): + Session.flash("errors", errors) + return Response diff --git a/src/masonite/middleware/route/VerifyCsrfToken.py b/src/masonite/middleware/route/VerifyCsrfToken.py new file mode 100644 index 000000000..3ac55c02c --- /dev/null +++ b/src/masonite/middleware/route/VerifyCsrfToken.py @@ -0,0 +1,69 @@ +from .. import Middleware +from markupsafe import Markup +from ...exceptions import InvalidCSRFToken +from hmac import compare_digest + + +class VerifyCsrfToken(Middleware): + + exempt = [] + + def before(self, request, response): + self.verify_token(request, self.get_token(request)) + + token = self.create_token(request, response) + + request.app.make("view").share( + { + "csrf_field": Markup( + f"" + ), + "csrf_token": token, + } + ) + + return request + + def after(self, request, response): + return request + + def create_token(self, request, response): + session = request.cookie("SESSID") + response.cookie("csrf_token", session) + return session + + def verify_token(self, request, token): + if self.in_exempt(request): + return True + if request.is_not_safe() and not token: + raise InvalidCSRFToken("Missing CSRF Token") + if request.is_not_safe(): + if request.cookie("csrf_token") and ( + compare_digest( + request.cookie("csrf_token"), + token, + ) + and compare_digest(token, request.cookie("SESSID")) + ): + return True + raise InvalidCSRFToken("Invalid CSRF token.") + return True + + def in_exempt(self, request): + """Determine if the request has a URI that should pass through CSRF verification. + + Returns: + bool + """ + for route in self.exempt: + if request.contains(route): + return True + + return False + + def get_token(self, request): + return ( + request.header("X-CSRF-TOKEN") + or request.header("X-XSRF-TOKEN") + or request.input("__token") + ) diff --git a/config/__init__.py b/src/masonite/middleware/route/__init__.py similarity index 100% rename from config/__init__.py rename to src/masonite/middleware/route/__init__.py diff --git a/src/masonite/notification/AnonymousNotifiable.py b/src/masonite/notification/AnonymousNotifiable.py new file mode 100644 index 000000000..d54bf5bcf --- /dev/null +++ b/src/masonite/notification/AnonymousNotifiable.py @@ -0,0 +1,39 @@ +"""Anonymous Notifiable mixin""" + +from .Notifiable import Notifiable + + +class AnonymousNotifiable(Notifiable): + """Anonymous notifiable allowing to send notification without having + a notifiable entity. + + Usage: + self.notification.route("sms", "+3346474764").send(WelcomeNotification()) + """ + + def __init__(self, application=None): + self.application = application + self._routes = {} + + def route(self, driver, recipient): + """Define which driver using to route the notification.""" + if driver == "database": + raise ValueError( + "The database driver does not support on-demand notifications." + ) + self._routes[driver] = recipient + return self + + def route_notification_for(self, driver): + try: + return self._routes[driver] + except KeyError: + raise ValueError( + "Routing has not been defined for the driver {}".format(driver) + ) + + def send(self, notification, dry=False, fail_silently=False): + """Send the given notification.""" + return self.application.make("notification").send( + self, notification, self._routes, dry, fail_silently + ) diff --git a/src/masonite/notification/DatabaseNotification.py b/src/masonite/notification/DatabaseNotification.py new file mode 100644 index 000000000..e59795494 --- /dev/null +++ b/src/masonite/notification/DatabaseNotification.py @@ -0,0 +1,38 @@ +"""DatabaseNotification Model.""" +import pendulum +from masoniteorm.relationships import morph_to +from masoniteorm.models import Model + + +class DatabaseNotification(Model): + """DatabaseNotification Model allowing notifications to be stored in database.""" + + __fillable__ = ["id", "type", "data", "read_at", "notifiable_id", "notifiable_type"] + __table__ = "notifications" + + @morph_to("notifiable_type", "notifiable_id") + def notifiable(self): + """Get the notifiable entity that the notification belongs to.""" + return + + def mark_as_read(self): + """Mark the notification as read.""" + if not self.read_at: + self.read_at = pendulum.now() + return self.save(query=True) + + def mark_as_unread(self): + """Mark the notification as unread.""" + if self.read_at: + self.read_at = None + return self.save(query=True) + + @property + def is_read(self): + """Determine if a notification has been read.""" + return self.read_at is not None + + @property + def is_unread(self): + """Determine if a notification has not been read yet.""" + return self.read_at is None diff --git a/src/masonite/notification/MockNotification.py b/src/masonite/notification/MockNotification.py new file mode 100644 index 000000000..f9c40e3c2 --- /dev/null +++ b/src/masonite/notification/MockNotification.py @@ -0,0 +1,120 @@ +from .NotificationManager import NotificationManager +from .AnonymousNotifiable import AnonymousNotifiable +from .Notification import Notification + + +class NotificationWithAsserts(Notification): + def assertSentVia(self, *drivers): + sent_via = self.via(self.notifiable) + for driver in drivers: + assert ( + driver in sent_via + ), f"notification sent via {sent_via}, not {driver}." + return self + + def assertEqual(self, value, reference): + assert value == reference, "{value} not equal to {reference}." + return self + + def assertNotEqual(self, value, reference): + assert value != reference, "{value} equal to {reference}." + return self + + def assertIn(self, value, container): + assert value in container, "{value} not in {container}." + return self + + @classmethod + def patch(cls, target): + for k in cls.__dict__: + obj = getattr(cls, k) + if not k.startswith("_") and callable(obj): + setattr(target, k, obj) + + +class MockNotification(NotificationManager): + def __init__(self, application, *args, **kwargs): + super().__init__(application, *args, **kwargs) + self.count = 0 + self.last_notifiable = None + self.last_notification = None + + def send( + self, notifiables, notification, drivers=[], dry=False, fail_silently=False + ): + _notifiables = [] + for notifiable in self._format_notifiables(notifiables): + if isinstance(notifiable, AnonymousNotifiable): + _notifiables.extend(notifiable._routes.values()) + else: + _notifiables.append(notifiable) + + notification_key = notification.type() + NotificationWithAsserts.patch(notification.__class__) + for notifiable in _notifiables: + notification.notifiable = notifiable # for asserts + old_notifs = self.sent_notifications.get(notifiable, {}) + old_notifs.update( + { + notification_key: old_notifs.get(notification_key, []) + + [notification] + } + ) + self.sent_notifications.update({notifiable: old_notifs}) + self.count += 1 + self.last_notification = notification + self.last_notifiable = notifiable + return self + + def resetCount(self): + """Reset sent notifications count.""" + self.count = 0 + self.sent_notifications = {} + self.last_notifiable = None + self.last_notification = None + return self + + def assertNothingSent(self): + assert self.count == 0, f"{self.count} notifications have been sent." + return self + + def assertCount(self, count): + assert ( + self.count == count + ), f"{self.count} notifications have been sent, not {count}." + return self + + def assertSentTo( + self, notifiable, notification_class, callable_assert=None, count=None + ): + notification_key = notification_class.__name__ + notifiable_notifications = self.sent_notifications.get(notifiable, []) + assert notification_key in notifiable_notifications + if count: + sent_count = len(notifiable_notifications.get(notification_key, [])) + assert ( + sent_count == count + ), f"{notification_key} has been sent to {notifiable} {sent_count} times" + if callable_assert: + # assert last notification sent for this notifiable + notification = notifiable_notifications.get(notification_key)[-1] + assert callable_assert(notifiable, notification) + return self + + def last(self): + """Get last sent mocked notification if any.""" + return self.last_notification + + def assertLast(self, callable_assert): + if not self.last_notifiable or not self.last_notification: + raise AssertionError("No notification has been sent.") + assert callable_assert(self.last_notifiable, self.last_notification) + return self + + def assertNotSentTo(self, notifiable, notification_class): + notification_key = notification_class.__name__ + notifiable_notifications = self.sent_notifications.get(notifiable, []) + assert ( + notification_key not in notifiable_notifications + ), f"{notification_key} has been sent to {notifiable}." + return self diff --git a/src/masonite/notification/Notifiable.py b/src/masonite/notification/Notifiable.py new file mode 100644 index 000000000..c1c467220 --- /dev/null +++ b/src/masonite/notification/Notifiable.py @@ -0,0 +1,63 @@ +"""Notifiable mixin""" +from masoniteorm.relationships import has_many + +from .DatabaseNotification import DatabaseNotification +from ..exceptions.exceptions import NotificationException + + +class Notifiable: + """Notifiable mixin allowing to send notification to a model. It's often used with the + User model. + + Usage: + user.notify(WelcomeNotification()) + """ + + def notify(self, notification, drivers=[], dry=False, fail_silently=False): + """Send the given notification.""" + from wsgi import application + + return application.make("notification").send( + self, notification, drivers, dry, fail_silently + ) + + def route_notification_for(self, driver): + """Get the notification routing information for the given driver. If method has not been + defined on the model: for mail driver try to use 'email' field of model.""" + # check if routing has been specified on the model + method_name = "route_notification_for_{0}".format(driver) + + try: + method = getattr(self, method_name) + return method() + except AttributeError: + # if no method is defined on notifiable use default + if driver == "database": + # with database channel, notifications are saved to database + pass + elif driver == "mail": + return self.email + else: + raise NotificationException( + "Notifiable model does not implement {}".format(method_name) + ) + + @has_many("id", "notifiable_id") + def notifications(self): + """Get all notifications sent to the model instance. Only for 'database' + notifications.""" + return DatabaseNotification.where("notifiable_type", "users").order_by( + "created_at", direction="DESC" + ) + + @property + def unread_notifications(self): + """Get the model instance unread notifications. Only for 'database' + notifications.""" + return self.notifications.where("read_at", "==", None) + + @property + def read_notifications(self): + """Get the model instance read notifications. Only for 'database' + notifications.""" + return self.notifications.where("read_at", "!=", None) diff --git a/src/masonite/notification/Notification.py b/src/masonite/notification/Notification.py new file mode 100644 index 000000000..9655c682d --- /dev/null +++ b/src/masonite/notification/Notification.py @@ -0,0 +1,39 @@ +"""Base Notification facade.""" + + +class Notification: + def via(self, notifiable): + """Defines the notification's delivery channels.""" + raise NotImplementedError("via() method should be implemented.") + + def should_send(self): + return True + + def ignore_errors(self): + return False + + def broadcast_on(self): + return "broadcast" + + @classmethod + def type(cls): + """Get notification type defined with class name.""" + return cls.__name__ + + def dry(self): + """Sets whether the notification should be sent or not. + + Returns: + self + """ + self._dry = True + return self + + def fail_silently(self): + """Sets whether the notification can fail silently (without raising exceptions). + + Returns: + self + """ + self._fail_silently = True + return self diff --git a/src/masonite/notification/NotificationManager.py b/src/masonite/notification/NotificationManager.py new file mode 100644 index 000000000..2f5818532 --- /dev/null +++ b/src/masonite/notification/NotificationManager.py @@ -0,0 +1,85 @@ +"""Notification handler class""" +import uuid + +from ..exceptions.exceptions import NotificationException +from ..queues import ShouldQueue +from .AnonymousNotifiable import AnonymousNotifiable + + +class NotificationManager: + """Notification handler which handle sending/queuing notifications anonymously + or to notifiables through different channels.""" + + sent_notifications = {} + dry_notifications = {} + + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {"dry": False} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + self.get_driver(name).set_options(self.get_config_options(name)) + + def get_driver(self, name): + return self.drivers[name] + + def set_configuration(self, config): + self.driver_config = config.get("drivers") + self.options.update({"dry": config.get("dry")}) + return self + + def get_config_options(self, driver): + return self.driver_config.get(driver, {}) + + def send( + self, notifiables, notification, drivers=[], dry=False, fail_silently=False + ): + """Send the given notification to the given notifiables.""" + notifiables = self._format_notifiables(notifiables) + if not notification.should_send() or dry or self.options.get("dry"): + key = notification.type() + self.dry_notifications.update( + {key: notifiables + self.dry_notifications.get(key, [])} + ) + return + results = [] + for notifiable in notifiables: + # get drivers to use for sending this notification + drivers = drivers if drivers else notification.via(notifiable) + if not drivers: + raise NotificationException( + "No drivers have been defined in via() method of {0} notification.".format( + notification.type() + ) + ) + notification.id = uuid.uuid4() + for driver in drivers: + driver_instance = self.get_driver(driver) + if isinstance(notifiable, AnonymousNotifiable) and driver == "database": + # this case is not possible but that should not stop other channels to be used + continue + try: + # if isinstance(notification, ShouldQueue): + # results.append(driver_instance.queue(notifiable, notification)) + # else: + results.append(driver_instance.send(notifiable, notification)) + except Exception as e: + if not notification.ignore_errors() and not fail_silently: + raise e + + return results[0] if len(results) == 1 else results + + def _format_notifiables(self, notifiables): + from masoniteorm.collection import Collection + + if isinstance(notifiables, (list, tuple, Collection)): + return notifiables + else: + return [notifiables] + + def route(self, driver, route): + """Specify how to send a notification to an anonymous notifiable.""" + return AnonymousNotifiable(self.application).route(driver, route) diff --git a/src/masonite/notification/SlackMessage.py b/src/masonite/notification/SlackMessage.py new file mode 100644 index 000000000..181317873 --- /dev/null +++ b/src/masonite/notification/SlackMessage.py @@ -0,0 +1,166 @@ +"""Class modelling a Slack message.""" +import json + + +class SlackMessage: + WEBHOOK_MODE = 1 + API_MODE = 2 + + def __init__(self): + self._text = "" + self._username = "masonite-bot" + self._icon_emoji = "" + self._icon_url = "" + self._text = "" + self._mrkdwn = True + self._as_current_user = False + self._reply_broadcast = False + # Indicates if channel names and usernames should be linked. + self._link_names = False + # Indicates if you want a preview of links inlined in the message. + self._unfurl_links = False + # Indicates if you want a preview of links to media inlined in the message. + self._unfurl_media = False + self._blocks = [] + + self._token = "" + self._webhook = "" + self._mode = None + + def from_(self, username, icon=None, url=None): + """Set a custom username and optional emoji icon for the Slack message.""" + self._username = username + if icon: + self._icon_emoji = icon + elif url: + self._icon_url = url + return self + + def to(self, to): + """Specifies the channel to send the message to. It can be a list or single + element. It can be either a channel ID or a channel name (with #), if it's + a channel name the channel ID will be resolved later. + """ + self._to = to + return self + + def text(self, text): + """Specifies the text to be sent in the message. + + Arguments: + text {string} -- The text to show in the message. + + Returns: + self + """ + self._text = text + return self + + def link_names(self): + """Find and link channel names and usernames in message.""" + self._link_names = True + return self + + def unfurl_links(self): + """Whether the message should unfurl any links. + + Unfurling is when it shows a bigger part of the message after the text is sent + like when pasting a link and it showing the header images. + + Returns: + self + """ + self._unfurl_links = True + self._unfurl_media = True + return self + + def without_markdown(self): + """Specifies whether the message should explicitly not honor markdown text. + + Returns: + self + """ + self._mrkdwn = False + return self + + def can_reply(self): + """Whether the message should be ably to be replied back to. + + Returns: + self + """ + self._reply_broadcast = True + return self + + def build(self, *args, **kwargs): + return self + + def get_options(self): + options = { + "text": self._text, + # optional + "link_names": self._link_names, + "unfurl_links": self._unfurl_links, + "unfurl_media": self._unfurl_media, + "username": self._username, + "as_user": self._as_current_user, + "icon_emoji": self._icon_emoji, + "icon_url": self._icon_url, + "mrkdwn": self._mrkdwn, + "reply_broadcast": self._reply_broadcast, + "blocks": json.dumps([block._resolve() for block in self._blocks]), + } + if self._mode == self.API_MODE: + options.update({"channel": self._to, "token": self._token}) + return options + + def token(self, token): + """[API_MODE only] Specifies the token to use for Slack authentication. + + Arguments: + token {string} -- The Slack authentication token. + + Returns: + self + """ + self._token = token + return self + + def as_current_user(self): + """[API_MODE only] Send message as the currently authenticated user. + + Returns: + self + """ + self._as_current_user = True + return self + + def webhook(self, webhook): + """[WEBHOOK_MODE only] Specifies the webhook to use to send the message and authenticate + to Slack. + + Arguments: + webhook {string} -- Slack configured webhook url. + + Returns: + self + """ + self._webhook = webhook + return self + + def block(self, block_instance): + try: + from slackblocks.blocks import Block + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'slackblocks' library. Run 'pip install slackblocks' to fix this." + ) + + if not isinstance(block_instance, Block): + raise Exception("Blocks should be imported from 'slackblocks' package.") + self._blocks.append(block_instance) + return self + + def mode(self, mode): + self._mode = mode + return self diff --git a/src/masonite/notification/Sms.py b/src/masonite/notification/Sms.py new file mode 100644 index 000000000..271098b77 --- /dev/null +++ b/src/masonite/notification/Sms.py @@ -0,0 +1,54 @@ +"""Sms Component""" + + +class Sms: + + _from = "" + _to = "" + _text = "" + _client_ref = "" + _type = "text" + + def __init__(self, text=""): + self._text = text + + def from_(self, number): + """Set the name or number the message should be sent from. Numbers should + be specified in E.164 format. Details can be found here: + https://developer.nexmo.com/messaging/sms/guides/custom-sender-id""" + self._from = number + return self + + def text(self, text): + self._text = text + return self + + def to(self, to): + self._to = to + return self + + def set_unicode(self): + """Set message as unicode to handle unicode characters in text.""" + self._type = "unicode" + return self + + def client_ref(self, client_ref): + """Set your own client reference (up to 40 characters).""" + if len(client_ref) > 40: + raise ValueError("client_ref should have less then 40 characters.") + self._client_ref = client_ref + return self + + def build(self, *args, **kwargs): + return self + + def get_options(self): + base_dict = { + "to": self._to, + "from": self._from, + "text": self._text, + "type": self._type, + } + if self._client_ref: + base_dict.update({"client-ref": self._client_ref}) + return base_dict diff --git a/src/masonite/notification/Textable.py b/src/masonite/notification/Textable.py new file mode 100644 index 000000000..a1fa53726 --- /dev/null +++ b/src/masonite/notification/Textable.py @@ -0,0 +1,6 @@ +from .Sms import Sms + + +class Textable: + def text_message(self, message): + return Sms().text(message) diff --git a/src/masonite/notification/__init__.py b/src/masonite/notification/__init__.py new file mode 100644 index 000000000..0db22d80c --- /dev/null +++ b/src/masonite/notification/__init__.py @@ -0,0 +1,9 @@ +from .NotificationManager import NotificationManager +from .MockNotification import MockNotification +from .DatabaseNotification import DatabaseNotification +from .Notification import Notification +from .Notifiable import Notifiable +from .AnonymousNotifiable import AnonymousNotifiable +from .Sms import Sms +from .SlackMessage import SlackMessage +from .Textable import Textable diff --git a/src/masonite/notification/commands/MakeNotificationCommand.py b/src/masonite/notification/commands/MakeNotificationCommand.py new file mode 100644 index 000000000..0cb20ec25 --- /dev/null +++ b/src/masonite/notification/commands/MakeNotificationCommand.py @@ -0,0 +1,42 @@ +"""New Notification Command""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import get_module_dir, make_directory, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeNotificationCommand(Command): + """ + Creates a new notification class. + + notification + {name : Name of the notification} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_stub_notification_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("notifications.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Notification Created ({relative_filename})") + + def get_stub_notification_path(self): + return os.path.join( + get_module_dir(__file__), "../../stubs/notification/Notification.py" + ) diff --git a/src/masonite/notification/commands/NotificationTableCommand.py b/src/masonite/notification/commands/NotificationTableCommand.py new file mode 100644 index 000000000..ee9a24763 --- /dev/null +++ b/src/masonite/notification/commands/NotificationTableCommand.py @@ -0,0 +1,37 @@ +"""Notification Table Command.""" +from cleo import Command +import os + +from ...utils.filesystem import get_module_dir, make_directory +from ...utils.time import migration_timestamp +from ...utils.location import base_path + + +class NotificationTableCommand(Command): + """ + Creates the notifications table needed for storing notifications in the database. + + notification:table + {--d|--directory=database/migrations : Specifies the directory to create the migration in} + """ + + def handle(self): + with open( + os.path.join( + get_module_dir(__file__), + "../../stubs/notification/create_notifications_table.py", + ) + ) as fp: + output = fp.read() + + relative_filename = os.path.join( + self.option("directory"), + f"{migration_timestamp()}_create_notifications_table.py", + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as fp: + fp.write(output) + + self.info(f"Migration file created: {relative_filename}") diff --git a/src/masonite/notification/commands/__init__.py b/src/masonite/notification/commands/__init__.py new file mode 100644 index 000000000..7ea3fdcbc --- /dev/null +++ b/src/masonite/notification/commands/__init__.py @@ -0,0 +1,2 @@ +from .MakeNotificationCommand import MakeNotificationCommand +from .NotificationTableCommand import NotificationTableCommand diff --git a/src/masonite/notification/drivers/BaseDriver.py b/src/masonite/notification/drivers/BaseDriver.py new file mode 100644 index 000000000..04b412488 --- /dev/null +++ b/src/masonite/notification/drivers/BaseDriver.py @@ -0,0 +1,19 @@ +class BaseDriver: + def send(self, notifiable, notification): + """Implements sending the notification to notifiables through + this driver.""" + raise NotImplementedError( + "send() method must be implemented for a notification driver." + ) + + def get_data(self, driver, notifiable, notification): + """Get the data for the notification.""" + method_name = f"to_{driver}" + try: + method = getattr(notification, method_name) + except AttributeError: + raise NotImplementedError( + f"Notification model should implement {method_name}() method." + ) + else: + return method(notifiable) diff --git a/src/masonite/notification/drivers/BroadcastDriver.py b/src/masonite/notification/drivers/BroadcastDriver.py new file mode 100644 index 000000000..cdda012c0 --- /dev/null +++ b/src/masonite/notification/drivers/BroadcastDriver.py @@ -0,0 +1,22 @@ +"""Broadcast notification driver.""" + +from .BaseDriver import BaseDriver + + +class BroadcastDriver(BaseDriver): + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def send(self, notifiable, notification): + """Used to broadcast a notification.""" + data = self.get_data("broadcast", notifiable, notification) + channels = notification.broadcast_on() or notifiable.route_notification_for( + "broadcast" + ) + event = notification.type() + self.application.make("broadcast").channel(channels, event, data) diff --git a/src/masonite/notification/drivers/DatabaseDriver.py b/src/masonite/notification/drivers/DatabaseDriver.py new file mode 100644 index 000000000..146c58d55 --- /dev/null +++ b/src/masonite/notification/drivers/DatabaseDriver.py @@ -0,0 +1,37 @@ +"""Database notification driver.""" +import json + +from .BaseDriver import BaseDriver + + +class DatabaseDriver(BaseDriver): + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_builder(self): + return ( + self.application.make("builder") + .on(self.options.get("connection")) + .table(self.options.get("table")) + ) + + def send(self, notifiable, notification): + """Used to send the email and run the logic for sending emails.""" + data = self.build(notifiable, notification) + return self.get_builder().new().create(data) + + def build(self, notifiable, notification): + """Build an array payload for the DatabaseNotification Model.""" + return { + "id": str(notification.id), + "type": notification.type(), + "notifiable_id": notifiable.id, + "notifiable_type": notifiable.get_table_name(), + "data": json.dumps(self.get_data("database", notifiable, notification)), + "read_at": None, + } diff --git a/src/masonite/notification/drivers/MailDriver.py b/src/masonite/notification/drivers/MailDriver.py new file mode 100644 index 000000000..a620f6cfb --- /dev/null +++ b/src/masonite/notification/drivers/MailDriver.py @@ -0,0 +1,22 @@ +"""Mail notification driver.""" + +from .BaseDriver import BaseDriver + + +class MailDriver(BaseDriver): + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def send(self, notifiable, notification): + """Used to send the email.""" + mailable = self.get_data("mail", notifiable, notification) + if not mailable._to: + recipients = notifiable.route_notification_for("mail") + mailable = mailable.to(recipients) + # TODO: allow changing driver how ????? + return self.application.make("mail").mailable(mailable).send(driver="terminal") diff --git a/src/masonite/notification/drivers/SlackDriver.py b/src/masonite/notification/drivers/SlackDriver.py new file mode 100644 index 000000000..2d297419f --- /dev/null +++ b/src/masonite/notification/drivers/SlackDriver.py @@ -0,0 +1,110 @@ +"""Slack notification driver""" +import requests + +from ...exceptions import NotificationException +from .BaseDriver import BaseDriver + + +class SlackDriver(BaseDriver): + + WEBHOOK_MODE = 1 + API_MODE = 2 + send_url = "https://slack.com/api/chat.postMessage" + channel_url = "https://slack.com/api/conversations.list" + + def __init__(self, application): + self.application = application + self.options = {} + self.mode = self.WEBHOOK_MODE + + def set_options(self, options): + self.options = options + return self + + def send(self, notifiable, notification): + """Used to send the notification to slack.""" + slack_message = self.build(notifiable, notification) + if slack_message._mode == self.WEBHOOK_MODE: + self.send_via_webhook(slack_message) + else: + self.send_via_api(slack_message) + + def build(self, notifiable, notification): + """Build Slack message payload sent to Slack API or through Slack webhook.""" + slack_message = self.get_data("slack", notifiable, notification) + recipients = self.get_recipients(notifiable) + mode = self.get_sending_mode(recipients) + slack_message = slack_message.mode(mode) + + if mode == self.WEBHOOK_MODE: + slack_message = slack_message.to(recipients) + elif mode == self.API_MODE: + slack_message = slack_message.to(recipients) + if not slack_message._token: + slack_message = slack_message.token(self.options.get("token")) + return slack_message + + def get_recipients(self, notifiable): + recipients = notifiable.route_notification_for("slack") + if not isinstance(recipients, (list, tuple)): + recipients = [recipients] + return recipients + + def get_sending_mode(self, recipients): + modes = [] + for recipient in recipients: + if recipient.startswith("https://hooks.slack.com"): + modes.append(self.WEBHOOK_MODE) + else: + modes.append(self.API_MODE) + if len(set(modes)) > 1: + raise NotificationException("Slack sending mode cannot be mixed.") + return modes[0] + + def send_via_webhook(self, slack_message): + webhook_urls = slack_message._to + payload = slack_message.build().get_options() + for webhook_url in webhook_urls: + response = requests.post( + webhook_url, + payload, + headers={"Content-Type": "application/json"}, + ) + if response.status_code != 200: + raise NotificationException( + "{}. Check Slack webhooks docs.".format(response.text) + ) + + def send_via_api(self, slack_message): + """Send Slack notification with Slack Web API as documented + here https://api.slack.com/methods/chat.postMessage""" + channels = slack_message._to + for channel in channels: + channel = self.convert_channel(channel, slack_message._token) + # set only one recipient at a time + slack_message.to(channel) + payload = slack_message.build().get_options() + response = requests.post(self.send_url, payload).json() + if not response["ok"]: + raise NotificationException( + "{}. Check Slack API docs.".format(response["error"]) + ) + else: + return response + + def convert_channel(self, name, token): + """Calls the Slack API to find the channel ID if not already a channel ID. + + Arguments: + name {string} -- The channel name to find. + """ + if "#" not in name: + return name + response = requests.post(self.channel_url, {"token": token}).json() + for channel in response["channels"]: + if channel["name"] == name.split("#")[1]: + return channel["id"] + + raise NotificationException( + f"The user or channel being addressed either do not exist or is invalid: {name}" + ) diff --git a/src/masonite/notification/drivers/__init__.py b/src/masonite/notification/drivers/__init__.py new file mode 100644 index 000000000..04c637c26 --- /dev/null +++ b/src/masonite/notification/drivers/__init__.py @@ -0,0 +1,5 @@ +from .BroadcastDriver import BroadcastDriver +from .DatabaseDriver import DatabaseDriver +from .MailDriver import MailDriver +from .SlackDriver import SlackDriver +from .vonage.VonageDriver import VonageDriver diff --git a/src/masonite/notification/drivers/vonage/VonageDriver.py b/src/masonite/notification/drivers/vonage/VonageDriver.py new file mode 100644 index 000000000..f2ac770fa --- /dev/null +++ b/src/masonite/notification/drivers/vonage/VonageDriver.py @@ -0,0 +1,69 @@ +"""Vonage notification driver.""" +from ....exceptions import NotificationException +from ..BaseDriver import BaseDriver + + +class VonageDriver(BaseDriver): + def __init__(self, application): + self.app = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def build(self, notifiable, notification): + """Build SMS payload sent to Vonage API.""" + sms = self.get_data("vonage", notifiable, notification) + if not sms._from: + sms = sms.from_(self.options.get("sms_from")) + if not sms._to: + recipients = notifiable.route_notification_for("vonage") + sms = sms.to(recipients) + return sms + + def get_sms_client(self): + try: + import vonage + from vonage.sms import Sms + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'vonage' library. Run 'pip install vonage' to fix this." + ) + client = vonage.Client( + key=self.options.get("key"), secret=self.options.get("secret") + ) + return Sms(client) + + def send(self, notifiable, notification): + """Used to send the SMS.""" + sms = self.build(notifiable, notification) + client = self.get_sms_client() + recipients = sms._to + for recipient in recipients: + payload = sms.to(recipient).build().get_options() + response = client.send_message(payload) + self._handle_errors(response) + return response + + def _handle_errors(self, response): + """Handle errors of Vonage API. Raises VonageAPIError if request does + not succeed. + + An error message is structured as follows: + {'message-count': '1', 'messages': [{'status': '2', 'error-text': 'Missing api_key'}]} + As a success message can be structured as follows: + {'message-count': '1', 'messages': [{'to': '3365231278', 'message-id': '140000012BD37332', 'status': '0', + 'remaining-balance': '1.87440000', 'message-price': '0.06280000', 'network': '20810'}]} + + More informations on status code errors: https://developer.nexmo.com/api-errors/sms + + """ + for message in response.get("messages", []): + status = message["status"] + if status != "0": + raise NotificationException( + "Vonage Code [{0}]: {1}. Please refer to API documentation for more details.".format( + status, message["error-text"] + ) + ) diff --git a/src/masonite/notification/providers/NotificationProvider.py b/src/masonite/notification/providers/NotificationProvider.py new file mode 100644 index 000000000..0a740e7c3 --- /dev/null +++ b/src/masonite/notification/providers/NotificationProvider.py @@ -0,0 +1,41 @@ +from ...providers import Provider +from ...utils.structures import load +from ..drivers import ( + BroadcastDriver, + DatabaseDriver, + MailDriver, + SlackDriver, + VonageDriver, +) +from ...configuration import config + +from ..NotificationManager import NotificationManager +from ..MockNotification import MockNotification +from ..commands import MakeNotificationCommand, NotificationTableCommand + + +class NotificationProvider(Provider): + """Notifications Provider""" + + def __init__(self, application): + self.application = application + + def register(self): + notification_manager = NotificationManager(self.application).set_configuration( + config("notification") + ) + notification_manager.add_driver("mail", MailDriver(self.application)) + notification_manager.add_driver("vonage", VonageDriver(self.application)) + notification_manager.add_driver("slack", SlackDriver(self.application)) + notification_manager.add_driver("database", DatabaseDriver(self.application)) + notification_manager.add_driver("broadcast", BroadcastDriver(self.application)) + + self.application.bind("notification", notification_manager) + self.application.bind("mock.notification", MockNotification) + self.application.make("commands").add( + MakeNotificationCommand(self.application), + NotificationTableCommand(), + ) + + def boot(self): + pass diff --git a/src/masonite/notification/providers/__init__.py b/src/masonite/notification/providers/__init__.py new file mode 100644 index 000000000..acd9c98e9 --- /dev/null +++ b/src/masonite/notification/providers/__init__.py @@ -0,0 +1 @@ +from .NotificationProvider import NotificationProvider diff --git a/src/masonite/packages.py b/src/masonite/packages.py deleted file mode 100644 index 510ad6a46..000000000 --- a/src/masonite/packages.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Third party package integrations.""" -import os -import shutil -import sys - - -def create_or_append_config(location, name=False): - if name: - file_name = name - else: - file_name = os.path.basename(location) - - # import it into the config directory - config_directory = os.path.join(os.getcwd(), "config") - - # if file does not exist - if not os.path.isfile(config_directory + "/" + file_name): - shutil.copyfile(location, config_directory + "/" + file_name) - print("\033[92mConfiguration File Created!\033[0m") - else: - # Append to the file - with open(config_directory + "/" + file_name, "a") as project_config, open( - location, "r" - ) as package_config: - project_config.write(package_config.read()) - - print("\033[92mConfiguration File Appended!\033[0m") - - -def append_web_routes(location): - # import it into the web.py file - routes_file = os.path.join(os.getcwd(), "routes/web.py") - - with open(routes_file, "a") as project_routes, open( - location, "r" - ) as package_routes: - project_routes.write(package_routes.read()) - - print("\033[92mroutes/web.py File Appended!\033[0m") - - -def append_file(from_location, to_location): - with open(from_location, "r") as from_file_pointer, open( - os.path.join(os.getcwd(), to_location), "a" - ) as to_file_pointer: - to_file_pointer.write(from_file_pointer.read()) - - print("\033[92m {} has been appended! \033[0m".format(to_location)) - - -def append_api_routes(location): - # import it into the web.py file - api_file = os.path.join(os.getcwd(), "routes/api.py") - - # Append to the file - with open(api_file, "a") as project_routes, open(location, "r") as package_routes: - project_routes.write(package_routes.read()) - - print("\033[92mroutes/api.py File Appended!\033[0m") - - -def create_controller(location, to="app/http/controllers"): - file_name = os.path.basename(location) - - controller_directory = os.path.join(os.getcwd(), to) - controller_file = os.path.join(controller_directory, file_name) - if not os.path.exists(controller_directory): - # Create the path to the model if it does not exist - os.makedirs(controller_directory) - - if os.path.isfile(controller_file): - # if file does exist - print("\033[91m{0} Controller Already Exists!\033[0m".format(file_name)) - else: - # copy controller over - shutil.copyfile(location, controller_file) - - print("\033[92m{0} Controller Created\033[0m".format(file_name)) - - -def add_venv_site_packages(): - try: - from config import packages - - # Add additional site packages to vendor if they exist - for directory in packages.SITE_PACKAGES: - path = os.path.join(os.getcwd(), directory) - sys.path.append(path) - except ImportError: - raise ImportError - - if "VIRTUAL_ENV" in os.environ: - python_version = None - venv_directory = os.listdir(os.path.join(os.environ["VIRTUAL_ENV"], "lib")) - - for directory in venv_directory: - if directory.startswith("python"): - python_version = directory - break - - if python_version: - site_packages_directory = os.path.join( - os.environ["VIRTUAL_ENV"], "lib", python_version, "site-packages" - ) - - sys.path.append(site_packages_directory) - else: - print( - "\033[93mWARNING: Could not add the virtual environment you are currently in. Attempting to add: {0}\033[93m".format( - os.environ["VIRTUAL_ENV"] - ) - ) - - -class PackageContainer: - def create(self): - from masonite.app import App - from config import providers - - container = App() - - container.bind("Container", container) - - container.bind("ProvidersConfig", providers) - container.bind("Providers", []) - container.bind("WSGIProviders", []) - - for provider in container.make("ProvidersConfig").PROVIDERS: - located_provider = provider() - located_provider.load_app(container).register() - if located_provider.wsgi: - container.make("WSGIProviders").append(located_provider) - else: - container.make("Providers").append(located_provider) - - for provider in container.make("Providers"): - container.resolve(provider.boot) - - return container diff --git a/src/masonite/packages/Package.py b/src/masonite/packages/Package.py new file mode 100644 index 000000000..51b6b4f8b --- /dev/null +++ b/src/masonite/packages/Package.py @@ -0,0 +1,46 @@ +import os + + +class Package: + def __init__(self): + self.root_dir = "" + self.name = "" + self.config = "" + self.commands = [] + self.views = [] + self.migrations = [] + self.controller_locations = [] + self.routes = [] + self.assets = [] + + def _build_path(self, rel_path): + return os.path.join(self.root_dir, rel_path) + + def add_config(self, config_path): + self.config = self._build_path(config_path) + return self + + def add_views(self, *locations): + for location in locations: + self.views.append(self._build_path(location)) + return self + + def add_migrations(self, *migrations): + for migration in migrations: + self.migrations.append(self._build_path(migration)) + return self + + def add_routes(self, *routes): + for route in routes: + self.routes.append(self._build_path(route)) + return self + + def add_assets(self, *assets): + for asset in assets: + self.assets.append(self._build_path(asset)) + return self + + def add_controller_locations(self, *controller_locations): + for loc in controller_locations: + self.controller_locations.append(self._build_path(loc)) + return self diff --git a/src/masonite/packages/PublishableResource.py b/src/masonite/packages/PublishableResource.py new file mode 100644 index 000000000..a4704e590 --- /dev/null +++ b/src/masonite/packages/PublishableResource.py @@ -0,0 +1,14 @@ +class PublishableResource: + def __init__(self, key): + self.key = key + self.files = [] + + def add(self, source, destination): + self.files.append((source, destination)) + return self + + # def add(self, *resources): + # for source, destination in resources: + # self.sources.append(source) + # self.destinations.append(destination) + # return self diff --git a/src/masonite/packages/__init__.py b/src/masonite/packages/__init__.py new file mode 100644 index 000000000..b3eeddb56 --- /dev/null +++ b/src/masonite/packages/__init__.py @@ -0,0 +1 @@ +from .providers import PackageProvider diff --git a/src/masonite/packages/providers/PackageProvider.py b/src/masonite/packages/providers/PackageProvider.py new file mode 100644 index 000000000..6724bd07b --- /dev/null +++ b/src/masonite/packages/providers/PackageProvider.py @@ -0,0 +1,176 @@ +import os +from collections import defaultdict +from os.path import relpath, join, abspath, basename, isdir, isfile +import shutil +from ...providers.Provider import Provider +from ...exceptions import InvalidPackageName +from ...utils.location import ( + base_path, + config_path, + views_path, + migrations_path, + resources_path, +) +from ...facades import Config +from ...utils.time import migration_timestamp +from ...routes import Route +from ...utils.structures import load + +from ..reserved_names import PACKAGE_RESERVED_NAMES +from ..Package import Package +from ..PublishableResource import PublishableResource + + +class PackageProvider(Provider): + + vendor_prefix = "vendor" + + def __init__(self, application): + self.application = application + # TODO: the default here could be set auto by deciding that its the dirname containing the provider ! + self.package = Package() + self.files = {} + self.default_resources = ["config", "views", "migrations", "assets"] + + def register(self): + self.configure() + + def boot(self): + pass + + # api + def configure(self): + pass + + def publish(self, resources, dry=False): + project_root = base_path() + resources_list = resources or self.default_resources + published_resources = defaultdict(lambda: []) + for resource in resources_list: + resource_files = self.files.get(resource, []) + for source, dest in resource_files: + if not dry: + shutil.copy(source, dest) + published_resources[resource].append(relpath(dest, project_root)) + return published_resources + + def root(self, abs_root_dir): + # TODO ensure abs path here! + self.package.root_dir = abs_root_dir + return self + + def name(self, name): + if name in PACKAGE_RESERVED_NAMES: + raise InvalidPackageName( + f"{name} is a reserved name. Please choose another name for your package." + ) + self.package.name = name + return self + + def vendor_name(self, name): + self.package.vendor_name = name + return self + + def config(self, config_filepath, publish=False): + # TODO: a name must be specified ! + self.package.add_config(config_filepath) + Config.merge_with(self.package.name, self.package.config) + if publish: + resource = PublishableResource("config") + resource.add(self.package.config, config_path(f"{self.package.name}.py")) + self.files.update({resource.key: resource.files}) + return self + + def views(self, *locations, publish=False): + """Register views location in the project. + locations must be a folder containinng the views you want to publish. + """ + self.package.add_views(*locations) + # register views into project + self.application.make("view").add_namespace( + self.package.name, self.package.views[0] + ) + + if publish: + resource = PublishableResource("views") + for location in self.package.views: + # views = get all files in this folder + for dirpath, _, filenames in os.walk(location): + for f in filenames: + resource.add( + abspath(join(dirpath, f)), + views_path( + join( + self.vendor_prefix, + self.package.name, + relpath(dirpath, location), + f, + ) + ), + ) + self.files.update({resource.key: resource.files}) + return self + + def commands(self, *commands): + self.application.make("commands").add(*commands) + return self + + def migrations(self, *migrations): + self.package.add_migrations(*migrations) + resource = PublishableResource("migrations") + for migration in self.package.migrations: + resource.add( + migration, + migrations_path(f"{migration_timestamp()}_{basename(migration)}"), + ) + self.files.update({resource.key: resource.files}) + return self + + def routes(self, *routes): + """Controller locations must have been loaded already !""" + self.package.add_routes(*routes) + for route_group in self.package.routes: + self.application.make("router").add( + Route.group( + load(route_group, "ROUTES", []), + ) + ) + return self + + def controllers(self, *controller_locations): + self.package.add_controller_locations(*controller_locations) + Route.add_controller_locations(*self.package.controller_locations) + return self + + def assets(self, *assets): + self.package.add_assets(*assets) + resource = PublishableResource("assets") + for asset_dir_or_file in self.package.assets: + # views = get all files in this folder + if isdir(asset_dir_or_file): + for dirpath, _, filenames in os.walk(asset_dir_or_file): + for f in filenames: + resource.add( + abspath(join(dirpath, f)), + resources_path( + join( + self.vendor_prefix, + self.package.name, + relpath(dirpath, asset_dir_or_file), + f, + ) + ), + ) + elif isfile(asset_dir_or_file): + resource.add( + abspath(asset_dir_or_file), + resources_path( + join( + self.vendor_prefix, + self.package.name, + asset_dir_or_file, + ) + ), + ) + self.files.update({resource.key: resource.files}) + return self diff --git a/src/masonite/packages/providers/__init__.py b/src/masonite/packages/providers/__init__.py new file mode 100644 index 000000000..a577a006a --- /dev/null +++ b/src/masonite/packages/providers/__init__.py @@ -0,0 +1 @@ +from .PackageProvider import PackageProvider diff --git a/src/masonite/packages/reserved_names.py b/src/masonite/packages/reserved_names.py new file mode 100644 index 000000000..52153d301 --- /dev/null +++ b/src/masonite/packages/reserved_names.py @@ -0,0 +1 @@ +PACKAGE_RESERVED_NAMES = ["application", "auth", "controller", "event", "notification"] diff --git a/src/masonite/pipeline/Pipeline.py b/src/masonite/pipeline/Pipeline.py new file mode 100644 index 000000000..afb2a0091 --- /dev/null +++ b/src/masonite/pipeline/Pipeline.py @@ -0,0 +1,11 @@ +class Pipeline: + def __init__(self, payload, *args): + self.payload = payload + self.args = args + + def through(self, pipe_list, handler="handle"): + passthrough = self.payload + for pipe in pipe_list: + response = getattr(pipe(), handler)(self.payload, *self.args) + if response != passthrough: + break diff --git a/src/masonite/pipeline/__init__.py b/src/masonite/pipeline/__init__.py new file mode 100644 index 000000000..1779011a5 --- /dev/null +++ b/src/masonite/pipeline/__init__.py @@ -0,0 +1 @@ +from .Pipeline import Pipeline diff --git a/app/providers/.gitignore b/src/masonite/pipeline/tasks/MiddlewareTask.py similarity index 100% rename from app/providers/.gitignore rename to src/masonite/pipeline/tasks/MiddlewareTask.py diff --git a/src/masonite/pipeline/tasks/ResponseTask.py b/src/masonite/pipeline/tasks/ResponseTask.py new file mode 100644 index 000000000..e4a873757 --- /dev/null +++ b/src/masonite/pipeline/tasks/ResponseTask.py @@ -0,0 +1,7 @@ +class ResponseTask: + def __init__(self): + pass + + def handle(self, request): + + return request diff --git a/src/masonite/provider.py b/src/masonite/provider.py deleted file mode 100644 index 41139520f..000000000 --- a/src/masonite/provider.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Module for the Service Provider.""" - -from .helpers import random_string -from .helpers.filesystem import copy_migration -from .packages import append_file - - -class ServiceProvider: - """Service provider class. Used as mediator for loading objects or entire features into the container.""" - - wsgi = True - - def __init__(self): - """Service provider constructor.""" - self.app = None - self._publishes = {} - self._publish_tags = {} - - self._publish_migrations = [] - self._publish_migrations_tags = {} - - def boot(self): - """Use to boot things into the container. Typically ran after the register method has been ran.""" - pass - - def register(self): - """Use to register objects into the container.""" - pass - - def load_app(self, app): - """Load the container into the service provider. - - Arguments: - app {masonite.app.App} -- Container object. - - Returns: - self - """ - self.app = app - return self - - def routes(self, routes): - """Add routes to the container. - - Arguments: - routes {list} -- List of routes to add to the container - """ - web_routes = self.app.make("WebRoutes") - web_routes += routes - - def http_middleware(self, middleware): - """Add HTTP middleware to the container. - - Arguments: - middleware {list} -- List of middleware to add - """ - http_middleware = self.app.make("HttpMiddleware") - http_middleware += middleware - - def route_middleware(self, middleware): - """Add route middleware to the container. - - Arguments: - middleware {dict} -- A dictionary of route middleware to add - """ - route_middleware = self.app.make("RouteMiddleware") - route_middleware.update(middleware) - - def migrations(self, *directories): - """Add migration directories to the container.""" - for directory in directories: - self.app.bind("{}_MigrationDirectory".format(random_string(4)), directory) - - def commands(self, *commands): - """Add commands to the container. Pass in the commands as arguments.""" - for command in commands: - self.app.bind( - "{}Command".format(command.__class__.__name__.replace("Command", "")), - command, - ) - - def assets(self, assets): - """Add assets to the container. - - Arguments: - assets {dict} -- A dictionary of assets to add - """ - - self.app.make("staticfiles").update(assets) - - def publishes(self, dictionary, tag=None): - self._publishes.update(dictionary) - if tag is not None: - self._publish_tags.update({tag: dictionary}) - - def publishes_migrations(self, migrations, tag=None): - self._publish_migrations += migrations - if tag is not None: - self._publish_migrations_tags.update({tag: migrations}) - - def publish(self, tag=None): - if tag is not None: - publishing_items = self._publish_tags.get(tag) - else: - publishing_items = self._publishes - - for from_location, to_location in publishing_items.items(): - append_file(from_location, to_location) - - def publish_migrations(self, tag=None): - if tag is not None: - publishing_items = self._publish_migrations_tags.get(tag) - else: - publishing_items = self._publish_migrations - - for from_location in publishing_items: - copy_migration(from_location) diff --git a/src/masonite/providers/AppProvider.py b/src/masonite/providers/AppProvider.py deleted file mode 100644 index 986d546ba..000000000 --- a/src/masonite/providers/AppProvider.py +++ /dev/null @@ -1,93 +0,0 @@ -"""An AppProvider Service Provider.""" - -from ..autoload import Autoload -from ..commands import ( - AuthCommand, - CommandCommand, - ControllerCommand, - DownCommand, - InfoCommand, - JobCommand, - KeyCommand, - MailableCommand, - MiddlewareCommand, - ModelCommand, - ModelDocstringCommand, - ProviderCommand, - PublishCommand, - PresetCommand, - QueueTableCommand, - QueueWorkCommand, - RoutesCommand, - ServeCommand, - TestCommand, - TinkerCommand, - UpCommand, - ViewCommand, -) -from ..exception_handler import DumpHandler, ExceptionHandler -from ..helpers import config, load -from ..helpers.routes import flatten_routes -from ..hook import Hook -from ..provider import ServiceProvider -from ..request import Request -from ..response import Response -from ..routes import Route - - -class AppProvider(ServiceProvider): - - wsgi = True - - def register(self): - self.app.bind("HookHandler", Hook(self.app)) - self.app.bind("WebRoutes", flatten_routes(load("routes.web.routes"))) - self.app.bind("Route", Route()) - - self.app.bind("Container", self.app) - - self.app.bind("ExceptionDumpExceptionHandler", DumpHandler) - - self.app.bind("RouteMiddleware", config("middleware.route_middleware")) - self.app.bind("HttpMiddleware", config("middleware.http_middleware")) - self.app.bind("staticfiles", config("storage.staticfiles", {})) - - # Insert Commands - self._load_commands() - - self._autoload(config("application.autoload")) - - def boot(self, route: Route): - self.app.bind("Request", Request(self.app.make("Environ")).load_app(self.app)) - self.app.simple(Response(self.app)) - route.load_environ(self.app.make("Environ")) - self.app.bind("ExceptionHandler", ExceptionHandler(self.app)) - - def _autoload(self, directories): - Autoload(self.app).load(directories) - - def _load_commands(self): - self.commands( - AuthCommand(), - CommandCommand(), - ControllerCommand(), - DownCommand(), - InfoCommand(), - JobCommand(), - KeyCommand(), - MailableCommand(), - MiddlewareCommand(), - ModelCommand(), - ModelDocstringCommand(), - PresetCommand(), - ProviderCommand(), - PublishCommand(), - QueueWorkCommand(), - QueueTableCommand(), - ViewCommand(), - RoutesCommand(), - ServeCommand(), - TestCommand(), - TinkerCommand(), - UpCommand(), - ) diff --git a/src/masonite/providers/AuthenticationProvider.py b/src/masonite/providers/AuthenticationProvider.py index 88b592176..3e5ba0a5e 100644 --- a/src/masonite/providers/AuthenticationProvider.py +++ b/src/masonite/providers/AuthenticationProvider.py @@ -1,20 +1,21 @@ -"""An Authentication Service Provider.""" +from ..foundation import response_handler +from ..request import Request +from ..response import Response +from ..authentication import Auth +from ..authentication.guards import WebGuard +from ..configuration import config +from .Provider import Provider -from ..auth.guards import Guard, WebGuard -from ..auth import Auth -from ..helpers import config -from ..provider import ServiceProvider - -class AuthenticationProvider(ServiceProvider): - - wsgi = False +class AuthenticationProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - guard = Guard(self.app) - guard.register_guard("web", WebGuard) - self.app.simple(guard) - self.app.swap(Auth, guard) + auth = Auth(self.application).set_configuration(config("auth.guards")) + auth.add_guard("web", WebGuard(self.application)) + + self.application.bind("auth", auth) - def boot(self, auth: Auth): - auth.set(config("auth.auth.defaults.guard")) + def boot(self): + pass diff --git a/src/masonite/providers/AuthorizationProvider.py b/src/masonite/providers/AuthorizationProvider.py new file mode 100644 index 000000000..dde3a101e --- /dev/null +++ b/src/masonite/providers/AuthorizationProvider.py @@ -0,0 +1,13 @@ +from ..authorization import Gate +from .Provider import Provider + + +class AuthorizationProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + self.application.bind("gate", Gate(self.application)) + + def boot(self): + pass diff --git a/src/masonite/providers/BroadcastProvider.py b/src/masonite/providers/BroadcastProvider.py deleted file mode 100644 index fa6a64128..000000000 --- a/src/masonite/providers/BroadcastProvider.py +++ /dev/null @@ -1,28 +0,0 @@ -"""A RedirectionProvider Service Provider.""" - -from ..drivers import BroadcastAblyDriver, BroadcastPusherDriver, BroadcastPubNubDriver -from ..managers import BroadcastManager -from ..provider import ServiceProvider -from .. import Broadcast -from ..helpers import config - - -class BroadcastProvider(ServiceProvider): - - wsgi = False - - def register(self): - self.app.bind("BroadcastPusherDriver", BroadcastPusherDriver) - self.app.bind("BroadcastAblyDriver", BroadcastAblyDriver) - self.app.bind("BroadcastPubnubDriver", BroadcastPubNubDriver) - self.app.bind("BroadcastManager", BroadcastManager(self.app)) - - def boot(self): - self.app.bind( - "Broadcast", - self.app.make("BroadcastManager").driver(config("broadcast.driver")), - ) - self.app.swap( - Broadcast, - self.app.make("BroadcastManager").driver(config("broadcast.driver")), - ) diff --git a/src/masonite/providers/CacheProvider.py b/src/masonite/providers/CacheProvider.py index 8b1067911..7de4ce9e4 100644 --- a/src/masonite/providers/CacheProvider.py +++ b/src/masonite/providers/CacheProvider.py @@ -1,23 +1,19 @@ -"""A Cache Service Provider.""" +from .Provider import Provider +from ..cache import Cache +from ..cache.drivers import FileDriver, RedisDriver, MemcacheDriver +from ..configuration import config -from .. import Cache -from ..drivers import CacheDiskDriver, CacheRedisDriver -from ..managers import CacheManager -from ..provider import ServiceProvider -from ..helpers import config - -class CacheProvider(ServiceProvider): - - wsgi = False +class CacheProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - # from config import cache - # self.app.bind('CacheConfig', cache) - self.app.bind("CacheDiskDriver", CacheDiskDriver) - self.app.bind("CacheRedisDriver", CacheRedisDriver) - self.app.bind("CacheManager", CacheManager(self.app)) + cache = Cache(self.application).set_configuration(config("cache.stores")) + cache.add_driver("file", FileDriver(self.application)) + cache.add_driver("redis", RedisDriver(self.application)) + cache.add_driver("memcache", MemcacheDriver(self.application)) + self.application.bind("cache", cache) - def boot(self, cache: CacheManager): - self.app.bind("Cache", cache.driver(config("cache").DRIVER)) - self.app.swap(Cache, cache.driver(config("cache").DRIVER)) + def boot(self): + pass diff --git a/src/masonite/providers/CorsProvider.py b/src/masonite/providers/CorsProvider.py deleted file mode 100644 index d4c8c351d..000000000 --- a/src/masonite/providers/CorsProvider.py +++ /dev/null @@ -1,22 +0,0 @@ -from ..provider import ServiceProvider -from ..request import Request -from ..response import Response -from ..helpers import config - - -class CorsProvider(ServiceProvider): - """Provides Services To The Service Container.""" - - wsgi = True - - def register(self): - """Register objects into the Service Container.""" - pass - - def boot(self, request: Request, response: Response): - """Boots services required by the container.""" - headers = config("middleware.cors") or {} - response.header(headers) - - if request.get_request_method().lower() == "options": - response.view("preflight") diff --git a/src/masonite/providers/CsrfProvider.py b/src/masonite/providers/CsrfProvider.py deleted file mode 100644 index 029fce8ae..000000000 --- a/src/masonite/providers/CsrfProvider.py +++ /dev/null @@ -1,16 +0,0 @@ -"""A Csrf Service Provider.""" - -from ..auth import Csrf -from ..provider import ServiceProvider - - -class CsrfProvider(ServiceProvider): - - wsgi = True - - def register(self): - pass - - def boot(self): - self.app.bind("Csrf", Csrf(self.app.make("Request"))) - pass diff --git a/src/masonite/providers/ExceptionProvider.py b/src/masonite/providers/ExceptionProvider.py new file mode 100644 index 000000000..b977a1d33 --- /dev/null +++ b/src/masonite/providers/ExceptionProvider.py @@ -0,0 +1,21 @@ +import builtins + +from .Provider import Provider +from ..exceptions import ExceptionHandler, DumpExceptionHandler, DD +from ..configuration import config + + +class ExceptionProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + handler = ExceptionHandler(self.application).set_options(config("exceptions")) + builtins.dd = DD(self.application).dump + self.application.bind("exception_handler", handler) + self.application.bind( + "DumpExceptionHandler", DumpExceptionHandler(self.application) + ) + + def boot(self): + pass diff --git a/src/masonite/providers/FrameworkProvider.py b/src/masonite/providers/FrameworkProvider.py new file mode 100644 index 000000000..723b9bb32 --- /dev/null +++ b/src/masonite/providers/FrameworkProvider.py @@ -0,0 +1,17 @@ +from ..foundation import response_handler +from ..request import Request +from ..response import Response + + +class FrameworkProvider: + def __init__(self, application): + self.application = application + + def register(self): + pass + + def boot(self): + request = Request(self.application.make("environ")) + request.app = self.application + self.application.bind("request", request) + self.application.bind("response", Response(self.application)) diff --git a/src/masonite/providers/HashServiceProvider.py b/src/masonite/providers/HashServiceProvider.py new file mode 100644 index 000000000..b575b3b08 --- /dev/null +++ b/src/masonite/providers/HashServiceProvider.py @@ -0,0 +1,20 @@ +from ..hashing import Hash +from ..hashing.drivers import BcryptHasher, Argon2Hasher +from .Provider import Provider +from ..configuration import config + + +class HashServiceProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + hashing = Hash(self.application).set_configuration( + config("application.hashing") + ) + hashing.add_driver("bcrypt", BcryptHasher()) + hashing.add_driver("argon2", Argon2Hasher()) + self.application.bind("hash", hashing) + + def boot(self): + pass diff --git a/src/masonite/providers/HelpersProvider.py b/src/masonite/providers/HelpersProvider.py index ee544514d..fed521133 100644 --- a/src/masonite/providers/HelpersProvider.py +++ b/src/masonite/providers/HelpersProvider.py @@ -1,55 +1,39 @@ -"""A Helpers Service Provider.""" - import builtins -import os - -from ..exception_handler import DD -from ..helpers.view_helpers import back, set_request_method, hidden, old -from ..helpers.sign import sign, unsign, decrypt, encrypt -from ..helpers import config, optional -from ..provider import ServiceProvider -from ..view import View -from ..request import Request -from ..managers import MailManager +from markupsafe import Markup +from ..providers import Provider +from ..configuration import config +from ..helpers import UrlsHelper, MixHelper -class HelpersProvider(ServiceProvider): - wsgi = False +class HelpersProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - pass + builtins.resolve = self.application.resolve + builtins.container = lambda: self.application + self.application.bind("url", UrlsHelper(self.application)) - def boot(self, view: View): - """Add helper functions to Masonite.""" - builtins.view = view.render - # builtins.auth = request.user - builtins.container = self.app.helper - builtins.env = os.getenv - builtins.resolve = self.app.resolve - # builtins.route = request.route - if self.app.has(MailManager): - builtins.mail_helper = self.app.make(MailManager).helper - builtins.dd = DD(self.app).dump + def boot(self): + request = self.application.make("request") + urls_helper = self.application.make("url") - view.share( + self.application.make("view").share( { - # "request": request.helper, - # "auth": request.user, - "request_method": set_request_method, - # "route": request.route, - "back": back, - "sign": sign, - "unsign": unsign, - "decrypt": decrypt, - "encrypt": encrypt, + "request": lambda: request, + "session": lambda: request.app.make("session"), + "auth": request.user, + "cookie": request.cookie, + "back": lambda url=request.get_path(): ( + Markup(f"") + ), + "asset": urls_helper.asset, + "url": urls_helper.url, + "mix": MixHelper(self.application).url, + "route": urls_helper.route, "config": config, - "optional": optional, - "dd": builtins.dd, - "hidden": hidden, - "exists": view.exists, - # "cookie": request.get_cookie, - "url": lambda name, params={}: request.route(name, params, full=True), - "old": old, + "can": self.application.make("gate").allows, + "cannot": self.application.make("gate").denies, } ) diff --git a/src/masonite/providers/MailProvider.py b/src/masonite/providers/MailProvider.py index d6fa80fb6..319754788 100644 --- a/src/masonite/providers/MailProvider.py +++ b/src/masonite/providers/MailProvider.py @@ -1,28 +1,22 @@ -"""A Mail Service Provider.""" +from .Provider import Provider +from ..mail import Mail +from ..mail.drivers import SMTPDriver, TerminalDriver, MailgunDriver +from ..utils.structures import load +from ..mail import MockMail +from ..facades import Config -from ..drivers import ( - MailMailgunDriver, - MailSmtpDriver, - MailLogDriver, - MailTerminalDriver, -) -from ..managers import MailManager -from ..provider import ServiceProvider -from .. import Mail -from ..helpers import config - -class MailProvider(ServiceProvider): - - wsgi = False +class MailProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - self.app.bind("MailSmtpDriver", MailSmtpDriver) - self.app.bind("MailMailgunDriver", MailMailgunDriver) - self.app.bind("MailLogDriver", MailLogDriver) - self.app.bind("MailTerminalDriver", MailTerminalDriver) - self.app.bind("MailManager", MailManager(self.app)) + mail = Mail(self.application).set_configuration(Config.get("mail.drivers")) + mail.add_driver("smtp", SMTPDriver(self.application)) + mail.add_driver("mailgun", MailgunDriver(self.application)) + mail.add_driver("terminal", TerminalDriver(self.application)) + self.application.bind("mail", mail) + self.application.bind("mock.mail", MockMail) - def boot(self, manager: MailManager): - self.app.bind("Mail", manager.driver(config("mail.driver"))) - self.app.swap(Mail, manager.driver(config("mail.driver"))) + def boot(self): + pass diff --git a/src/masonite/providers/Provider.py b/src/masonite/providers/Provider.py new file mode 100644 index 000000000..665a0b50b --- /dev/null +++ b/src/masonite/providers/Provider.py @@ -0,0 +1,2 @@ +class Provider: + pass diff --git a/src/masonite/providers/QueueProvider.py b/src/masonite/providers/QueueProvider.py index 0f9c7e509..f30c19c5d 100644 --- a/src/masonite/providers/QueueProvider.py +++ b/src/masonite/providers/QueueProvider.py @@ -1,23 +1,18 @@ -"""A RedirectionProvider Service Provider.""" +from ..drivers.queue import DatabaseDriver, AsyncDriver, AMQPDriver +from ..queues import Queue +from ..configuration import config -from ..drivers import QueueAsyncDriver, QueueAmqpDriver, QueueDatabaseDriver -from ..managers import QueueManager -from ..provider import ServiceProvider -from .. import Queue -from ..helpers import config - - -class QueueProvider(ServiceProvider): - - wsgi = False +class QueueProvider: + def __init__(self, application): + self.application = application def register(self): - self.app.bind("QueueAsyncDriver", QueueAsyncDriver) - self.app.bind("QueueAmqpDriver", QueueAmqpDriver) - self.app.bind("QueueDatabaseDriver", QueueDatabaseDriver) - self.app.bind("QueueManager", QueueManager) + queue = Queue(self.application).set_configuration(config("queue.drivers")) + queue.add_driver("database", DatabaseDriver(self.application)) + queue.add_driver("async", AsyncDriver(self.application)) + queue.add_driver("amqp", AMQPDriver(self.application)) + self.application.bind("queue", queue) - def boot(self, queue: QueueManager): - self.app.bind("Queue", queue.driver(config("queue.driver"))) - self.app.swap(Queue, queue.driver(config("queue.driver"))) + def boot(self): + pass diff --git a/src/masonite/providers/RequestHelpersProviders.py b/src/masonite/providers/RequestHelpersProviders.py deleted file mode 100644 index e49db49df..000000000 --- a/src/masonite/providers/RequestHelpersProviders.py +++ /dev/null @@ -1,28 +0,0 @@ -"""A Helpers Service Provider.""" - -import builtins -import os - -from ..provider import ServiceProvider -from ..view import View -from ..request import Request - - -class RequestHelpersProvider(ServiceProvider): - def register(self): - pass - - def boot(self, view: View, request: Request): - """Add helper functions to Masonite.""" - builtins.auth = request.user - builtins.route = request.route - - view.share( - { - "request": request.helper, - "auth": request.user, - "route": request.route, - "cookie": request.get_cookie, - "url": lambda name, params={}: request.route(name, params, full=True), - } - ) diff --git a/src/masonite/providers/RouteProvider.py b/src/masonite/providers/RouteProvider.py index 2ab036135..364ef6b67 100644 --- a/src/masonite/providers/RouteProvider.py +++ b/src/masonite/providers/RouteProvider.py @@ -1,124 +1,66 @@ -"""A RouteProvider Service Provider.""" +from inspect import isclass -from ..helpers.routes import create_matchurl -from ..provider import ServiceProvider -from ..request import Request from ..response import Response +from ..facades import Response as ResponseFacade +from .Provider import Provider from ..routes import Route +from ..pipeline import Pipeline -class RouteProvider(ServiceProvider): - def register(self): - pass - - def boot(self, router: Route, request: Request, response: Response): - # All routes joined - from config import application - - for route in self.app.make("WebRoutes"): - - """Make a better match for trailing slashes - Sometimes a user will end with a trailing slash. Because the user might - create routes like `/url/route` and `/url/route/` and how the regex - is compiled down, we may need to adjust for urls that end or dont - end with a trailing slash. - """ - - matchurl = create_matchurl(router.url, route) - - """Houston, we've got a match - Check to see if a route matches the corresponding router url. If a match - is found, execute that route and break out of the loop. We only need - one match. Routes are executed on a first come, first serve basis - """ - if ( - matchurl - and matchurl.match(router.url) - and request.get_request_method() in route.method_type - ): - route.load_request(request) +class RouteProvider(Provider): + def __init__(self, application): + self.application = application - """Check if subdomains are active and if the route matches on the subdomain - It needs to match to. - """ - - if request.has_subdomain(): - # Check if the subdomain matches the correct routes domain - if not route.has_required_domain(): - continue - - """Get URL Parameters - This will create a dictionary of parameters given. This is sort of a short - but complex way to retrieve the url parameters. - This is the code used to convert /url/@firstname/@lastname - to {'firstmane': 'joseph', 'lastname': 'mancuso'}. - """ - - try: - parameter_dict = {} - for index, value in enumerate(matchurl.match(router.url).groups()): - parameter_dict[ - route.url_list[index] - ] = value or route.get_default_parameter(route.url_list[index]) - request.set_params(parameter_dict) - except AttributeError: + def register(self): + # Register the routes? + Route.set_controller_locations(self.application.make("controllers.location")) + + def boot(self): + router = self.application.make("router") + request = self.application.make("request") + response = self.application.make("response") + + route = router.find( + request.get_path(), request.get_request_method(), request.get_subdomain() + ) + + # Run before middleware + + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="before", + ) + + exception = None + + if route: + request.load_params(route.extract_parameters(request.get_path())) + self.application.make("middleware").run_route_middleware( + route.list_middleware, request, response, callback="before" + ) + + try: + data = route.get_response(self.application) + if isinstance(data, Response) or ( + isclass(data) and issubclass(data, ResponseFacade) + ): pass + else: + response.view(data) + except Exception as e: + exception = e - """Excute HTTP before middleware - Only those middleware that have a "before" method are ran. - """ - - for http_middleware in self.app.make("HttpMiddleware"): - located_middleware = self.app.resolve(http_middleware) - if hasattr(located_middleware, "before"): - located_middleware.before() + self.application.make("middleware").run_route_middleware( + route.list_middleware, request, response, callback="after" + ) - """Execute Route Before Middleware - This is middleware that contains a before method. - """ - - route.run_middleware("before") - - # Show a helper in the terminal of which route has been visited - if application.DEBUG: - print(request.get_request_method() + " Route: " + router.url) - - # If no routes have been found and no middleware has changed the status code - if not response.get_status_code(): - - """Get the response from the route and set it on the 'Response' key. - This data is typically the output of the controller method depending - on the type of route. - """ - response.view(route.get_response(), status=200) - - """Execute Route After Route Middleware - This is middleware that contains an after method. - """ - - route.run_middleware("after") - - """Excute HTTP after middleware - Only those middleware that have an "after" method are ran. - Check here if the middleware even has the required method. - """ - - for http_middleware in self.app.make("HttpMiddleware"): - located_middleware = self.app.resolve(http_middleware) - - if hasattr(located_middleware, "after"): - located_middleware.after() + else: + response.view("route not found", status=404) - """Return breaks the loop because the incoming route is found and executed. - There is no need to continue searching the route list. First come - first serve around these parts of the woods. - """ - return + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="after", + ) - """No Response was found in the for loop so let's set an arbitrary response now. - """ - # If the route exists but not the method is incorrect - if request.route_exists(request.path): - response.view("Method not allowed", status=405) - else: - response.view("Route not found. Error 404", status=404) + if exception: + raise exception diff --git a/src/masonite/providers/SessionProvider.py b/src/masonite/providers/SessionProvider.py index 341b9342e..0108eac72 100644 --- a/src/masonite/providers/SessionProvider.py +++ b/src/masonite/providers/SessionProvider.py @@ -1,25 +1,22 @@ -"""A RedirectionProvider Service Provider.""" +from .Provider import Provider +from ..sessions import Session +from ..drivers.session import CookieDriver +from ..utils.structures import load +from ..configuration import config -from ..drivers import SessionCookieDriver, SessionMemoryDriver -from ..managers import SessionManager -from ..provider import ServiceProvider -from ..view import View -from ..request import Request -from .. import Session -from ..helpers import config +class SessionProvider(Provider): + def __init__(self, application): + self.application = application -class SessionProvider(ServiceProvider): def register(self): - # from config import session - # self.app.bind('SessionConfig', session) - self.app.bind("SessionMemoryDriver", SessionMemoryDriver) - self.app.bind("SessionCookieDriver", SessionCookieDriver) - self.app.bind("SessionManager", SessionManager(self.app)) + session = Session(self.application).set_configuration(config("session.drivers")) + session.add_driver("cookie", CookieDriver(self.application)) + self.application.bind("session", session) + self.application.make("view").share({"old": self.old}) - def boot(self, request: Request, view: View, session: SessionManager): - self.app.bind("Session", session.driver(config("session").DRIVER)) - self.app.swap(Session, session.driver(config("session").DRIVER)) - request.session = self.app.make("Session") + def boot(self): + pass - view.share({"session": self.app.make("Session").helper}) + def old(self, key): + return self.application.make("session").get(key) or "" diff --git a/src/masonite/providers/StatusCodeProvider.py b/src/masonite/providers/StatusCodeProvider.py deleted file mode 100644 index ae715049b..000000000 --- a/src/masonite/providers/StatusCodeProvider.py +++ /dev/null @@ -1,68 +0,0 @@ -"""A StatusProvider Service Provider.""" - -import json - -from ..helpers import config -from ..provider import ServiceProvider -from ..response import Response - - -class ServerErrorExceptionHook: - def load(self, app): - from config import application - - if application.DEBUG: - return - - response = app.make(Response) - - response.status(500) - if app.make("ViewClass").exists("errors/500"): - rendered_view = app.make("View")("errors/500") - else: - rendered_view = app.make("View")( - config( - "application.templates.statuscode", "/masonite/snippets/statuscode" - ), - {"code": "500 Internal Server Error"}, - ) - - response.view(rendered_view) - - -class StatusCodeProvider(ServiceProvider): - def register(self): - self.app.bind("ServiceErrorExceptionHook", ServerErrorExceptionHook()) - - def boot(self): - request = self.app.make("Request") - response = self.app.make(Response) - if response.is_status(200): - return - - if response.get_status() in (500, 405, 404): - if "application/json" in request.header("Content-Type"): - # Returns json response when we want the client to receive a json response - body = json.loads(self.app.make("Response").decode("utf-8")) - json_response = { - "error": {"status": response.get_status(), "body": body} - } - response.view(json_response, status=response.get_status()) - else: - # Returns html response when json is not explicitly specified - if self.app.make("ViewClass").exists( - "errors/{}".format(response.get_status()) - ): - rendered_view = self.app.make("View")( - "errors/{}".format(response.get_status()) - ) - else: - rendered_view = self.app.make("View")( - config( - "application.templates.statuscode", - "/masonite/snippets/statuscode", - ), - {"code": response.get_status_code()}, - ) - - response.view(rendered_view, status=response.get_status()) diff --git a/src/masonite/providers/UploadProvider.py b/src/masonite/providers/UploadProvider.py deleted file mode 100644 index be8080ae1..000000000 --- a/src/masonite/providers/UploadProvider.py +++ /dev/null @@ -1,26 +0,0 @@ -"""An Upload Service Provider.""" - -from ..drivers import UploadDiskDriver, UploadS3Driver -from ..helpers.static import static -from ..managers import UploadManager -from ..provider import ServiceProvider -from ..view import View -from .. import Upload -from ..helpers import config - - -class UploadProvider(ServiceProvider): - - wsgi = False - - def register(self): - # from config import storage - # self.app.bind('StorageConfig', storage) - self.app.bind("UploadDiskDriver", UploadDiskDriver) - self.app.bind("UploadS3Driver", UploadS3Driver) - self.app.bind("UploadManager", UploadManager(self.app)) - - def boot(self, manager: UploadManager, view: View): - self.app.bind("Upload", manager.driver(config("storage").DRIVER)) - self.app.swap(Upload, manager.driver(config("storage").DRIVER)) - view.share({"static": static}) diff --git a/src/masonite/providers/ViewProvider.py b/src/masonite/providers/ViewProvider.py index a20ef8c61..15d09ef68 100644 --- a/src/masonite/providers/ViewProvider.py +++ b/src/masonite/providers/ViewProvider.py @@ -1,20 +1,16 @@ -"""A View Service Provider.""" +from ..views import View +from .Provider import Provider -from jinja2 import FileSystemLoader -from ..provider import ServiceProvider -from ..view import View - - -class ViewProvider(ServiceProvider): - - wsgi = False +class ViewProvider(Provider): + def __init__(self, app): + self.application = app def register(self): - view = View(self.app) - self.app.bind("ViewClass", view) - self.app.bind("View", view.render) + view = View(self.application) + view.add_location(self.application.make("views.location")) + + self.application.bind("view", view) - def boot(self, view: View): - view.add_environment("src/masonite/snippets", loader=FileSystemLoader) - self.publishes_migrations(["storage/append_from.txt"]) + def boot(self): + pass diff --git a/src/masonite/providers/WhitenoiseProvider.py b/src/masonite/providers/WhitenoiseProvider.py index 985c78e10..92511d97a 100644 --- a/src/masonite/providers/WhitenoiseProvider.py +++ b/src/masonite/providers/WhitenoiseProvider.py @@ -1,30 +1,26 @@ -"""A WhiteNoiseProvider Service Provider.""" - +from .Provider import Provider from whitenoise import WhiteNoise - -from ..provider import ServiceProvider -from ..helpers import config +import os -class WhitenoiseProvider(ServiceProvider): - - wsgi = False +class WhitenoiseProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - pass - - def boot(self): - """Wrap the WSGI server in a whitenoise container.""" - from config import application - self.app.bind( - "WSGI", - WhiteNoise( - self.app.make("WSGI"), - root=config("application.static_root"), - autorefresh=application.DEBUG, - ), + response_handler = WhiteNoise( + self.application.get_response_handler(), + root=self.application.get_storage_path(), + autorefresh=True, ) - for location, alias in self.app.make("staticfiles").items(): - self.app.make("WSGI").add_files(location, prefix=alias) + for location, alias in ( + self.application.make("storage_capsule").get_storage_assets().items() + ): + response_handler.add_files(location, prefix=alias) + + self.application.set_response_handler(response_handler) + + def boot(self): + return diff --git a/src/masonite/providers/__init__.py b/src/masonite/providers/__init__.py index 34caf2d36..3629c3e7d 100644 --- a/src/masonite/providers/__init__.py +++ b/src/masonite/providers/__init__.py @@ -1,16 +1,21 @@ -from .AppProvider import AppProvider -from .AuthenticationProvider import AuthenticationProvider -from .BroadcastProvider import BroadcastProvider -from .CacheProvider import CacheProvider -from .CsrfProvider import CsrfProvider -from .CorsProvider import CorsProvider -from .HelpersProvider import HelpersProvider -from .MailProvider import MailProvider -from .QueueProvider import QueueProvider +from .FrameworkProvider import FrameworkProvider from .RouteProvider import RouteProvider -from .SessionProvider import SessionProvider -from .UploadProvider import UploadProvider from .ViewProvider import ViewProvider -from .RequestHelpersProviders import RequestHelpersProvider from .WhitenoiseProvider import WhitenoiseProvider -from .StatusCodeProvider import StatusCodeProvider +from .ExceptionProvider import ExceptionProvider +from .AuthenticationProvider import AuthenticationProvider +from .AuthorizationProvider import AuthorizationProvider +from .Provider import Provider +from .MailProvider import MailProvider +from .SessionProvider import SessionProvider +from .HelpersProvider import HelpersProvider +from .QueueProvider import QueueProvider +from .CacheProvider import CacheProvider +from ..events.providers import EventProvider +from ..filesystem.providers import StorageProvider +from ..broadcasting.providers import BroadcastProvider +from ..scheduling.providers import ScheduleProvider +from ..essentials.providers.HashIDProvider import HashIDProvider +from .HashServiceProvider import HashServiceProvider +from ..validation.providers import ValidationProvider +from ..configuration.providers import ConfigurationProvider diff --git a/src/masonite/queues/Queue.py b/src/masonite/queues/Queue.py new file mode 100644 index 000000000..3bb42b643 --- /dev/null +++ b/src/masonite/queues/Queue.py @@ -0,0 +1,45 @@ +class Queue: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config.get(self.driver_config.get("default"), {}) + + return self.driver_config.get(driver, {}) + + def push(self, *jobs, **options): + driver = self.get_driver(options.get("driver")) + config_options = self.get_config_options(options.get("driver")) + config_options.update({"queue": options.get("queue", "default")}) + driver.set_options(config_options) + driver.push(*jobs) + + def consume(self, options): + driver = self.get_driver(options.get("driver")) + config_options = self.get_config_options(options.get("driver")) + config_options.update(options) + options.update(self.get_config_options(options.get("driver"))) + return driver.set_options(config_options).consume() + + def retry(self, options): + driver = self.get_driver(options.get("driver")) + config_options = self.get_config_options(options.get("driver")) + config_options.update(options) + options.update(self.get_config_options(options.get("driver"))) + return driver.set_options(config_options).retry() diff --git a/src/masonite/queues/Queueable.py b/src/masonite/queues/Queueable.py index a81c53dd9..040165e8c 100644 --- a/src/masonite/queues/Queueable.py +++ b/src/masonite/queues/Queueable.py @@ -8,16 +8,10 @@ class Queueable: run_times = 3 def handle(self): - """Put the queue logic in this handle method.""" pass - def dispatch(self): - """Responsible for dispatching the job to the Queue service. - - Returns: - self.handle - """ - return self.handle + def failed(self, obj, e): + pass - def should_run(self, job): - return True + def __repr__(self): + return self.__class__.__name__ diff --git a/src/masonite/queues/__init__.py b/src/masonite/queues/__init__.py index 74ed50308..8452f1aa1 100644 --- a/src/masonite/queues/__init__.py +++ b/src/masonite/queues/__init__.py @@ -1,2 +1,3 @@ from .Queueable import Queueable from .ShouldQueue import ShouldQueue +from .Queue import Queue diff --git a/src/masonite/request.py b/src/masonite/request.py deleted file mode 100644 index de6ca4f22..000000000 --- a/src/masonite/request.py +++ /dev/null @@ -1,1156 +0,0 @@ -"""Request Module. - -Request Module handles many different aspects of a single request -Methods which require the request and help ease development should -be put here. - -Methods may return another object if necessary to expand capabilities -of this class. -""" - -import re -import cgi -import json -from cgi import MiniFieldStorage -from http import cookies -from urllib.parse import parse_qs, quote - -import tldextract -from cryptography.fernet import InvalidToken -from .auth.Sign import Sign -from .exceptions import InvalidHTTPStatusCode, RouteException -from .helpers import Dot as DictDot -from .helpers import clean_request_input, dot -from .helpers.Extendable import Extendable -from .helpers.routes import compile_route_to_regex, query_parse -from .helpers.status import response_statuses -from .helpers.time import cookie_expire_time -from .cookies import CookieJar -from .headers import HeaderBag, Header -from .response import Response - - -class Request(Extendable): - """Handles many different aspects of a single request. - - This is the object passed through to the controllers - as a request parameter - - Arguments: - Extendable {masonite.helpers.Extendable.Extendable} -- Makes this class - have the ability to extend another class at runtime. - """ - - def __init__(self, environ=None): - """Request class constructor. - - Initializes several properties and sets various methods - depending on the environtment. - - Keyword Arguments: - environ {dictionary} -- WSGI environ dictionary. (default: {None}) - """ - self.cookie_jar = CookieJar() - self.header_bag = HeaderBag() - self.url_params = {} - self.redirect_url = False - self.redirect_route = False - self.user_model = None - self.subdomain = None - self._activate_subdomains = False - self.request_variables = {} - self._test_user = False - self.raw_input = None - self.query_params = {} - - if environ: - self.load_environ(environ) - - self.encryption_key = False - self.container = None - - def input(self, name, default=False, clean=False, quote=True): - """Get a specific input value. - - Arguments: - name {string} -- Key of the input data - - Keyword Arguments: - default {string} -- Default value if input does not exist (default: {False}) - clean {bool} -- Whether or not the return value should be - cleaned (default: {True}) - - Returns: - string - """ - name = str(name) - - if "." in name and isinstance( - self.request_variables.get(name.split(".")[0]), dict - ): - return clean_request_input( - DictDot().dot(name, self.request_variables, default=default), - clean=clean, - ) - - elif "." in name: - name = dot(name, "{1}[{.}]") - - return clean_request_input( - self.request_variables.get(name, default), clean=clean, quote=quote - ) - - def query(self, name, default=None, multi=False): - """Get a specific query string value. - - Arguments: - name {string} -- Key of the input data - - Keyword Arguments: - default {any} -- Default value if input does not exist (default: {None}) - multi {bool} -- Whether to return all values of a multi value query string param - ie. quuery_param=one&query_param=two (default: {False}) - - Returns: - any - """ - try: - value = self.query_params[name] - except KeyError: - return default - - if not multi and value: - return value[0] - return value - - def all_query(self): - """Get all query string values - - Returns: - any - """ - return self.query_params - - def is_post(self): - """Check if the current request is a POST request. - - Returns: - bool - """ - if self.environ["REQUEST_METHOD"] == "POST": - return True - - return False - - def is_not_get_request(self): - """Check if the current request is not a get request. - - Returns: - bool - """ - if not self.environ["REQUEST_METHOD"] == "GET": - return True - - return False - - def is_not_safe(self): - """Check if the current request is not a get request. - - Returns: - bool - """ - if ( - not self.environ["REQUEST_METHOD"] == "GET" - and not self.environ["REQUEST_METHOD"] == "OPTIONS" - and not self.environ["REQUEST_METHOD"] == "HEAD" - ): - return True - - return False - - def __set_request_method(self): - """Private method for manually setting the request method. - - Returns: - bool - """ - if self.has("__method"): - self.environ["REQUEST_METHOD"] = self.input("__method") - return True - - return False - - def key(self, key): - """Set the encryption key. - - Arguments: - key {string} -- Encryption key - - Returns: - self - """ - self.encryption_key = key - return self - - def all(self, internal_variables=True, clean=True, quote=True): - """Get all the input data. - - Keyword Arguments: - internal_variables {bool} -- Get the internal framework variables - as well (default: {True}) - clean {bool} -- Whether or not the return value should be - cleaned (default: {True}) - - Returns: - dict - """ - - if isinstance(self.raw_input, list): - return self.raw_input - - if not internal_variables: - without_internals = {} - for key, value in self.request_variables.items(): - if not key.startswith("__"): - without_internals.update({key: value}) - return clean_request_input(without_internals, clean=clean, quote=quote) - - return clean_request_input(self.request_variables, clean=clean, quote=quote) - - def only(self, *names): - """Return the specified request variables in a dictionary. - - Returns: - dict - """ - only_vars = {} - - for name in names: - only_vars[name] = self.request_variables.get(name) - - return only_vars - - def without(self, *names): - """Return the request variables in a dictionary without specified values. - - Returns: - dict - """ - only_vars = {} - - for name in self.request_variables: - if name not in names: - only_vars[name] = self.request_variables.get(name) - - return only_vars - - def load_app(self, app): - """Load the container into the request class. - - Arguments: - app {masonite.app.App} -- Application Container - - Returns: - self - """ - self.container = app - return self - - def load_environ(self, environ): - """Load the wsgi environment and sets various properties. - - Arguments: - environ {dict} -- WSGI environ - - Returns: - self - """ - self.environ = environ - self.header_bag.load(environ) - self.method = environ["REQUEST_METHOD"] - self.path = environ["PATH_INFO"] - self.request_variables = {} - self.raw_input = None - - if "QUERY_STRING" in environ and environ["QUERY_STRING"]: - self.query_params = parse_qs(environ["QUERY_STRING"]) - - if self.is_not_get_request(): - environ["POST_DATA"] = self.get_post_params() - - if "POST_DATA" in environ: - self._set_standardized_request_variables(environ["POST_DATA"]) - elif "QUERY_STRING" in environ and environ["QUERY_STRING"]: - self._set_standardized_request_variables(environ["QUERY_STRING"]) - - if "HTTP_COOKIE" in environ: - self.cookie_jar.load(environ["HTTP_COOKIE"]) - - if self.has("__method"): - self.__set_request_method() - - return self - - def get_post_params(self): - """Return the correct input. - - Returns: - dict -- Dictionary of post parameters. - """ - fields = None - if ( - "CONTENT_TYPE" in self.environ - and "application/json" in self.environ["CONTENT_TYPE"].lower() - ): - try: - request_body_size = int(self.environ.get("CONTENT_LENGTH", 0)) - except ValueError: - request_body_size = 0 - - request_body = self.environ["wsgi.input"].read(request_body_size) - - if isinstance(request_body, bytes): - request_body = request_body.decode("utf-8") - - return json.loads(request_body or "{}") - else: - fields = cgi.FieldStorage( - fp=self.environ["wsgi.input"], - environ=self.environ, - keep_blank_values=1, - ) - return fields - - def _set_standardized_request_variables(self, variables): - """The input data is not perfect so we have to standardize it into a dictionary. - - Arguments: - variables {string|dict} - """ - # vv = variables - if isinstance(variables, str): - variables = query_parse(variables) - - self.raw_input = variables - if isinstance(variables, list): - variables = {str(i): v for i, v in enumerate(variables)} - - try: - for name in variables.keys(): - value = self._get_standardized_value(variables[name]) - self.request_variables[name.replace("[]", "")] = value - return - except TypeError: - pass - - self.request_variables = {} - - def _get_standardized_value(self, value): - """Get the standardized value based on the type of the value parameter. - - Arguments: - value {list|dict|cgi.FileStorage|string} - - Returns: - string|bool - """ - if value is None: - return None - - if isinstance(value, list): - - # If the list contains MiniFieldStorage objects then loop - # through and get the values. - if any(isinstance(storage_obj, MiniFieldStorage) for storage_obj in value): - values = [storage_obj.value for storage_obj in value] - - # TODO: This needs to be removed in 2.2. A breaking change but - # this code will result in inconsistent values - # If there is only 1 element in the list then return the only value in the list - if len(values) == 1: - return values[0] - return values - - return value - - if isinstance(value, (str, int, dict)): - return value - - if not value.filename: - return value.value - - if value.filename: - return value - - return False - - def app(self): - """Return the application container. - - Returns: - masonite.app.App -- Application container - """ - # if self.container is None: - # raise AttributeError("The container has not been loaded into the Request class. Use the 'load_app' method to load the container.") - return self.container - - def has(self, *args): - """Check if all given keys in request variable exists. - - Returns: - bool - """ - return all((arg in self.request_variables) for arg in args) - - def scheme(self): - """Get the current request url scheme - - Returns: - string -- the scheme used for the request (http|https) - """ - return self.environ["wsgi.url_scheme"] - - def referrer(self): - """Gets the URL of the request that the current URL came from. - - Returns: - string -- Returns the previous referring URL. - """ - - return self.environ.get("HTTP_REFERER") - - def host(self): - """Get the server's hostname for the current request. - - Returns: - string -- the hostname - """ - host = self.environ.get("HTTP_HOST") - if not host: - host = self.environ["SERVER_NAME"] - return host.split(":", 1)[0] - - def port(self): - """Get the server's port number for the current request. - - Returns: - string -- the server's port number. - """ - return self.environ["SERVER_PORT"] - - def full_path(self, quoted=True): - """Get the path part of the current request url. (including the application path). - - Args: - quoted {bool} -- whether to escape special chars (default: {True}). - - Returns: - string -- the path of the url - """ - url = self.environ.get("SCRIPT_NAME", "") + self.environ.get("PATH_INFO", "") - if quoted: - url = quote(url) - return url - - def url(self, include_standard_port=False): - """Get the url of the current request including the scheme://host:port/path. - - Args: - include_standard_port {bool} -- whether to include the port - when the request uses the standard http(s) port (default: {False}). - - Returns: - string -- the requested url. - """ - scheme = self.scheme() - host = self.host() - port = self.port() - path = self.full_path() - if ( - include_standard_port - or (scheme == "https" and port != "443") - or (scheme == "http" and port != "80") - ): - port_part = ":{}".format(port) - else: - port_part = "" - return "{}://{}{}{}".format(scheme, host, port_part, path) - - def full_url(self, include_standard_port=False): - """Get the full url including query string of the current request. - example: - scheme://host:port/path?query-string - - Args: - include_standard_port {bool} -- whether to include the port - when the request uses the standard http(s) port (default: {False}). - - Returns: - string -- The full request url - """ - url = self.url(include_standard_port=include_standard_port) - query_string = self.query_string() - if query_string: - return "{}?{}".format(url, query_string) - else: - return url - - def query_string(self): - """Get the raw query string of the current request url. - - Returns: - string -- The query-string of the request - """ - return self.environ.get("QUERY_STRING", "") - - def status(self, status): - """Set the HTTP status code. - - Arguments: - status {string|integer} -- A string or integer with the standardized status code - - Returns: - self - """ - return self.app().make(Response).status(status) - - def route_exists(self, url): - web_routes = self.container.make("WebRoutes") - - for route in web_routes: - if route.route_url == url: - return True - - return False - - def get_request_method(self): - """Get the current request method. - - Returns: - string -- returns GET, POST, PUT, etc - """ - return self.environ["REQUEST_METHOD"] - - def header(self, key, value=None): - """Set or gets a header depending on if "value" is passed in or not. - - Arguments: - key {string|dict} -- The header you want to set or get. If the key is a dictionary, loop through each key pair - and add them to the headers. - - Keyword Arguments: - value {string} -- The value you want to set (default: {None}) - - Returns: - string|None|True -- Either return the value if getting a header, - None if it doesn't exist or True if setting the headers. - """ - if isinstance(key, dict): - for dic_key, dic_value in key.items(): - self._set_header(dic_key, dic_value) - return - - # Get Headers - if value is None: - header = self.header_bag.get(key) - if header: - return header.value - return "" - - self._set_header(key, value) - - def _set_header(self, key, value): - # Set Headers - - self.header_bag.add(Header(key, value)) - - def has_raw_header(self, key): - return key in self.header_bag - - def get_headers(self): - """Return all current headers to be set. - - Returns: - list -- List containing a tuple of headers. - """ - - return self._compile_headers_to_tuple() + self.cookie_jar.render_response() - - def _compile_headers_to_tuple(self): - """Compiles the current headers to a list of tuples. - - Returns: - list -- A list of tuples. - """ - - return self.header_bag.render() - - def reset_headers(self): - """Reset all headers being set. - - Typically ran at the end of the request - because of this object acts like a singleton. - - Returns: - None - """ - self.header_bag = HeaderBag() - - def get_and_reset_headers(self): - """Gets the headers but resets at the same time. - - This is useful at the end of the WSGI request to prevent - Several requests from - - Returns: - tuple - """ - headers = self.get_headers() - self.reset_headers() - self.url_params = {} - self.cookie_jar = CookieJar() - return headers - - def set_params(self, params): - """Load the params into the class. - - These parameters are where the developer can retrieve the - /url/@variable:string/ from the url. - - Arguments: - params {dict} -- Dictionary of parameters to store on the class. - - Returns: - self - """ - self.url_params = params - return self - - def param(self, parameter): - """Retrieve the param from the URL. - - The "parameter" parameter in this method should be the name of the - @variable passed into the url in web.py. - - Arguments: - parameter {string} -- Specific argument to return. - - Returns: - string|False -- Returns False if key does not exist. - """ - if parameter in self.url_params: - return self.url_params.get(parameter) - return False - - def cookie( - self, - key, - value, - encrypt=True, - http_only="HttpOnly;", - path="/", - expires=None, - secure=False, - ): - """Set a cookie in the browser. - - Arguments: - key {string} -- Name of the cookie you want set. - value {string} -- Value of the cookie you want set. - - Keyword Arguments: - encrypt {bool} -- Whether or not you want to encrypt the - cookie (default: {True}) - http_only {str} -- If the cookie is HttpOnly or not (default: {"HttpOnly;"}) - path {str} -- The path of the cookie to be set to. (default: {'/'}) - expires {string} -- When the cookie expires - (5 minutes, 1 minute, 10 hours, etc) (default: {''}) - - Returns: - self - """ - - if self.environ.get("SECURE_COOKIES") == "True": - secure = True - - if encrypt: - value = Sign(self.encryption_key).sign(value) - else: - value = value - - if expires: - expires = cookie_expire_time(expires) - - self.cookie_jar.add( - key, - value, - expires=expires, - http_only=http_only, - secure=secure, - path=path, - timezone="GMT", - ) - - return self - - def get_cookies(self): - """Retrieve all cookies from the browser. - - Returns: - dict -- Returns all the cookies. - """ - return self.cookie_jar - - def get_raw_cookie(self, provided_cookie): - return self.cookie_jar.get(provided_cookie) - - def get_cookie(self, provided_cookie, decrypt=True): - """Retrieve a specific cookie from the browser. - - Arguments: - provided_cookie {string} -- Name of the cookie to retrieve - - Keyword Arguments: - decrypt {bool} -- Whether Masonite should try to decrypt the cookie. - This should only be True if the cookie was encrypted - in the first place. (default: {True}) - - Returns: - string|None -- Returns None if the cookie does not exist. - """ - if decrypt: - try: - return Sign(self.encryption_key).unsign( - self.cookie_jar.get(provided_cookie).value - ) - except InvalidToken: - self.delete_cookie(provided_cookie) - return None - except AttributeError: - pass - if self.cookie_jar.exists(provided_cookie): - return self.cookie_jar.get(provided_cookie).value - - def append_cookie(self, value): - """Append cookie to the string or create a new string. - - Whether a new cookie should append on to the string of cookies to be set - or create a new string. This string is used by the browser to interpret how - handle setting a cookie. - - Arguments: - key {string} -- Name of cookie to be stored - value {string} -- Value of cookie to be stored - """ - if "HTTP_COOKIE" in self.environ and self.environ["HTTP_COOKIE"]: - self.environ["HTTP_COOKIE"] += ";{}".format(value) - else: - self.environ["HTTP_COOKIE"] = "{}".format(value) - - def delete_cookie(self, key): - """Delete cookie. - - Arguments: - key {string} -- Name of cookie to be deleted. - - Returns: - bool -- Whether or not the cookie was successfully deleted. - """ - self.cookie_jar.delete(key) - - self.cookie(key, "", expires="expired") - - def set_user(self, user_model): - """Load the user into the class. - - Arguments: - user_model {app.User.User} -- Defaults to loading this class - unless specifically changed. - - Returns: - self - """ - if self._test_user: - self.user_model = self._test_user - else: - self.user_model = user_model - - return self - - def reset_user(self): - """Resets the user back to none""" - self.user_model = None - - def user(self): - """Load the user into the class. - - Returns: - app.User.User|None -- Returns None if the user is not loaded or logged in. - """ - # if self.app().has("User") and self.app().make("User"): - # return self.app().make("User") - return self.user_model - - def redirect( - self, route=None, params={}, name=None, controller=None, url=None, status=302 - ): - """Redirect the user based on the route specified. - - Arguments: - route {string} -- URI of the route (/dashboard/user) - - Keyword Arguments: - params {dict} -- Dictionary of parameters to set for the URI. - Use this when the URI has something like - /dashboard/user/@id. (default: {{}}) - - Returns: - self - """ - if name: - return self.redirect_to(name, params, status=status) - elif route: - self.redirect_url = self.compile_route_to_url(route, params) - elif controller: - self.redirect_url = self.url_from_controller(controller, params) - elif url: - self.redirect_url = url - - self.status(status) - return self - - def with_input(self): - self.flash_inputs_to_session() - return self - - def redirect_to(self, route_name, params={}, status=302): - """Redirect to a named route. - - Arguments: - route_name {string} -- Name of a named route. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to set for the URI. - Use this when the URI has something like - /dashboard/user/@id. (default: {{}}) - - Returns: - self - """ - self.redirect_url = self._get_named_route(route_name, params) - self.status(status) - - return self - - def _get_named_route(self, name, params): - """Search the list of routes and returns the route with the name passed. - - Arguments: - name {string} -- Route name to search for (dashboard.user). - params {dict} -- Dictionary of items to pass to the named route. - - Returns: - string|None -- Returns None if the route was not found or returns the - compiled URI. - """ - web_routes = self.container.make("WebRoutes") - - for route in web_routes: - if route.named_route == name: - return self.compile_route_to_url(route.route_url, params) - - raise RouteException( - "Could not find the route with the name of '{}'".format(name) - ) - - def _get_route_from_controller(self, controller): - """Get the route using the controller. - - This finds the route with the attached controller and returns that route. - This does not compile the URI but actually returns the Route object. - - Arguments: - controller {string|object} -- Can pass in either a string controller - or the controller itself (the object) - - Returns: - masonite.routes.Route|None -- Returns None if the route could not be found. - """ - web_routes = self.container.make("WebRoutes") - - if not isinstance(controller, str): - module_location = controller.__module__ - controller = controller.__qualname__.split(".") - else: - module_location = "app.http.controllers" - controller = controller.split("@") - - for route in web_routes: - if ( - route.controller.__name__ == controller[0] - and route.controller_method == controller[1] - and route.module_location == module_location - ): - return route - - def url_from_controller(self, controller, params={}): - """Return the compiled URI using a controller. - - Arguments: - controller {string|object} -- Can be a string controller or - a controller object. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route - for compilation. (default: {{}}) - - Returns: - masonite.routes.Route|None -- Returns None if the route could not be found. - """ - return self.compile_route_to_url( - self._get_route_from_controller(controller).route_url, params - ) - - def route(self, name, params={}, full=False): - """Get a route URI by its name. - - Arguments: - name {string} -- Name of the route. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route - for compilation. (default: {{}}) - full {bool} -- Specifies whether the full application url should - be returned or not. (default: {False}) - - Returns: - masonite.routes.Route|None -- Returns None if the route cannot be found. - """ - from config import application - - if full: - route = application.URL + self._get_named_route(name, params) - else: - try: - route = self._get_named_route(name, params) - except KeyError: - params = {} - params.update(self.url_params) - route = self._get_named_route(name, params) - - if not route: - raise RouteException( - "Route with the name of '{}' was not found.".format(name) - ) - - return route - - def __getattr__(self, key): - inp = self.input(key) - if inp: - return inp - - inp = self.param(key) - if inp: - return inp - - raise AttributeError("class 'Request' has no attribute {}".format(key)) - - def with_errors(self, errors): - """Easily attach errors message to session request.""" - return self.with_flash("errors", errors) - - def with_success(self, success): - """Easily attach success message to session request.""" - return self.with_flash("success", success) - - def with_flash(self, key, value): - """Easily attach data to session request.""" - self.session.flash(key, value) - return self - - def reset_redirections(self): - """Reset the redirections because of this class acting like a singleton pattern.""" - self.redirect_url = False - self.redirect_route = False - - def back(self, default=None): - """Return a URI for redirection depending on several use cases. - - Keyword Arguments: - default {string} -- Default value if nothing can be found. (default: {None}) - - Returns: - self - """ - self.with_input() - - redirect_url = self.input("__back") - - if not redirect_url and default: - return self.redirect(url=default) - elif not redirect_url and not default: - return self.redirect(url=self.path) - - return self.redirect(url=redirect_url) - - def then_back(self): - self.session.set("__intend", self.path) - return self - - def redirect_intended(self, default=None): - if self.session.get("__intend"): - self.redirect(self.session.get("__intend")) - self.session.delete("__intend") - else: - self.redirect(default) - - return self - - def flash_inputs_to_session(self): - if not hasattr(self, "session"): - return - - for key, value in self.all().items(): - if isinstance(value, bytes): - continue - - self.session.flash(key, value) - - def is_named_route(self, name, params={}): - """Check if the current URI is a specific named route. - - Arguments: - name {string} -- The name of a route. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route. (default: {{}}) - - Returns: - bool - """ - if self._get_named_route(name, params) == self.path: - return True - - return False - - def contains(self, route, show=None): - """If the specified URI is in the current URI path. - - Arguments: - route {string} -- Part of a URI (/dashboard) - - Returns: - bool - """ - if show is not None: - if re.match(compile_route_to_regex(route), self.path): - return show - - return "" - - return re.match(compile_route_to_regex(route), self.path) - - def compile_route_to_url(self, route, params={}): - """Compile the route url into a usable url. - - Converts /url/@id into /url/1. Used for redirection - - Arguments: - route {string} -- An uncompiled route - like (/dashboard/@user:string/@id:int) - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route (default: {{}}) - - Returns: - string -- Returns a compiled string (/dashboard/joseph/1) - """ - if "http" in route: - return route - - # Split the url into a list - split_url = route.split("/") - - # Start beginning of the new compiled url - compiled_url = "/" - - # Iterate over the list - for url in split_url: - if url: - # if the url contains a parameter variable like @id:int - if "@" in url: - url = url.replace("@", "").split(":")[0] - if isinstance(params, dict): - compiled_url += str(params[url]) + "/" - elif isinstance(params, list): - compiled_url += str(params.pop(0)) + "/" - elif "?" in url: - url = url.replace("?", "").split(":")[0] - if isinstance(params, dict): - compiled_url += str(params.get(url, "/")) + "/" - elif isinstance(params, list): - compiled_url += str(params.pop(0)) + "/" - else: - compiled_url += url + "/" - - compiled_url = compiled_url.replace("//", "") - # The loop isn't perfect and may have an unwanted trailing slash - if compiled_url.endswith("/") and not route.endswith("/"): - compiled_url = compiled_url[:-1] - - # The loop isn't perfect and may have 2 slashes next to eachother - if "//" in compiled_url: - compiled_url = compiled_url.replace("//", "/") - - return compiled_url - - def activate_subdomains(self): - """Activate subdomains abilities.""" - self.app().bind("Subdomains", True) - - def has_subdomain(self): - """Check if the current URI has a subdomain. - - Returns: - bool - """ - if self.app().has("Subdomains") and self.app().make("Subdomains"): - url = tldextract.extract(self.environ["HTTP_HOST"]) - - if url.subdomain: - self.subdomain = url.subdomain - self.url_params.update({"subdomain": self.subdomain}) - return True - - return False - - def send(self, params): - """DEPRECATED :: sets a dictionary to be compiled for a route. - - Arguments: - params {dict} -- Dictionary of parameters you want to pass to the route. - - Returns: - self - """ - self.set_params(params) - return self - - def helper(self): - """Dummy method to work with returning the class. Used for helper methods in the View class. - - Returns: - self - """ - return self - - def pop(self, *input_variables): - """Delete keys from the request input.""" - for key in input_variables: - if key in self.request_variables: - del self.request_variables[key] - - def validate(self, *rules): - validator = self.app().make("Validator") - return validator.validate(self.request_variables, *rules) diff --git a/src/masonite/request/__init__.py b/src/masonite/request/__init__.py new file mode 100644 index 000000000..c8b376b74 --- /dev/null +++ b/src/masonite/request/__init__.py @@ -0,0 +1 @@ +from .request import Request diff --git a/src/masonite/request/request.py b/src/masonite/request/request.py new file mode 100644 index 000000000..01cd635d7 --- /dev/null +++ b/src/masonite/request/request.py @@ -0,0 +1,137 @@ +from ..cookies import CookieJar +from ..headers import HeaderBag, Header +from ..input import InputBag +import re +import tldextract +from .validation import ValidatesRequest +from ..authorization import AuthorizesRequest + + +class Request(ValidatesRequest, AuthorizesRequest): + def __init__(self, environ): + """Request class constructor. + + Initializes several properties and sets various methods + depending on the environtment. + + Keyword Arguments: + environ {dictionary} -- WSGI environ dictionary. (default: {None}) + """ + self.environ = environ + self.cookie_jar = CookieJar() + self.header_bag = HeaderBag() + self.input_bag = InputBag() + self.params = {} + self._user = None + self.load() + + def load(self): + self.cookie_jar.load(self.environ.get("HTTP_COOKIE", "")) + self.header_bag.load(self.environ) + self.input_bag.load(self.environ) + + def load_params(self, params=None): + if not params: + params = {} + + self.params.update(params) + + def param(self, param, default=""): + return self.params.get(param, default) + + def get_path(self): + return self.environ.get("PATH_INFO") + + def get_back_path(self): + return self.input("__back") or self.get_path() + + def get_request_method(self): + return self.environ.get("REQUEST_METHOD") + + def input(self, name, default=False): + """Get a specific input value. + + Arguments: + name {string} -- Key of the input data + + Keyword Arguments: + default {string} -- Default value if input does not exist (default: {False}) + clean {bool} -- Whether or not the return value should be + cleaned (default: {True}) + + Returns: + string + """ + name = str(name) + + return self.input_bag.get(name, default=default) + + def cookie(self, name, value=None, **options): + if value is None: + cookie = self.cookie_jar.get(name) + if not cookie: + return + return cookie.value + + return self.cookie_jar.add(name, value, **options) + + def delete_cookie(self, name): + self.cookie_jar.delete(name) + return self + + def header(self, name, value=None): + if value is None: + header = self.header_bag.get(name) + if not header: + return + return header.value + else: + return self.header_bag.add(Header(name, value)) + + def all(self): + return self.input_bag.all_as_values() + + def only(self, *inputs): + return self.input_bag.only(*inputs) + + def is_not_safe(self): + """Check if the current request is not a get request. + + Returns: + bool + """ + if not self.get_request_method() in ("GET", "OPTIONS", "HEAD"): + return True + + return False + + def user(self): + return self._user + + def set_user(self, user): + self._user = user + return self + + def remove_user(self): + self._user = None + return self + + def contains(self, route): + if not route.startswith("/"): + route = "/" + route + + regex = re.compile(route.replace("*", "[a-zA-Z0-9_]+")) + + return regex.match(self.get_path()) + + def get_subdomain(self, exclude_www=True): + url = tldextract.extract(self.get_host()) + if url.subdomain == "" or ( + url.subdomain and exclude_www and url.subdomain == "www" + ): + return None + + return url.subdomain + + def get_host(self): + return self.environ.get("HTTP_HOST") diff --git a/src/masonite/request/validation.py b/src/masonite/request/validation.py new file mode 100644 index 000000000..ba276cdb1 --- /dev/null +++ b/src/masonite/request/validation.py @@ -0,0 +1,7 @@ +from ..validation import Validator + + +class ValidatesRequest: + def validate(self, *rules): + validator = Validator() + return validator.validate(self.all(), *rules) diff --git a/src/masonite/response/__init__.py b/src/masonite/response/__init__.py new file mode 100644 index 000000000..b2c18bf4a --- /dev/null +++ b/src/masonite/response/__init__.py @@ -0,0 +1 @@ +from .response import Response diff --git a/src/masonite/response.py b/src/masonite/response/response.py similarity index 65% rename from src/masonite/response.py rename to src/masonite/response/response.py index bbdffa68f..81455c34b 100644 --- a/src/masonite/response.py +++ b/src/masonite/response/response.py @@ -4,28 +4,28 @@ import mimetypes from pathlib import Path -from .app import App -from .exceptions import ResponseError -from .helpers.Extendable import Extendable -from .headers import HeaderBag, Header -from .helpers.status import response_statuses -from .exceptions import InvalidHTTPStatusCode +from ..routes.Router import Router +from ..exceptions import ResponseError, InvalidHTTPStatusCode +from ..headers import HeaderBag, Header +from ..utils.http import HTTP_STATUS_CODES +from ..cookies import CookieJar -class Response(Extendable): +class Response: """A Response object to be used to abstract the logic of getting a response ready to be returned. Arguments: app {masonite.app.App} -- The Masonite container. """ - def __init__(self, app: App): + def __init__(self, app): self.app = app - self.request = self.app.make("Request") self.content = "" self._status = None - self.statuses = response_statuses() + self.statuses = HTTP_STATUS_CODES self.header_bag = HeaderBag() + self.cookie_jar = CookieJar() + self.original = None def json(self, payload, status=200): """Gets the response ready for a JSON response. @@ -56,17 +56,30 @@ def make_headers(self, content_type="text/html; charset=utf-8"): def header(self, name, value=None): if value is None and isinstance(name, dict): for name, value in name.items(): - self.header_bag.add(Header(name, value)) + self.header_bag.add(Header(name, str(value))) elif value is None: - return self.header_bag.get(name) + header = self.header_bag.get(name) + if isinstance(header, str): + return header + return header.value return self.header_bag.add(Header(name, value)) - def get_and_reset_headers(self): - header = self.header_bag - self.header_bag = HeaderBag() - self._status = None - return header.render() + self.request.cookie_jar.render_response() + def get_headers(self): + return self.header_bag.render() + + def cookie(self, name, value=None, **options): + if value is None: + cookie = self.cookie_jar.get(name) + if not cookie: + return + return cookie.value + + return self.cookie_jar.add(name, value, **options) + + def delete_cookie(self, name): + self.cookie_jar.delete(name) + return self def get_response_content(self): return self.data() @@ -144,7 +157,7 @@ def view(self, view, status=200): Returns: string|dict|list -- Returns the data to be returned. """ - + self.original = view if isinstance(view, tuple): view, status = view self.status(status) @@ -158,10 +171,8 @@ def view(self, view, status=200): return self.json(view.serialize(), status=self.get_status_code()) elif isinstance(view, int): view = str(view) - elif isinstance(view, Responsable) or hasattr(view, "get_response"): + elif hasattr(view, "get_response"): view = view.get_response() - elif isinstance(view, self.request.__class__): - view = self.data() elif view is None: raise ResponseError( "Responses cannot be of type: None. Did you return anything in your responsable method?" @@ -169,34 +180,44 @@ def view(self, view, status=200): if isinstance(view, str): self.content = bytes(view, "utf-8") - self.app.bind("Response", bytes(view, "utf-8")) else: self.content = view - self.app.bind("Response", view) self.make_headers() - return self.data() + return self - def redirect(self, location=None, status=302): + def back(self): + return self.redirect(url=self.app.make("request").get_back_path()) + + def redirect(self, location=None, name=None, params={}, url=None, status=302): """Set the redirection on the server. Keyword Arguments: location {string} -- The URL to redirect to (default: {None}) status {int} -- The Response status code. (default: {302}) + params {dict} -- The route params (default: {}) Returns: string -- Returns the data to be returned. """ self.status(status) - if not location: - location = self.request.redirect_url - self.request.reset_headers() - self.header_bag.add(Header("Location", location)) + if location: + self.header_bag.add(Header("Location", location)) + elif name: + url = self._get_url_from_route_name(name, params) + self.header_bag.add(Header("Location", url)) + elif url: + self.header_bag.add(Header("Location", url)) self.view("Redirecting ...") + return self - return self.data() + def _get_url_from_route_name(self, name, params={}): + route = self.app.make("router").find_by_name(name) + if not route: + raise ValueError(f"Route with the name '{name}' not found.") + return Router.compile_to_url(route.url, params) def to_bytes(self): """Converts the data to bytes so the WSGI server can handle it. @@ -206,69 +227,23 @@ def to_bytes(self): """ return self.converted_data() - -class Responsable: - def get_response(self): - raise NotImplementedError( - "This class does not implement a 'get_response()' method" - ) - - -class Download(Responsable): - """Download class to help show files in the browser or force - a download for the client browser. - - Arguments: - location {string} -- The path you want to download. - - Keyword Arguments: - force {bool} -- Whether you want the client's browser to force the file download (default: {False}) - name {str} -- The name you want the file to be called when downloaded (default: {'profile.jpg'}) - """ - - def __init__(self, location, force=False, name="1"): - self.location = location - self._force = force - self.name = name - self.container = None - - def force(self): - """Sets the force option. - - Returns: - self - """ - self._force = True - return self - - def get_response(self): - """Handles the way the response should be handled by the server. - - Returns: - bytes - Returns bytes required for the server to handle the download. - """ - if not self.container: - from wsgi import container - - self.container = container - - response = self.container.make(Response) - - with open(self.location, "rb") as filelike: - data = filelike.read() - - if self._force: - response.header("Content-Type", "application/octet-stream") - response.header( + def download(self, name, location, force=False): + if force: + self.header("Content-Type", "application/octet-stream") + self.header( "Content-Disposition", - 'attachment; filename="{}{}"'.format( - self.name, self.extension(self.location) - ), + 'attachment; filename="{}{}"'.format(name, self.extension(location)), ) else: - response.header("Content-Type", self.mimetype(self.location)) + self.header("Content-Type", self.mimetype(location)) + + with open(location, "rb") as filelike: + data = filelike.read() + + return self.view(data) - return data + def extension(self, path): + return Path(path).suffix def mimetype(self, path): """Gets the mimetime of a path @@ -280,6 +255,3 @@ def mimetype(self, path): string -- The mimetype for use in headers """ return mimetypes.guess_type(path)[0] - - def extension(self, path): - return Path(path).suffix diff --git a/src/masonite/routes.py b/src/masonite/routes.py deleted file mode 100644 index 777946cff..000000000 --- a/src/masonite/routes.py +++ /dev/null @@ -1,772 +0,0 @@ -"""Module for the Routing System.""" - -import cgi -import importlib -import json -import re - -from .exceptions import ( - RouteMiddlewareNotFound, - InvalidRouteCompileException, - RouteException, -) -from .view import View - - -class Route: - """Route class used to handle routing.""" - - route_compilers = { - "int": r"(\d+)", - "integer": r"(\d+)", - "string": r"([a-zA-Z]+)", - "default": r"([\w.-]+)", - "signed": r"([\w\-=]+)", - } - - def __init__(self, environ=None): - """Route constructor. - - Keyword Arguments: - environ {dict} -- WSGI environ (default: {None}) - """ - self.url_list = [] - self.method_type = ["GET"] - - if environ: - self.load_environ(environ) - - def load_environ(self, environ): - """Load the WSGI environ into the class. - - Arguments: - environ {dict} -- WSGI environ - - Returns: - self - """ - self.environ = environ - self.url = environ["PATH_INFO"] - - return self - - def is_post(self): - """Check to see if the current request is a POST request. - - Returns: - bool - """ - if self.environ["REQUEST_METHOD"] == "POST": - return True - - return False - - def is_not_get_request(self): - """Check if current request is not a get request. - - Returns: - bool - """ - if not self.environ["REQUEST_METHOD"] == "GET": - return True - - return False - - def compile(self, key, to=""): - self.route_compilers.update({key: to}) - return self - - def generated_url_list(self): - """Return the URL list. - - Returns: - list -- URL list. - """ - return self.url_list - - -class BaseHttpRoute: - """Base route for HTTP routes.""" - - def __init__(self): - self.method_type = ["GET"] - self.output = False - self.route_url = None - self.request = None - self.named_route = None - self.required_domain = None - self.module_location = "app.http.controllers" - self.list_middleware = [] - self.default_parameters = {} - self.e = False - - def default(self, dictionary): - self.default_parameters.update(dictionary) - return self - - def get_default_parameter(self, key): - return self.default_parameters.get(key, None) - - def route(self, route, output): - """Load the route into the class. This also looks for the controller and attaches it to the route. - - Arguments: - route {string} -- This is a URI to attach to the route (/dashboard/user). - output {string|object} -- Controller to attach to the route. - - Returns: - self - """ - self.output = output - self._find_controller(output) - - if not route.startswith("/"): - route = "/" + route - - if route.endswith("/") and route != "/": - route = route[:-1] - - self.route_url = route - self._compiled_url = self.compile_route_to_regex() - return self - - def view(self, route, template, dictionary={}): - view_route = ViewRoute(self.method_type, route, template, dictionary) - return view_route - - def _find_controller(self, controller): - """Find the controller to attach to the route. - - Arguments: - controller {string|object} -- String or object controller to search for. - - Returns: - None - """ - module_location = self.module_location - # If the output specified is a string controller - if isinstance(controller, str): - mod = controller.split("@") - # If trying to get an absolute path via a string - if mod[0].startswith("/"): - module_location = ".".join(mod[0].replace("/", "").split(".")[0:-1]) - elif "." in mod[0]: - # This is a deeper module controller - module_location += "." + ".".join(mod[0].split(".")[:-1]) - else: - if controller is None: - return None - - fully_qualified_name = controller.__qualname__ - mod = fully_qualified_name.split(".") - module_location = controller.__module__ - - # Gets the controller name from the output parameter - # This is used to add support for additional modules - # like 'LoginController' and 'Auth.LoginController' - get_controller = mod[0].split(".")[-1] - - try: - # Import the module - if isinstance(controller, str): - module = importlib.import_module( - "{0}.".format(module_location) + get_controller - ) - else: - module = importlib.import_module("{0}".format(module_location)) - - # Get the controller from the module - self.controller = getattr(module, get_controller) - - # Set the controller method on class. This is a string - self.controller_method = mod[1] if len(mod) == 2 else "__call__" - except ImportError as e: - import sys - import traceback - - _, _, exc_tb = sys.exc_info() - self.e = e - except Exception as e: # skipcq - import sys - import traceback - - _, _, exc_tb = sys.exc_info() - self.e = e - print("\033[93mTrouble importing controller!", str(e), "\033[0m") - if not self.e: - self.module_location = module_location - - def get_response(self): - # Resolve Controller Constructor - if self.e: - print( - "\033[93mCannot find controller {}. Did you create this one?".format( - self.output - ), - "\033[0m", - ) - raise SyntaxError(str(self.e)) - - controller = self.request.app().resolve(self.controller) - - # Resolve Controller Method - response = self.request.app().resolve( - getattr(controller, self.controller_method), - *self.request.url_params.values() - ) - # save original content - self.original = response - - if isinstance(response, View): - response = response.rendered_template - - return response - - def domain(self, domain): - """Set the subdomain for the route. - - Arguments: - domain {string|list|tuple} -- The string or list of subdomains to attach to this route. - - Returns: - self - """ - self.required_domain = domain - return self - - def module(self, module): - """DEPRECATED :: The base module to look for string controllers. - - Arguments: - module {string} -- The string representation of a module to look for controllers. - - Returns: - self - """ - self.module_location = module - return self - - def has_required_domain(self): - """Check if the route has the required subdomain before executing the route. - - Returns: - bool - """ - if self.request.has_subdomain() and ( - self.required_domain == "*" - or self.request.subdomain == self.required_domain - ): - return True - return False - - def name(self, name): - """Specify the name of the route. - - Arguments: - name {string} -- Sets a name for the route. - - Returns: - self - """ - self.named_route = name - return self - - def load_request(self, request): - """Load the request into this class. - - Arguments: - request {masonite.request.Request} -- Request object. - - Returns: - self - """ - self.request = request - return self - - def middleware(self, *args): - """Load a list of middleware to run. - - Returns: - self - """ - for arg in args: - if arg not in self.list_middleware: - self.list_middleware.append(arg) - - return self - - def run_middleware(self, type_of_middleware): - """Run route middleware. - - Arguments: - type_of_middleware {string} -- Type of middleware to be ran (before|after) - - Raises: - RouteMiddlewareNotFound -- Thrown when the middleware could not be found. - """ - # Get the list of middleware to run for a route. - for arg in self.list_middleware: - if ":" in arg: - middleware_to_run, arguments = arg.split(":") - # Splits "name:value1,value2" into ['value1', 'value2'] - arguments = arguments.split(",") - for index, argument in enumerate(arguments): - if argument.startswith("@"): - _, argument = argument.split("@") - arguments[index] = self.request.param(argument) - else: - middleware_to_run = arg - arguments = [] - - middleware_to_run = self.request.app().make("RouteMiddleware")[ - middleware_to_run - ] - if not isinstance(middleware_to_run, list): - middleware_to_run = [middleware_to_run] - - try: - for middleware in middleware_to_run: - located_middleware = self.request.app().resolve(middleware) - if hasattr(located_middleware, type_of_middleware): - getattr(located_middleware, type_of_middleware)(*arguments) - except KeyError: - raise RouteMiddlewareNotFound( - "Could not find the '{0}' route middleware".format(arg) - ) - - def compile_route_to_regex(self): - """Compile the given route to a regex string. - - Arguments: - route {string} -- URI of the route to compile. - - Returns: - string -- Compiled URI string. - """ - # Split the route - split_given_route = self.route_url.split("/") - # compile the provided url into regex - url_list = [] - regex = "^" - for regex_route in split_given_route: - # if not regex_route: - # continue - if "@" in regex_route: - if ":" in regex_route: - try: - regex += Route.route_compilers[regex_route.split(":")[1]] - except KeyError: - if self.request: - raise InvalidRouteCompileException( - 'Route compiler "{}" is not an available route compiler. ' - "Verify you spelled it correctly or that you have added it using the compile() method.".format( - regex_route.split(":")[1] - ) - ) - self._compiled_regex = None - self._compiled_regex_end = None - return - - else: - regex += Route.route_compilers["default"] - - regex += r"\/" - - # append the variable name passed @(variable):int to a list - url_list.append(regex_route.replace("@", "").split(":")[0]) - elif "?" in regex_route: - # Make the preceding token match 0 or more - regex += "?" - - if ":" in regex_route: - - try: - regex += Route.route_compilers[regex_route.split(":")[1]] + "*" - except KeyError: - if self.request: - raise InvalidRouteCompileException( - 'Route compiler "{}" is not an available route compiler. ' - "Verify you spelled it correctly or that you have added it using the compile() method.".format( - regex_route.split(":")[1] - ) - ) - self._compiled_regex = None - self._compiled_regex_end = None - return - - else: - regex += Route.route_compilers["default"] + "*" - - regex += r"\/" - - url_list.append(regex_route.replace("?", "").split(":")[0]) - else: - regex += regex_route + r"\/" - - self.url_list = url_list - regex += "$" - self._compiled_regex = re.compile(regex.replace(r"\/$", r"$")) - self._compiled_regex_end = re.compile(regex) - - return regex - - -class Get(BaseHttpRoute): - """Class for specifying GET requests.""" - - def __init__(self, route=None, output=None): - """Get constructor.""" - super().__init__() - self.method_type = ["GET"] - # self.list_middleware = [] - if route is not None and output is not None: - self.route(route, output) - - -class Head(BaseHttpRoute): - """Class for specifying HEAD requests.""" - - def __init__(self, route=None, output=None): - """Head constructor.""" - super().__init__() - self.method_type = ["HEAD"] - if route is not None and output is not None: - self.route(route, output) - - -class Post(BaseHttpRoute): - """Class for specifying POST requests.""" - - def __init__(self, route=None, output=None): - """Post constructor.""" - super().__init__() - self.method_type = ["POST"] - if route is not None and output is not None: - self.route(route, output) - - -class Match(BaseHttpRoute): - """Class for specifying Match requests.""" - - def __init__(self, method_type=["GET"], route=None, output=None): - """Match constructor.""" - super().__init__() - if not isinstance(method_type, list): - raise RouteException( - "Method type needs to be a list. Got '{}'".format(method_type) - ) - - # Make all method types in list uppercase - self.method_type = [x.upper() for x in method_type] - if route is not None and output is not None: - self.route(route, output) - - -class Put(BaseHttpRoute): - """Class for specifying PUT requests.""" - - def __init__(self, route=None, output=None): - """Put constructor.""" - super().__init__() - self.method_type = ["PUT"] - if route is not None and output is not None: - self.route(route, output) - - -class Patch(BaseHttpRoute): - """Class for specifying Patch requests.""" - - def __init__(self, route=None, output=None): - """Patch constructor.""" - super().__init__() - self.method_type = ["PATCH"] - if route is not None and output is not None: - self.route(route, output) - - -class Delete(BaseHttpRoute): - """Class for specifying Delete requests.""" - - def __init__(self, route=None, output=None): - """Delete constructor.""" - super().__init__() - self.method_type = ["DELETE"] - if route is not None and output is not None: - self.route(route, output) - - -class Connect(BaseHttpRoute): - """Class for specifying Connect requests.""" - - def __init__(self, route=None, output=None): - """Connect constructor.""" - super().__init__() - self.method_type = ["CONNECT"] - if route is not None and output is not None: - self.route(route, output) - - -class Options(BaseHttpRoute): - """Class for specifying Options requests.""" - - def __init__(self, route=None, output=None): - """Options constructor.""" - super().__init__() - self.method_type = ["OPTIONS"] - if route is not None and output is not None: - self.route(route, output) - - print( - "The Masonite development server is not capable of handling OPTIONS preflight requests." - ) - print("You should use a more powerful server if using the Option") - - -class Trace(BaseHttpRoute): - """Class for specifying Trace requests.""" - - def __init__(self, route=None, output=None): - """Trace constructor.""" - super().__init__() - self.method_type = ["TRACE"] - if route is not None and output is not None: - self.route(route, output) - - -class ViewRoute(BaseHttpRoute): - def __init__(self, method_type, route, template, dictionary): - """Class used for view routes. - - This class should be returned when a view is called on an HTTP route. - This is useful when returning a view that doesn't need any special logic and only needs a dictionary. - - Arguments: - method_type {string} -- The method type (GET, POST, PUT etc) - route {string} -- The current route (/test/url) - template {string} -- The template to use (dashboard/user) - dictionary {dict} -- The dictionary to use to render the template. - """ - - super().__init__() - self.method_type = method_type - self.route_url = route - self.template = template - self.dictionary = dictionary - self._compiled_url = self.compile_route_to_regex() - - def get_response(self): - return ( - self.request.app() - .make("ViewClass") - .render(self.template, self.dictionary) - .rendered_template - ) - - -class Redirect(BaseHttpRoute): - def __init__(self, current_route, future_route, status=302, methods=["GET"]): - """Class used for view routes. - - This class should be returned when a view is called on an HTTP route. - This is useful when returning a view that doesn't need any special logic and only needs a dictionary. - - Arguments: - method_type {string} -- The method type (GET, POST, PUT etc) - route {string} -- The current route (/test/url) - template {string} -- The template to use (dashboard/user) - dictionary {dict} -- The dictionary to use to render the template. - """ - super().__init__() - self.method_type = methods - self.route_url = current_route - self.status = status - self.future_route = future_route - self._compiled_url = self.compile_route_to_regex() - - def get_response(self): - return self.request.redirect(self.future_route, status=self.status) - - -class RouteGroup: - """Class for specifying Route Groups.""" - - def __new__( - cls, - routes=[], - middleware=[], - domain=[], - prefix="", - name="", - add_methods=[], - namespace="", - ): - """Call when this class is first called. This is to give the ability to return a value in the constructor. - - Keyword Arguments: - routes {list} -- List of routes. (default: {[]}) - middleware {list} -- List of middleware. (default: {[]}) - domain {list} -- String or list of domains to attach to all the routes. (default: {[]}) - prefix {str} -- Prefix to attach to all the route URI's. (default: {''}) - name {str} -- Base name to attach to all the routes. (default: {''}) - namespace {str} -- Namespace path to attach to all the routes. (default: {''}) - - Returns: - list -- Returns a list of routes. - """ - from .helpers.routes import flatten_routes - - cls.routes = flatten_routes(routes) - - if middleware: - cls._middleware(cls, *middleware) - - if add_methods: - cls._add_methods(cls, *add_methods) - - if domain: - cls._domain(cls, domain) - - if namespace: - cls._namespace(cls, namespace) - - if prefix: - cls._prefix(cls, prefix) - - if name: - cls._name(cls, name) - - return cls.routes - - def _middleware(self, *middleware): - """Attach middleware to all routes. - - Returns: - list -- Returns list of routes. - """ - for route in self.routes: - route.middleware(*middleware) - - return self.routes - - def _add_methods(self, *methods): - """Attach more methods to all routes. - - Returns: - list -- Returns list of routes. - """ - for route in self.routes: - route.method_type.append(*methods) - - return self.routes - - def _domain(self, domain): - """Attach a domain to all routes. - - Arguments: - domain {str|list|tuple} -- List of domains to attach to all the routes. - """ - for route in self.routes: - route.domain(domain) - - def _prefix(self, prefix): - """Prefix a string to all domain URI's. - - Arguments: - prefix {str} -- String to prefix to all Routes. - """ - for route in self.routes: - if route.route_url == "/": - route.route_url = "" - - route.route_url = prefix + route.route_url - route.compile_route_to_regex() - - def _name(self, name): - """Name to prefix to all routes. - - Arguments: - name {str} -- String to prefix to all routes. - """ - for route in self.routes: - if isinstance(route.named_route, str): - route.named_route = name + route.named_route - - def _namespace(self, namespace): - """Namespace of the controller for all routes - - Arguments: - namespace {str} -- String to add to find controllers for all Routes. - """ - if not namespace.endswith("."): - namespace += "." - for route in self.routes: - if isinstance(route.output, str): - route.e = False # reset any previous find_controller attempt - route.output = namespace + route.output - route._find_controller(route.output) - - -class Resource: - def __new__( - cls, - base="", - controller="", - only=["index", "create", "store", "show", "edit", "update", "destroy"], - names={}, - ): - if not names: - base_name = base.replace("/", ".") - if base_name[0] == ".": - base_name = base_name[1:] - - names = { - "index": "{}.index".format(base_name), - "create": "{}.create".format(base_name), - "store": "{}.store".format(base_name), - "show": "{}.show".format(base_name), - "edit": "{}.edit".format(base_name), - "update": "{}.update".format(base_name), - "destroy": "{}.destroy".format(base_name), - } - - routes = [] - - if "index" in only: - route = Get("{}".format(base), "{}@index".format(controller)) - if "index" in names: - route.name(names["index"]) - routes.append(route) - if "create" in only: - route = Get("{}/create".format(base), "{}@create".format(controller)) - if "create" in names: - route.name(names["create"]) - routes.append(route) - if "store" in only: - route = Post("{}".format(base), "{}@store".format(controller)) - if "store" in names: - route.name(names["store"]) - routes.append(route) - if "show" in only: - route = Get("{}/@id".format(base), "{}@show".format(controller)) - if "show" in names: - route.name(names["show"]) - routes.append(route) - if "edit" in only: - route = Get("{}/@id/edit".format(base), "{}@edit".format(controller)) - if "edit" in names: - route.name(names["edit"]) - routes.append(route) - if "update" in only: - route = Match(["PUT", "PATCH"]).route( - "{}/@id".format(base), "{}@update".format(controller) - ) - if "update" in names: - route.name(names["update"]) - routes.append(route) - if "destroy" in only: - route = Delete("{}/@id".format(base), "{}@destroy".format(controller)) - if "destroy" in names: - route.name(names["destroy"]) - routes.append(route) - - return routes diff --git a/src/masonite/routes/HTTPRoute.py b/src/masonite/routes/HTTPRoute.py new file mode 100644 index 000000000..a6783522c --- /dev/null +++ b/src/masonite/routes/HTTPRoute.py @@ -0,0 +1,284 @@ +import re +import os + +from ..utils.str import modularize, removeprefix +from ..exceptions import InvalidRouteCompileException +from ..facades import Loader +from ..controllers import Controller +from ..exceptions import LoaderNotFound + + +class HTTPRoute: + def __init__( + self, + url, + controller=None, + request_method=["get"], + name=None, + compilers=None, + controllers_locations=["app.http.controllers"], + controller_bindings=[], + **options, + ): + if not url.startswith("/"): + url = "/" + url + + self.url = url + self.controllers_locations = controllers_locations + self.controller = controller + self.controller_class = None + self.controller_method = None + self._domain = None + self._name = name + self.request_method = [x.lower() for x in request_method] + self.list_middleware = [] + self.e = None + self.compilers = compilers or {} + self._find_controller(controller) + self.controller_bindings = controller_bindings + self.compile_route_to_regex() + + def match(self, path, request_method, subdomain=None): + + route_math = ( + re.match(self._compiled_regex, path) + or re.match(self._compiled_regex_end, path) + ) and request_method.lower() in self.request_method + + domain_match = subdomain == self._domain + + return route_math and domain_match + + def get_name(self): + return self._name + + def matches(self, path): + return re.match(self._compiled_regex, path) or re.match( + self._compiled_regex_end, path + ) + + def match_name(self, name): + return name == self._name + + def name(self, name): + self._name = name + return self + + def domain(self, subdomain): + self._domain = subdomain + return self + + def to_url(self, parameters={}): + + # Split the url into a list + split_url = self.url.split("/") + + # Start beginning of the new compiled url + compiled_url = "/" + + # Iterate over the list + for url in split_url: + if url: + # if the url contains a parameter variable like @id:int + if "@" in url: + url = url.replace("@", "").split(":")[0] + if isinstance(parameters, dict): + compiled_url += str(parameters[url]) + "/" + elif isinstance(parameters, list): + compiled_url += str(parameters.pop(0)) + "/" + elif "?" in url: + url = url.replace("?", "").split(":")[0] + if isinstance(parameters, dict): + compiled_url += str(parameters.get(url, "/")) + "/" + elif isinstance(parameters, list): + compiled_url += str(parameters.pop(0)) + "/" + else: + compiled_url += url + "/" + + # The loop isn't perfect and may have an unwanted trailing slash + if compiled_url.endswith("/"): + compiled_url = compiled_url[:-1] + + # The loop isn't perfect and may have 2 slashes next to eachother + if "//" in compiled_url: + compiled_url = compiled_url.replace("//", "/") + + return compiled_url + + def _find_controller(self, controller): + """Find the controller to attach to the route. Look for controller (str or class) in all + specified controllers_location. + + Arguments: + controller {string|object} -- String or object controller to search for. + + Returns: + None + """ + if controller is None: + return None + # If the output specified is a string controller e.g. "WelcomeController@show" + elif isinstance(controller, str): + if "@" in controller: + controller_path, controller_method_str = controller.split("@") + else: + controller_path = controller + controller_method_str = "__call__" + + controller_path = modularize(controller_path).split(".") + if len(controller_path) > 1: + controller_name = controller_path.pop() + prefix_path = ".".join(controller_path) + else: + controller_name = controller_path[0] + prefix_path = "" + # build a list of all locations where the controller can be found + # if the controller is defined such as auth.WelcomeController, append the prefix path to + # the locations + locations = list( + map( + lambda loc: f"{loc}.{removeprefix(prefix_path, loc)}" + if prefix_path + else loc, + self.controllers_locations, + ) + ) + try: + self.controller_class = Loader.find( + Controller, locations, controller_name, raise_exception=True + ) + except LoaderNotFound as e: + self.e = e + print("\033[93mTrouble importing controller!", str(e), "\033[0m") + # Else it's a controller instance, we don't have to find it, just get the class + else: + if "." in controller.__qualname__: + controller_name, controller_method_str = controller.__qualname__.split( + "." + ) + else: + controller_name = controller.__qualname__ + controller_method_str = "__call__" + try: + self.controller_class = Loader.get_object( + controller.__module__, controller_name, raise_exception=True + ) + except LoaderNotFound as e: + self.e = e + print("\033[93mTrouble importing controller!", str(e), "\033[0m") + + # Set the controller method on class. This is a string + self.controller_method = controller_method_str + + def get_response(self, app=None): + # Resolve Controller Constructor + if self.e: + print( + "\033[93mCannot find controller {}. Did you create this one?".format( + self.controller + ), + "\033[0m", + ) + raise SyntaxError(str(self.e)) + + if app: + controller = app.resolve(self.controller_class, *self.controller_bindings) + # resolve route parameters + params = self.extract_parameters(app.make("request").get_path()).values() + # Resolve Controller Method + response = app.resolve(getattr(controller, self.controller_method), *params) + return response + + return getattr(self.controller_class(), self.controller_method)() + + def middleware(self, *args): + """Load a list of middleware to run. + + Returns: + self + """ + for arg in args: + if arg and arg not in self.list_middleware: + self.list_middleware.append(arg) + + return self + + def compile_route_to_regex(self): + """Compile the given route to a regex string. + + Arguments: + route {string} -- URI of the route to compile. + + Returns: + string -- Compiled URI string. + """ + # Split the route + split_given_route = self.url.split("/") + # compile the provided url into regex + url_list = [] + regex = "^" + for regex_route in split_given_route: + if "@" in regex_route: + if ":" in regex_route: + try: + regex += self.compilers[regex_route.split(":")[1]] + except KeyError: + raise InvalidRouteCompileException( + 'Route compiler "{}" is not an available route compiler. ' + "Verify you spelled it correctly or that you have added it using the compile() method.".format( + regex_route.split(":")[1] + ) + ) + + else: + regex += self.compilers["default"] + + regex += r"\/" + + # append the variable name passed @(variable):int to a list + url_list.append(regex_route.replace("@", "").split(":")[0]) + elif "?" in regex_route: + # Make the preceding token match 0 or more + regex += "?" + + if ":" in regex_route: + + try: + regex += self.compilers[regex_route.split(":")[1]] + "*" + except KeyError: + if self.request: + raise InvalidRouteCompileException( + 'Route compiler "{}" is not an available route compiler. ' + "Verify you spelled it correctly or that you have added it using the compile() method.".format( + regex_route.split(":")[1] + ) + ) + self._compiled_regex = None + self._compiled_regex_end = None + return + + else: + regex += self.compilers["default"] + "*" + + regex += r"\/" + + url_list.append(regex_route.replace("?", "").split(":")[0]) + else: + regex += regex_route + r"\/" + + self.url_list = url_list + regex += "$" + self._compiled_regex = re.compile(regex.replace(r"\/$", r"$")) + self._compiled_regex_end = re.compile(regex) + + return regex + + def extract_parameters(self, path): + if not self.url_list: + return {} + + if (not path.endswith("/")) or path == "/": + matching_regex = self._compiled_regex + else: + matching_regex = self._compiled_regex_end + return dict(zip(self.url_list, matching_regex.match(path).groups())) diff --git a/src/masonite/routes/Route.py b/src/masonite/routes/Route.py new file mode 100644 index 000000000..c525b9b83 --- /dev/null +++ b/src/masonite/routes/Route.py @@ -0,0 +1,165 @@ +from .HTTPRoute import HTTPRoute +from ..utils.collections import flatten +from ..utils.str import modularize +from ..controllers import RedirectController + + +class Route: + + routes = [] + compilers = { + "int": r"(\d+)", + "integer": r"(\d+)", + "string": r"([a-zA-Z]+)", + "default": r"([\w.-]+)", + "signed": r"([\w\-=]+)", + } + controllers_locations = [] + + def __init__(self): + pass + + @classmethod + def get(self, url, controller, module_location=None, **options): + return HTTPRoute( + url, + controller, + request_method=["get"], + compilers=self.compilers, + controllers_locations=module_location or self.controllers_locations, + **options + ) + + @classmethod + def post(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["post"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def put(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["put"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def patch(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["patch"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def delete(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["delete"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def options(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["options"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def default(self, url, controller, **options): + return self + + @classmethod + def redirect(self, url, new_url, **options): + return HTTPRoute( + url, + RedirectController.redirect, + request_method=["get"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + controller_bindings=[new_url, options.get("status", 302)], + **options + ) + + @classmethod + def permanent_redirect(self, url, new_url, **options): + return HTTPRoute( + url, + RedirectController.redirect, + request_method=["get"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + controller_bindings=[new_url, 301], + **options + ) + + @classmethod + def match(self, request_methods, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=request_methods, + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def group(self, *routes, **options): + inner = [] + for route in flatten(routes): + if options.get("prefix"): + if route.url == "" or route.url == "/": + route.url = options.get("prefix") + else: + route.url = options.get("prefix") + route.url + + route.compile_route_to_regex() + + if options.get("name"): + route._name = options.get("name") + route._name + + if options.get("domain"): + route.domain(options.get("domain")) + + if options.get("middleware"): + route.middleware(*options.get("middleware", [])) + + inner.append(route) + self.routes = inner + return inner + + @classmethod + def compile(self, key, to=""): + self.compilers.update({key: to}) + return self + + @classmethod + def set_controller_locations(self, *controllers_locations): + self.controllers_locations = list(map(modularize, controllers_locations)) + return self + + @classmethod + def add_controller_locations(self, *controllers_locations): + self.controllers_locations.extend(list(map(modularize, controllers_locations))) + return self diff --git a/src/masonite/routes/Router.py b/src/masonite/routes/Router.py new file mode 100644 index 000000000..8184e5cc3 --- /dev/null +++ b/src/masonite/routes/Router.py @@ -0,0 +1,93 @@ +from ..utils.collections import flatten +from ..exceptions import RouteNotFoundException + + +class Router: + def __init__(self, *routes, module_location=None): + self.routes = flatten(routes) + + def find(self, path, request_method, subdomain=None): + + for route in self.routes: + if route.match(path, request_method, subdomain=subdomain): + return route + + def matches(self, path): + for route in self.routes: + if route.matches(path): + return route + + def find_by_name(self, name): + for route in self.routes: + if route.match_name(name): + return route + + def route(self, name, parameters={}): + route = self.find_by_name(name) + if route: + return route.to_url(parameters) + raise RouteNotFoundException(f"Could not find route with the name '{name}'") + + def set_controller_locations(self, location): + self.controller_locations = location + return self + + def add(self, *routes): + self.routes.append(*routes) + self.routes = flatten(self.routes) + + def set(self, *routes): + self.routes = [] + self.routes.append(*routes) + self.routes = flatten(self.routes) + + @classmethod + def compile_to_url(cls, uncompiled_route, params={}): + """Compile the route url into a usable url: converts /url/@id into /url/1. + Used for redirection + + Arguments: + route {string} -- An uncompiled route like (/dashboard/@user:string/@id:int) + Keyword Arguments: + params {dict} -- Dictionary of parameters to pass to the route (default: {{}}) + Returns: + string -- Returns a compiled string (/dashboard/joseph/1) + """ + if "http" in uncompiled_route: + return uncompiled_route + + # Split the url into a list + split_url = uncompiled_route.split("/") + + # Start beginning of the new compiled url + compiled_url = "/" + + # Iterate over the list + for url in split_url: + if url: + # if the url contains a parameter variable like @id:int + if "@" in url: + url = url.replace("@", "").split(":")[0] + if isinstance(params, dict): + compiled_url += str(params[url]) + "/" + elif isinstance(params, list): + compiled_url += str(params.pop(0)) + "/" + elif "?" in url: + url = url.replace("?", "").split(":")[0] + if isinstance(params, dict): + compiled_url += str(params.get(url, "/")) + "/" + elif isinstance(params, list): + compiled_url += str(params.pop(0)) + "/" + else: + compiled_url += url + "/" + + compiled_url = compiled_url.replace("//", "") + # The loop isn't perfect and may have an unwanted trailing slash + if compiled_url.endswith("/") and not uncompiled_route.endswith("/"): + compiled_url = compiled_url[:-1] + + # The loop isn't perfect and may have 2 slashes next to eachother + if "//" in compiled_url: + compiled_url = compiled_url.replace("//", "/") + + return compiled_url diff --git a/src/masonite/routes/__init__.py b/src/masonite/routes/__init__.py new file mode 100644 index 000000000..39981be49 --- /dev/null +++ b/src/masonite/routes/__init__.py @@ -0,0 +1,3 @@ +from .Route import Route +from .HTTPRoute import HTTPRoute +from .Router import Router diff --git a/src/masonite/scheduling/CanSchedule.py b/src/masonite/scheduling/CanSchedule.py new file mode 100644 index 000000000..4cdf44c7b --- /dev/null +++ b/src/masonite/scheduling/CanSchedule.py @@ -0,0 +1,10 @@ +class CanSchedule: + def call(self, command): + command_class = CommandTask(command) + self.app.make("scheduler").add(command_class) + return command_class + + def schedule(self, task): + task_class = task + self.app.make("scheduler").add(task_class) + return task_class diff --git a/src/masonite/scheduling/CommandTask.py b/src/masonite/scheduling/CommandTask.py new file mode 100644 index 000000000..f8b2e27e9 --- /dev/null +++ b/src/masonite/scheduling/CommandTask.py @@ -0,0 +1,13 @@ +from .Task import Task +import subprocess + + +class CommandTask(Task): + + run_every_minute = True + + def __init__(self, command=""): + self.command = command + + def handle(self): + subprocess.call(self.command.split(" ")) diff --git a/src/masonite/scheduling/Task.py b/src/masonite/scheduling/Task.py new file mode 100644 index 000000000..97c92771e --- /dev/null +++ b/src/masonite/scheduling/Task.py @@ -0,0 +1,148 @@ +import pendulum + + +class Task: + + run_every = False + run_at = False + run_every_hour = False + run_every_minute = False + twice_daily = False + run_weekly = False + + _date = None + + name = "" + + def __init__(self): + """ + Should only be on the child class. Also needs to be resolved by the container. + """ + + pass + + def every(self, time): + self.run_every = time + return self + + def every_minute(self): + self.run_every = "1 minute" + return self + + def every_15_minutes(self): + self.run_every = "15 minutes" + return self + + def every_30_minutes(self): + self.run_every = "30 minutes" + return self + + def every_45_minutes(self): + self.run_every = "45 minutes" + return self + + def hourly(self): + self.run_every = "1 hour" + return self + + def daily(self): + self.run_every = "1 day" + return self + + def weekly(self): + self.run_every = "1 week" + return self + + def monthly(self): + self.run_every = "1 month" + return self + + def at(self, run_time): + self.run_at = run_time + return self + + def at_twice(self, run_time): + self.twice_daily = run_time + return self + + def daily_at(self, run_time): + return self.daily().at(run_time) + + def handle(self): + """Fires the task""" + + pass + + def should_run(self, date=None): + """If the task should run""" + + # set the date + self._set_date() + + return self._verify_run() + + def _set_date(self): + if not self._date: + self._date = pendulum.now() + if hasattr(self, "timezone"): + self._date.in_timezone(self.timezone) + + def _verify_run(self): + if self.run_every: + length, frequency = self.run_every.split(" ") + + if frequency in ("minute", "minutes"): + time = int(length) + if self._date.minute == 0 or self._date.minute % time == 0 or time == 1: + return True + + elif frequency in ("hour", "hours"): + time = int(length) + if self._date.hour % time == 0 and self._date.minute == 0: + return True + + elif frequency in ("day", "days"): + time = int(length) + if self._date.day_of_year % time == 0 and ( + self._date.hour == 0 + and self._date.minute == 0 + or self._verify_run_at() + ): + return True + elif frequency in ("month", "months"): + time = int(length) + if ( + self._date.month % time == 0 + and self._date.day == 1 + and ( + self._date.hour == 0 + and self._date.minute == 0 + or (self._date.day == 0 and self._verify_run_at()) + ) + ): + return True + + elif self.run_at: + return self._verify_run_at() + + if self.run_every_minute: + return True + elif self.run_every_hour: + if self._date.hour / 1 == 1: + return True + elif self.twice_daily: + if self._date.hour in self.twice_daily: + return True + + return False + + def _verify_run_at(self): + if self._date.minute < 10: + minute = f"0{self._date.minute}" + else: + minute = self._date.minute + + if f"{self._date.hour}:{minute}" == self.run_at: + return True + + return False diff --git a/src/masonite/scheduling/TaskHandler.py b/src/masonite/scheduling/TaskHandler.py new file mode 100644 index 000000000..56fa4f511 --- /dev/null +++ b/src/masonite/scheduling/TaskHandler.py @@ -0,0 +1,30 @@ +import pendulum +import inspect + + +class TaskHandler: + def __init__(self, application, tasks=None): + if tasks is None: + tasks = [] + + self.tasks = tasks + self.application = application + + def add(self, *tasks): + self.tasks += list(tasks) + + def run(self, run_name=None): + app = self.application + for task_class in self.tasks: + # Resolve the task with the container + if run_name and run_name != task_class.name: + continue + + if inspect.isclass(task_class): + task = app.resolve(task_class) + else: + task = task_class + + # If the class should run then run it + if task.should_run(): + task.handle() diff --git a/src/masonite/scheduling/__init__.py b/src/masonite/scheduling/__init__.py new file mode 100644 index 000000000..23d8fd7be --- /dev/null +++ b/src/masonite/scheduling/__init__.py @@ -0,0 +1,4 @@ +from .CanSchedule import CanSchedule +from .CommandTask import CommandTask +from .Task import Task +from .TaskHandler import TaskHandler diff --git a/src/masonite/scheduling/commands/MakeTaskCommand.py b/src/masonite/scheduling/commands/MakeTaskCommand.py new file mode 100644 index 000000000..7d8d54c21 --- /dev/null +++ b/src/masonite/scheduling/commands/MakeTaskCommand.py @@ -0,0 +1,46 @@ +"""New Task Command """ +import os +import inflection +from cleo import Command +from os.path import exists + +from ...utils.filesystem import make_directory, get_module_dir, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeTaskCommand(Command): + """ + Create a new task + task + {name : Name of the task you want to create} + {--d|--directory=? : Override the directory to create the task in} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + output = render_stub_file(self.get_stub_task_path(), name) + + relative_file_name = os.path.join( + self.option("directory") or as_filepath(self.app.make("tasks.location")), + f"{name}.py", + ) + filepath = base_path(relative_file_name) + + if exists(relative_file_name): + return self.line_error( + f"Task already exists at: {relative_file_name}", style="error" + ) + + make_directory(filepath) + with open(filepath, "w") as fp: + fp.write(output) + + self.info(f"Task Created ({relative_file_name})") + + def get_stub_task_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/scheduling/Task.py") diff --git a/src/masonite/scheduling/commands/ScheduleRunCommand.py b/src/masonite/scheduling/commands/ScheduleRunCommand.py new file mode 100644 index 000000000..976456c60 --- /dev/null +++ b/src/masonite/scheduling/commands/ScheduleRunCommand.py @@ -0,0 +1,21 @@ +""" A ScheduleRunCommand Command """ +import pendulum +import inspect +from cleo import Command + +from ..Task import Task + + +class ScheduleRunCommand(Command): + """ + Run the scheduled tasks + schedule:run + {--t|task=None : Name of task you want to run} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + return self.app.make("scheduler").run() diff --git a/src/masonite/scheduling/commands/__init__.py b/src/masonite/scheduling/commands/__init__.py new file mode 100644 index 000000000..d211b748b --- /dev/null +++ b/src/masonite/scheduling/commands/__init__.py @@ -0,0 +1,2 @@ +from .MakeTaskCommand import MakeTaskCommand +from .ScheduleRunCommand import ScheduleRunCommand diff --git a/src/masonite/scheduling/providers/ScheduleProvider.py b/src/masonite/scheduling/providers/ScheduleProvider.py new file mode 100644 index 000000000..541dbf5d5 --- /dev/null +++ b/src/masonite/scheduling/providers/ScheduleProvider.py @@ -0,0 +1,20 @@ +""" A ScheduleProvider Service Provider """ +from ...providers import Provider + +from ..commands import MakeTaskCommand, ScheduleRunCommand +from ..TaskHandler import TaskHandler + + +class ScheduleProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + self.application.make("commands").add( + MakeTaskCommand(self.application), ScheduleRunCommand(self.application) + ) + + self.application.bind("scheduler", TaskHandler(self.application)) + + def boot(self): + pass diff --git a/src/masonite/scheduling/providers/__init__.py b/src/masonite/scheduling/providers/__init__.py new file mode 100644 index 000000000..67d89c4f8 --- /dev/null +++ b/src/masonite/scheduling/providers/__init__.py @@ -0,0 +1 @@ +from .ScheduleProvider import ScheduleProvider diff --git a/src/masonite/sessions/Session.py b/src/masonite/sessions/Session.py new file mode 100644 index 000000000..0922d7e0b --- /dev/null +++ b/src/masonite/sessions/Session.py @@ -0,0 +1,147 @@ +import json + + +class Session: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self._driver = None + self.driver_config = driver_config or {} + self.options = {} + self.data = {} + self.added = {} + self.flashed = {} + self.deleted = [] + self.deleted_flashed = [] + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def driver(self, driver): + return self.drivers[driver] + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config[self.driver_config.get("default")] + + return self.driver_config.get(driver, {}) + + # Start of methods + def start(self, driver=None): + self.data = {} + self.added = {} + self.flashed = {} + self.deleted = [] + self.deleted_flashed = [] + started_data = self.get_driver(name=driver).start() + self.data = started_data.get("data") + self.flashed = started_data.get("flashed") + return self + + def get_data(self): + data = self.data + data.update(self.added) + data.update(self.flashed) + for deleted in self.deleted: + if deleted in data: + data.pop(deleted) + for deleted in self.deleted_flashed: + if deleted in data: + data.pop(deleted) + return data + + def save(self, driver=None): + return self.get_driver(name=driver).save( + added=self.added, + deleted=self.deleted, + flashed=self.flashed, + deleted_flashed=self.deleted_flashed, + ) + + def set(self, key, value): + try: + if isinstance(value, (dict, list, int)) or ( + isinstance(value, str) and value.isnumeric() + ): + value = json.dumps(value) + except json.decoder.JSONDecodeError: + pass + + return self.added.update({key: value}) + + def increment(self, key, count=1): + return self.set(key, str(int(self.get(key)) + count)) + + def decrement(self, key, count=1): + return self.set(key, str(int(self.get(key)) - count)) + + def has(self, key): + return key in self.added or key in self.flashed + + def get(self, key): + if key in self.flashed: + value = self.flashed.get(key) + + try: + if value is not None: + value = json.loads(value) + except json.decoder.JSONDecodeError: + pass + self.flashed.pop(key) + self.deleted_flashed.append(key) + return value + + value = self.get_data().get(key) + try: + if value is not None: + value = json.loads(value) + except json.decoder.JSONDecodeError: + pass + return value + + def pull(self, key): + key_value = self.get(key) + self.delete(key) + return key_value + + def flush(self): + self.deleted += list(self.get_data().keys()) + + def delete(self, key): + self.deleted.append(key) + if key in self.flashed: + self.flashed.pop(key) + + def flash(self, key, value): + """Add temporary data to the session. + + Arguments: + key {string} -- The key to set as the session key. + value {string} -- The value to set in the session. + """ + try: + if isinstance(value, (dict, list, int)) or ( + isinstance(value, str) and value.isnumeric() + ): + value = json.dumps(value) + except json.decoder.JSONDecodeError: + pass + + self.flashed.update({key: value}) + + def all(self): + """Get all session data. + + Returns: + dict + """ + return self.get_data() diff --git a/src/masonite/sessions/__init__.py b/src/masonite/sessions/__init__.py new file mode 100644 index 000000000..c99ac5368 --- /dev/null +++ b/src/masonite/sessions/__init__.py @@ -0,0 +1 @@ +from .Session import Session diff --git a/src/masonite/snippets/auth/controllers/ConfirmController.py b/src/masonite/snippets/auth/controllers/ConfirmController.py deleted file mode 100644 index cea5df288..000000000 --- a/src/masonite/snippets/auth/controllers/ConfirmController.py +++ /dev/null @@ -1,69 +0,0 @@ -"""The ConfirmController Module.""" -import datetime - -from masonite.auth import Auth, MustVerifyEmail -from masonite.auth.Sign import Sign -from masonite.managers import MailManager -from masonite.request import Request -from masonite.view import View -from masonite.helpers import config - - -class ConfirmController: - """The ConfirmController class.""" - - def __init__(self): - """The ConfirmController Constructor.""" - pass - - def verify_show(self, view: View, auth: Auth): - """Show the Verify Email page for unverified users. - - Arguments: - request {masonite.view.view} -- The Masonite view class. - request {masonite.auth.auth} -- The Masonite Auth class. - - Returns: - [type] -- [description] - """ - return view.render("auth/verify", {"app": config("application"), "Auth": auth}) - - def confirm_email(self, request: Request, view: View, auth: Auth): - """Confirm User email and show the correct response. - - Arguments: - request {masonite.request.request} -- The Masonite request class. - request {masonite.view.view} -- The Masonite view class. - request {masonite.auth.auth} -- The Masonite Auth class. - - Returns: - [type] -- [description] - """ - sign = Sign() - token = sign.unsign(request.param("id")) - if token is not None: - tokenParts = token.split("::") - if len(tokenParts) > 1: - user = auth.auth_model.find(tokenParts[0]) - if user.verified_at or user.verified_at is None: - timestamp = datetime.datetime.fromtimestamp(float(tokenParts[1])) - now = datetime.datetime.now() - timestamp_plus_10 = timestamp + datetime.timedelta(minutes=10) - - if now < timestamp_plus_10: - user.verified_at = datetime.datetime.now() - user.save() - - return view.render( - "auth/confirm", {"app": config("application"), "Auth": auth} - ) - - return view.render("auth/error", {"app": config("application"), "Auth": auth}) - - def send_verify_email(self, manager: MailManager, request: Request): - user = request.user() - - if isinstance(user, MustVerifyEmail): - user.verify_email(manager, request) - - return request.redirect("/home") diff --git a/src/masonite/snippets/auth/controllers/HomeController.py b/src/masonite/snippets/auth/controllers/HomeController.py deleted file mode 100644 index e756b7cfd..000000000 --- a/src/masonite/snippets/auth/controllers/HomeController.py +++ /dev/null @@ -1,17 +0,0 @@ -"""The HomeController Module.""" - -from masonite.auth import Auth -from masonite.request import Request -from masonite.view import View - - -class HomeController: - """Home Dashboard Controller.""" - - def __init__(self): - pass - - def show(self, request: Request, view: View, auth: Auth): - if not auth.user(): - request.redirect("/login") - return view.render("auth/home") diff --git a/src/masonite/snippets/auth/controllers/LoginController.py b/src/masonite/snippets/auth/controllers/LoginController.py deleted file mode 100644 index 6296ce7cd..000000000 --- a/src/masonite/snippets/auth/controllers/LoginController.py +++ /dev/null @@ -1,66 +0,0 @@ -"""A LoginController Module.""" - -from masonite.auth import Auth -from masonite.request import Request -from masonite.validation import Validator -from masonite.view import View - - -class LoginController: - """Login Form Controller.""" - - def __init__(self): - """LoginController Constructor.""" - pass - - def show(self, request: Request, view: View): - """Show the login page. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - view {masonite.view.View} -- The Masonite view class. - - Returns: - masonite.view.View -- Returns the Masonite view class. - """ - if request.user(): - return request.redirect("/home") - - return view.render("auth/login") - - def store(self, request: Request, auth: Auth, validate: Validator): - """Login the user. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - auth {masonite.auth.auth} -- The Masonite auth class. - validate {masonite.validator.Validator} -- The Masonite Validator class. - - Returns: - masonite.request.Request -- The Masonite request class. - """ - errors = request.validate( - validate.required(["email", "password"]), - validate.email("email"), - ) - - if errors: - return request.back().with_errors(errors).with_input() - - if auth.login(request.input("email"), request.input("password")): - return request.redirect("/home") - - return request.back().with_errors({"email": ["Email or password is incorrect"]}) - - def logout(self, request: Request, auth: Auth): - """Log out the user. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - auth {masonite.auth.auth} -- The Masonite auth class. - - Returns: - masonite.request.Request -- The Masonite request class. - """ - auth.logout() - return request.redirect("/login") diff --git a/src/masonite/snippets/auth/controllers/PasswordController.py b/src/masonite/snippets/auth/controllers/PasswordController.py deleted file mode 100644 index 421533afe..000000000 --- a/src/masonite/snippets/auth/controllers/PasswordController.py +++ /dev/null @@ -1,79 +0,0 @@ -"""A PasswordController Module.""" - -import uuid - -from masonite import env, Mail, Session -from masonite.auth import Auth -from masonite.helpers import config, password as bcrypt_password -from masonite.request import Request -from masonite.view import View -from masonite.validation import Validator -from config.auth import AUTH - - -class PasswordController: - """Password Controller.""" - - def forget(self, view: View, auth: Auth): - return view.render("auth/forget", {"app": config("application"), "Auth": auth}) - - def reset(self, view: View, request: Request, auth: Auth): - token = request.param("token") - user = AUTH["guards"]["web"]["model"].where("remember_token", token).first() - if user: - return view.render( - "auth/reset", - {"token": token, "app": config("application"), "Auth": auth}, - ) - - def send(self, request: Request, session: Session, mail: Mail, validate: Validator): - errors = request.validate(validate.required("email"), validate.email("email")) - - if errors: - return request.back().with_errors(errors) - - email = request.input("email") - user = AUTH["guards"]["web"]["model"].where("email", email).first() - - if user: - if not user.remember_token: - user.remember_token = str(uuid.uuid4()) - user.save() - message = "Please visit {}/password/{}/reset to reset your password".format( - env("SITE", "http://localhost:8000"), user.remember_token - ) - mail.subject("Reset Password Instructions").to(user.email).send(message) - - session.flash( - "success", - "If we found that email in our system then the email has been sent. Please follow the instructions in the email to reset your password.", - ) - return request.redirect("/password") - - def update(self, request: Request, validate: Validator): - errors = request.validate( - validate.required("password"), - # TODO: only available in masonite latest versions (which are not compatible with Masonite 2.2) - validate.strong( - "password", - length=8, - special=1, - uppercase=1, - # breach=True checks if the password has been breached before. - # Requires 'pip install pwnedapi' - breach=False, - ), - ) - - if errors: - return request.back().with_errors(errors) - - user = ( - AUTH["guards"]["web"]["model"] - .where("remember_token", request.param("token")) - .first() - ) - if user: - user.password = bcrypt_password(request.input("password")) - user.save() - return request.redirect("/login") diff --git a/src/masonite/snippets/auth/controllers/RegisterController.py b/src/masonite/snippets/auth/controllers/RegisterController.py deleted file mode 100644 index 6feec9a76..000000000 --- a/src/masonite/snippets/auth/controllers/RegisterController.py +++ /dev/null @@ -1,77 +0,0 @@ -"""The RegisterController Module.""" - -from masonite.auth import Auth, MustVerifyEmail -from masonite.managers import MailManager -from masonite.request import Request -from masonite.validation import Validator -from masonite.view import View - - -class RegisterController: - """The RegisterController class.""" - - def __init__(self): - """The RegisterController Constructor.""" - pass - - def show(self, view: View): - """Show the registration page. - - Arguments: - Request {masonite.request.request} -- The Masonite request class. - - Returns: - masonite.view.View -- The Masonite View class. - """ - return view.render("auth/register") - - def store( - self, - request: Request, - mail_manager: MailManager, - auth: Auth, - validate: Validator, - ): - """Register the user with the database. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - - Returns: - masonite.request.Request -- The Masonite request class. - """ - errors = request.validate( - validate.required(["name", "email", "password"]), - validate.email("email"), - validate.strong( - "password", - length=8, - special=1, - uppercase=1, - # breach=True checks if the password has been breached before. - # Requires 'pip install pwnedapi' - breach=False, - ), - ) - - if errors: - return request.back().with_errors(errors).with_input() - - user = auth.register( - { - "name": request.input("name"), - "password": request.input("password"), - "email": request.input("email"), - } - ) - - if isinstance(user, MustVerifyEmail): - user.verify_email(mail_manager, request) - - # Login the user - if auth.login(request.input("email"), request.input("password")): - # Redirect to the homepage - return request.redirect("/home") - - # Login failed. Redirect to the register page. - return request.back().with_input() diff --git a/src/masonite/snippets/auth/templates/auth/base.html b/src/masonite/snippets/auth/templates/auth/base.html deleted file mode 100644 index 4a5f39fc0..000000000 --- a/src/masonite/snippets/auth/templates/auth/base.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - {{ config('application.name') }} - - - {% block css %}{% endblock %} - - - - - - -
- {% block content %}{% endblock %} -
- - - - - diff --git a/src/masonite/snippets/auth/templates/auth/confirm.html b/src/masonite/snippets/auth/templates/auth/confirm.html deleted file mode 100644 index 909a2c71d..000000000 --- a/src/masonite/snippets/auth/templates/auth/confirm.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
Verified
-
- Thank you for confirming your email. Click here to go to the home page. -
-
-
-{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/error.html b/src/masonite/snippets/auth/templates/auth/error.html deleted file mode 100644 index 8cca33c18..000000000 --- a/src/masonite/snippets/auth/templates/auth/error.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
Verifying Error
-
- Confirming email failed. Click here to go home -
-
-
-{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/forget.html b/src/masonite/snippets/auth/templates/auth/forget.html deleted file mode 100644 index e99f4937f..000000000 --- a/src/masonite/snippets/auth/templates/auth/forget.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
-
- {% if session().has('error') %} - - {% endif %} - {% if session().has('success') %} - - {% endif %} -
-
-
Password Reset
-
-
-
- {{ csrf_field }} -
- - - {% if session().has('errors') %} - {% for error in session().get('errors').get('email').items() %} - - {% endfor %} - {% endif %} -
- -
-
-
-
-
-
-
-{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/home.html b/src/masonite/snippets/auth/templates/auth/home.html deleted file mode 100644 index 7967ebe18..000000000 --- a/src/masonite/snippets/auth/templates/auth/home.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-

Dashboard

-

This is a dashboard. Hello {{ auth().name }}

-
-
-{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/login.html b/src/masonite/snippets/auth/templates/auth/login.html deleted file mode 100644 index def049c36..000000000 --- a/src/masonite/snippets/auth/templates/auth/login.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
-
-
-
-
Login
-
-
- {% if bag().any() %} - {% for error in bag().get_errors() %} - - {% endfor %} - {% endif %} -
- {{ csrf_field }} - {{ back() }} -
- - -
-
- - -
- - - - -
-
-
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/src/masonite/snippets/auth/templates/auth/register.html b/src/masonite/snippets/auth/templates/auth/register.html deleted file mode 100644 index 2236f3dd2..000000000 --- a/src/masonite/snippets/auth/templates/auth/register.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
-
-
-
-
Register
-
-
- {% if bag().any() %} - {% for error in bag().get_errors() %} - - {% endfor %} - {% endif %} -
- {{ csrf_field }} -
- - -
-
- - -
-
- - -
- -
-
-
-
-
-
-
-{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/reset.html b/src/masonite/snippets/auth/templates/auth/reset.html deleted file mode 100644 index 201aadd13..000000000 --- a/src/masonite/snippets/auth/templates/auth/reset.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
-
-
-
-
Reset Password
-
-
- {% if bag().any() %} - {% for error in bag().get_errors() %} - - {% endfor %} - {% endif %} -
- {{ csrf_field }} -
- - -
- -
-
-
-
-
-
-
-{% endblock %} \ No newline at end of file diff --git a/src/masonite/snippets/auth/templates/auth/verify.html b/src/masonite/snippets/auth/templates/auth/verify.html deleted file mode 100644 index 1d588a787..000000000 --- a/src/masonite/snippets/auth/templates/auth/verify.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -
-
-
Verify
-
- Please check your email and follow the link to verify your email. If - you need us to resend the email. Click here -
-
-
-{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/verifymail.html b/src/masonite/snippets/auth/templates/auth/verifymail.html deleted file mode 100644 index b3841b731..000000000 --- a/src/masonite/snippets/auth/templates/auth/verifymail.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - Document - - - - - - - - - - -
-
- - - - -
- - - - - - - - - - - - - -
- Please confirm your email address by clicking the link below. -
- We may need to send you critical information about our service and it is important that we have an accurate email address. -
- Confirm Email Address -
- — Masonite Framework -
-
- -
-
- - diff --git a/src/masonite/snippets/exception.html b/src/masonite/snippets/exception.html deleted file mode 100644 index 4fd55e622..000000000 --- a/src/masonite/snippets/exception.html +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - - - {{ tb[0].__name__ }} > {{ exception }} - - - - - - - - -
- - -
-
-
-
-
-

StackTrace

-
-
-
-
- {% for stack in stacktrace %} - {% if 'site-packages' in stack[0] %} - {% set local_file_background_color = 'inherit' %} - {% else %} - {% set local_file_background_color = '#d9ffc2' %} - {% endif %} -
-

{{ stack[0] }}, line {{ stack[1] }} in {{ stack[2]}}

- - {% for i, line in enumerate(open(stack[0])) %} - {% if stack[1] - 5 <= i <= stack[1] + 5 %} - - {% if i == stack[1] - 1 %} -
- {{ i + 1 }}. {{ line }} -
- {% elif stack[1] - 3 <= i <= stack[1] + 1 %} -
- {{ i + 1 }}. {{ line }} -
- {% else %} -
- {{ i + 1 }}. {{ line }} -
- {% endif %} - - {% endif %} - {% endfor %} - -
- {% endfor %} -
-
-
-
-
-
-
- - - -

Request

- -
-
-
-

Request Method

-
-
-
-
-

{{ app.make('Request').environ['REQUEST_METHOD'] }}

-
-
-
-
-
-
-

Path

-
-
-
-
-

{{ app.make('Request').path }}

-
-
-
-
-
-
-

Inputs

-
-
-
-
- {% for key in app.make('Request').all() %} -

{{ key }}={{ app.make('Request').input(key) }}

- {% else %} -

None

- {% endfor %} -
-
-
-
-
-
-

Parameters

-
-
-
-
- {% for key in app.make('Request').url_params %} -

{{ key }}={{ app.make('Request').param(key) }}

- {% else %} -

None

- {% endfor %} -
-
-
-
-
-
-

Headers

-
-
-
-
- {% for header, value in app.make('Environ').items() %} - {% if header.startswith('HTTP_') %} -

{{ header }}: {{ value }}

- {% endif %} - {% else %} -

None

- {% endfor %} -
-
-
- -
- -

Container

- -
-
-
-

Provider Classes

-
-
-
-
-
{{ app.bind('Response', '') }}
- {% for provider, class in app.providers.items() %} -

{{ provider }} - - {% if platform.system() != 'Windows' %} - - {{ class }} - {% endif %} -

- {% endfor %} - -
-
-
-
-
-
-

Environ

-
-
-
-
- {% for key, value in app.make('Environ').items() %} -

{{ key }} - {{ value }}

- {% endfor %} - -
-
-
-
-
-
-

Routes

-
-
-
-
- {% for route in app.make('WebRoutes') %} -

URL: {{ route.route_url }}

-

Method: {{ route.method_type }}

-

Name: {{ route.named_route }}

-

Module: {{ route.module_location }}

-

Route Middleware: - {% for middleware in route.list_middleware %} - {{ middleware }}, - {% else %} - None - {% endfor %} -

- -
- {% endfor %} - -
-
-
-
-
-
-

Service Providers

-
-
-
-
- {% for provider in providers %} -

{{ provider }}

- {% endfor %} -
-
-
-
-
-
-

Static File Directories

-
-
-
-
- {% for directory in app.make('staticfiles') %} -

- {{ directory}} -

- {% else %} -

None

- {% endfor %} -
-
-
- - -
- - - \ No newline at end of file diff --git a/src/masonite/snippets/exceptions/css/go-icon.png b/src/masonite/snippets/exceptions/css/go-icon.png deleted file mode 100644 index fcc918cca9e2cc625626b05e9676c7fcd6998f80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15196 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 PkC45!lhu6-kGTH;mfIZT diff --git a/src/masonite/snippets/exceptions/css/style.css b/src/masonite/snippets/exceptions/css/style.css deleted file mode 100644 index 3d98e436c..000000000 --- a/src/masonite/snippets/exceptions/css/style.css +++ /dev/null @@ -1,5 +0,0 @@ - -.breadcrumb{background-color:transparent;border-bottom:1px solid #eee;padding-top:12px;padding-bottom:12px;margin-bottom:40px}nav.navbar.navbar-default{margin-bottom:0}@media (min-width:992px){.product h2{margin-top:0}}.navbar .navbar-brand{font-size:24px;line-height:18px}.reviewer-name{margin-right:10px}.site-footer{padding:20px 0;text-align:center}@media (min-width:768px){.site-footer h5{text-align:left}}.site-footer h5{color:inherit;font-size:16px}.site-footer .social-icons a:hover{opacity:1}.site-footer .social-icons a{display:inline-block;width:32px;border:none;font-size:20px;border-radius:50%;margin:4px;color:#fff;text-align:center;background-color:#798FA5;height:32px;opacity:.8;line-height:32px}@media (min-width:768px){.site-footer .social-icons{text-align:right}}.btn.write-review{float:right;margin-top:-6px} -.stack-row:nth-child(even) { - background-color: white !important; -} \ No newline at end of file diff --git a/src/masonite/snippets/exceptions/obj_loop.html b/src/masonite/snippets/exceptions/obj_loop.html deleted file mode 100644 index bd39033b6..000000000 --- a/src/masonite/snippets/exceptions/obj_loop.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
- - {% if type(getattr(obj, key)) in show_methods %} - - {% if key.startswith('__') %} - - - {% elif key.startswith('_') %} - # - {% else %} - + - {% endif %} - {{ key }} - - - {% if not hasattr(property, '__self__') %} - {{ type(getattr(obj, key)).__name__ }} - {% if type(getattr(obj, key)) == dict %} - : {{ len(getattr(obj, key)) }} - {% if len(getattr(obj, key)) %} -
- {% for key, value in getattr(obj, key).items() %} -
{{ key }}: {{ value }}
- {% endfor %} - {% endif %} - {% elif type(getattr(obj, key)) == list %} - : {{ len(getattr(obj, key)) }} - {% if len(getattr(obj, key)) %} -
- {% for key in getattr(obj, key) %} - {% if isinstance(key, Model) %} -
{{ key }}
- {% endif %} - {% endfor %} - {% endif %} - {% else %} - {{ property }} - {% endif %} - {% endif %} - {% endif %} -
-
- -
\ No newline at end of file diff --git a/src/masonite/snippets/scaffold/command.html b/src/masonite/snippets/scaffold/command.html deleted file mode 100644 index 5229a2839..000000000 --- a/src/masonite/snippets/scaffold/command.html +++ /dev/null @@ -1,15 +0,0 @@ -"""A {{ class }} Command.""" -from cleo import Command - - -class {{ class }}(Command): - """ - Description of command - - command:name - {argument : description} - """ - - def handle(self): - pass - diff --git a/src/masonite/snippets/scaffold/controller.html b/src/masonite/snippets/scaffold/controller.html deleted file mode 100644 index 21ca72ac6..000000000 --- a/src/masonite/snippets/scaffold/controller.html +++ /dev/null @@ -1,21 +0,0 @@ -"""A {{ class }} Module.""" - -from masonite.request import Request -from masonite.view import View -from masonite.controllers import Controller - - -class {{ class }}(Controller): - """{{ class }} Controller Class.""" - - def __init__(self, request: Request): - """{{ class }} Initializer - - Arguments: - request {masonite.request.Request} -- The Masonite Request class. - """ - self.request = request - - def show(self, view: View): - pass - diff --git a/src/masonite/snippets/scaffold/controller_resource.html b/src/masonite/snippets/scaffold/controller_resource.html deleted file mode 100644 index 2c274146d..000000000 --- a/src/masonite/snippets/scaffold/controller_resource.html +++ /dev/null @@ -1,61 +0,0 @@ -""" A {{ class }} Module """ - -from masonite.controllers import Controller - - -class {{ class }}(Controller): - """Class Docstring Description - """ - - def show(self): - """Show a single resource listing - ex. Model.find('id') - Get().route("/show", {{ class }}) - """ - - pass - - def index(self): - """Show several resource listings - ex. Model.all() - Get().route("/index", {{ class }}) - """ - - pass - - def create(self): - """Show form to create new resource listings - ex. Get().route("/create", {{ class }}) - """ - - pass - - def store(self): - """Create a new resource listing - ex. Post target to create new Model - Post().route("/store", {{ class }}) - """ - - pass - - def edit(self): - """Show form to edit an existing resource listing - ex. Get().route("/edit", {{ class }}) - """ - - pass - - def update(self): - """Edit an existing resource listing - ex. Post target to update new Model - Post().route("/update", {{ class }}) - """ - - pass - - def destroy(self): - """Delete an existing resource listing - ex. Delete().route("/destroy", {{ class }}) - """ - - pass diff --git a/src/masonite/snippets/scaffold/job.html b/src/masonite/snippets/scaffold/job.html deleted file mode 100644 index 6f628b3ff..000000000 --- a/src/masonite/snippets/scaffold/job.html +++ /dev/null @@ -1,16 +0,0 @@ -"""A {{ class }} Queue Job.""" - -from masonite.queues import Queueable - - -class {{ class }}(Queueable): - """A {{ class }} Job.""" - - def __init__(self): - """A {{ class }} Constructor.""" - pass - - def handle(self): - """Logic to handle the job.""" - pass - diff --git a/src/masonite/snippets/scaffold/mailable.html b/src/masonite/snippets/scaffold/mailable.html deleted file mode 100644 index 2bf5fb3f7..000000000 --- a/src/masonite/snippets/scaffold/mailable.html +++ /dev/null @@ -1,21 +0,0 @@ -"""A {{ class }} Mailable.""" - -from masonite.drivers import Mailable - - -class {{ class }}(Mailable): - """A {{ class }} Mailable.""" - - def __init__(self, to): - """A {{ class }} Initializer.""" - self._to = to - - def build(self): - """Logic to handle the job.""" - return ( - self.subject('Subject Line') - .send_from('admin@example.com') - .reply_to('service@example.com') - .to(self._to) - # .view('template') - ) diff --git a/src/masonite/snippets/scaffold/middleware.html b/src/masonite/snippets/scaffold/middleware.html deleted file mode 100644 index a76832962..000000000 --- a/src/masonite/snippets/scaffold/middleware.html +++ /dev/null @@ -1,24 +0,0 @@ -"""{{ class }} Middleware.""" - -from masonite.request import Request - - -class {{ class }}Middleware: - """{{ class }} Middleware.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container. - - Arguments: - Request {masonite.request.Request} -- The Masonite request object - """ - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - pass - - def after(self): - """Run This Middleware After The Route Executes.""" - pass - diff --git a/src/masonite/snippets/scaffold/model.html b/src/masonite/snippets/scaffold/model.html deleted file mode 100644 index 260745b53..000000000 --- a/src/masonite/snippets/scaffold/model.html +++ /dev/null @@ -1,8 +0,0 @@ -"""{{ class }} Model.""" - -from masoniteorm.models import Model - - -class {{ class }}(Model): - """{{ class }} Model.""" - pass diff --git a/src/masonite/snippets/scaffold/provider.html b/src/masonite/snippets/scaffold/provider.html deleted file mode 100644 index 0f82fe695..000000000 --- a/src/masonite/snippets/scaffold/provider.html +++ /dev/null @@ -1,18 +0,0 @@ -"""A {{ class }} Service Provider.""" - -from masonite.provider import ServiceProvider - - -class {{ class }}(ServiceProvider): - """Provides Services To The Service Container.""" - - wsgi = False - - def register(self): - """Register objects into the Service Container.""" - pass - - def boot(self): - """Boots services required by the container.""" - pass - diff --git a/src/masonite/snippets/scaffold/test.html b/src/masonite/snippets/scaffold/test.html deleted file mode 100644 index f192d0950..000000000 --- a/src/masonite/snippets/scaffold/test.html +++ /dev/null @@ -1,23 +0,0 @@ -"""{{ class }} Testcase.""" - -from masonite.testing import TestCase - - -class {{ class }}(TestCase): - - """All tests by default will run inside of a database transaction.""" - transactions = True - - def setUp(self): - """Anytime you override the setUp method you must call the setUp method - on the parent class like below. - """ - super().setUp() - - def setUpFactories(self): - """This runs when the test class first starts up. - This does not run before every test case. Use this method to - set your database up. - """ - pass - diff --git a/src/masonite/snippets/scaffold/validator.html b/src/masonite/snippets/scaffold/validator.html deleted file mode 100644 index d9b1bcdc6..000000000 --- a/src/masonite/snippets/scaffold/validator.html +++ /dev/null @@ -1,8 +0,0 @@ -"""A {{ class }} Validator""" - -from masonite.validator import Validator - - -class {{ class }}(Validator): - pass - diff --git a/src/masonite/snippets/statuscode.html b/src/masonite/snippets/statuscode.html deleted file mode 100644 index 8d14b6127..000000000 --- a/src/masonite/snippets/statuscode.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - {{ code }} - - - - - - - - - -
- -
-
- {{ code }} -
- -
-
- - - \ No newline at end of file diff --git a/src/masonite/storage.py b/src/masonite/storage.py deleted file mode 100644 index bdea7fa63..000000000 --- a/src/masonite/storage.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Module for Storage class.""" - -import os - - -class Storage: - """Responsible for compiling Sass.""" - - def __init__(self): - """Storage constructor.""" - pass - - def compile_sass(self): - """Compile sass.""" - from config import application, storage - - try: - import sass - except ImportError: - pass - else: - matches = [] - for files in storage.SASSFILES["importFrom"]: - for root, _, filenames in os.walk( - os.path.join(application.BASE_DIRECTORY, files) - ): - for filename in filenames: - if filename.endswith( - (".sass", ".scss") - ) and not filename.startswith("_"): - matches.append(os.path.join(root, filename)) - - for filename in matches: - with open(filename) as f: - compiled_sass = sass.compile( - string=f.read(), include_paths=storage.SASSFILES["includePaths"] - ) - name = ( - filename.split(os.sep)[-1] - .replace(".scss", "") - .replace(".sass", "") - ) - write_file = os.path.join( - os.path.join( - application.BASE_DIRECTORY, storage.SASSFILES["compileTo"] - ), - "{0}.css".format(name), - ) - with open(write_file, "w") as r: - r.write(compiled_sass) diff --git a/src/masonite/storage/__init__.py b/src/masonite/storage/__init__.py new file mode 100644 index 000000000..abff98db6 --- /dev/null +++ b/src/masonite/storage/__init__.py @@ -0,0 +1 @@ +from .storage import StorageCapsule diff --git a/src/masonite/storage/storage.py b/src/masonite/storage/storage.py new file mode 100644 index 000000000..46acb4b21 --- /dev/null +++ b/src/masonite/storage/storage.py @@ -0,0 +1,10 @@ +class StorageCapsule: + def __init__(self): + self.storage_templates = {} + + def add_storage_assets(self, templates): + self.storage_templates.update(templates) + return self + + def get_storage_assets(self): + return self.storage_templates diff --git a/src/masonite/stubs/controllers/Controller.py b/src/masonite/stubs/controllers/Controller.py new file mode 100644 index 000000000..a42e6e35b --- /dev/null +++ b/src/masonite/stubs/controllers/Controller.py @@ -0,0 +1,7 @@ +from masonite.controllers import Controller +from masonite.views import View + + +class __class__(Controller): + def show(self, view: View): + return view.render("") diff --git a/src/masonite/stubs/controllers/auth/HomeController.py b/src/masonite/stubs/controllers/auth/HomeController.py new file mode 100644 index 000000000..c44ee0f1a --- /dev/null +++ b/src/masonite/stubs/controllers/auth/HomeController.py @@ -0,0 +1,7 @@ +from masonite.controllers import Controller +from masonite.views import View + + +class HomeController(Controller): + def show(self, view: View): + return view.render("auth.home") diff --git a/src/masonite/stubs/controllers/auth/LoginController.py b/src/masonite/stubs/controllers/auth/LoginController.py new file mode 100644 index 000000000..dcc25cb39 --- /dev/null +++ b/src/masonite/stubs/controllers/auth/LoginController.py @@ -0,0 +1,19 @@ +from masonite.controllers import Controller +from masonite.views import View +from masonite.request import Request +from masonite.response import Response +from masonite.authentication import Auth + + +class LoginController(Controller): + def show(self, view: View): + return view.render("auth.login") + + def store(self, view: View, request: Request, auth: Auth, response: Response): + login = auth.attempt(request.input("username"), request.input("password")) + + if login: + return response.redirect(name="home") + + # Go back to login page + return response.redirect(name="login") diff --git a/src/masonite/stubs/controllers/auth/PasswordResetController.py b/src/masonite/stubs/controllers/auth/PasswordResetController.py new file mode 100644 index 000000000..2d432d9c5 --- /dev/null +++ b/src/masonite/stubs/controllers/auth/PasswordResetController.py @@ -0,0 +1,28 @@ +from masonite.controllers import Controller +from masonite.views import View +from masonite.request import Request +from masonite.response import Response +from masonite.authentication import Auth + + +class PasswordResetController(Controller): + def show(self, view: View): # Show password_reset page + return view.render("auth.password_reset") + + def store( + self, auth: Auth, request: Request, response: Response + ): # store password_reset record + auth.password_reset(request.input("email")) + return "event fired" + + def change_password(self, view: View): # store password_reset record + return view.render("auth.change_password") + + def store_changed_password( + self, auth: Auth, request: Request, response: Response + ): # store password_reset record + auth.reset_password(request.input("password"), request.input("token")) + + # Need to validate?? + # Redirect back? + return response.back() diff --git a/src/masonite/stubs/controllers/auth/RegisterController.py b/src/masonite/stubs/controllers/auth/RegisterController.py new file mode 100644 index 000000000..1c2089af7 --- /dev/null +++ b/src/masonite/stubs/controllers/auth/RegisterController.py @@ -0,0 +1,20 @@ +from masonite.controllers import Controller +from masonite.views import View +from masonite.request import Request +from masonite.response import Response +from masonite.authentication import Auth + + +class RegisterController(Controller): + def show(self, view: View): # Show register page + return view.render("auth.register") + + def store( + self, auth: Auth, request: Request, response: Response + ): # store register user + user = auth.register(request.only("name", "email", "password")) + + if not user: + return response.redirect("/register") + + return response.redirect("/home") diff --git a/src/masonite/stubs/events/event.py b/src/masonite/stubs/events/event.py new file mode 100644 index 000000000..a6853fded --- /dev/null +++ b/src/masonite/stubs/events/event.py @@ -0,0 +1,2 @@ +class __class__: + pass diff --git a/src/masonite/stubs/events/listener.py b/src/masonite/stubs/events/listener.py new file mode 100644 index 000000000..00879d870 --- /dev/null +++ b/src/masonite/stubs/events/listener.py @@ -0,0 +1,3 @@ +class __class__: + def handle(self, event): + pass diff --git a/src/masonite/stubs/jobs/Job.py b/src/masonite/stubs/jobs/Job.py new file mode 100644 index 000000000..8845bc01b --- /dev/null +++ b/src/masonite/stubs/jobs/Job.py @@ -0,0 +1,6 @@ +from masonite.queues import Queueable + + +class __class__(Queueable): + def handle(self): + pass diff --git a/src/masonite/stubs/mailable/Mailable.py b/src/masonite/stubs/mailable/Mailable.py new file mode 100644 index 000000000..afe38eea9 --- /dev/null +++ b/src/masonite/stubs/mailable/Mailable.py @@ -0,0 +1,12 @@ +from masonite.mail import Mailable + + +class __class__(Mailable): + def build(self): + return ( + self.to("user@gmail.com") + .subject("Masonite 4") + .from_("admin@gmail.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + ) diff --git a/src/masonite/stubs/notification/Notification.py b/src/masonite/stubs/notification/Notification.py new file mode 100644 index 000000000..b4d6330b9 --- /dev/null +++ b/src/masonite/stubs/notification/Notification.py @@ -0,0 +1,15 @@ +from masonite.notification import Notification +from masonite.mail import Mailable + + +class __class__(Notification, Mailable): + def to_mail(self, notifiable): + return ( + self.to(notifiable.email) + .subject("Masonite 4") + .from_("hello@email.com") + .text(f"Hello {notifiable.name}") + ) + + def via(self, notifiable): + return ["mail"] diff --git a/src/masonite/stubs/notification/create_notifications_table.py b/src/masonite/stubs/notification/create_notifications_table.py new file mode 100644 index 000000000..af83657ca --- /dev/null +++ b/src/masonite/stubs/notification/create_notifications_table.py @@ -0,0 +1,17 @@ +from masoniteorm.migrations import Migration + + +class CreateNotificationsTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("notifications") as table: + table.big_increments("id").primary() + table.string("type") + table.text("data") + table.morphs("notifiable") + table.datetime("read_at").nullable() + table.timestamps() + + def down(self): + """Revert the migrations.""" + self.schema.drop("notifications") diff --git a/src/masonite/stubs/policies/ModelPolicy.py b/src/masonite/stubs/policies/ModelPolicy.py new file mode 100644 index 000000000..681ea4835 --- /dev/null +++ b/src/masonite/stubs/policies/ModelPolicy.py @@ -0,0 +1,24 @@ +from masonite.authorization import Policy + + +class __class__(Policy): + def create(self, user): + return False + + def view_any(self, user): + return False + + def view(self, user, instance): + return False + + def update(self, user, instance): + return False + + def delete(self, user, instance): + return False + + def force_delete(self, user, instance): + return False + + def restore(self, user, instance): + return False diff --git a/src/masonite/stubs/policies/Policy.py b/src/masonite/stubs/policies/Policy.py new file mode 100644 index 000000000..5d5b29eb3 --- /dev/null +++ b/src/masonite/stubs/policies/Policy.py @@ -0,0 +1,6 @@ +from masonite.authorization import Policy + + +class __class__(Policy): + def view_admin(self, user): + return False diff --git a/src/masonite/stubs/providers/Provider.py b/src/masonite/stubs/providers/Provider.py new file mode 100644 index 000000000..16b7f9f23 --- /dev/null +++ b/src/masonite/stubs/providers/Provider.py @@ -0,0 +1,12 @@ +from masonite.providers import Provider + + +class __class__(Provider): + def __init__(self, application): + self.application = application + + def register(self): + pass + + def boot(self): + pass diff --git a/src/masonite/snippets/migrations/create_failed_jobs_table.py b/src/masonite/stubs/queue/create_failed_jobs_table.py similarity index 51% rename from src/masonite/snippets/migrations/create_failed_jobs_table.py rename to src/masonite/stubs/queue/create_failed_jobs_table.py index 1a9cd749e..ca039e888 100644 --- a/src/masonite/snippets/migrations/create_failed_jobs_table.py +++ b/src/masonite/stubs/queue/create_failed_jobs_table.py @@ -6,12 +6,14 @@ def up(self): """Run the migrations.""" with self.schema.create("failed_jobs") as table: table.increments("id") - table.string("queue") - table.string("driver") - table.string("channel") + table.string("queue").nullable() + table.string("connection").nullable() + table.string("name").nullable() + table.string("driver").nullable() table.binary("payload") - table.timestamp("failed_at") - table.timestamps() + table.text("exception").nullable() + table.timestamp("failed_at").nullable() + table.timestamp("created_at").nullable() def down(self): """Revert the migrations.""" diff --git a/src/masonite/snippets/migrations/create_queue_jobs_table.py b/src/masonite/stubs/queue/create_queue_jobs_table.py similarity index 60% rename from src/masonite/snippets/migrations/create_queue_jobs_table.py rename to src/masonite/stubs/queue/create_queue_jobs_table.py index 3ee3e3102..f4c27b11d 100644 --- a/src/masonite/snippets/migrations/create_queue_jobs_table.py +++ b/src/masonite/stubs/queue/create_queue_jobs_table.py @@ -4,18 +4,17 @@ class CreateQueueJobsTable(Migration): def up(self): """Run the migrations.""" - with self.schema.create("queue_jobs") as table: + with self.schema.create("jobs") as table: table.increments("id") - table.string("queue") table.string("name") - table.binary("serialized") + table.string("queue") + table.binary("payload") table.integer("attempts") - table.integer("failed").nullable() table.timestamp("ran_at").nullable() - table.timestamp("created_at").nullable() - table.timestamp("available_at").nullable() + table.timestamp("available_at", now=True).nullable() table.timestamp("reserved_at").nullable() + table.timestamp("created_at", now=True).nullable() def down(self): """Revert the migrations.""" - self.schema.drop("queue_jobs") + self.schema.drop("jobs") diff --git a/src/masonite/stubs/scheduling/task.py b/src/masonite/stubs/scheduling/task.py new file mode 100644 index 000000000..f523ae31c --- /dev/null +++ b/src/masonite/stubs/scheduling/task.py @@ -0,0 +1,7 @@ +"""Task Module Description""" +from masonite.scheduling import Task + + +class __class__(Task): + def handle(self): + pass diff --git a/src/masonite/stubs/templates/auth/base.html b/src/masonite/stubs/templates/auth/base.html new file mode 100644 index 000000000..015d2dee8 --- /dev/null +++ b/src/masonite/stubs/templates/auth/base.html @@ -0,0 +1,13 @@ + + + + + + + Masonite 4 + + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/change_password.html b/src/masonite/stubs/templates/auth/change_password.html new file mode 100644 index 000000000..158fd756b --- /dev/null +++ b/src/masonite/stubs/templates/auth/change_password.html @@ -0,0 +1,49 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ + +

+ Password Reset Request +

+ +
+ + + + + + + + + + +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/home.html b/src/masonite/stubs/templates/auth/home.html new file mode 100644 index 000000000..51c6eef7b --- /dev/null +++ b/src/masonite/stubs/templates/auth/home.html @@ -0,0 +1,17 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ Welcome! +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/login.html b/src/masonite/stubs/templates/auth/login.html new file mode 100644 index 000000000..fd22091f4 --- /dev/null +++ b/src/masonite/stubs/templates/auth/login.html @@ -0,0 +1,66 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/password_reset.html b/src/masonite/stubs/templates/auth/password_reset.html new file mode 100644 index 000000000..9de6964be --- /dev/null +++ b/src/masonite/stubs/templates/auth/password_reset.html @@ -0,0 +1,40 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ + +

+ Password Reset Request +

+ +
+ + + + + + +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/register.html b/src/masonite/stubs/templates/auth/register.html new file mode 100644 index 000000000..a03f122eb --- /dev/null +++ b/src/masonite/stubs/templates/auth/register.html @@ -0,0 +1,72 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ + +

+ New Account +

+ +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/validation/Rule.py b/src/masonite/stubs/validation/Rule.py new file mode 100644 index 000000000..4838dbbb9 --- /dev/null +++ b/src/masonite/stubs/validation/Rule.py @@ -0,0 +1,48 @@ +"""__class__ validation""" + +from masonite.validation import BaseValidation + + +class __class__(BaseValidation): + """__class__ validation class""" + + def passes(self, attribute, key, dictionary): + """The passing criteria for this rule. + + This should return a True boolean value. + + Arguments: + attribute {mixed} -- The value found within the dictionary + key {string} -- The key in the dictionary being searched for. + This key may or may not exist in the dictionary. + dictionary {dict} -- The dictionary being searched + + Returns: + bool + """ + return attribute + + def message(self, key): + """A message to show when this rule fails + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return f"{key} is required" + + def negated_message(self, key): + """A message to show when this rule is negated using a negation rule like 'isnt()' + + For example if you have a message that says 'this is required' you may have a negated statement + that says 'this is not required'. + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return "{key} is not required" diff --git a/src/masonite/stubs/validation/RuleEnclosure.py b/src/masonite/stubs/validation/RuleEnclosure.py new file mode 100644 index 000000000..8d4adaf95 --- /dev/null +++ b/src/masonite/stubs/validation/RuleEnclosure.py @@ -0,0 +1,18 @@ +""" __class__ Validation Enclosure """ + +from masonite.validation import RuleEnclosure + + +class __class__(RuleEnclosure): + """__class__ Validation Enclosure Class.""" + + def rules(self): + """Used to return a list of rules in order to make validation + more reusable. + + Returns: + list -- List of rules + """ + return [ + # Rules go here + ] diff --git a/src/masonite/snippets/__init__.py b/src/masonite/templates/__init__.py similarity index 100% rename from src/masonite/snippets/__init__.py rename to src/masonite/templates/__init__.py diff --git a/src/masonite/snippets/exceptions/dump.html b/src/masonite/templates/dump.html similarity index 100% rename from src/masonite/snippets/exceptions/dump.html rename to src/masonite/templates/dump.html diff --git a/src/masonite/templates/obj_loop.html b/src/masonite/templates/obj_loop.html new file mode 100644 index 000000000..1bd5d3141 --- /dev/null +++ b/src/masonite/templates/obj_loop.html @@ -0,0 +1,43 @@ +
+
+ + {% if type(getattr(obj, key)) in show_methods %} + + {% if key.startswith('__') %} + - + {% elif key.startswith('_') %} + # + {% else %} + + + {% endif %} + {{ key }} + - + {% if not hasattr(property, '__self__') %} + {{ type(getattr(obj, key)).__name__ }} + {% if type(getattr(obj, key)) == dict %} + : {{ len(getattr(obj, key)) }} + {% if len(getattr(obj, key)) %} +
+ {% for key, value in getattr(obj, key).items() %} +
{{ key }}: {{ value }}
+ {% endfor %} + {% endif %} + {% elif type(getattr(obj, key)) == list %} + : {{ len(getattr(obj, key)) }} + {% if len(getattr(obj, key)) %} +
+ {% for key in getattr(obj, key) %} + {% if hasattr(key, 'serialize') %} +
{{ key.serialize() }}
+ {% endif %} + {% endfor %} + {% endif %} + {% else %} + {{ property }} + {% endif %} + {% endif %} + {% endif %} +
+
+ +
\ No newline at end of file diff --git a/src/masonite/testing/BaseRequest.py b/src/masonite/testing/BaseRequest.py deleted file mode 100644 index 61d6038dc..000000000 --- a/src/masonite/testing/BaseRequest.py +++ /dev/null @@ -1,18 +0,0 @@ -class BaseRequest: - def user(self, obj): - self._user = obj - self.container.on_resolve("Request", self._bind_user_to_request) - wsgi = generate_wsgi() - wsgi["PATH_INFO"] = self.url - self._run_container(wsgi) - - return self - - def ok(self): - return self.status("200 OK") - - def status(self, value=None): - if not value: - return self.container.make("Request").get_status_code() - - return self.container.make("Request").get_status_code() == value diff --git a/src/masonite/testing/MockRoute.py b/src/masonite/testing/MockRoute.py deleted file mode 100644 index ffacf7eb9..000000000 --- a/src/masonite/testing/MockRoute.py +++ /dev/null @@ -1,381 +0,0 @@ -import json - -from ..view import View -from ..helpers import Dot -from ..request import Request -from ..response import Response - - -class MockRoute: - def __init__(self, route, container, wsgi=None): - self.route = route - self.container = container - self.wsgi = wsgi - - def assertIsNamed(self, name): - assert self.route.named_route == name, "Route name is {}. Asserted {}".format( - self.route.named_route, name - ) - return self - - def assertIsNotNamed(self): - assert self.route.named_route is None, "Route has a name: {}".format( - self.route.named_route - ) - return self - - def isNamed(self, name): - return self.route.named_route == name - - def hasMiddleware(self, *middleware): - return all(elem in self.route.list_middleware for elem in middleware) - - def hasController(self, controller): - return self.route.controller == controller - - def ensure_argument_is_controller_name(self, controller_name): - return isinstance(controller_name, str) and "@" in controller_name - - def assertHasController(self, controller): - if self.ensure_argument_is_controller_name(controller): - controller, method = controller.split("@") - assert ( - self.route.controller.__name__ == controller - ), "Controller is {}. Asserted {}".format( - self.route.controller.__name__, controller - ) - assert ( - self.route.controller_method == method - ), "Controller method is {}. Asserted {}".format( - self.route.controller_method, method - ) - - return self - - def contains(self, value): - return value in self.container.make(Response).content.decode("utf-8") - - def assertContains(self, value): - assert self.contains(value), "Response does not contain {}".format(value) - return self - - def assertNotFound(self): - return self.assertIsStatus(404) - - def ok(self): - return "200 OK" in self.container.make(Response).get_status_code() - - def canView(self): - return self.ok() - - def get_string_response(self): - response = self.container.make(Response).content - - if isinstance(response, str): - return response - - return response.decode("utf-8") - - def hasJson(self, key, value=""): - - response = json.loads(self.get_string_response()) - if isinstance(key, dict): - for item_key, key_value in key.items(): - if not Dot().dot(item_key, response, False) == key_value: - return False - return True - return Dot().dot(key, response, False) - - def assertHasJson(self, key, value): - response = json.loads(self.get_string_response()) - if isinstance(key, dict): - for item_key, key_value in key.items(): - assert Dot().dot(item_key, response, False) == key_value - else: - assert ( - Dot().dot(key, response, False) == value - ), "Key '{}' with the value of '{}' could not find a match in {}".format( - key, value, response - ) - return self - - def assertJsonContains(self, key, value): - response = json.loads(self.get_string_response()) - if not isinstance(response, list): - raise ValueError( - "This method can only be used if the response is a list of elements." - ) - - found = False - for element in response: - if Dot().dot(key, element, False): - assert Dot().dot(key, element, False) - found = True - - if not found: - raise AssertionError( - "Could not find a key of: {} that had the value of {}".format( - key, value - ) - ) - return self - - def count(self, amount): - return len(json.loads(self.get_string_response())) == amount - - def assertCount(self, amount): - response_amount = len(json.loads(self.get_string_response())) - assert ( - response_amount == amount - ), "Response has an count of {}. Asserted {}".format(response_amount, amount) - return self - - def amount(self, amount): - return self.count(amount) - - def hasAmount(self, key, amount): - response = json.loads(self.get_string_response()) - try: - return len(response[key]) == amount - except TypeError: - raise TypeError( - "The json response key of: {} is not iterable but has the value of {}".format( - key, response[key] - ) - ) - - def assertHasAmount(self, key, amount): - response = json.loads(self.get_string_response()) - try: - assert len(response[key]) == amount, "{} is not equal to {}".format( - len(response[key]), amount - ) - except TypeError: - raise TypeError( - "The json response key of: {} is not iterable but has the value of {}".format( - key, response[key] - ) - ) - - return self - - def assertNotHasAmount(self, key, amount): - response = json.loads(self.get_string_response()) - try: - assert ( - not len(response[key]) == amount - ), "{} is equal to {} but should not be".format(len(response[key]), amount) - except TypeError: - raise TypeError( - "The json response key of: {} is not iterable but has the value of {}".format( - key, response[key] - ) - ) - - return self - - def user(self, obj): - self._user = obj - self.container.on_resolve(Request, self._bind_user_to_request) - return self - - def isPost(self): - return "POST" in self.route.method_type - - def isGet(self): - return "GET" in self.route.method_type - - def isPut(self): - return "PUT" in self.route.method_type - - def isPatch(self): - return "PATCH" in self.route.method_type - - def isDelete(self): - return "DELETE" in self.route.method_type - - def on_bind(self, obj, method): - self.container.on_bind(obj, method) - return self - - def hasSession(self, key): - return self.container.make("Session").has(key) - - def assertParameterIs(self, key, value): - request = self.container.make("Request") - if key not in request.url_params: - raise AssertionError( - "Request class does not have the '{}' url parameter".format(key) - ) - - if request.param(key) != value: - raise AssertionError( - "parameter {} is equal to {} of type {}, not {} of type {}".format( - key, - request.param(key), - type(request.param(key)), - value, - type(value), - ) - ) - - def assertIsStatus(self, status): - response = self.container.make(Response) - assert response.is_status(status), AssertionError( - "{} is not equal to {}".format(response.get_status_code(), status) - ) - if not response.is_status(status): - raise AssertionError( - "{} is not equal to {}".format(response.get_status_code(), status) - ) - - return self - - def assertHasHeader(self, key): - response = self.container.make(Response) - assert response.header(key), "Header '{}' does not exist".format(key) - return self - - def assertNotHasHeader(self, key): - request = self.container.make("Request") - assert not request.header( - key - ), "Header '{}' exists but asserting it should not".format(key) - return self - - def assertHeaderIs(self, key, value): - response = self.container.make(Response) - - header = response.header(key) - if not header: - raise ValueError(f"Header {key} is not set") - if header: - header = header.value - - assert header == str(value), AssertionError( - "{} is not equal to {}".format(header, value) - ) - - return self - - def assertPathIs(self, url): - path = self.container.make("Request").path - assert path == url, "Asserting the path is '{}' but it is '{}'".format( - url, path - ) - return True - - def session(self, key): - return self.container.make("Session").get(key) - - def on_make(self, obj, method): - self.container.on_make(obj, method) - return self - - def on_resolve(self, obj, method): - self.container.on_resolve(obj, method) - return self - - def _bind_user_to_request(self, request, container): - request.set_user(self._user) - return self - - def headerIs(self, key, value): - response = self.container.make(Response) - header = response.header(key) - if not header: - raise AssertionError(f"Could not found the {header} header") - assertion = header.value == value - if not assertion: - raise AssertionError( - "header {} does not equal {}".format(response.header(key), value) - ) - return assertion - - def parameterIs(self, key, value): - request = self.container.make("Request") - assertion = request.param(key) == value - if not assertion: - raise AssertionError( - "parameter {} is equal to {} of type {}, not {} of type {}".format( - key, - request.param(key), - type(request.param(key)), - value, - type(value), - ) - ) - return assertion - - @property - def request(self): - return self.container.make("Request") - - @property - def response(self): - """Gets the string response from the container. This isinstance check here - is to support Python 3.5. Once python3.5 goes away we can can remove this check. - - @required for 3.5 - - Returns: - string - """ - response = self.get_string_response() - if isinstance(response, str): - return response - - return response.decode("utf-8") - - def asDictionary(self): - try: - return json.loads(self.response) - except ValueError: - raise ValueError("The response was not json serializable") - - def ensure_response_has_view(self): - """Ensure that the response has a view as its original content.""" - if not self.response_has_view(): - raise ValueError("The response is not a view") - - def response_has_view(self): - return self.route.original and isinstance(self.route.original, View) - - def assertViewIs(self, name): - """Assert that request renders the given view name.""" - self.ensure_response_has_view() - assert self.route.original.template == name - return self - - def assertViewHas(self, key, value=None): - """Assert that view context contains a given data key (and eventually associated value).""" - self.ensure_response_has_view() - assert key in self.route.original.dictionary - if value: - assert self.route.original.dictionary[key] == value - - def assertViewHasAll(self, keys): - """Assert that view context contains exactly the data keys (or the complete data dict).""" - self.ensure_response_has_view() - if isinstance(keys, list): - assert set(keys) == set(self.route.original.dictionary.keys()) - set( - self.route.original._shared.keys() - ) - else: - view_data = self.route.original.dictionary - for key in self.route.original._shared: - del view_data[key] - assert keys == view_data - - def assertViewMissing(self, key): - """Assert that given data key is not in the view context.""" - self.ensure_response_has_view() - assert key not in self.route.original.dictionary - - def assertRedirect(self, redirect_uri): - """Assert that response is redirection to the given view name or URI or Controller@method.""" - request = self.container.make("Request") - response = self.container.make(Response) - self.route.get_response() - assert response.is_status(302) or response.is_status(301) - assert request.redirect_url == redirect_uri diff --git a/src/masonite/testing/TestCase.py b/src/masonite/testing/TestCase.py deleted file mode 100644 index 621a5ab6c..000000000 --- a/src/masonite/testing/TestCase.py +++ /dev/null @@ -1,325 +0,0 @@ -import io -import json -import sys -import unittest -from contextlib import contextmanager -from urllib.parse import urlencode - -from .. import env -from ..exceptions import RouteNotFoundException -from ..helpers.migrations import Migrations -from ..helpers.routes import create_matchurl, flatten_routes -from .generate_wsgi import generate_wsgi -from .create_container import create_container -from masoniteorm.factories import Factory -from ..response import Response - -from .MockRoute import MockRoute -from ..helpers import config -from ..auth import Sign - - -class TestCase(unittest.TestCase): - - sqlite = True - transactions = True - refreshes_database = False - _transaction = False - - def setUp(self): - from wsgi import container - - self.container = container - self._with_subdomains = False - self.wsgi_overrides = {} - - self.acting_user = False - self.factory = Factory - self.withoutExceptionHandling() - self.withoutCsrf() - if not self._transaction: - self.startTransaction() - if hasattr(self, "setUpFactories"): - self.setUpFactories() - - if self.sqlite and env("DB_CONNECTION") != "sqlite": - raise Exception("Cannot run tests without using the 'sqlite' database.") - - if not self.transactions and self.refreshes_database: - self.refreshDatabase() - - self.route_middleware = {} - self.http_middleware = [] - self.use_http_middleware = True - self.headers = {} - - def buildOwnContainer(self): - self.container = self.create_container() - return self - - @classmethod - def setUpClass(cls): - cls.staticSetUpDatabase() - - @classmethod - def tearDownClass(cls): - if not cls.refreshes_database and cls.transactions: - cls.staticStopTransaction() - else: - cls.staticTearDownDatabase() - - def refreshDatabase(self): - if not self.refreshes_database and self.transactions: - self.stopTransaction() - self.startTransaction() - if hasattr(self, "setUpFactories"): - self.setUpFactories() - else: - self.tearDownDatabase() - self.setUpDatabase() - - def startTransaction(self): - from config.database import DB - - DB.begin_transaction() - self.__class__._transaction = True - - def stopTransaction(self): - from config.database import DB - - DB.rollback() - self.__class__._transaction = False - - def withWSGIOverride(self, wsgi_values={}): - self.wsgi_overrides = wsgi_values - return self - - @classmethod - def staticStopTransaction(cls): - from config.database import DB - - DB.rollback() - cls._transaction = False - - def make(self, model, factory, amount=50): - self.registerFactory(model, factory) - self.makeFactory(model, amount) - - def makeFactory(self, model, amount): - return self.factory(model, amount).create() - - def registerFactory(self, model, callable_factory): - self.factory.register(model, callable_factory) - - def setUpDatabase(self): - self.tearDownDatabase() - Migrations().run() - if hasattr(self, "setUpFactories"): - self.setUpFactories() - - def tearDownDatabase(self): - Migrations().reset() - - @staticmethod - def staticSetUpDatabase(): - Migrations().run() - - @staticmethod - def staticTearDownDatabase(): - Migrations().reset() - - def tearDown(self): - if not self.transactions and self.refreshes_database: - self.tearDownDatabase() - - if self.container.has("Request"): - self.container.make("Request").get_and_reset_headers() - - if self.container.has(Response): - self.container.make(Response).get_and_reset_headers() - - def call(self, method, url, params, wsgi={}): - custom_wsgi = {"PATH_INFO": url, "REQUEST_METHOD": method} - - custom_wsgi.update(wsgi) - if not self._with_csrf: - token = Sign().sign("secret") - params.update({"__token": token}) - custom_wsgi.update( - { - "HTTP_COOKIE": "csrf_token=" + token, - "CONTENT_LENGTH": len(str(json.dumps(params))), - "wsgi.input": io.BytesIO(bytes(json.dumps(params), "utf-8")), - } - ) - - custom_wsgi.update({"QUERY_STRING": urlencode(params)}) - self.run_container(custom_wsgi) - self.container.make("Request").request_variables = params - return self.route(url, method) - - def get(self, url, params={}, wsgi={}): - return self.call("GET", url, params, wsgi=wsgi) - - def withSubdomains(self): - self._with_subdomains = True - return self - - def json(self, method, url, params={}): - return self.call( - method, - url, - params, - wsgi={ - "CONTENT_TYPE": "application/json", - "CONTENT_LENGTH": len(str(json.dumps(params))), - "wsgi.input": io.BytesIO(bytes(json.dumps(params), "utf-8")), - }, - ) - - def post(self, url, params={}): - return self.call("POST", url, params) - - def put(self, url, params={}): - return self.json("PUT", url, params) - - def patch(self, url, params={}): - return self.json("PATCH", url, params) - - def delete(self, url, params={}): - return self.json("DELETE", url, params) - - def actingAs(self, user): - if not user: - raise TypeError("Cannot act as a user of type: {}".format(type(user))) - self.acting_user = user - return self - - def route(self, url, method=False): - for route in self.container.make("WebRoutes"): - matchurl = create_matchurl(url, route) - if self.container.make("Request").has_subdomain(): - route.load_request(self.container.make("Request")) - # Check if the subdomain matches the correct routes domain - if not route.has_required_domain(): - continue - - if matchurl.match(url) and method in route.method_type: - return MockRoute(route, self.container) - - raise RouteNotFoundException( - "Could not find a route based on the url '{}'".format(url) - ) - - def routes(self, routes=[], only=False): - if only: - self.container.bind("WebRoutes", flatten_routes(only)) - return - - self.container.bind( - "WebRoutes", flatten_routes(self.container.make("WebRoutes") + routes) - ) - - @contextmanager - def captureOutput(self): - new_out, new_err = io.StringIO(), io.StringIO() - old_out, old_err = sys.stdout, sys.stderr - try: - sys.stdout, sys.stderr = new_out, new_err - yield sys.stdout - finally: - sys.stdout, sys.stderr = old_out, old_err - - def run_container(self, wsgi_values={}): - wsgi = generate_wsgi() - wsgi.update(wsgi_values) - wsgi.update(self.wsgi_overrides) - self.container.bind("Environ", wsgi) - self.container.bind("User", self.acting_user) - - if self._with_subdomains: - self.container.bind("Subdomains", True) - - # if self.headers: - # self.container.make("Request").header(self.headers) - - if self.route_middleware: - self.container.bind("RouteMiddleware", self.route_middleware) - else: - self.container.bind( - "RouteMiddleware", config("middleware.route_middleware") - ) - - if self.use_http_middleware: - self.container.bind("HttpMiddleware", config("middleware.http_middleware")) - else: - self.container.bind("HttpMiddleware", self.http_middleware) - # self.container.bind("RouteMiddleware", config("middleware.route_middleware")) - - # if self.http_middleware is not False: - # self.container.bind("HttpMiddleware", self.http_middleware) - - try: - for provider in self.container.make("WSGIProviders"): - self.container.resolve(provider.boot) - self.container.make("Request")._test_user = self.acting_user - except Exception as e: # skipcq - if self._exception_handling: - self.container.make("ExceptionHandler").load_exception(e) - else: - raise e - - def withExceptionHandling(self): - self._exception_handling = True - - def withoutExceptionHandling(self): - self._exception_handling = False - - def withCsrf(self): - self._with_csrf = True - return self - - def withoutCsrf(self): - self._with_csrf = False - return self - - def assertDatabaseHas(self, schema, value): - from masoniteorm.query import QueryBuilder - - table = schema.split(".")[0] - column = schema.split(".")[1] - - self.assertTrue(QueryBuilder().table(table).where(column, value).first()) - - def assertDatabaseNotHas(self, schema, value): - from masoniteorm.query import QueryBuilder - - table = schema.split(".")[0] - column = schema.split(".")[1] - - self.assertFalse(QueryBuilder().table(table).where(column, value).first()) - - def on_bind(self, obj, method): - self.container.on_bind(obj, method) - return self - - def withRouteMiddleware(self, middleware): - self.route_middleware = middleware - return self - - def withHttpMiddleware(self, middleware): - self.use_http_middleware = False - self.http_middleware = middleware - return self - - def withHeaders(self, headers={}): - self.headers = headers - return self - - def withoutHttpMiddleware(self): - self.use_http_middleware = False - self.http_middleware = [] - return self - - def create_container(self): - return create_container() diff --git a/src/masonite/testing/__init__.py b/src/masonite/testing/__init__.py deleted file mode 100644 index e33b67d89..000000000 --- a/src/masonite/testing/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .TestCase import TestCase -from .MockRoute import MockRoute -from .generate_wsgi import generate_wsgi, MockWsgiInput -from .create_container import create_container diff --git a/src/masonite/testing/create_container.py b/src/masonite/testing/create_container.py deleted file mode 100644 index 811ccf3ca..000000000 --- a/src/masonite/testing/create_container.py +++ /dev/null @@ -1,43 +0,0 @@ -from ..app import App -import copy - - -def create_container(): - container = copy.deepcopy(App()) - from .generate_wsgi import generate_wsgi - from config import providers - - container.bind("WSGI", generate_wsgi()) - container.bind("Container", container) - - # container.bind('ProvidersConfig', providers) - container.bind("Providers", []) - container.bind("WSGIProviders", []) - - """Bind all service providers - Let's register everything into the Service Container. Once everything is - in the container we can run through all the boot methods. For reasons - some providers don't need to execute with every request and should - only run once when the server is started. Providers will be ran - once if the wsgi attribute on a provider is False. - """ - - for provider in providers.PROVIDERS: - located_provider = provider() - located_provider.load_app(container).register() - if located_provider.wsgi: - container.make("WSGIProviders").append(located_provider) - else: - container.make("Providers").append(located_provider) - - for provider in container.make("Providers"): - container.resolve(provider.boot) - - """Get the application from the container - Some providers may change the WSGI Server like wrapping the WSGI server - in a Whitenoise container for an example. Let's get a WSGI instance - from the container and pass it to the application variable. This - will allow WSGI servers to pick it up from the command line - """ - - return container diff --git a/src/masonite/testing/generate_wsgi.py b/src/masonite/testing/generate_wsgi.py deleted file mode 100644 index f2519e1fc..000000000 --- a/src/masonite/testing/generate_wsgi.py +++ /dev/null @@ -1,39 +0,0 @@ -import io - - -class MockWsgiInput: - def __init__(self, data): - self.data = data - - def read(self, _): - return self.data - - -def generate_wsgi(): - return { - "wsgi.version": (1, 0), - "wsgi.multithread": False, - "wsgi.multiprocess": True, - "wsgi.run_once": False, - "wsgi.input": io.BytesIO(), - "SERVER_SOFTWARE": "gunicorn/19.7.1", - "REQUEST_METHOD": "GET", - "QUERY_STRING": "application=Masonite", - "RAW_URI": "/", - "SERVER_PROTOCOL": "HTTP/1.1", - "HTTP_HOST": "127.0.0.1:8000", - "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "HTTP_UPGRADE_INSECURE_REQUESTS": "1", - "HTTP_COOKIE": "setcookie=value", - "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7", - "HTTP_ACCEPT_LANGUAGE": "en-us", - "HTTP_ACCEPT_ENCODING": "gzip, deflate", - "HTTP_CONNECTION": "keep-alive", - "wsgi.url_scheme": "http", - "REMOTE_ADDR": "127.0.0.1", - "REMOTE_PORT": "62241", - "SERVER_NAME": "127.0.0.1", - "SERVER_PORT": "8000", - "PATH_INFO": "/", - "SCRIPT_NAME": "", - } diff --git a/src/masonite/tests/DatabaseTransactions.py b/src/masonite/tests/DatabaseTransactions.py new file mode 100644 index 000000000..296c208fc --- /dev/null +++ b/src/masonite/tests/DatabaseTransactions.py @@ -0,0 +1,8 @@ +class DatabaseTransactions: + def startTestRun(self): + self.application.make("resolver").begin_transaction(self.connection) + return self + + def stopTestRun(self): + self.application.make("resolver").rollback(self.connection) + return self diff --git a/src/masonite/tests/HttpTestResponse.py b/src/masonite/tests/HttpTestResponse.py new file mode 100644 index 000000000..853f19d5d --- /dev/null +++ b/src/masonite/tests/HttpTestResponse.py @@ -0,0 +1,323 @@ +import json +from ..views import View +from ..controllers import Controller +from ..utils.structures import data_get + + +class HttpTestResponse: + def __init__(self, application, request, response, route): + self.application = application + self.request = request + self.response = response + self.route = route + self.content = None + self.status = None + self.get_response() + + def get_response(self): + self.content = self.response.get_response_content() + return self + + def get_content(self): + """Take care of decoding content if bytes and returns str.""" + return ( + self.content.decode("utf-8") + if isinstance(self.content, bytes) + else str(self.content) + ) + + def assertContains(self, content): + assert ( + content in self.get_content() + ), f"{content} not found in {self.get_content()}" + return self + + def assertNotContains(self, content): + assert content not in self.get_content() + return self + + def assertContainsInOrder(self, *content): + response_content = self.get_content() + index = 0 + for content_string in content: + found_at_index = response_content.find(content_string, index) + assert found_at_index != -1 + index = found_at_index + len(content_string) + return self + + def assertIsNamed(self, name): + assert ( + self.route.get_name() == name + ), f"Route name is {self.route.get_name()}. Asserted {name}" + return self + + def assertIsNotNamed(self, name=None): + if name is None: + assert self.route.name is None, "Route has a name: {}".format( + self.route.name + ) + else: + assert ( + self.route.get_name() != name + ), f"Route name {self.route.get_name()} matches expected {name}" + return self + + def assertIsStatus(self, status): + assert self.response.is_status( + status + ), f"Status is {self.response.get_status_code()}. Asserted {status}" + return self + + def assertNotFound(self): + return self.assertIsStatus(404) + + def assertOk(self): + return self.assertIsStatus(200) + + def assertCreated(self): + return self.assertIsStatus(201) + + def assertSuccessful(self): + assert 200 <= self.response.get_status_code() < 300 + return self + + def assertNoContent(self, status=204): + assert not self.get_content() + return self.assertIsStatus(status) + + def assertUnauthorized(self): + return self.assertIsStatus(401) + + def assertForbidden(self): + return self.assertIsStatus(403) + + def assertHasHeader(self, name, value=None): + header_value = self.response.header(name) + assert header_value, f"Could not find the header {name}" + if value: + assert value == header_value, f"Header '{name}' does not equal {value}" + + def assertHeaderMissing(self, name): + assert not self.response.header(name) + + def assertLocation(self, location): + return self.assertHasHeader("Location", location) + + def assertRedirect(self, url=None, name=None, params={}): + # we could assert 301 or 302 code => what if user uses another status code in redirect() + # here we are sure + assert self.get_content() == "Redirecting ..." + if url: + self.assertLocation(url) + elif name: + url = self.response._get_url_from_route_name(name, params) + self.assertLocation(url) + return self + + def assertCookie(self, name, value=None): + assert self.request.cookie_jar.exists(name) + if value is not None: + assert self.request.cookie_jar.get(name).value == value + return self + + def assertPlainCookie(self, name): + assert self.request.cookie_jar.exists(name) + assert not self.request.cookie_jar.get(name).secure + return self + + def assertCookieExpired(self, name): + self.assertCookie(name) + assert self.request.cookie_jar.is_expired(name) + return self + + def assertCookieNotExpired(self, name): + return not self.assertCookieExpired(name) + + def assertCookieMissing(self, name): + assert not self.request.cookie_jar.exists(name) + return self + + def assertSessionHas(self, key, value=None): + """Assert that session contains the given key with the corresponding value if given. + The session driver can be specified if necessary.""" + session = self.request.session + assert session.has(key) + if value is not None: + assert session.get(key) == value + return self + + def assertSessionMissing(self, key): + """Assert that session does not contain the given key. The session driver can be specified + if necessary.""" + assert not self.request.session.get(key) + return self + + def assertSessionHasErrors(self, keys=[]): + """Assert that session contains errors for the given list of keys (meaning that each given key + exists in 'errors' dict in session.) If no keys are given this will assert that the + sessions has errors without checking specific keys.""" + session = self.request.session + assert session.has("errors") + if keys: + errors = session.get("errors") + for key in keys: + assert errors.get(key) + return self + + def assertSessionHasNoErrors(self, keys=[]): + """Assert that session does not have any errors (meaning that session does not contain an + 'errors' key or 'errors' key is empty. If a list of keys is given, this will check + that there are no errors for each of those keys.""" + session = self.request.session + if not keys: + assert not session.has("errors") + else: + errors = session.get("errors") + for key in keys: + assert not errors.get(key) + return self + + def _ensure_response_has_view(self): + """Ensure that the response has a view as its original content.""" + if not (self.response.original and isinstance(self.response.original, View)): + raise ValueError("The response is not a view") + + def assertViewIs(self, name): + """Assert that request renders the given view name.""" + self._ensure_response_has_view() + assert ( + self.response.original.template == name + ), f"Template {self.response.original.template} is not equal to {name}" + return self + + def assertViewHas(self, key, value=None): + """Assert that view context contains a given data key (and eventually associated value).""" + self._ensure_response_has_view() + value_at_key = data_get(self.response.original.dictionary, key) + assert value_at_key + if value: + assert value_at_key == value + return self + + def assertViewHasExact(self, keys): + """Assert that view context contains exactly the data keys (or the complete data dict).""" + self._ensure_response_has_view() + if isinstance(keys, list): + assert set(keys) == set(self.response.original.dictionary.keys()) - set( + self.response.original._shared.keys() + ) + else: + view_data = self.response.original.dictionary + for key in self.response.original._shared: + del view_data[key] + assert keys == view_data + return self + + def assertViewMissing(self, key): + """Assert that given data key is not in the view context.""" + self._ensure_response_has_view() + assert not data_get(self.response.original.dictionary, key) + return self + + def assertAuthenticated(self): + assert self.application.make("auth").guard("web").user() + return self + + def assertGuest(self): + assert not self.application.make("auth").guard("web").user() + return self + + def assertAuthenticatedAs(self, user): + user = self.application.make("auth").guard("web").user() + assert user == user + return self + + def assertHasHttpMiddleware(self, middleware): + """Assert that the request/response cycle has the given middleware. The HTTP middleware + class should be given.""" + assert middleware in self.application.make("middleware").http_middleware + return self + + def assertHasRouteMiddleware(self, middleware): + """Assert that the route has the given middleware. The registration key of the middleware + should be used.""" + assert middleware in self.application.make("middleware").route_middleware + return self + + def assertHasController(self, controller): + """Assert that route used the given controller. The controller can be a class or + a string. If it's a string it should be formatted as follow: ControllerName@method""" + if isinstance(controller, str) and "@" in controller: + assert self.route.controller == controller + elif issubclass(controller, Controller): + assert self.route.controller_class == controller + else: + raise ValueError( + "controller must be a string like YourController@index or a Controller class" + ) + return self + + def assertRouteHasParameter(self, key, value=None): + assert key in self.route.url_list, "Route does not contain parameter {key}." + if value is not None: + # TODO + # @josephmancuso not sure how to check if the route parameter has the given value + # 1. play with the compiled regex but not sure how to do it + # 2. see below, correct ? after testing it it's not correct, there are several issues with + # this. forgot all this + # real_url = self.route.url.replace(f"@{key}", str(value)) + # assert self.route.matches(real_url) + pass + return self + + def _ensure_response_is_json(self): + """Parse response back from JSON into a dict, if an error happens the response was not + a JSON string.""" + try: + return json.loads(self.response.content) + except ValueError: + raise ValueError("The response was not JSON serializable") + + def assertJson(self, data): + """Assert that response is JSON and contains the given data dictionary. The assertion will + pass even if it is not an exact match.""" + response_data = self._ensure_response_is_json() + assert data.items() <= response_data.items() + return self + + def assertJsonPath(self, path, value=None): + """Assert that response is JSON and contains the given path, with eventually the given + value if provided. The path is a dotted path.""" + response_data = self._ensure_response_is_json() + data_at_path = data_get(response_data, path) + + assert data_at_path == value, f"'{data_at_path}' does not equal {value}" + return self + + def assertJsonExact(self, data): + """Assert that response is JSON and is exactly the given data.""" + response_data = self._ensure_response_is_json() + assert response_data == data, f"'{response_data}' does not equal {data}" + return self + + def assertJsonCount(self, count, key=None): + """Assert that JSON response is JSON and has the given count of keys at root level + or at the given key.""" + response_data = self._ensure_response_is_json() + if key is not None: + response_data = response_data.get(key, {}) + + response_count = len(response_data.keys()) + assert ( + response_count == count + ), f"JSON response count is {response_count}. Asserted {count}." + return self + + def assertJsonMissing(self, path): + """Assert that JSON response is JSON and does not contain given path. + The path can be a dotted path""" + response_data = self._ensure_response_is_json() + assert not data_get( + response_data, path + ), f"'{response_data}' is not missing from {data_get(response_data, path)}" + return self diff --git a/src/masonite/tests/MockInput.py b/src/masonite/tests/MockInput.py new file mode 100644 index 000000000..b7eb61a3a --- /dev/null +++ b/src/masonite/tests/MockInput.py @@ -0,0 +1,6 @@ +class MockInput: + def __init__(self, data): + self.data = data + + def read(self, _): + return self.data diff --git a/src/masonite/tests/TestCase.py b/src/masonite/tests/TestCase.py new file mode 100644 index 000000000..012886e99 --- /dev/null +++ b/src/masonite/tests/TestCase.py @@ -0,0 +1,251 @@ +import json +import io +import unittest +import pendulum + + +from ..routes import Route +from ..foundation.response_handler import testcase_handler +from ..utils.http import generate_wsgi +from ..request import Request +from ..response import Response +from ..environment import LoadEnvironment +from ..providers.RouteProvider import RouteProvider +from .TestCommand import TestCommand + + +class TestCase(unittest.TestCase): + def setUp(self): + LoadEnvironment("testing") + from wsgi import application + + self.application = application + self.original_class_mocks = {} + self._test_cookies = {} + self._test_headers = {} + if hasattr(self, "startTestRun"): + self.startTestRun() + self.withoutCsrf() + self._exception_handling = False + # boot providers as they won't not be loaded if the test is not doing a request + self.application.bind("environ", generate_wsgi()) + try: + for provider in self.application.get_providers(): + # if no request is made we don't need RouteProvider, and we can't load it anyway + # because we don't have created a CSRF token yet + if not isinstance(provider, RouteProvider): + application.resolve(provider.boot) + except Exception as e: + if not self._exception_handling: + raise e + self.application.make("exception_handler").handle(e) + + def tearDown(self): + # be sure to reset this between each test + self._exception_handling = False + if hasattr(self, "stopTestRun"): + self.stopTestRun() + + def withExceptionsHandling(self): + """Enable for the duration of a test the handling of exception through the exception + handler.""" + self._exception_handling = True + + def setRoutes(self, *routes): + self.application.make("router").set(Route.group(*routes, middleware=["web"])) + return self + + def addRoutes(self, *routes): + self.application.make("router").add(Route.group(*routes, middleware=["web"])) + return self + + def withCsrf(self): + self._csrf = True + return self + + def withoutCsrf(self): + self._csrf = False + return self + + def get(self, route, data=None): + return self.fetch(route, data, method="GET") + + def post(self, route, data=None): + return self.fetch(route, data, method="POST") + + def put(self, route, data=None): + return self.fetch(route, data, method="PUT") + + def patch(self, route, data=None): + return self.fetch(route, data, method="PATCH") + + def make_request( + self, data={}, path="/", query_string="application=Masonite", method="GET" + ): + request = Request(generate_wsgi(data, path, query_string, method)) + request.app = self.application + + self.application.bind("request", request) + return request + + def make_response(self, data={}): + request = Response(generate_wsgi(data)) + request.app = self.application + + self.application.bind("response", request) + return request + + def withExceptionsHandling(self): + self._exception_handling = True + + def fetch(self, route, data=None, method=None): + if data is None: + data = {} + if not self._csrf: + token = self.application.make("sign").sign("cookie") + data.update({"__token": "cookie"}) + wsgi_request = generate_wsgi( + { + "HTTP_COOKIE": f"SESSID={token}; csrf_token={token}", + "CONTENT_LENGTH": len(str(json.dumps(data))), + "REQUEST_METHOD": method, + "PATH_INFO": route, + "wsgi.input": io.BytesIO(bytes(json.dumps(data), "utf-8")), + } + ) + else: + wsgi_request = generate_wsgi( + { + "CONTENT_LENGTH": len(str(json.dumps(data))), + "REQUEST_METHOD": method, + "PATH_INFO": route, + "wsgi.input": io.BytesIO(bytes(json.dumps(data), "utf-8")), + } + ) + + request, response = testcase_handler( + self.application, + wsgi_request, + self.mock_start_response, + exception_handling=self._exception_handling, + ) + # add eventual cookies added inside the test (not encrypted to be able to assert value ?) + for name, value in self._test_cookies.items(): + request.cookie(name, value) + # add eventual headers added inside the test + for name, value in self._test_headers.items(): + request.header(name, value) + + route = self.application.make("router").find(route, method) + if route: + return self.application.make("tests.response").build( + self.application, request, response, route + ) + raise Exception(f"No route found for {route}") + + def mock_start_response(self, *args, **kwargs): + pass + + def craft(self, command, arguments_str=""): + """Run a given command in tests and obtain a TestCommand instance to assert command + outputs. + self.craft("controller", "Welcome").assertSuccess() + """ + return TestCommand(self.application).run(command, arguments_str) + + def fake(self, binding): + """Mock a service with its mocked implementation or with a given custom + one.""" + + # save original first + self.original_class_mocks.update( + {binding: self.application.make(binding, self.application)} + ) + # mock by overriding with mocked version + mock = self.application.make(f"mock.{binding}", self.application) + self.application.bind(binding, mock) + return mock + + def withCookies(self, cookies_dict): + self._test_cookies = cookies_dict + return self + + def withHeaders(self, headers_dict): + self._test_headers = headers_dict + return self + + def actingAs(self, user): + self.make_request() + self.application.make("auth").guard("web").login_by_id( + user.get_primary_key_value() + ) + + def restore(self, binding): + """Restore the service previously mocked to the original one.""" + original = self.original_class_mocks.get(binding) + self.application.bind(binding, original) + + def fakeTime(self, pendulum_datetime): + """Set a given pendulum instance to be returned when a "now" (or "today", "tomorrow", + "yesterday") instance is created. It's really useful during tests to check + timestamps logic.""" + pendulum.set_test_now(pendulum_datetime) + + def fakeTimeTomorrow(self): + """Set the mocked time as tomorrow.""" + self.fakeTime(pendulum.tomorrow()) + + def fakeTimeYesterday(self): + """Set the mocked time as yesterday.""" + self.fakeTime(pendulum.yesterday()) + + def fakeTimeInFuture(self, offset, unit="days"): + """Set the mocked time as an offset of days in the future. Unit can be specified + among pendulum units: seconds, minutes, hours, days, weeks, months, years.""" + self.restoreTime() + datetime = pendulum.now().add(**{unit: offset}) + self.fakeTime(datetime) + + def fakeTimeInPast(self, offset, unit="days"): + """Set the mocked time as an offset of days in the past. Unit can be specified + among pendulum units: seconds, minutes, hours, days, weeks, months, years.""" + self.restoreTime() + datetime = pendulum.now().subtract(**{unit: offset}) + self.fakeTime(datetime) + + def restoreTime(self): + """Restore time to correct one, so that pendulum new "now" instance are corrects. + This method will be typically called in tearDown() method of a test class.""" + # this will clear the mock + pendulum.set_test_now() + + def assertDatabaseCount(self, table, count): + self.assertEqual(self.application.make("builder").table(table).count(), count) + + def assertDatabaseHas(self, table, query_dict): + self.assertGreaterEqual( + self.application.make("builder").table(table).where(query_dict).count(), 1 + ) + + def assertDatabaseMissing(self, table, query_dict): + self.assertEqual( + self.application.make("builder").table(table).where(query_dict).count(), 0 + ) + + def assertDeleted(self, instance): + self.assertFalse( + self.application.make("builder") + .table(instance.get_table_name()) + .where(instance.get_primary_key(), instance.get_primary_key_value()) + .get() + ) + + def assertSoftDeleted(self, instance): + deleted_at_column = instance.get_deleted_at_column() + self.assertTrue( + self.application.make("builder") + .table(instance.get_table_name()) + .where(instance.get_primary_key(), instance.get_primary_key_value()) + .where_not_null(deleted_at_column) + .get() + ) diff --git a/src/masonite/tests/TestCommand.py b/src/masonite/tests/TestCommand.py new file mode 100644 index 000000000..f82df9c2b --- /dev/null +++ b/src/masonite/tests/TestCommand.py @@ -0,0 +1,58 @@ +from cleo import CommandTester + + +class TestCommand: + """This class allows us to test craft commands and asserts command outputs.""" + + def __init__(self, application): + self.application = application + + def run(self, command, arguments_str=""): + command = self.application.make("commands").command_application.find(command) + self.command_tester = CommandTester(command) + self.command_tester.execute(arguments_str) + return self + + def assertExactOutput(self, ref_output): + """Assert command output to be exactly the same as the given reference output.""" + output = self._get_output() + assert ref_output == output, f"Command output was: {output}, not {ref_output}" + return self + + def assertOutputContains(self, ref_output): + output = self._get_output() + assert ( + ref_output in output + ), f"Command output was: {output} and does not contain {ref_output}." + return self + + def assertOutputMissing(self, ref_output): + """Assert command output does not contain the given reference output.""" + output = self._get_output() + assert ( + ref_output not in output + ), f"Command output was: {output}, not {ref_output}" + return self + + def assertHasErrors(self): + assert self._get_errors() + return self + + def assertExactErrors(self, ref_errors): + errors = self._get_errors() + assert ( + errors == ref_errors + ), f"Command output has errors: {errors}, not {ref_errors}." + return self + + def assertSuccess(self): + """Assert that command returned a 0 exit code meaning that it ran successfully.""" + code = self.command_tester.status_code + assert 0 == code, "Command exited code is not 0: {code}." + return self + + def _get_errors(self): + return self.command_tester.io.fetch_error() + + def _get_output(self): + return self.command_tester.io.fetch_output() diff --git a/src/masonite/tests/TestResponseCapsule.py b/src/masonite/tests/TestResponseCapsule.py new file mode 100644 index 000000000..27cced8e8 --- /dev/null +++ b/src/masonite/tests/TestResponseCapsule.py @@ -0,0 +1,17 @@ +class TestResponseCapsule: + def __init__(self, base_test_response_class): + self.base_test_response_class = base_test_response_class + self.test_responses_classes = [] + + def add(self, *classes): + self.test_responses_classes.extend(classes) + return self + + def build(self, *args): + """Apply other test response class as mixins to base test response class.""" + obj = self.base_test_response_class(*args) + for cls in self.test_responses_classes: + base_cls = obj.__class__ + base_cls_name = obj.__class__.__name__ + obj.__class__ = type(base_cls_name, (base_cls, cls), {}) + return obj diff --git a/src/masonite/tests/__init__.py b/src/masonite/tests/__init__.py new file mode 100644 index 000000000..56f52af71 --- /dev/null +++ b/src/masonite/tests/__init__.py @@ -0,0 +1,4 @@ +from .TestCase import TestCase +from .MockInput import MockInput +from .HttpTestResponse import HttpTestResponse +from .DatabaseTransactions import DatabaseTransactions diff --git a/src/masonite/snippets/exceptions/__init__.py b/src/masonite/utils/__init__.py similarity index 100% rename from src/masonite/snippets/exceptions/__init__.py rename to src/masonite/utils/__init__.py diff --git a/src/masonite/utils/collections.py b/src/masonite/utils/collections.py new file mode 100644 index 000000000..441f552f8 --- /dev/null +++ b/src/masonite/utils/collections.py @@ -0,0 +1,545 @@ +import json +import random +import operator +from functools import reduce +from dotty_dict import Dotty + +from .structures import data_get + + +class Collection: + """Wraps various data types to make working with them easier.""" + + def __init__(self, items=None): + self._items = items or [] + self.__appends__ = [] + + def take(self, number: int): + """Takes a specific number of results from the items. + + Arguments: + number {integer} -- The number of results to take. + + Returns: + int + """ + if number < 0: + return self[number:] + + return self[:number] + + def first(self, callback=None): + """Takes the first result in the items. + + If a callback is given then the first result will be the result after the filter. + + Keyword Arguments: + callback {callable} -- Used to filter the results before returning the first item. (default: {None}) + + Returns: + mixed -- Returns whatever the first item is. + """ + filtered = self + if callback: + filtered = self.filter(callback) + response = None + if filtered: + response = filtered[0] + return response + + def last(self, callback=None): + """Takes the last result in the items. + + If a callback is given then the last result will be the result after the filter. + + Keyword Arguments: + callback {callable} -- Used to filter the results before returning the last item. (default: {None}) + + Returns: + mixed -- Returns whatever the last item is. + """ + filtered = self + if callback: + filtered = self.filter(callback) + return filtered[-1] + + def all(self): + """Returns all the items. + + Returns: + mixed -- Returns all items. + """ + return self._items + + def avg(self, key=None): + """Returns the average of the items. + + If a key is given it will return the average of all the values of the key. + + Keyword Arguments: + key {string} -- The key to use to find the average of all the values of that key. (default: {None}) + + Returns: + int -- Returns the average. + """ + result = 0 + items = self._get_value(key) or self._items + try: + result = sum(items) / len(items) + except TypeError: + pass + return result + + def max(self, key=None): + """Returns the average of the items. + + If a key is given it will return the average of all the values of the key. + + Keyword Arguments: + key {string} -- The key to use to find the average of all the values of that key. (default: {None}) + + Returns: + int -- Returns the average. + """ + result = 0 + items = self._get_value(key) or self._items + + try: + return max(items) + except (TypeError, ValueError): + pass + return result + + def chunk(self, size: int): + """Chunks the items. + + Keyword Arguments: + size {integer} -- The number of values in each chunk. + + Returns: + int -- Returns the average. + """ + items = [] + for i in range(0, self.count(), size): + items.append(self[i : i + size]) + return self.__class__(items) + + def collapse(self): + items = [] + for item in self: + items += self.__get_items(item) + return self.__class__(items) + + def contains(self, key, value=None): + if value: + return self.contains(lambda x: self._data_get(x, key) == value) + + if self._check_is_callable(key, raise_exception=False): + return self.first(key) is not None + + return key in self + + def count(self): + return len(self._items) + + def diff(self, items): + items = self.__get_items(items) + return self.__class__([x for x in self if x not in items]) + + def each(self, callback): + self._check_is_callable(callback) + + for k, v in enumerate(self): + result = callback(v) + if not result: + break + self[k] = result + + def every(self, callback): + self._check_is_callable(callback) + return all([callback(x) for x in self]) + + def filter(self, callback): + self._check_is_callable(callback) + return self.__class__(list(filter(callback, self))) + + def flatten(self): + def _flatten(items): + if isinstance(items, dict): + for v in items.values(): + for x in _flatten(v): + yield x + elif isinstance(items, list): + for i in items: + for j in _flatten(i): + yield j + else: + yield items + + return self.__class__(list(_flatten(self._items))) + + def forget(self, *keys): + keys = reversed(sorted(keys)) + + for key in keys: + del self[key] + + return self + + def for_page(self, page, number): + return self.__class__(self[page:number]) + + def get(self, key, default=None): + try: + return self[key] + except IndexError: + pass + + return self._value(default) + + def implode(self, glue=",", key=None): + first = self.first() + if not isinstance(first, str) and key: + return glue.join(self.pluck(key)) + return glue.join([str(x) for x in self]) + + def is_empty(self): + return not self + + def map(self, callback): + self._check_is_callable(callback) + items = [callback(x) for x in self] + return self.__class__(items) + + def map_into(self, cls, method=None, **kwargs): + results = [] + for item in self: + if method: + results.append(getattr(cls, method)(item, **kwargs)) + else: + results.append(cls(item)) + + return self.__class__(results) + + def merge(self, items): + if not isinstance(items, list): + raise ValueError("Unable to merge uncompatible types") + + items = self.__get_items(items) + + self._items += items + return self + + def pluck(self, value, key=None): + if key: + attributes = {} + else: + attributes = [] + + if isinstance(self._items, dict): + return Collection([self._items.get(value)]) + + for item in self: + if isinstance(item, dict): + iterable = item.items() + elif hasattr(item, "serialize"): + iterable = item.serialize().items() + else: + iterable = self.all().items() + + for k, v in iterable: + if k == value: + if key: + attributes[self._data_get(item, key)] = self._data_get( + item, value + ) + else: + attributes.append(v) + + return Collection(attributes) + + def pop(self): + last = self._items.pop() + return last + + def prepend(self, value): + self._items.insert(0, value) + return self + + def pull(self, key): + value = self.get(key) + self.forget(key) + return value + + def push(self, value): + self._items.append(value) + + def put(self, key, value): + self[key] = value + return self + + def random(self, count=None): + """Returns a random item of the collection.""" + collection_count = self.count() + if collection_count == 0: + return None + elif count and count > collection_count: + raise ValueError("count argument must be inferior to collection length.") + elif count: + self._items = random.sample(self._items, k=count) + return self + else: + return random.choice(self._items) + + def reduce(self, callback, initial=0): + return reduce(callback, self, initial) + + def reject(self, callback): + self._check_is_callable(callback) + + items = self._get_value(callback) or self._items + self._items = items + + def reverse(self): + self._items = self[::-1] + + def serialize(self): + def _serialize(item): + if self.__appends__: + item.set_appends(self.__appends__) + + if hasattr(item, "serialize"): + return item.serialize() + elif hasattr(item, "to_dict"): + return item.to_dict() + return item + + return list(map(_serialize, self)) + + def add_relation(self, result=None): + for model in self._items: + model.add_relations(result or {}) + + return self + + def shift(self): + return self.pull(0) + + def sort(self, key=None): + if key: + self._items.sort(key=lambda x: x[key], reverse=False) + return self + + self._items = sorted(self) + return self + + def sum(self, key=None): + result = 0 + items = self._get_value(key) or self._items + try: + result = sum(items) + except TypeError: + pass + return result + + def to_json(self, **kwargs): + return json.dumps(self.serialize(), **kwargs) + + def group_by(self, key): + + from itertools import groupby + + self.sort(key) + + new_dict = {} + + for k, v in groupby(self._items, key=lambda x: x[key]): + new_dict.update({k: list(v)}) + + return Collection(new_dict) + + def transform(self, callback): + self._check_is_callable(callback) + self._items = self._get_value(callback) + + def unique(self, key=None): + if not key: + items = list(set(self._items)) + return self.__class__(items) + + keys = set() + items = [] + if isinstance(self.all(), dict): + return self + + for item in self: + if isinstance(item, dict): + comparison = item.get(key) + elif isinstance(item, str): + comparison = item + else: + comparison = getattr(item, key) + if comparison not in keys: + items.append(item) + keys.add(comparison) + + return self.__class__(items) + + def where(self, key, *args): + op = "==" + value = args[0] + + if len(args) >= 2: + op = args[0] + value = args[1] + + attributes = [] + + for item in self._items: + if isinstance(item, dict): + comparison = item.get(key) + else: + comparison = getattr(item, key) + if self._make_comparison(comparison, value, op): + attributes.append(item) + + return self.__class__(attributes) + + def zip(self, items): + items = self.__get_items(items) + if not isinstance(items, list): + raise ValueError("The 'items' parameter must be a list or a Collection") + + _items = [] + for x, y in zip(self, items): + _items.append([x, y]) + return self.__class__(_items) + + def set_appends(self, appends): + """ + Set the attributes that should be appended to the Collection. + + :rtype: list + """ + self.__appends__ += appends + return self + + def _get_value(self, key): + if not key: + return None + + items = [] + for item in self: + if isinstance(key, str): + if hasattr(item, key) or (key in item): + items.append(getattr(item, key, item[key])) + elif callable(key): + result = key(item) + if result: + items.append(result) + return items + + def _data_get(self, item, key, default=None): + try: + if isinstance(item, (list, tuple)): + item = item[key] + elif isinstance(item, (dict, Dotty)): + item = data_get(item, key, default) + elif isinstance(item, object): + item = getattr(item, key) + except (IndexError, AttributeError, KeyError, TypeError): + return self._value(default) + + return item + + def _value(self, value): + if callable(value): + return value() + return value + + def _check_is_callable(self, callback, raise_exception=True): + if not callable(callback): + if not raise_exception: + return False + raise ValueError("The 'callback' should be a function") + return True + + def _make_comparison(self, a, b, op): + operators = { + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">": operator.gt, + ">=": operator.ge, + } + return operators[op](a, b) + + def __iter__(self): + for item in self._items: + yield item + + def __eq__(self, other): + if isinstance(other, Collection): + return other == other.all() + return other == self._items + + def __getitem__(self, item): + if isinstance(item, slice): + return self.__class__(self._items[item]) + + return self._items[item] + + def __setitem__(self, key, value): + self._items[key] = value + + def __delitem__(self, key): + del self._items[key] + + def __ne__(self, other): + other = self.__get_items(other) + return other != self._items + + def __len__(self): + return len(self._items) + + def __le__(self, other): + other = self.__get_items(other) + return self._items <= other + + def __lt__(self, other): + other = self.__get_items(other) + return self._items < other + + def __ge__(self, other): + other = self.__get_items(other) + return self._items >= other + + def __gt__(self, other): + other = self.__get_items(other) + return self._items > other + + @classmethod + def __get_items(cls, items): + if isinstance(items, Collection): + items = items.all() + + return items + + +def collect(iterable): + """Transform an iterable into a collection.""" + return Collection(iterable) + + +def flatten(iterable): + """Flatten all sub-iterables of an iterable structure (recursively).""" + flat_list = [] + for item in iterable: + if isinstance(item, list): + for subitem in flatten(item): + flat_list.append(subitem) + else: + flat_list.append(item) + + return flat_list diff --git a/src/masonite/utils/console.py b/src/masonite/utils/console.py new file mode 100644 index 000000000..1bb23c017 --- /dev/null +++ b/src/masonite/utils/console.py @@ -0,0 +1,14 @@ +class HasColoredOutput: + """Add level-colored output print functions to a class.""" + + def success(self, message): + print("\033[92m {0} \033[0m".format(message)) + + def warning(self, message): + print("\033[93m {0} \033[0m".format(message)) + + def danger(self, message): + print("\033[91m {0} \033[0m".format(message)) + + def info(self, message): + return self.success(message) diff --git a/src/masonite/utils/filesystem.py b/src/masonite/utils/filesystem.py new file mode 100644 index 000000000..1b599249b --- /dev/null +++ b/src/masonite/utils/filesystem.py @@ -0,0 +1,72 @@ +import os +import platform + + +def make_directory(directory): + """Create a directory at the given path for a file if it does not exist""" + if not os.path.isfile(directory): + if not os.path.exists(os.path.dirname(directory)): + # Create the path to the model if it does not exist + os.makedirs(os.path.dirname(directory)) + + return True + + return False + + +def file_exists(directory): + """Create a directory at the given path for a file if it does not exist""" + return os.path.exists(os.path.dirname(directory)) + + +def make_full_directory(directory): + """Create all directories to the given path if they do not exist""" + if not os.path.isfile(directory): + if not os.path.exists(directory): + # Create the path to the model if it does not exist + os.makedirs(directory) + + return True + + return False + + +def creation_date(path_to_file): + """Try to get the date that a file was created, falling back to when it was + last modified if that isn't possible. + """ + if platform.system() == "Windows": + return os.path.getctime(path_to_file) + else: + stat = os.stat(path_to_file) + try: + return stat.st_birthtime + except AttributeError: + # We're probably on Linux. No easy way to get creation dates here, + # so we'll settle for when its content was last modified. + return stat.st_mtime + + +def modified_date(path_to_file): + if platform.system() == "Windows": + return os.path.getmtime(path_to_file) + else: + stat = os.stat(path_to_file) + try: + return stat.st_mtime + except AttributeError: + # We're probably on Linux. No easy way to get creation dates here, + # so we'll settle for when its content was last modified. + return 0 + + +def render_stub_file(stub_file, name): + """Read stub file, replace placeholders and return content.""" + with open(stub_file, "r") as f: + content = f.read() + content = content.replace("__class__", name) + return content + + +def get_module_dir(module_file): + return os.path.dirname(os.path.realpath(module_file)) diff --git a/src/masonite/helpers/status.py b/src/masonite/utils/http.py similarity index 58% rename from src/masonite/helpers/status.py rename to src/masonite/utils/http.py index fab711477..4a672a128 100644 --- a/src/masonite/helpers/status.py +++ b/src/masonite/utils/http.py @@ -1,6 +1,7 @@ -"""Helper Functions for working with Status Codes.""" +"""Helpers for working with HTTP.""" -statuses = { + +HTTP_STATUS_CODES = { 100: "100 Continue", 101: "101 Switching Protocol", 102: "102 Processing", @@ -65,5 +66,36 @@ } -def response_statuses(): - return statuses +def generate_wsgi(wsgi={}, path="/", query_string="application=Masonite", method="GET"): + """Generate the WSGI environment dictionary that we receive from a HTTP request.""" + import io + + data = { + "wsgi.version": (1, 0), + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + "wsgi.input": io.BytesIO(), + "SERVER_SOFTWARE": "gunicorn/19.7.1", + "REQUEST_METHOD": method, + "QUERY_STRING": query_string, + "RAW_URI": path, + "SERVER_PROTOCOL": "HTTP/1.1", + "HTTP_HOST": "127.0.0.1:8000", + "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "HTTP_UPGRADE_INSECURE_REQUESTS": "1", + "HTTP_COOKIE": "setcookie=value", + "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7", + "HTTP_ACCEPT_LANGUAGE": "en-us", + "HTTP_ACCEPT_ENCODING": "gzip, deflate", + "HTTP_CONNECTION": "keep-alive", + "wsgi.url_scheme": "http", + "REMOTE_ADDR": "127.0.0.1", + "REMOTE_PORT": "62241", + "SERVER_NAME": "127.0.0.1", + "SERVER_PORT": "8000", + "PATH_INFO": path, + "SCRIPT_NAME": "", + } + data.update(wsgi) + return data diff --git a/src/masonite/utils/location.py b/src/masonite/utils/location.py new file mode 100644 index 000000000..859d40c04 --- /dev/null +++ b/src/masonite/utils/location.py @@ -0,0 +1,74 @@ +"""Helpers to resolve absolute paths to the different app resources using a configured +location.""" +from os.path import join, abspath + +from .str import as_filepath + + +def _build_path(location_key, relative_path, absolute): + from wsgi import application + + relative_dir = join(as_filepath(application.make(location_key)), relative_path) + return abspath(relative_dir) if absolute else relative_dir + + +def base_path(relative_path=""): + """Build the absolute path to the project root directory or build the absolute path to a + given file relative to the project root directory.""" + return abspath(relative_path) + + +def views_path(relative_path="", absolute=True): + """Build the absolute path to the project views directory or build the absolute path to a given + file relative the project views directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("views.location", relative_path, absolute) + + +def controllers_path(relative_path="", absolute=True): + """Build the absolute path to the project controllers directory or build the absolute path to a given + file relative the project controllers directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("controllers.location", relative_path, absolute) + + +def config_path(relative_path="", absolute=True): + """Build the absolute path to the project configuration directory or build the absolute path to a given + file relative the project configuration directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("config.location", relative_path, absolute) + + +def migrations_path(relative_path="", absolute=True): + """Build the absolute path to the project migrations directory or build the absolute path to a given + file relative the project migrations directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("migrations.location", relative_path, absolute) + + +def seeds_path(relative_path="", absolute=True): + """Build the absolute path to the project seeds directory or build the absolute path to a given + file relative the project seeds directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("seeds.location", relative_path, absolute) + + +def jobs_path(relative_path="", absolute=True): + """Build the absolute path to the project jobs directory or build the absolute path to a given + file relative the project jobs directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("jobs.location", relative_path, absolute) + + +def resources_path(relative_path="", absolute=True): + """Build the absolute path to the project resources directory or build the absolute path to a given + file relative the project resources directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("resources.location", relative_path, absolute) diff --git a/src/masonite/utils/str.py b/src/masonite/utils/str.py new file mode 100644 index 000000000..2bb8d5bc6 --- /dev/null +++ b/src/masonite/utils/str.py @@ -0,0 +1,58 @@ +"""String generators and helpers""" +import random +import string + + +def random_string(length=4): + """Generate a random string based on the given length. + + Keyword Arguments: + length {int} -- The amount of the characters to generate (default: {4}) + + Returns: + string + """ + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(length) + ) + + +def modularize(file_path, suffix=".py"): + """Transforms a file path to a dotted path. + + Keyword Arguments: + file_path {str} -- A file path such app/controllers + + Returns: + value {str} -- a dotted path such as app.controllers + """ + # if the file had the .py extension remove it as it's not needed for a module + return removesuffix(file_path.replace("/", "."), suffix) + + +def as_filepath(dotted_path): + """Inverse of modularize, transforms a dotted path to a file path (with /). + + Keyword Arguments: + dotted_path {str} -- A dotted path such app.controllers + + Returns: + value {str} -- a file path such as app/controllers + """ + return dotted_path.replace(".", "/") + + +def removeprefix(string, prefix): + """Implementation of str.removeprefix() function available for Python versions lower than 3.9.""" + if string.startswith(prefix): + return string[len(prefix) :] + else: + return string + + +def removesuffix(string, suffix): + """Implementation of str.removesuffix() function available for Python versions lower than 3.9.""" + if suffix and string.endswith(suffix): + return string[: -len(suffix)] + else: + return string diff --git a/src/masonite/utils/structures.py b/src/masonite/utils/structures.py new file mode 100644 index 000000000..6d42ec93a --- /dev/null +++ b/src/masonite/utils/structures.py @@ -0,0 +1,93 @@ +"""Helpers for multiple data structures""" +import importlib +from importlib.abc import Loader +from dotty_dict import dotty + +from ..exceptions.exceptions import LoaderNotFound + +from .str import modularize + + +def load(path, object_name=None, default=None, raise_exception=False): + """Load the given object from a Python module located at path and returns a default + value if not found. If no object name is provided, loads the module. + + Arguments: + path {str} -- A file path or a dotted path of a Python module + object {str} -- The object name to load in this module (None) + default {str} -- The default value to return if object not found in module (None) + Returns: + {object} -- The value (or default) read in the module or the module if no object name + """ + # modularize path if needed + module_path = modularize(path) + # module = pydoc.locate(dotted_path) + try: + module = importlib.import_module(module_path) + except ModuleNotFoundError: + if raise_exception: + raise LoaderNotFound( + f"{module_path} not found or error when importing this module." + ) + return None + + if object_name is None: + return module + else: + try: + return getattr(module, object_name) + except KeyError: + if raise_exception: + raise LoaderNotFound(f"{object_name} not found in {module_path}") + else: + return default + + +def data(dictionary={}): + """Transform the given dictionary to be read/written with dot notation. + + Arguments: + dictionary {dict} -- a dictionary structure + + Returns: + {dict} -- A dot dictionary + """ + return dotty(dictionary) + + +def data_get(dictionary, key, default=None): + """Read dictionary value from key using nested notation. + + Arguments: + dictionary {dict} -- a dictionary structure + key {str} -- the dotted (or not) key to look for + default {object} -- the default value to return if the key does not exist (None) + + Returns: + value {object} + """ + # dotty dict uses : instead of * for wildcards + dotty_key = key.replace("*", ":") + return data(dictionary).get(dotty_key, default) + + +def data_set(dictionary, key, value, overwrite=True): + """Set dictionary value at key using nested notation. Values are overriden by default but + this behaviour can be changed by passing overwrite=False. + The dictionary is edited in place but is also returned. + + Arguments: + dictionary {dict} -- a dictionary structure + key {str} -- the dotted (or not) key to set + value {object} -- the value to set at key + overwrite {bool} -- override the value if key exists in dictionary (True) + + Returns: + dictionary {dict} -- the edited dictionary + """ + if "*" in key: + raise ValueError("You cannot set values with wildcards *") + if not overwrite and data_get(dictionary, key): + return + data(dictionary)[key] = value + return dictionary diff --git a/src/masonite/utils/time.py b/src/masonite/utils/time.py new file mode 100644 index 000000000..ecbf7cc31 --- /dev/null +++ b/src/masonite/utils/time.py @@ -0,0 +1,58 @@ +"""Time related helpers""" +import pendulum + + +def cookie_expire_time(str_time): + """Take a string like 1 month or 5 minutes and returns a datetime formatted with cookie format. + + Arguments: + str_time {string} -- Could be values like 1 second or 3 minutes + + Returns: + str -- Cookie expiration time (Thu, 21 Oct 2021 07:28:00) + """ + instance = parse_human_time(str_time) + return instance.format("ddd, DD MMM YYYY HH:mm:ss") + + +def parse_human_time(str_time): + """Take a string like 1 month or 5 minutes and returns a pendulum instance. + + Arguments: + str_time {string} -- Could be values like 1 second or 3 minutes + + Returns: + pendulum -- Returns Pendulum instance + """ + if str_time == "now": + return pendulum.now("GMT") + + if str_time != "expired": + number = int(str_time.split(" ")[0]) + length = str_time.split(" ")[1] + + if length in ("second", "seconds"): + return pendulum.now("GMT").add(seconds=number) + elif length in ("minute", "minutes"): + return pendulum.now("GMT").add(minutes=number) + elif length in ("hour", "hours"): + return pendulum.now("GMT").add(hours=number) + elif length in ("day", "days"): + return pendulum.now("GMT").add(days=number) + elif length in ("week", "weeks"): + return pendulum.now("GMT").add(weeks=number) + elif length in ("month", "months"): + return pendulum.now("GMT").add(months=number) + elif length in ("year", "years"): + return pendulum.now("GMT").add(years=number) + + return None + else: + return pendulum.now("GMT").subtract(years=20) + + +def migration_timestamp(): + """Return current time formatted for creating migration filenames. + Example: 2021_01_09_043202 + """ + return pendulum.now().format("YYYY_MM_DD_HHmmss") diff --git a/src/masonite/validation/MessageBag.py b/src/masonite/validation/MessageBag.py new file mode 100644 index 000000000..2df8ed1e0 --- /dev/null +++ b/src/masonite/validation/MessageBag.py @@ -0,0 +1,120 @@ +"""The Message Bag Module""" + +import json + + +class MessageBag: + def __init__(self, items={}): + self.items = items + + def add(self, error, message): + """Adds an error and message to the message bag + + Arguments: + error {string} -- The error to add + message {string} -- The message to add + """ + + if error in self.items: + self.items[error].append(message) + else: + self.items.update({error: [message]}) + + def all(self): + """Get all errors and messages""" + return self.items + + def any(self): + """If the messagebag has any errors""" + return len(self.items) > 0 + + def has(self, key): + """If the messagebag has any errors""" + return key in self.all() + + def empty(self): + """If the messagebag has any errors""" + return not self.any() + + def first(self, key): + """Gets the first error and message""" + return self.get(key)[0] + + def count(self): + """Gets the amount of errors""" + return len(self.items) + + def json(self): + """Gets the amount of errors""" + return json.dumps(self.items) + + def amount(self, key): + """Gets the amount of messages + + Arguments: + key {string} -- the error to get the amount of. + + Returns: + int -- Returns the amount of messages + """ + return len(self.items[key]) + + def get(self, key): + """Gets all the messages for a specific error. + + Arguments: + key {string} -- the error to get the messages for + + Returns: + list -- list of errors + """ + return self.items[key] + + def errors(self): + """Gets a list of errors""" + return list(self.items.keys()) + + def messages(self): + """Gets a list of all the messages""" + messages = [] + for error, message in self.items.items(): + messages += message + + return messages + + def reset(self): + """Gets a list of all the messages""" + self.items = {} + + def merge(self, dictionary): + """Merge a dictionary into the message bag. + + Arguments: + dictionary {dict} -- dictionary of errors and messages. + + Returns: + dictionary -- Returns a dictionary of the new errors. + """ + self.items.update(dictionary) + return self.any() + + def new(self, dictionary): + return self.__class__(dictionary) + + def __len__(self): + return len(self.items) + + def __str__(self): + return json.dumps(self.items) + + def get_response(self): + return json.dumps(self.items) + + @staticmethod + def view_helper(errors={}): + if errors: + return MessageBag(errors) + + from wsgi import application + + return MessageBag(application.make("request").session.get("errors") or {}) diff --git a/src/masonite/validation/RuleEnclosure.py b/src/masonite/validation/RuleEnclosure.py new file mode 100644 index 000000000..db381b4b3 --- /dev/null +++ b/src/masonite/validation/RuleEnclosure.py @@ -0,0 +1,2 @@ +class RuleEnclosure: + pass diff --git a/src/masonite/validation/Validator.py b/src/masonite/validation/Validator.py new file mode 100644 index 000000000..2a19cd674 --- /dev/null +++ b/src/masonite/validation/Validator.py @@ -0,0 +1,1308 @@ +from .RuleEnclosure import RuleEnclosure +from .MessageBag import MessageBag +from ..utils.structures import data_get +import inspect +import re +import os +import mimetypes + + +class BaseValidation: + def __init__(self, validations, messages={}, raises={}): + self.errors = {} + self.messages = messages + if isinstance(validations, str): + self.validations = [validations] + else: + self.validations = validations + self.negated = False + self.raises = raises + + def passes(self, attribute, key, dictionary): + return True + + def error(self, key, message): + if key in self.messages: + if key in self.errors: + self.errors[key].append(self.messages[key]) + return + self.errors.update({key: [self.messages[key]]}) + return + + if not isinstance(message, list): + self.errors.update({key: [message]}) + else: + self.errors.update({key: message}) + + def find(self, key, dictionary, default=False): + return data_get(dictionary, key, default) + + def message(self, key): + return "" + + def negate(self): + self.negated = True + return self + + def raise_exception(self, key): + if self.raises is not True and key in self.raises: + error = self.raises.get(key) + raise error(self.errors[next(iter(self.errors))][0]) + + raise ValueError(self.errors[next(iter(self.errors))][0]) + + def handle(self, dictionary): + boolean = True + + for key in self.validations: + if self.negated: + + if self.passes(self.find(key, dictionary), key, dictionary): + boolean = False + if hasattr(self, "negated_message"): + self.error(key, self.negated_message(key)) + else: + self.error(key, self.message(key)) + + continue + attribute = self.find(key, dictionary) + if not self.passes(attribute, key, dictionary): + boolean = False + self.error(key, self.message(key)) + + if self.errors and self.raises: + return self.raise_exception(key) + + return boolean + + def reset(self): + self.errors = {} + + +class required(BaseValidation): + def passes(self, attribute, key, dictionary): + """The passing criteria for this rule. + + The key must exist in the dictionary and return a True boolean value. + The key can use * notation. + + Arguments: + attribute {mixed} -- The value found within the dictionary + key {string} -- The key in the dictionary being searched for. + dictionary {dict} -- The dictionary being searched + + Returns: + bool + """ + return self.find(key, dictionary) and attribute + + def message(self, key): + """A message to show when this rule fails + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return "The {} field is required.".format(key) + + def negated_message(self, key): + """A message to show when this rule is negated using a negation rule like 'isnt()' + + For example if you have a message that says 'this is required' you may have a negated statement + that says 'this is not required'. + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return "The {} field is not required.".format(key) + + +class timezone(BaseValidation): + def passes(self, attribute, key, dictionary): + import pytz + + return attribute in pytz.all_timezones + + def message(self, attribute): + return "The {} must be a valid timezone.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid timezone.".format(attribute) + + +class one_of(BaseValidation): + def passes(self, attribute, key, dictionary): + for validation in self.validations: + if validation in dictionary: + return True + + return False + + def message(self, attribute): + if len(self.validations) > 2: + text = ", ".join(self.validations) + else: + text = " or ".join(self.validations) + + return "The {} is required.".format(text) + + def negated_message(self, attribute): + if len(self.validations) > 2: + text = ", ".join(self.validations) + else: + text = " or ".join(self.validations) + + return "The {} is not required.".format(text) + + +class accepted(BaseValidation): + def passes(self, attribute, key, dictionary): + return ( + attribute is True + or attribute == "on" + or attribute == "yes" + or attribute == "1" + or attribute == 1 + ) + + def message(self, attribute): + return "The {} must be accepted.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be accepted.".format(attribute) + + +class ip(BaseValidation): + def passes(self, attribute, key, dictionary): + import socket + + try: + socket.inet_aton(attribute) + return True + except socket.error: + return False + + def message(self, attribute): + return "The {} must be a valid ipv4 address.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid ipv4 address.".format(attribute) + + +class date(BaseValidation): + def passes(self, attribute, key, dictionary): + import pendulum + + try: + date = pendulum.parse(attribute) + return date + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a valid date.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid date.".format(attribute) + + +class before_today(BaseValidation): + def __init__(self, validations, tz="UTC", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz) <= pendulum.yesterday() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a date before today.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a date before today.".format(attribute) + + +class after_today(BaseValidation): + def __init__(self, validations, tz="Universal", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz) >= pendulum.yesterday() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a date after today.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a date after today.".format(attribute) + + +class is_past(BaseValidation): + def __init__(self, validations, tz="Universal", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz).is_past() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a time in the past.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a time in the past.".format(attribute) + + +class is_future(BaseValidation): + def __init__(self, validations, tz="Universal", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz).is_future() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a time in the past.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a time in the past.".format(attribute) + + +class email(BaseValidation): + def passes(self, attribute, key, dictionary): + return re.compile( + r"^[^.][^@]*@([?)[a-zA-Z0-9-.])+.([a-zA-Z]{2,3}|[0-9]{1,3})(]?)$" + ).match(attribute) + + def message(self, attribute): + return "The {} must be a valid email address.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid email address.".format(attribute) + + +class matches(BaseValidation): + def __init__(self, validations, match, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.match = match + + def passes(self, attribute, key, dictionary): + return attribute == dictionary[self.match] + + def message(self, attribute): + return "The {} must match {}.".format(attribute, self.match) + + def negated_message(self, attribute): + return "The {} must not match {}.".format(attribute, self.match) + + +class exists(BaseValidation): + def passes(self, attribute, key, dictionary): + return key in dictionary + + def message(self, attribute): + return "The {} must exist.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not exist.".format(attribute) + + +class active_domain(BaseValidation): + def passes(self, attribute, key, dictionary): + import socket + + try: + if "@" in attribute: + # validation is for an email address + return socket.gethostbyname(attribute.split("@")[1]) + + return socket.gethostbyname( + attribute.replace("https://", "") + .replace("http://", "") + .replace("www.", "") + ) + except socket.gaierror: + return False + + def message(self, attribute): + return "The {} must be an active domain name.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be an active domain name.".format(attribute) + + +class numeric(BaseValidation): + def passes(self, attribute, key, dictionary): + if isinstance(attribute, list): + for value in attribute: + if not str(value).isdigit(): + return False + else: + return str(attribute).isdigit() + + return True + + def message(self, attribute): + return "The {} must be a numeric.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a numeric.".format(attribute) + + +class is_list(BaseValidation): + def passes(self, attribute, key, dictionary): + return isinstance(attribute, list) + + def message(self, attribute): + return "The {} must be a list.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a list.".format(attribute) + + +class string(BaseValidation): + def passes(self, attribute, key, dictionary): + if isinstance(attribute, list): + for attr in attribute: + if not isinstance(attr, str): + return False + + return True + + return isinstance(attribute, str) + + def message(self, attribute): + return "The {} must be a string.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a string.".format(attribute) + + +class none(BaseValidation): + def passes(self, attribute, key, dictionary): + return attribute is None + + def message(self, attribute): + return "The {} must be None.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be None.".format(attribute) + + +class length(BaseValidation): + def __init__(self, validations, min=0, max=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + if isinstance(min, str) and ".." in min: + self.min = int(min.split("..")[0]) + self.max = int(min.split("..")[1]) + else: + self.min = min + self.max = max + + def passes(self, attribute, key, dictionary): + if not hasattr(attribute, "__len__"): + attribute = str(attribute) + if self.max: + return len(attribute) >= self.min and len(attribute) <= self.max + else: + return len(attribute) >= self.min + + def message(self, attribute): + if self.min and not self.max: + return "The {} must be at least {} characters.".format(attribute, self.min) + else: + return "The {} length must be between {} and {}.".format( + attribute, self.min, self.max + ) + + def negated_message(self, attribute): + if self.min and not self.max: + return "The {} must be {} characters maximum.".format(attribute, self.max) + else: + return "The {} length must not be between {} and {}.".format( + attribute, self.min, self.max + ) + + +class in_range(BaseValidation): + def __init__(self, validations, min=1, max=255, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.min = min + self.max = max + + def passes(self, attribute, key, dictionary): + + attribute = str(attribute) + + if attribute.isalpha(): + return False + + if "." in attribute: + try: + attribute = float(attribute) + except Exception: + pass + + elif attribute.isdigit(): + attribute = int(attribute) + + return attribute >= self.min and attribute <= self.max + + def message(self, attribute): + return "The {} must be between {} and {}.".format(attribute, self.min, self.max) + + def negated_message(self, attribute): + return "The {} must not be between {} and {}.".format( + attribute, self.min, self.max + ) + + +class equals(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute == self.value + + def message(self, attribute): + return "The {} must be equal to {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not be equal to {}.".format(attribute, self.value) + + +class contains(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return self.value in attribute + + def message(self, attribute): + return "The {} must contain {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not contain {}.".format(attribute, self.value) + + +class is_in(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute in self.value + + def message(self, attribute): + return "The {} must contain an element in {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not contain an element in {}.".format(attribute, self.value) + + +class greater_than(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute > self.value + + def message(self, attribute): + return "The {} must be greater than {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must be greater than {}.".format(attribute, self.value) + + +class less_than(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute < self.value + + def message(self, attribute): + return "The {} must be less than {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not be less than {}.".format(attribute, self.value) + + +class strong(BaseValidation): + def __init__( + self, + validations, + length=8, + uppercase=2, + numbers=2, + special=2, + breach=False, + messages={}, + raises={}, + ): + super().__init__(validations, messages=messages, raises=raises) + self.length = length + self.uppercase = uppercase + self.numbers = numbers + self.special = special + self.breach = breach + self.length_check = True + self.uppercase_check = True + self.numbers_check = True + self.special_check = True + self.breach_check = True + + def passes(self, attribute, key, dictionary): + all_clear = True + + if len(attribute) < self.length: + all_clear = False + self.length_check = False + + if self.uppercase != 0: + uppercase = 0 + for letter in attribute: + if letter.isupper(): + uppercase += 1 + + if uppercase < self.uppercase: + self.uppercase_check = False + all_clear = False + + if self.numbers != 0: + numbers = 0 + for letter in attribute: + if letter.isdigit(): + numbers += 1 + + if numbers < self.numbers: + self.numbers_check = False + all_clear = False + + if self.breach: + try: + from pwnedapi import Password + except ImportError: + raise ImportError( + "Checking for breaches requires the 'pwnedapi' library. Please install it with 'pip install pwnedapi'" + ) + + password = Password(attribute) + if password.is_pwned(): + self.breach_check = False + all_clear = False + + if self.special != 0: + if len(re.findall("[^A-Za-z0-9]", attribute)) < self.special: + self.special_check = False + all_clear = False + + return all_clear + + def message(self, attribute): + message = [] + if not self.length_check: + message.append( + "The {} field must be {} characters in length".format( + attribute, self.length + ) + ) + + if not self.uppercase_check: + message.append( + "The {} field must have {} uppercase letters".format( + attribute, self.uppercase + ) + ) + + if not self.special_check: + message.append( + "The {} field must have {} special characters".format( + attribute, self.special + ) + ) + + if not self.numbers_check: + message.append( + "The {} field must have {} numbers".format(attribute, self.numbers) + ) + + if not self.breach_check: + message.append( + "The {} field has been breached in the past. Try another {}".format( + attribute, attribute + ) + ) + + return message + + def negated_message(self, attribute): + return "The {} must not be less than {}.".format(attribute, self.value) + + +class isnt(BaseValidation): + def __init__(self, *rules, messages={}, raises={}): + super().__init__(rules) + + def handle(self, dictionary): + for rule in self.validations: + rule.negate().handle(dictionary) + self.errors.update(rule.errors) + + +class does_not(BaseValidation): + def __init__(self, *rules, messages={}, raises={}): + super().__init__(rules) + self.should_run_then = True + + def handle(self, dictionary): + self.dictionary = dictionary + errors = False + for rule in self.validations: + if rule.handle(dictionary): + errors = True + + if not errors: + for rule in self.then_rules: + if not rule.handle(dictionary): + self.errors.update(rule.errors) + + def then(self, *rules): + self.then_rules = rules + return self + + +class when(BaseValidation): + def __init__(self, *rules, messages={}, raises={}): + super().__init__(rules) + self.should_run_then = True + + def handle(self, dictionary): + self.dictionary = dictionary + errors = False + for rule in self.validations: + if rule.handle(dictionary): + errors = True + + if errors: + for rule in self.then_rules: + if not rule.handle(dictionary): + self.errors.update(rule.errors) + + def then(self, *rules): + self.then_rules = rules + return self + + +class truthy(BaseValidation): + def passes(self, attribute, key, dictionary): + return attribute + + def message(self, attribute): + return "The {} must be a truthy value.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a truthy value.".format(attribute) + + +class json(BaseValidation): + def passes(self, attribute, key, dictionary): + import json as json_module + + try: + return json_module.loads(str(attribute)) + except (TypeError, json_module.decoder.JSONDecodeError): + return False + + def message(self, attribute): + return "The {} must be a valid JSON.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid JSON.".format(attribute) + + +class phone(BaseValidation): + def __init__(self, *rules, pattern="123-456-7890", messages={}, raises={}): + super().__init__(rules, messages={}, raises={}) + # 123-456-7890 + # (123)456-7890 + self.pattern = pattern + + def passes(self, attribute, key, dictionary): + if self.pattern == "(123)456-7890": + return re.compile(r"^\(\w{3}\)\w{3}\-\w{4}$").match(attribute) + elif self.pattern == "123-456-7890": + return re.compile(r"^\w{3}\-\w{3}\-\w{4}$").match(attribute) + + def message(self, attribute): + if self.pattern == "(123)456-7890": + return "The {} must be in the format (XXX)XXX-XXXX.".format(attribute) + elif self.pattern == "123-456-7890": + return "The {} must be in the format XXX-XXX-XXXX.".format(attribute) + + def negated_message(self, attribute): + if self.pattern == "(123)456-7890": + return "The {} must not be in the format (XXX)XXX-XXXX.".format(attribute) + elif self.pattern == "123-456-7890": + return "The {} must not be in the format XXX-XXX-XXXX.".format(attribute) + + +class confirmed(BaseValidation): + def passes(self, attribute, key, dictionary): + if key in dictionary and key + "_confirmation" in dictionary: + return dictionary[key] == dictionary["{}".format(key + "_confirmation")] + return False + + def message(self, attribute): + return "The {} confirmation does not match.".format(attribute) + + def negated_message(self, attribute): + return "The {} confirmation matches.".format(attribute) + + +class regex(BaseValidation): + def __init__(self, validations, pattern, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.pattern = pattern + + def passes(self, attribute, key, dictionary): + return re.compile(r"{}".format(self.pattern)).match(attribute) + + def message(self, attribute): + return "The {} does not match pattern {} .".format(attribute, self.pattern) + + def negated_message(self, attribute): + return "The {} matches pattern {} .".format(attribute, self.pattern) + + +def parse_size(size): + """Parse humanized size into bytes""" + from hfilesize import FileSize + + return FileSize(size, case_sensitive=False) + + +class BaseFileValidation(BaseValidation): + def __init__(self, validations, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.file_check = True + self.size_check = True + self.mimes_check = True + self.all_clear = True + + def passes(self, attribute, key, dictionary): + if not os.path.isfile(attribute): + self.file_check = False + return False + if self.size: + file_size = os.path.getsize(attribute) + if file_size > self.size: + self.size_check = False + self.all_clear = False + if self.allowed_extensions: + mimetype, encoding = mimetypes.guess_type(attribute) + if mimetype not in self.allowed_mimetypes: + self.mimes_check = False + self.all_clear = False + return self.all_clear + + +class file(BaseFileValidation): + def __init__(self, validations, size=False, mimes=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.size = parse_size(size) + + # parse allowed extensions to a list of mime types + self.allowed_extensions = mimes + if mimes: + self.allowed_mimetypes = list( + map(lambda mt: mimetypes.types_map.get("." + mt, None), mimes) + ) + + def message(self, attribute): + messages = [] + if not self.file_check: + messages.append("The {} is not a valid file.".format(attribute)) + + if not self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size exceeds {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + if not self.mimes_check: + messages.append( + "The {} mime type is not valid. Allowed formats are {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + + return messages + + def negated_message(self, attribute): + messages = [] + if self.file_check: + messages.append("The {} is a valid file.".format(attribute)) + if self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size is less or equal than {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + if self.mimes_check: + messages.append( + "The {} mime type is in {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + return messages + + +class image(BaseFileValidation): + def __init__(self, validations, size=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.size = parse_size(size) + image_mimetypes = { + ext: mimetype + for ext, mimetype in mimetypes.types_map.items() + if mimetype.startswith("image") + } + self.allowed_extensions = list(image_mimetypes.keys()) + self.allowed_mimetypes = list(image_mimetypes.values()) + + def message(self, attribute): + messages = [] + if not self.file_check: + messages.append("The {} is not a valid file.".format(attribute)) + + if not self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size exceeds {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if not self.mimes_check: + messages.append( + "The {} file is not a valid image. Allowed formats are {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + + return messages + + def negated_message(self, attribute): + messages = [] + if self.file_check: + messages.append("The {} is a valid file.".format(attribute)) + if self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size is less or equal than {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if self.mimes_check: + messages.append("The {} file is a valid image.".format(attribute)) + + return messages + + +class video(BaseFileValidation): + def __init__(self, validations, size=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.size = parse_size(size) + + video_mimetypes = { + ext: mimetype + for ext, mimetype in mimetypes.types_map.items() + if mimetype.startswith("video") + } + + self.allowed_extensions = list(video_mimetypes.keys()) + self.allowed_mimetypes = list(video_mimetypes.values()) + + def message(self, attribute): + messages = [] + if not self.file_check: + messages.append("The {} is not a valid file.".format(attribute)) + + if not self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size exceeds {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if not self.mimes_check: + messages.append( + "The {} file is not a valid video. Allowed formats are {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + + return messages + + def negated_message(self, attribute): + messages = [] + if self.file_check: + messages.append("The {} is a valid file.".format(attribute)) + + if self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size is less or equal than {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if self.mimes_check: + messages.append("The {} file is a valid video.".format(attribute)) + + return messages + + +class postal_code(BaseValidation): + def __init__(self, validations, locale, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + from .resources.postal_codes import PATTERNS + + self.locales = [] + self.patterns = [] + self.patterns_example = [] + self.locales = locale.split(",") + + for locale in self.locales: + pattern_dict = PATTERNS.get(locale, None) + if pattern_dict is None or pattern_dict["pattern"] is None: + raise NotImplementedError( + "Unsupported country code {}. Check that it is a ISO 3166-1 country code or open a PR to require support of this country code.".format( + locale + ) + ) + else: + self.patterns.append(pattern_dict["pattern"]) + self.patterns_example.append(pattern_dict["example"]) + + def passes(self, attribute, key, dictionary): + for pattern in self.patterns: + # check that at least one pattern match attribute + if re.compile(r"{}".format(pattern)).match(attribute): + return True + return False + + def message(self, attribute): + return "The {} is not a valid {} postal code. Valid {} {}.".format( + attribute, + ",".join(self.locales), + "examples are" if len(self.locales) > 1 else "example is", + ",".join(self.patterns_example), + ) + + def negated_message(self, attribute): + return "The {} is a valid {} postal code.".format(attribute, self.locale) + + +class different(BaseValidation): + """The field under validation must be different than an other given field.""" + + def __init__(self, validations, other_field, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.other_field = other_field + + def passes(self, attribute, key, dictionary): + other_value = dictionary.get(self.other_field, None) + return attribute != other_value + + def message(self, attribute): + return "The {} value must be different than {} value.".format( + attribute, self.other_field + ) + + def negated_message(self, attribute): + return "The {} value be the same as {} value.".format( + attribute, self.other_field + ) + + +class uuid(BaseValidation): + """The field under validation must be a valid UUID. The UUID version standard + can be precised (1,3,4,5).""" + + def __init__(self, validations, version=4, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.version = version + self.uuid_type = "UUID" + if version: + self.uuid_type = "UUID {0}".format(self.version) + + def passes(self, attribute, key, dictionary): + from uuid import UUID + + try: + uuid_value = UUID(str(attribute)) + return uuid_value.version == int(self.version) + except ValueError: + return False + + def message(self, attribute): + return "The {} value must be a valid {}.".format(attribute, self.uuid_type) + + def negated_message(self, attribute): + return "The {} value must not be a valid {}.".format(attribute, self.uuid_type) + + +class required_if(BaseValidation): + """The field under validation must be present and not empty only + if an other field has a given value.""" + + def __init__(self, validations, other_field, value, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.other_field = other_field + self.value = value + + def passes(self, attribute, key, dictionary): + if dictionary.get(self.other_field, None) == self.value: + return required.passes(self, attribute, key, dictionary) + + return True + + def message(self, attribute): + return "The {} is required because {}={}.".format( + attribute, self.other_field, self.value + ) + + def negated_message(self, attribute): + return "The {} is not required because {}={} or {} is not present.".format( + attribute, self.other_field, self.value, self.other_field + ) + + +class required_with(BaseValidation): + """The field under validation must be present and not empty only + if any of the other specified fields are present.""" + + def __init__(self, validations, other_fields, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + if not isinstance(other_fields, list): + if "," in other_fields: + self.other_fields = other_fields.split(",") + else: + self.other_fields = [other_fields] + else: + self.other_fields = other_fields + + def passes(self, attribute, key, dictionary): + for field in self.other_fields: + if field in dictionary: + return required.passes(self, attribute, key, dictionary) + else: + return True + + def message(self, attribute): + fields = ",".join(self.other_fields) + return "The {} is required because {} is present.".format( + attribute, + "one in {}".format(fields) + if len(self.other_fields) > 1 + else self.other_fields[0], + ) + + def negated_message(self, attribute): + return "The {} is not required because {} {} is not present.".format( + attribute, + "none of" if len(self.other_fields) > 1 else "", + ",".join(self.other_fields), + ) + + +class distinct(BaseValidation): + """When working with list, the field under validation must not have any + duplicate values.""" + + def passes(self, attribute, key, dictionary): + # check if list contains duplicates + return len(set(attribute)) == len(attribute) + + def message(self, attribute): + return "The {} field has duplicate values.".format(attribute) + + def negated_message(self, attribute): + return "The {} field has only different values.".format(attribute) + + +class Validator: + def __init__(self): + pass + + def validate(self, dictionary, *rules): + rule_errors = {} + try: + for rule in rules: + if isinstance(rule, str): + rule = self.parse_string(rule) + # continue + elif isinstance(rule, dict): + rule = self.parse_dict(rule, dictionary, rule_errors) + continue + + elif inspect.isclass(rule) and isinstance(rule(), RuleEnclosure): + rule_errors.update(self.run_enclosure(rule(), dictionary)) + continue + + rule.handle(dictionary) + for error, message in rule.errors.items(): + if error not in rule_errors: + rule_errors.update({error: message}) + else: + messages = rule_errors[error] + messages += message + rule_errors.update({error: messages}) + rule.reset() + return MessageBag(rule_errors) + + except Exception as e: + e.errors = rule_errors + raise e + + return MessageBag(rule_errors) + + def parse_string(self, rule): + rule, parameters = rule.split(":")[0], rule.split(":")[1].split(",") + return ValidationFactory().registry[rule](parameters) + + def parse_dict(self, rule, dictionary, rule_errors): + for value, rules in rule.items(): + for rule in rules.split("|"): + rule, args = rule.split(":")[0], rule.split(":")[1:] + rule = ValidationFactory().registry[rule](value, *args) + + rule.handle(dictionary) + for error, message in rule.errors.items(): + if error not in rule_errors: + rule_errors.update({error: message}) + else: + messages = rule_errors[error] + messages += message + rule_errors.update({error: messages}) + + def run_enclosure(self, enclosure, dictionary): + rule_errors = {} + for rule in enclosure.rules(): + rule.handle(dictionary) + for error, message in rule.errors.items(): + if error not in rule_errors: + rule_errors.update({error: message}) + else: + messages = rule_errors[error] + messages += message + rule_errors.update({error: messages}) + rule.reset() + return rule_errors + + def extend(self, key, obj=None): + if isinstance(key, dict): + self.__dict__.update(key) + return self + + self.__dict__.update({key: obj}) + return self + + def register(self, *cls): + for obj in cls: + self.__dict__.update({obj.__name__: obj}) + ValidationFactory().register(obj) + + +class ValidationFactory: + + registry = {} + + def __init__(self): + self.register( + accepted, + active_domain, + after_today, + before_today, + confirmed, + contains, + date, + does_not, + different, + distinct, + equals, + email, + exists, + file, + greater_than, + image, + in_range, + is_future, + is_in, + isnt, + is_list, + is_past, + ip, + json, + length, + less_than, + matches, + none, + numeric, + one_of, + phone, + postal_code, + regex, + required, + required_if, + required_with, + string, + strong, + timezone, + truthy, + uuid, + video, + when, + ) + + def register(self, *cls): + for obj in cls: + self.registry.update({obj.__name__: obj}) diff --git a/src/masonite/validation/__init__.py b/src/masonite/validation/__init__.py new file mode 100644 index 000000000..1b907c265 --- /dev/null +++ b/src/masonite/validation/__init__.py @@ -0,0 +1,48 @@ +from .RuleEnclosure import RuleEnclosure +from .MessageBag import MessageBag +from .Validator import ( + BaseValidation, + ValidationFactory, + Validator, + accepted, + active_domain, + after_today, + before_today, + confirmed, + contains, + date, + different, + distinct, + does_not, + email, + equals, + exists, + file, + greater_than, + image, + in_range, + ip, + is_future, + is_list, + is_in, + is_past, + isnt, + json, + length, + less_than, + none, + numeric, + phone, + postal_code, + regex, + required, + required_if, + required_with, + string, + strong, + timezone, + truthy, + uuid, + video, + when, +) diff --git a/src/masonite/validation/commands/MakeRuleCommand.py b/src/masonite/validation/commands/MakeRuleCommand.py new file mode 100644 index 000000000..52e652756 --- /dev/null +++ b/src/masonite/validation/commands/MakeRuleCommand.py @@ -0,0 +1,46 @@ +"""New Rule Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import get_module_dir, make_directory, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeRuleCommand(Command): + """ + Creates a new rule. + + rule + {name : Name of the rule} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_stub_rule_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("validation.location")), name + ".py" + ) + + if os.path.exists(relative_filename): + return self.line( + f"File ({relative_filename}) already exists" + ) + + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Validation Rule Created ({relative_filename})") + + def get_stub_rule_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/validation/Rule.py") diff --git a/src/masonite/validation/commands/MakeRuleEnclosureCommand.py b/src/masonite/validation/commands/MakeRuleEnclosureCommand.py new file mode 100644 index 000000000..acc101755 --- /dev/null +++ b/src/masonite/validation/commands/MakeRuleEnclosureCommand.py @@ -0,0 +1,46 @@ +"""New Rule Enclosure Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import get_module_dir, make_directory, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeRuleEnclosureCommand(Command): + """ + Creates a new rule enclosure. + + rule:enclosure + {name : Name of the rule enclosure} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_stub_rule_enclosure_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("validation.location")), name + ".py" + ) + + if os.path.exists(relative_filename): + return self.line( + f"File ({relative_filename}) already exists" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Validation Created ({relative_filename})") + + def get_stub_rule_enclosure_path(self): + return os.path.join( + get_module_dir(__file__), "../../stubs/validation/RuleEnclosure.py" + ) diff --git a/storage/__init__.py b/src/masonite/validation/commands/__init__.py similarity index 100% rename from storage/__init__.py rename to src/masonite/validation/commands/__init__.py diff --git a/src/masonite/validation/decorators.py b/src/masonite/validation/decorators.py new file mode 100644 index 000000000..26dc2702f --- /dev/null +++ b/src/masonite/validation/decorators.py @@ -0,0 +1,20 @@ +def validate(*rules, redirect=None, back=None): + def decorator(func, rules=rules): + def wrapper(*args, **kwargs): + from wsgi import container + + request = container.make("Request") + response = container.make("Response") + errors = request.validate(*rules) + if errors: + if redirect: + return response.redirect(redirect).with_errors(errors).with_input() + if back: + return response.back().with_errors(errors).with_input() + return errors + else: + return container.resolve(func) + + return wrapper + + return decorator diff --git a/src/masonite/validation/providers/ValidationProvider.py b/src/masonite/validation/providers/ValidationProvider.py new file mode 100644 index 000000000..1f35f905a --- /dev/null +++ b/src/masonite/validation/providers/ValidationProvider.py @@ -0,0 +1,35 @@ +"""A Validation Service Provider.""" +from ...providers import Provider +from .. import Validator, ValidationFactory, MessageBag +from ..commands.MakeRuleEnclosureCommand import MakeRuleEnclosureCommand +from ..commands.MakeRuleCommand import MakeRuleCommand + + +class ValidationProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + validator = Validator() + self.application.bind("validator", validator) + self.application.make("commands").add( + MakeRuleEnclosureCommand(self.application), + MakeRuleCommand(self.application), + ) + + MessageBag.get_errors = self._get_errors + self.application.make("view").share({"bag": MessageBag.view_helper}) + validator.extend(ValidationFactory().registry) + + def boot(self): + pass + + def _get_errors(self): + request = self.application.make("request") + messages = [] + for error, message in ( + request.session.get_flashed_messages().get("errors", {}).items() + ): + messages += message + + return messages diff --git a/src/masonite/validation/providers/__init__.py b/src/masonite/validation/providers/__init__.py new file mode 100644 index 000000000..2c972a335 --- /dev/null +++ b/src/masonite/validation/providers/__init__.py @@ -0,0 +1 @@ +from .ValidationProvider import ValidationProvider diff --git a/src/masonite/validation/resources/postal_codes.py b/src/masonite/validation/resources/postal_codes.py new file mode 100644 index 000000000..79380a487 --- /dev/null +++ b/src/masonite/validation/resources/postal_codes.py @@ -0,0 +1,1011 @@ +# noqa: W605 +PATTERNS = { + "AC": { + "example": "ASCN 1ZZ", + "pattern": r"^(?:ASCN 1ZZ)$", + }, + "AD": { + "example": "AD100", + "pattern": r"^(?:AD[1-7]0\d)$", + }, + "AE": { + "example": None, + "pattern": None, + }, + "AF": { + "example": "1001", + "pattern": r"^(?:\d{4})$", + }, + "AG": { + "example": None, + "pattern": None, + }, + "AI": { + "example": "2640", + "pattern": r"^(?:(?:AI-)?2640)$", + }, + "AL": { + "example": "1001", + "pattern": r"^(?:\d{4})$", + }, + "AM": { + "example": "375010", + "pattern": r"^(?:(?:37)?\d{4})$", + }, + "AO": { + "example": None, + "pattern": None, + }, + "AQ": { + "example": None, + "pattern": None, + }, + "AR": { + "example": "C1070AAM", + "pattern": r"^(?:((?:[A-HJ-NP-Z])?\d{4})([A-Z]{3})?)$", + }, + "AS": { + "example": "96799", + "pattern": r"^(?:(96799)(?:[ \-](\d{4}))?)$", + }, + "AT": { + "example": "1010", + "pattern": r"^(?:\d{4})$", + }, + "AU": { + "example": "2060", + "pattern": r"^(?:\d{4})$", + }, + "AW": { + "example": None, + "pattern": None, + }, + "AX": { + "example": "22150", + "pattern": r"^(?:22\d{3})$", + }, + "AZ": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "BA": { + "example": "71000", + "pattern": r"^(?:\d{5})$", + }, + "BB": { + "example": "BB23026", + "pattern": r"^(?:BB\d{5})$", + }, + "BD": { + "example": "1340", + "pattern": r"^(?:\d{4})$", + }, + "BE": { + "example": "4000", + "pattern": r"^(?:\d{4})$", + }, + "BF": { + "example": None, + "pattern": None, + }, + "BG": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "BH": { + "example": "317", + "pattern": r"^(?:(?:\d|1[0-2])\d{2})$", + }, + "BI": { + "example": None, + "pattern": None, + }, + "BJ": { + "example": None, + "pattern": None, + }, + "BL": { + "example": "97100", + "pattern": r"^(?:9[78][01]\d{2})$", + }, + "BM": { + "example": "FL 07", + "pattern": r"^(?:[A-Z]{2} ?[A-Z0-9]{2})$", + }, + "BN": { + "example": "BT2328", + "pattern": r"^(?:[A-Z]{2} ?\d{4})$", + }, + "BO": { + "example": None, + "pattern": None, + }, + "BQ": { + "example": None, + "pattern": None, + }, + "BR": { + "example": "40301-110", + "pattern": r"^(?:\d{5}-?\d{3})$", + }, + "BS": { + "example": None, + "pattern": None, + }, + "BT": { + "example": "11001", + "pattern": r"^(?:\d{5})$", + }, + "BV": { + "example": None, + "pattern": None, + }, + "BW": { + "example": None, + "pattern": None, + }, + "BY": { + "example": "223016", + "pattern": r"^(?:\d{6})$", + }, + "BZ": { + "example": None, + "pattern": None, + }, + "CA": { + "example": "H3Z 2Y7", + "pattern": r"^(?:[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d)$", + }, + "CC": { + "example": "6799", + "pattern": r"^(?:6799)$", + }, + "CD": { + "example": None, + "pattern": None, + }, + "CF": { + "example": None, + "pattern": None, + }, + "CG": { + "example": None, + "pattern": None, + }, + "CH": { + "example": "2544", + "pattern": r"^(?:\d{4})$", + }, + "CI": { + "example": None, + "pattern": None, + }, + "CK": { + "example": None, + "pattern": None, + }, + "CL": { + "example": "8340457", + "pattern": r"^(?:\d{7})$", + }, + "CM": { + "example": None, + "pattern": None, + }, + "CN": { + "example": "266033", + "pattern": r"^(?:\d{6})$", + }, + "CO": { + "example": "111221", + "pattern": r"^(?:\d{6})$", + }, + "CR": { + "example": "1000", + "pattern": r"^(?:\d{4,5}|\d{3}-\d{4})$", + }, + "CU": { + "example": "10700", + "pattern": r"^(?:\d{5})$", + }, + "CV": { + "example": "7600", + "pattern": r"^(?:\d{4})$", + }, + "CW": { + "example": None, + "pattern": None, + }, + "CX": { + "example": "6798", + "pattern": r"^(?:6798)$", + }, + "CY": { + "example": "2008", + "pattern": r"^(?:\d{4})$", + }, + "CZ": { + "example": "100 00", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "DE": { + "example": "26133", + "pattern": r"^(?:\d{5})$", + }, + "DJ": { + "example": None, + "pattern": None, + }, + "DK": { + "example": "8660", + "pattern": r"^(?:\d{4})$", + }, + "DM": { + "example": None, + "pattern": None, + }, + "DO": { + "example": "11903", + "pattern": r"^(?:\d{5})$", + }, + "DZ": { + "example": "40304", + "pattern": r"^(?:\d{5})$", + }, + "EC": { + "example": "090105", + "pattern": r"^(?:\d{6})$", + }, + "EE": { + "example": "69501", + "pattern": r"^(?:\d{5})$", + }, + "EG": { + "example": "12411", + "pattern": r"^(?:\d{5})$", + }, + "EH": { + "example": "70000", + "pattern": r"^(?:\d{5})$", + }, + "ER": { + "example": None, + "pattern": None, + }, + "ES": { + "example": "28039", + "pattern": r"^(?:\d{5})$", + }, + "ET": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "FI": { + "example": "00550", + "pattern": r"^(?:\d{5})$", + }, + "FJ": { + "example": None, + "pattern": None, + }, + "FK": { + "example": "FIQQ 1ZZ", + "pattern": r"^(?:FIQQ 1ZZ)$", + }, + "FM": { + "example": "96941", + "pattern": r"^(?:(9694[1-4])(?:[ \-](\d{4}))?)$", + }, + "FO": { + "example": "100", + "pattern": r"^(?:\d{3})$", + }, + "FR": { + "example": "33380", + "pattern": r"^(?:\d{2} ?\d{3})$", + }, + "GA": { + "example": None, + "pattern": None, + }, + "GB": { + "example": "EC1Y 8SY", + "pattern": r"^(?:GIR ?0AA|(?:(?:AB|AL|B|BA|BB|BD|BF|BH|BL|BN|BR|BS|BT|BX|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(?:\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2}))|BFPO ?\d{1,4})$", + }, + "GD": { + "example": None, + "pattern": None, + }, + "GE": { + "example": "0101", + "pattern": r"^(?:\d{4})$", + }, + "GF": { + "example": "97300", + "pattern": r"^(?:9[78]3\d{2})$", + }, + "GG": { + "example": "GY1 1AA", + "pattern": r"^(?:GY\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2})$", + }, + "GH": { + "example": None, + "pattern": None, + }, + "GI": { + "example": "GX11 1AA", + "pattern": r"^(?:GX11 1AA)$", + }, + "GL": { + "example": "3900", + "pattern": r"^(?:39\d{2})$", + }, + "GM": { + "example": None, + "pattern": None, + }, + "GN": { + "example": "001", + "pattern": r"^(?:\d{3})$", + }, + "GP": { + "example": "97100", + "pattern": r"^(?:9[78][01]\d{2})$", + }, + "GQ": { + "example": None, + "pattern": None, + }, + "GR": { + "example": "151 24", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "GS": { + "example": "SIQQ 1ZZ", + "pattern": r"^(?:SIQQ 1ZZ)$", + }, + "GT": { + "example": "09001", + "pattern": r"^(?:\d{5})$", + }, + "GU": { + "example": "96910", + "pattern": r"^(?:(969(?:[12]\d|3[12]))(?:[ \-](\d{4}))?)$", + }, + "GW": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "GY": { + "example": None, + "pattern": None, + }, + "HK": { + "example": None, + "pattern": None, + }, + "HM": { + "example": "7050", + "pattern": r"^(?:\d{4})$", + }, + "HN": { + "example": "31301", + "pattern": r"^(?:\d{5})$", + }, + "HR": { + "example": "10000", + "pattern": r"^(?:\d{5})$", + }, + "HT": { + "example": "6120", + "pattern": r"^(?:\d{4})$", + }, + "HU": { + "example": "1037", + "pattern": r"^(?:\d{4})$", + }, + "ID": { + "example": "40115", + "pattern": r"^(?:\d{5})$", + }, + "IE": { + "example": "A65 F4E2", + "pattern": r"^(?:[\dA-Z]{3} ?[\dA-Z]{4})$", + }, + "IL": { + "example": "9614303", + "pattern": r"^(?:\d{5}(?:\d{2})?)$", + }, + "IM": { + "example": "IM2 1AA", + "pattern": r"^(?:IM\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2})$", + }, + "IN": { + "example": "110034", + "pattern": r"^(?:\d{6})$", + }, + "IO": { + "example": "BBND 1ZZ", + "pattern": r"^(?:BBND 1ZZ)$", + }, + "IQ": { + "example": "31001", + "pattern": r"^(?:\d{5})$", + }, + "IR": { + "example": "11936-12345", + "pattern": r"^(?:\d{5}-?\d{5})$", + }, + "IS": { + "example": "320", + "pattern": r"^(?:\d{3})$", + }, + "IT": { + "example": "00144", + "pattern": r"^(?:\d{5})$", + }, + "JE": { + "example": "JE1 1AA", + "pattern": r"^(?:JE\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2})$", + }, + "JM": { + "example": None, + "pattern": None, + }, + "JO": { + "example": "11937", + "pattern": r"^(?:\d{5})$", + }, + "JP": { + "example": "154-0023", + "pattern": r"^(?:\d{3}-?\d{4})$", + }, + "KE": { + "example": "20100", + "pattern": r"^(?:\d{5})$", + }, + "KG": { + "example": "720001", + "pattern": r"^(?:\d{6})$", + }, + "KH": { + "example": "12203", + "pattern": r"^(?:\d{5})$", + }, + "KI": { + "example": None, + "pattern": None, + }, + "KM": { + "example": None, + "pattern": None, + }, + "KN": { + "example": None, + "pattern": None, + }, + "KP": { + "example": None, + "pattern": None, + }, + "KR": { + "example": "03051", + "pattern": r"^(?:\d{5})$", + }, + "KW": { + "example": "54541", + "pattern": r"^(?:\d{5})$", + }, + "KY": { + "example": "KY1-1100", + "pattern": r"^(?:KY\d-\d{4})$", + }, + "KZ": { + "example": "040900", + "pattern": r"^(?:\d{6})$", + }, + "LA": { + "example": "01160", + "pattern": r"^(?:\d{5})$", + }, + "LB": { + "example": "2038 3054", + "pattern": r"^(?:(?:\d{4})(?: ?(?:\d{4}))?)$", + }, + "LC": { + "example": None, + "pattern": None, + }, + "LI": { + "example": "9496", + "pattern": r"^(?:948[5-9]|949[0-8])$", + }, + "LK": { + "example": "20000", + "pattern": r"^(?:\d{5})$", + }, + "LR": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "LS": { + "example": "100", + "pattern": r"^(?:\d{3})$", + }, + "LT": { + "example": "04340", + "pattern": r"^(?:\d{5})$", + }, + "LU": { + "example": "4750", + "pattern": r"^(?:\d{4})$", + }, + "LV": { + "example": "LV-1073", + "pattern": r"^(?:LV-\d{4})$", + }, + "LY": { + "example": None, + "pattern": None, + }, + "MA": { + "example": "53000", + "pattern": r"^(?:\d{5})$", + }, + "MC": { + "example": "98000", + "pattern": r"^(?:980\d{2})$", + }, + "MD": { + "example": "2012", + "pattern": r"^(?:\d{4})$", + }, + "ME": { + "example": "81257", + "pattern": r"^(?:8\d{4})$", + }, + "MF": { + "example": "97100", + "pattern": r"^(?:9[78][01]\d{2})$", + }, + "MG": { + "example": "501", + "pattern": r"^(?:\d{3})$", + }, + "MH": { + "example": "96960", + "pattern": r"^(?:(969[67]\d)(?:[ \-](\d{4}))?)$", + }, + "MK": { + "example": "1314", + "pattern": r"^(?:\d{4})$", + }, + "ML": { + "example": None, + "pattern": None, + }, + "MM": { + "example": "11181", + "pattern": r"^(?:\d{5})$", + }, + "MN": { + "example": "65030", + "pattern": r"^(?:\d{5})$", + }, + "MO": { + "example": None, + "pattern": None, + }, + "MP": { + "example": "96950", + "pattern": r"^(?:(9695[012])(?:[ \-](\d{4}))?)$", + }, + "MQ": { + "example": "97220", + "pattern": r"^(?:9[78]2\d{2})$", + }, + "MR": { + "example": None, + "pattern": None, + }, + "MS": { + "example": None, + "pattern": None, + }, + "MT": { + "example": "NXR 01", + "pattern": r"^(?:[A-Z]{3} ?\d{2,4})$", + }, + "MU": { + "example": "42602", + "pattern": r"^(?:\d{3}(?:\d{2}|[A-Z]{2}\d{3}))$", + }, + "MV": { + "example": "20026", + "pattern": r"^(?:\d{5})$", + }, + "MW": { + "example": None, + "pattern": None, + }, + "MX": { + "example": "02860", + "pattern": r"^(?:\d{5})$", + }, + "MY": { + "example": "43000", + "pattern": r"^(?:\d{5})$", + }, + "MZ": { + "example": "1102", + "pattern": r"^(?:\d{4})$", + }, + "NA": { + "example": "10001", + "pattern": r"^(?:\d{5})$", + }, + "NC": { + "example": "98814", + "pattern": r"^(?:988\d{2})$", + }, + "NE": { + "example": "8001", + "pattern": r"^(?:\d{4})$", + }, + "NF": { + "example": "2899", + "pattern": r"^(?:2899)$", + }, + "NG": { + "example": "930283", + "pattern": r"^(?:\d{6})$", + }, + "NI": { + "example": "52000", + "pattern": r"^(?:\d{5})$", + }, + "NL": { + "example": "1234 AB", + "pattern": r"^(?:\d{4} ?[A-Z]{2})$", + }, + "NO": { + "example": "0025", + "pattern": r"^(?:\d{4})$", + }, + "NP": { + "example": "44601", + "pattern": r"^(?:\d{5})$", + }, + "NR": { + "example": None, + "pattern": None, + }, + "NU": { + "example": None, + "pattern": None, + }, + "NZ": { + "example": "6001", + "pattern": r"^(?:\d{4})$", + }, + "OM": { + "example": "133", + "pattern": r"^(?:(?:PC )?\d{3})$", + }, + "PA": { + "example": None, + "pattern": None, + }, + "PE": { + "example": "LIMA 23", + "pattern": r"^(?:(?:LIMA \d{1,2}|CALLAO 0?\d)|[0-2]\d{4})$", + }, + "PF": { + "example": "98709", + "pattern": r"^(?:987\d{2})$", + }, + "PG": { + "example": "111", + "pattern": r"^(?:\d{3})$", + }, + "PH": { + "example": "1008", + "pattern": r"^(?:\d{4})$", + }, + "PK": { + "example": "44000", + "pattern": r"^(?:\d{5})$", + }, + "PL": { + "example": "00-950", + "pattern": r"^(?:\d{2}-\d{3})$", + }, + "PM": { + "example": "97500", + "pattern": r"^(?:9[78]5\d{2})$", + }, + "PN": { + "example": "PCRN 1ZZ", + "pattern": r"^(?:PCRN 1ZZ)$", + }, + "PR": { + "example": "00930", + "pattern": r"^(?:(00[679]\d{2})(?:[ \-](\d{4}))?)$", + }, + "PS": { + "example": None, + "pattern": None, + }, + "PT": { + "example": "2725-079", + "pattern": r"^(?:\d{4}-\d{3})$", + }, + "PW": { + "example": "96940", + "pattern": r"^(?:(969(?:39|40))(?:[ \-](\d{4}))?)$", + }, + "PY": { + "example": "1536", + "pattern": r"^(?:\d{4})$", + }, + "QA": { + "example": None, + "pattern": None, + }, + "RE": { + "example": "97400", + "pattern": r"^(?:9[78]4\d{2})$", + }, + "RO": { + "example": "060274", + "pattern": r"^(?:\d{6})$", + }, + "RS": { + "example": "106314", + "pattern": r"^(?:\d{5,6})$", + }, + "RU": { + "example": "247112", + "pattern": r"^(?:\d{6})$", + }, + "RW": { + "example": None, + "pattern": None, + }, + "SA": { + "example": "11564", + "pattern": r"^(?:\d{5})$", + }, + "SB": { + "example": None, + "pattern": None, + }, + "SC": { + "example": None, + "pattern": None, + }, + "SD": { + "example": "11042", + "pattern": r"^(?:\d{5})$", + }, + "SE": { + "example": "11455", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "SG": { + "example": "546080", + "pattern": r"^(?:\d{6})$", + }, + "SH": { + "example": "STHL 1ZZ", + "pattern": r"^(?:(?:ASCN|STHL) 1ZZ)$", + }, + "SI": { + "example": "4000", + "pattern": r"^(?:\d{4})$", + }, + "SJ": { + "example": "9170", + "pattern": r"^(?:\d{4})$", + }, + "SK": { + "example": "010 01", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "SL": { + "example": None, + "pattern": None, + }, + "SM": { + "example": "47890", + "pattern": r"^(?:4789\d)$", + }, + "SN": { + "example": "12500", + "pattern": r"^(?:\d{5})$", + }, + "SO": { + "example": "JH 09010", + "pattern": r"^(?:[A-Z]{2} ?\d{5})$", + }, + "SR": { + "example": None, + "pattern": None, + }, + "SS": { + "example": None, + "pattern": None, + }, + "ST": { + "example": None, + "pattern": None, + }, + "SV": { + "example": "CP 1101", + "pattern": r"^(?:CP [1-3][1-7][0-2]\d)$", + }, + "SX": { + "example": None, + "pattern": None, + }, + "SY": { + "example": None, + "pattern": None, + }, + "SZ": { + "example": "H100", + "pattern": r"^(?:[HLMS]\d{3})$", + }, + "TA": { + "example": "TDCU 1ZZ", + "pattern": r"^(?:TDCU 1ZZ)$", + }, + "TC": { + "example": "TKCA 1ZZ", + "pattern": r"^(?:TKCA 1ZZ)$", + }, + "TD": { + "example": None, + "pattern": None, + }, + "TF": { + "example": None, + "pattern": None, + }, + "TG": { + "example": None, + "pattern": None, + }, + "TH": { + "example": "10150", + "pattern": r"^(?:\d{5})$", + }, + "TJ": { + "example": "735450", + "pattern": r"^(?:\d{6})$", + }, + "TK": { + "example": None, + "pattern": None, + }, + "TL": { + "example": None, + "pattern": None, + }, + "TM": { + "example": "744000", + "pattern": r"^(?:\d{6})$", + }, + "TN": { + "example": "1002", + "pattern": r"^(?:\d{4})$", + }, + "TO": { + "example": None, + "pattern": None, + }, + "TR": { + "example": "01960", + "pattern": r"^(?:\d{5})$", + }, + "TT": { + "example": None, + "pattern": None, + }, + "TV": { + "example": None, + "pattern": None, + }, + "TW": { + "example": "104", + "pattern": r"^(?:\d{3}(?:\d{2,3})?)$", + }, + "TZ": { + "example": "6090", + "pattern": r"^(?:\d{4,5})$", + }, + "UA": { + "example": "15432", + "pattern": r"^(?:\d{5})$", + }, + "UG": { + "example": None, + "pattern": None, + }, + "UM": { + "example": "96898", + "pattern": r"^(?:96898)$", + }, + "US": { + "example": "95014", + "pattern": r"^(?:(\d{5})(?:[ \-](\d{4}))?)$", + }, + "UY": { + "example": "11600", + "pattern": r"^(?:\d{5})$", + }, + "UZ": { + "example": "702100", + "pattern": r"^(?:\d{6})$", + }, + "VA": { + "example": "00120", + "pattern": r"^(?:00120)$", + }, + "VC": { + "example": "VC0100", + "pattern": r"^(?:VC\d{4})$", + }, + "VE": { + "example": "1010", + "pattern": r"^(?:\d{4})$", + }, + "VG": { + "example": "VG1110", + "pattern": r"^(?:VG\d{4})$", + }, + "VI": { + "example": "00802-1222", + "pattern": r"^(?:(008(?:(?:[0-4]\d)|(?:5[01])))(?:[ \-](\d{4}))?)$", + }, + "VN": { + "example": "70010", + "pattern": r"^(?:\d{5}\d?)$", + }, + "VU": { + "example": None, + "pattern": None, + }, + "WF": { + "example": "98600", + "pattern": r"^(?:986\d{2})$", + }, + "WS": { + "example": None, + "pattern": None, + }, + "XK": { + "example": "10000", + "pattern": r"^(?:[1-7]\d{4})$", + }, + "YE": { + "example": None, + "pattern": None, + }, + "YT": { + "example": "97600", + "pattern": r"^(?:976\d{2})$", + }, + "ZA": { + "example": "0083", + "pattern": r"^(?:\d{4})$", + }, + "ZM": { + "example": "50100", + "pattern": r"^(?:\d{5})$", + }, + "ZW": { + "example": None, + "pattern": None, + }, +} diff --git a/src/masonite/view.py b/src/masonite/view.py deleted file mode 100644 index 04ee85fdf..000000000 --- a/src/masonite/view.py +++ /dev/null @@ -1,321 +0,0 @@ -"""View Module.""" - - -from jinja2 import ChoiceLoader, Environment, PackageLoader, select_autoescape -from jinja2.exceptions import TemplateNotFound - -from .exceptions import RequiredContainerBindingNotFound, ViewException -from .response import Responsable - - -class View(Responsable): - """View class. Responsible for handling everything involved with views and view environments.""" - - _splice = "/" - - def __init__(self, container): - """View constructor. - - Arguments: - container {masonite.app.App} -- Container object. - """ - self.dictionary = {} - self.composers = {} - self.container = container - - # If the cache_for method is declared - self.cache = False - # Cache time of cache_for - self.cache_time = None - # Cache type of cache_for - self.cache_type = None - - self.template = None - self.environments = [] - self.extension = ".html" - self._jinja_extensions = ["jinja2.ext.loopcontrols"] - self._filters = {} - self._tests = {} - self._shared = {} - - def render(self, template, dictionary={}): - """Get the string contents of the view. - - Arguments: - template {string} -- Name of the template you want to render. - - Keyword Arguments: - dictionary {dict} -- Data that you want to pass into your view. (default: {{}}) - - Returns: - self - """ - if not isinstance(dictionary, dict): - raise ViewException( - "Second parameter to render method needs to be a dictionary, {} passed.".format( - type(dictionary).__name__ - ) - ) - - self.__load_environment(template) - self.dictionary = {} - - self.dictionary.update(dictionary) - self.dictionary.update(self._shared) - - # Check if use cache and return template from cache if exists - if ( - self.container.has("Cache") - and self.__cached_template_exists() - and not self.__is_expired_cache() - ): - return self.__get_cached_template() - - # Check if composers are even set for a speed improvement - if self.composers: - self._update_from_composers() - - if self._tests: - self.env.tests.update(self._tests) - - self.rendered_template = self._render() - - return self - - def _render(self): - try: - # Try rendering the template with '.html' appended - return self.env.get_template(self.filename).render(self.dictionary) - except TemplateNotFound: - # Try rendering the direct template the user has supplied - return self.env.get_template(self.template).render(self.dictionary) - - def _update_from_composers(self): - """Add data into the view from specified composers.""" - # Check if the template is directly specified in the composer - if self.template in self.composers: - self.dictionary.update(self.composers.get(self.template)) - - # Check if there is just an astericks in the composer - if "*" in self.composers: - self.dictionary.update(self.composers.get("*")) - - # We will append onto this string for an easier way to search through wildcard routes - compiled_string = "" - - # Check for wildcard view composers - for template in self.template.split(self._splice): - # Append the template onto the compiled_string - compiled_string += template - if self.composers.get("{}*".format(compiled_string)): - self.dictionary.update(self.composers["{}*".format(compiled_string)]) - else: - # Add a slash to symbolize going into a deeper directory structure - compiled_string += "/" - - def composer(self, composer_name, dictionary): - """Update composer dictionary. - - Arguments: - composer_name {string} -- Key to bind dictionary of data to. - dictionary {dict} -- Dictionary of data to add to controller. - - Returns: - self - """ - if isinstance(composer_name, str): - self.composers[composer_name] = dictionary - - if isinstance(composer_name, list): - for composer in composer_name: - self.composers[composer] = dictionary - - return self - - def share(self, dictionary): - """Share data to all templates. - - Arguments: - dictionary {dict} -- Dictionary of key value pairs to add to all views. - - Returns: - self - """ - self._shared.update(dictionary) - return self - - def cache_for(self, time=None, cache_type=None): - """Set time and type for cache. - - Keyword Arguments: - time {string} -- Time to cache template for (default: {None}) - cache_type {string} -- Type of the cache. (default: {None}) - - Raises: - RequiredContainerBindingNotFound -- Thrown when the Cache key binding is not found in the container. - - Returns: - self - """ - if not self.container.has("Cache"): - raise RequiredContainerBindingNotFound( - "The 'Cache' container binding is required to use this method and wasn't found in the container. You may be missing a Service Provider" - ) - - self.cache = True - self.cache_time = float(time) - self.cache_type = cache_type - if self.__is_expired_cache(): - self.__create_cache_template(self.template) - return self - - def exists(self, template): - """Check if a template exists. - - Arguments: - template {string} -- Name of the template to check for. - - Returns: - bool - """ - self.__load_environment(template) - - try: - self.env.get_template(self.filename) - return True - except TemplateNotFound: - return False - - def add_environment(self, template_location, loader=PackageLoader): - """Add an environment to the templates. - - Arguments: - template_location {string} -- Directory location to attach the environment to. - - Keyword Arguments: - loader {jinja2.Loader} -- Type of Jinja2 loader to use. (default: {jinja2.PackageLoader}) - """ - if loader == PackageLoader: - template_location = template_location.split(self._splice) - - self.environments.append( - loader(template_location[0], "/".join(template_location[1:])) - ) - else: - self.environments.append(loader(template_location)) - - def filter(self, name, function): - """Use to add filters to views. - - Arguments: - name {string} -- Key to bind the filter to. - function {object} -- Function used for the template filter. - """ - self._filters.update({name: function}) - - def test(self, key, obj): - self._tests.update({key: obj}) - return self - - def add_extension(self, extension): - self._jinja_extensions.append(extension) - return self - - def __load_environment(self, template): - """Private method for loading all the environments. - - Arguments: - template {string} -- Template to load environment from. - """ - self.template = template - self.filename = ( - template.replace(self._splice, "/").replace(".", "/") + self.extension - ) - - if template.startswith("/"): - # Filter blanks strings from the split - location = list(filter(None, template.split("/"))) - self.filename = location[-1] + self.extension - - loader = ChoiceLoader( - [PackageLoader(location[0], "/".join(location[1:-1]))] - + self.environments - ) - self.env = Environment( - loader=loader, - autoescape=select_autoescape(["html", "xml"]), - extensions=self._jinja_extensions, - line_statement_prefix="@", - ) - - else: - loader = ChoiceLoader( - [PackageLoader("resources", "templates")] + self.environments - ) - - # Set the searchpath since some packages look for this object - # This is sort of a hack for now - loader.searchpath = "" - - self.env = Environment( - loader=loader, - autoescape=select_autoescape(["html", "xml"]), - extensions=self._jinja_extensions, - line_statement_prefix="@", - ) - - self.env.filters.update(self._filters) - - def __create_cache_template(self, template): - """Save in the cache the template. - - Arguments: - template {string} -- Creates the cached templates. - """ - self.container.make("Cache").store_for( - template, - self.rendered_template, - self.cache_time, - self.cache_type, - ".html", - ) - - def __cached_template_exists(self): - """Check if the cache template exists. - - Returns: - bool - """ - return self.container.make("Cache").exists(self.template) - - def __is_expired_cache(self): - """Check if cache is expired. - - Returns: - bool - """ - # Check if cache_for is set and configurate - if self.cache_time is None or self.cache_type is None and self.cache: - return True - - driver_cache = self.container.make("Cache") - - # True is expired - return not driver_cache.is_valid(self.template) - - def __get_cached_template(self): - """Return the cached version of the template. - - Returns: - self - """ - driver_cache = self.container.make("Cache") - self.rendered_template = driver_cache.get(self.template) - return self - - def set_splice(self, splice): - self._splice = splice - return self - - def get_response(self): - return self.rendered_template diff --git a/bootstrap/cache/.gitignore b/src/masonite/views/ViewCapsule.py similarity index 100% rename from bootstrap/cache/.gitignore rename to src/masonite/views/ViewCapsule.py diff --git a/src/masonite/views/__init__.py b/src/masonite/views/__init__.py new file mode 100644 index 000000000..7c633a131 --- /dev/null +++ b/src/masonite/views/__init__.py @@ -0,0 +1 @@ +from .view import View diff --git a/src/masonite/views/view.py b/src/masonite/views/view.py new file mode 100644 index 000000000..d613b1d09 --- /dev/null +++ b/src/masonite/views/view.py @@ -0,0 +1,275 @@ +"""View Module.""" +from collections import defaultdict +from os.path import split, exists +from jinja2 import ChoiceLoader, Environment, PackageLoader, select_autoescape +from jinja2.exceptions import TemplateNotFound + +from ..exceptions import ViewException +from ..utils.str import as_filepath +from ..utils.location import views_path + + +def path_to_package(path, separator="/"): + # ensure no leading/trailing slashes before splitting to avoid blank strings + location = path.strip(separator).split(separator) + package_name = location[0] + package_path = "/".join(location[1:]) + return package_name, package_path + + +class View: + """Responsible for handling everything involved with views and view environments.""" + + separator = "/" + extension = ".html" + + def __init__(self, application): + self.application = application + + # specific to given view rendering + self.dictionary = {} + self.composers = {} + self.template = None + self.loaders = [] + self.namespaces = defaultdict(list) + self.env = None + self._jinja_extensions = ["jinja2.ext.loopcontrols"] + self._filters = {} + self._shared = {} + self._tests = {} + + def render(self, template, dictionary={}): + """Get the string contents of the view. + + Arguments: + template {string} -- Name of the template you want to render. + + Keyword Arguments: + dictionary {dict} -- Data that you want to pass into your view. (default: {{}}) + + Returns: + self + """ + if not isinstance(dictionary, dict): + raise ViewException( + "Second parameter to render method needs to be a dictionary, {} passed.".format( + type(dictionary).__name__ + ) + ) + + self.load_template(template) + + # prepare template context + self.dictionary = {} + self.dictionary.update(dictionary) + self.dictionary.update(self._shared) + if self.composers: + self.hydrate_from_composers() + + if self._tests: + self.env.tests.update(self._tests) + + self.rendered_template = self._render() + + return self + + def get_content(self): + return self.rendered_template + + def _render(self): + try: + # Try rendering the template with '.html' appended + return self.env.get_template(self.filename).render(self.dictionary) + except TemplateNotFound: + # Try rendering the direct template the user has supplied + return self.env.get_template(self.template).render(self.dictionary) + + def hydrate_from_composers(self): + """Add data into the view from specified composers.""" + # Check if the template is directly specified in the composer + if self.template in self.composers: + self.dictionary.update(self.composers.get(self.template)) + + # Check if there is just an astericks in the composer + if "*" in self.composers: + self.dictionary.update(self.composers.get("*")) + + # We will append onto this string for an easier way to search through wildcard routes + compiled_string = "" + + # Check for wildcard view composers + for template in self.template.split(self.separator): + # Append the template onto the compiled_string + compiled_string += template + if self.composers.get("{}*".format(compiled_string)): + self.dictionary.update(self.composers["{}*".format(compiled_string)]) + else: + # Add a slash to symbolize going into a deeper directory structure + compiled_string += "/" + + def composer(self, composer_name, dictionary): + """Update composer dictionary. + + Arguments: + composer_name {string} -- Key to bind dictionary of data to. + dictionary {dict} -- Dictionary of data to add to controller. + + Returns: + self + """ + if isinstance(composer_name, str): + self.composers[composer_name] = dictionary + + if isinstance(composer_name, list): + for composer in composer_name: + self.composers[composer] = dictionary + + return self + + def share(self, dictionary): + """Share data to all templates. + + Arguments: + dictionary {dict} -- Dictionary of key value pairs to add to all views. + + Returns: + self + """ + self._shared.update(dictionary) + return self + + def exists(self, template): + """Check if a template exists. + + Arguments: + template {string} -- Name of the template to check for. + + Returns: + bool + """ + self.load_template(template) + + try: + self.env.get_template(self.filename) + return True + except TemplateNotFound: + return False + + def add_location(self, template_location, loader=PackageLoader): + """Add locations from which view templates can be loaded. + + Arguments: + template_location {str} -- Directory location + + Keyword Arguments: + loader {jinja2.Loader} -- Type of Jinja2 loader to use. (default: {jinja2.PackageLoader}) + """ + if loader == PackageLoader: + package_name, package_path = path_to_package(template_location) + self.loaders.append(loader(package_name, package_path)) + else: + self.loaders.append(loader(template_location)) + + def add_namespaced_location(self, namespace, template_location): + # if views have been published, add the published view directory as a location + published_path = views_path(f"vendor/{namespace}/", absolute=False) + if exists(published_path): + self.namespaces[namespace].append( + views_path(f"vendor/{namespace}/", absolute=False) + ) + # put this one in 2nd as project views must be used first to be able to override package views + self.namespaces[namespace].append(template_location) + + def add_from_package(self, package_name, path_in_package): + self.environments.append(PackageLoader(package_name, path_in_package)) + + def add_namespace(self, namespace, path): + # TODO: if views have been published, add an other path corresponding to this namespace + self.namespaces[namespace].append( + views_path(f"vendor/{namespace}/", absolute=False) + ) + # put this one in 2nd as project (overriden) views must be used first + self.namespaces[namespace].append(path) + + def filter(self, name, function): + """Use to add filters to views. + + Arguments: + name {string} -- Key to bind the filter to. + function {object} -- Function used for the template filter. + """ + self._filters.update({name: function}) + + def add_extension(self, extension): + self._jinja_extensions.append(extension) + return self + + def load_template(self, template): + """Private method for loading all the locations into the current environment. + + Arguments: + template {string} -- Template to load environment from. + """ + self.template = template + # transform given template path into a real file path with the configured extension + self.filename = ( + as_filepath(template).replace(self.extension, "") + self.extension + ) + # assess if new loaders are required for the given template + template_loaders = [] + # Case 1: the templates needs to be loaded from a namespace + if ":" in template: + namespace, rel_template_path = template.split(":") + self.filename = ( + as_filepath(rel_template_path).replace(self.extension, "") + + self.extension + ) + namespace_paths = self.namespaces.get(namespace, None) + if not namespace_paths: + raise Exception(f"No such view namespace {namespace}.") + for namespace_path in namespace_paths: + package_name, package_path = path_to_package(namespace_path) + template_loaders.append(PackageLoader(package_name, package_path)) + + # Case 2: an absolute path has been given + elif template.startswith("/"): + directory, filename = split(template) + self.filename = filename.replace(self.extension, "") + self.extension + package_name, package_path = path_to_package(directory) + template_loaders.append(PackageLoader(package_name, package_path)) + + # Else: use already defined view locations to load this template + loader = ChoiceLoader(template_loaders + self.loaders) + + # @josephmancuso: what is this ?? + # Set the searchpath since some packages look for this object + # This is sort of a hack for now + loader.searchpath = "" + + self.env = Environment( + loader=loader, + autoescape=select_autoescape(["html", "xml"]), + extensions=self._jinja_extensions, + line_statement_prefix="@", + ) + # add filters to environment + self.env.filters.update(self._filters) + + def get_current_loaders(self): + if self.env: + return self.env.loader.loaders + + def set_separator(self, token): + self.separator = token + return self + + def set_file_extension(self, extension): + self.extension = extension + return self + + def get_response(self): + return self.rendered_template + + def test(self, key, obj): + self._tests.update({key: obj}) + return self diff --git a/storage/append_from.txt b/storage/append_from.txt deleted file mode 100644 index 75b9d029c..000000000 --- a/storage/append_from.txt +++ /dev/null @@ -1,3 +0,0 @@ -ROUTES += [ - Get('/some/appended/url', 'ControllerTest@show') -] \ No newline at end of file diff --git a/storage/file.txt b/storage/file.txt deleted file mode 100644 index 56f44d360..000000000 --- a/storage/file.txt +++ /dev/null @@ -1 +0,0 @@ -HI \ No newline at end of file diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore deleted file mode 100644 index c96a04f00..000000000 --- a/storage/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/storage/some_file.txt b/storage/some_file.txt deleted file mode 100644 index 56f44d360..000000000 --- a/storage/some_file.txt +++ /dev/null @@ -1 +0,0 @@ -HI \ No newline at end of file diff --git a/storage/static/__init__.py b/storage/static/__init__.py deleted file mode 100644 index 9999d58e4..000000000 --- a/storage/static/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Static file storage directory. - -This directory is responsible for storing static assets such as -CSS, JS, public assets etc. -""" diff --git a/storage/static/profile.jpg b/storage/static/profile.jpg deleted file mode 100644 index 359954839c4b7e3cb5f6e840fbf6a7aa63cba8ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 237344 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 z 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 Sta { - color: #636b6f; - padding: 0 25px; - font-size: 12px; - font-weight: 600; - letter-spacing: .1rem; - text-decoration: none; - text-transform: uppercase; -} - -.m-b-md { - margin-bottom: 30px; -} \ No newline at end of file diff --git a/storage/templates/tests/test.html b/storage/templates/tests/test.html deleted file mode 100644 index c930433d9..000000000 --- a/storage/templates/tests/test.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/storage/test_location.html b/storage/test_location.html deleted file mode 100644 index c930433d9..000000000 --- a/storage/test_location.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/templates/test.html b/templates/test.html deleted file mode 100644 index c930433d9..000000000 --- a/templates/test.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/templates/tests/test.html b/templates/tests/test.html deleted file mode 100644 index c930433d9..000000000 --- a/templates/tests/test.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/testpackage/test-config.py b/testpackage/test-config.py deleted file mode 100644 index 136100b72..000000000 --- a/testpackage/test-config.py +++ /dev/null @@ -1 +0,0 @@ -ROUTES = [] diff --git a/tests/TestCase.py b/tests/TestCase.py new file mode 100644 index 000000000..44a7bfa86 --- /dev/null +++ b/tests/TestCase.py @@ -0,0 +1,11 @@ +from src.masonite.tests import TestCase +from src.masonite.routes import Route + + +class TestCase(TestCase): + def setUp(self): + super().setUp() + self.addRoutes( + Route.get("/", "WelcomeController@show"), + Route.post("/", "WelcomeController@show"), + ) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb..975feafbb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +from .TestCase import TestCase diff --git a/tests/broadcasts/test_sockets.py b/tests/broadcasts/test_sockets.py deleted file mode 100644 index 58b758e48..000000000 --- a/tests/broadcasts/test_sockets.py +++ /dev/null @@ -1,53 +0,0 @@ -import os -import unittest -from src.masonite.drivers import BroadcastPusherDriver -from src.masonite.managers import BroadcastManager -from src.masonite.testing import TestCase - - -class TestSockets(TestCase): - - def setUp(self): - super().setUp() - self.container.bind('BroadcastPusherDriver', BroadcastPusherDriver) - self.container.bind('BroadcastManager', BroadcastManager) - # skip tests depending on drivers keys presence - self.run_pusher_tests = bool(os.getenv('PUSHER_SECRET')) - self.run_ably_tests = bool(os.getenv('ABLY_SECRET')) - self.run_pubnub_tests = bool(os.getenv('PUBNUB_SECRET')) - - def test_broadcast_loads_into_container(self): - if not self.run_pusher_tests: - self.skipTest("require Pusher keys") - self.container.bind('Broadcast', self.container.make('BroadcastManager').driver('pusher')) - - self.assertIsNotNone(self.container.make('BroadcastManager')) - self.assertEqual(self.container.make('Broadcast').channel('random', 'from driver'), {'message': 'from driver'}) - self.assertEqual(self.container.make('Broadcast').channel('random', {'message': 'dictionary'}), {'message': 'dictionary'}) - self.assertEqual(self.container.make('Broadcast').channel(['channel1', 'channel2'], {'message': 'dictionary'}), {'message': 'dictionary'}) - self.assertEqual(self.container.make('Broadcast').channel(['channel1', 'channel2'], {'message': 'dictionary'}, 'test-event'), {'message': 'dictionary'}) - self.assertTrue(self.container.make('Broadcast').ssl(True).ssl_message) - - def test_broadcast_loads_into_container_with_ably(self): - if not self.run_ably_tests: - self.skipTest("require Ably keys") - self.container.bind('Broadcast', self.container.make('BroadcastManager').driver('ably')) - - self.assertIsNotNone(self.container.make('BroadcastManager')) - self.assertEqual(self.container.make('Broadcast').channel('test-channel', 'from driver'), {'message': 'from driver'}) - self.assertEqual(self.container.make('Broadcast').channel('test-channel', {'message': 'from driver'}), {'message': 'from driver'}) - self.assertEqual(self.container.make('Broadcast').channel(['channel-1', 'channel-2'], {'message': 'dictionary'}), {'message': 'dictionary'}) - self.assertEqual(self.container.make('Broadcast').channel(['channel-1', 'channel-2'], {'message': 'dictionary'}, 'test-event'), {'message': 'dictionary'}) - self.assertTrue(self.container.make('Broadcast').ssl(True).ssl_message) - - def test_broadcast_loads_into_container_with_pubnub(self): - if not self.run_pubnub_tests: - self.skipTest("require PubNub keys") - self.container.bind('Broadcast', self.container.make('BroadcastManager').driver('pubnub')) - - self.assertIsNotNone(self.container.make('BroadcastManager')) - self.assertEqual(self.container.make('Broadcast').channel('test-channel', 'from driver'), {'message': 'from driver'}) - self.assertEqual(self.container.make('Broadcast').channel('test-channel', {'message': 'from driver'}), {'message': 'from driver'}) - self.assertEqual(self.container.make('Broadcast').channel(['channel-1', 'channel-2'], {'message': 'dictionary'}), {'message': 'dictionary'}) - self.assertEqual(self.container.make('Broadcast').channel(['channel-1', 'channel-2'], {'message': 'dictionary'}, 'test-event'), {'message': 'dictionary'}) - self.assertTrue(self.container.make('Broadcast').ssl(True).ssl_message) diff --git a/tests/commands/test_new.py b/tests/commands/test_new.py deleted file mode 100644 index e347875ff..000000000 --- a/tests/commands/test_new.py +++ /dev/null @@ -1,303 +0,0 @@ -import unittest -import requests -import responses -import shutil -import os -import json -from mock import patch -from cleo import Application -from cleo import CommandTester - -from src.masonite.commands.NewCommand import NewCommand -from src.masonite.exceptions import ( - ProjectLimitReached, - ProjectProviderTimeout, - ProjectProviderHttpError, -) -from src.masonite import __cookie_cutter_version__ - - -class TestNewCommand(unittest.TestCase): - - test_project_dir = os.path.join(os.getcwd(), "new_project") - - def setUp(self): - self.application = Application() - self.application.add(NewCommand()) - self.command = self.application.find("new") - self.command_tester = CommandTester(self.command) - - self.default_repo = "MasoniteFramework/cookie-cutter" - self.default_branch = __cookie_cutter_version__ - - def tearDown(self): - # ensure cleaning after each test - shutil.rmtree(self.test_project_dir, ignore_errors=True) - - @classmethod - def tearDownClass(cls): - # ensure cleaning even if test suite fails - shutil.rmtree(cls.test_project_dir, ignore_errors=True) - - def test_handle_not_implemented_provider(self): - self.command_tester.execute("new_project --provider bitbucket") - self.assertEqual( - "'provider' option must be in github,gitlab\n", - self.command_tester.io.fetch_error(), - ) - - @responses.activate - def test_handle_incorrect_branch(self): - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/branches/unknown".format( - self.default_repo - ), - body="[]", - ) - - self.command_tester.execute("new_project --branch unknown") - self.assertEqual( - "Branch unknown does not exist.\n", self.command_tester.io.fetch_error() - ) - - @responses.activate - def test_handle_incorrect_version(self): - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/releases".format(self.default_repo), - body="{}", - ) - - self.command_tester.execute("new_project --release 0.0.0") - self.assertEqual( - "Version 0.0.0 could not be found.\n", self.command_tester.io.fetch_error() - ) - - @responses.activate - def test_handle_incorrect_version(self): - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/releases".format(self.default_repo), - body="{}", - ) - - self.command_tester.execute("new_project --release 0.0.0") - self.assertEqual( - "Version 0.0.0 could not be found\n", self.command_tester.io.fetch_error() - ) - - @responses.activate - def test_correct_version_is_displayed(self): - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/releases".format(self.default_repo), - body='[{"name": "v2.3.6", "tag_name": "v2.3.6", "zipball_url": "https://api.github.com/repos/MasoniteFramework/cookie-cutter/zipball/v2.3.6"}]', - ) - # authorize requests not using the API - responses.add_passthru( - "https://api.github.com/repos/MasoniteFramework/cookie-cutter/zipball/v2.3.6" - ) - responses.add_passthru( - "https://codeload.github.com/MasoniteFramework/cookie-cutter/legacy.zip/v2.3.6" - ) - - self.command_tester.execute("new_project --release 2.3.6") - self.assertTrue( - self.command_tester.io.fetch_output().startswith( - "Installing version v2.3.6" - ) - ) - - @responses.activate - def test_warning_is_displayed_when_no_tags_in_repo_if_not_official(self): - # there is no tags on this test repo - # mock this one as it can be rate limited - responses.add( - responses.GET, - "https://gitlab.com/api/v4/projects/samuelgirardin%2Fmasonite-tests/releases", - body="[]", - ) - # authorize requests not using the API - responses.add_passthru( - "https://gitlab.com/api/v4/projects/samuelgirardin%2Fmasonite-tests/repository/archive.zip?sha=master" - ) - self.command_tester.execute( - "new_project --repo samuelgirardin/masonite-tests --provider gitlab" - ) - self.assertTrue( - "No tags has been found, using latest commit on master." - in self.command_tester.io.fetch_output() - ) - - @responses.activate - def test_api_rate_limit_are_handled(self): - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/branches/{1}".format( - self.default_repo, self.default_branch - ), - status=403, - ) - - # allow reproducing error raised by Github API - with patch( - "six.moves.http_client.responses", get=lambda status: "rate limit exceeded" - ): - with self.assertRaises(ProjectLimitReached): - self.command_tester.execute("new_project") - - @responses.activate - def test_unknown_repos_are_handled(self): - responses.add( - responses.GET, - "https://api.github.com/repos/MasoniteFramework/unknown_repo/releases", - status=404, - ) - - with self.assertRaises(ProjectProviderHttpError) as e: - self.command_tester.execute( - "new_project --repo MasoniteFramework/unknown_repo" - ) - - self.assertTrue(str(e.exception).startswith("Not Found(404)")) - - @responses.activate - def test_private_repos_errors_are_handled(self): - responses.add( - responses.GET, - "https://api.github.com/repos/MasoniteFramework/secret/releases", - status=403, - ) - - with self.assertRaises(ProjectProviderHttpError) as e: - self.command_tester.execute("new_project --repo MasoniteFramework/secret") - - self.assertTrue(str(e.exception).startswith("Forbidden(403)")) - - @responses.activate - def test_timeouts_are_handled(self): - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/branches/{1}".format( - self.default_repo, self.default_branch - ), - body=requests.Timeout(), - ) - - with self.assertRaises(ProjectProviderTimeout) as e: - self.command_tester.execute("new_project") - - self.assertTrue(str(e.exception).startswith("github provider is not reachable")) - - # Following tests are close to integration tests but still quick and without rate limiting issue so - # they can be ran as unit tests - # ---------------------------------------------------------------------------------------------- - @responses.activate - def test_can_craft_default_repo_successfully(self): - # still mocking requests to avoid failures from api rate limits - body = {"name": self.default_branch} - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/branches/{1}".format( - self.default_repo, self.default_branch - ), - body=json.dumps(body), - ) - # authorize requests not using the API - responses.add_passthru( - "https://github.com/MasoniteFramework/cookie-cutter/archive/{0}.zip".format( - self.default_branch - ) - ) - responses.add_passthru( - "https://codeload.github.com/MasoniteFramework/cookie-cutter/zip/{0}".format( - self.default_branch - ) - ) - - self.command_tester.execute("new_project") - self.assertTrue( - "Project Created Successfully" in self.command_tester.io.fetch_output() - ) - # verify that project has really been created by checking files - self.assertTrue("craft" in os.listdir(self.test_project_dir)) - self.assertTrue("app" in os.listdir(self.test_project_dir)) - - @responses.activate - def test_can_craft_default_repo_successfully_with_release(self): - # still mocking requests to avoid failures from api rate limits - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/releases".format(self.default_repo), - body='[{"name": "v2.3.6", "tag_name": "v2.3.6", "zipball_url": "https://api.github.com/repos/MasoniteFramework/cookie-cutter/zipball/v2.3.6", "prerelease": false}]', - ) - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/releases/tags/v2.3.6".format( - self.default_repo - ), - body='{"zipball_url": "https://api.github.com/repos/MasoniteFramework/cookie-cutter/zipball/v2.3.6"}', - ) - # authorize requests not using the API - responses.add_passthru( - "https://api.github.com/repos/MasoniteFramework/cookie-cutter/zipball/v2.3.6" - ) - responses.add_passthru( - "https://codeload.github.com/MasoniteFramework/cookie-cutter/legacy.zip/refs/tags/v2.3.6" - ) - - self.command_tester.execute("new_project --release 2.3.6") - self.assertTrue( - "Project Created Successfully" in self.command_tester.io.fetch_output() - ) - # verify that project has really been created by checking files - self.assertTrue("craft" in os.listdir(self.test_project_dir)) - self.assertTrue("app" in os.listdir(self.test_project_dir)) - - @responses.activate - def test_can_craft_default_repo_successfully_with_branch(self): - # still mocking requests to avoid failures from api rate limits - responses.add( - responses.GET, - "https://api.github.com/repos/{0}/branches/2.3".format(self.default_repo), - body='{"name": "2.3"}', - ) - # authorize requests not using the API - responses.add_passthru( - "https://github.com/MasoniteFramework/cookie-cutter/archive/2.3.zip" - ) - responses.add_passthru( - "https://codeload.github.com/MasoniteFramework/cookie-cutter/zip/2.3" - ) - - self.command_tester.execute("new_project --branch 2.3") - self.assertTrue( - "Project Created Successfully" in self.command_tester.io.fetch_output() - ) - # verify that project has really been created by checking files - self.assertTrue("craft" in os.listdir(self.test_project_dir)) - self.assertTrue("app" in os.listdir(self.test_project_dir)) - - @responses.activate - def test_can_craft_project_with_gitlab_provider(self): - # mock this one as it can be rate limited - responses.add( - responses.GET, - "https://gitlab.com/api/v4/projects/samuelgirardin%2Fmasonite-tests/releases", - body="[]", - ) - # authorize requests not using the API - responses.add_passthru( - "https://gitlab.com/api/v4/projects/samuelgirardin%2Fmasonite-tests/repository/archive.zip?sha=master" - ) - - repo = "samuelgirardin/masonite-tests" - self.command_tester.execute( - "new_project --provider gitlab --repo {0}".format(repo) - ) - self.assertTrue( - "Project Created Successfully" in self.command_tester.io.fetch_output() - ) - # verify that project has really been created by checking files - self.assertTrue("README.md" in os.listdir(self.test_project_dir)) diff --git a/tests/core/__init__.py b/tests/core/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/core/authentication/test_authentication2.py b/tests/core/authentication/test_authentication2.py new file mode 100644 index 000000000..6429b5289 --- /dev/null +++ b/tests/core/authentication/test_authentication2.py @@ -0,0 +1,44 @@ +from tests import TestCase +from src.masonite.foundation import Application +import os +from masoniteorm.models import Model +from src.masonite.authentication import Authenticates, Auth + + +class TestAuthentication(TestCase): + def setUp(self): + super().setUp() + self.auth = self.application.make("auth") + self.make_request() + self.make_response() + + def test_attempt(self): + self.assertTrue(self.auth.attempt("idmann509@gmail.com", "secret")) + self.assertFalse(self.auth.attempt("idmann509@gmail.com", "secret1")) + + def test_auth_class_registers_cookie(self): + self.auth.guard("web").attempt("idmann509@gmail.com", "secret") + self.assertTrue(self.application.make("response").cookie("token")) + + def test_logout(self): + self.application.make("auth").guard("web").attempt( + "idmann509@gmail.com", "secret" + ) + + self.assertTrue(self.application.make("response").cookie("token")) + + self.application.make("auth").guard("web").logout() + self.assertFalse(self.application.make("request").cookie("token")) + + def test_attempt_by_id(self): + self.application.make("auth").guard("web").attempt_by_id(1) + + self.assertTrue(self.application.make("request").cookie("token")) + + self.application.make("auth").guard("web").logout() + self.assertFalse(self.application.make("request").cookie("token")) + + def test_attempt_by_id_once(self): + self.application.make("auth").guard("web").attempt_by_id(1, once=True) + + self.assertIsNone(self.application.make("request").cookie("token")) diff --git a/tests/core/authorization/test_authorizes.py b/tests/core/authorization/test_authorizes.py new file mode 100644 index 000000000..519e43088 --- /dev/null +++ b/tests/core/authorization/test_authorizes.py @@ -0,0 +1,29 @@ +from tests import TestCase +from masoniteorm.models import Model + +from src.masonite.authorization import Authorizes +from src.masonite.facades import Gate + + +class User(Model, Authorizes): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class TestAuthorizes(TestCase): + def setUp(self): + super().setUp() + self.make_request() + + def test_user_can(self): + user = User.find(1) + Gate.define("create-post", lambda user: user.email == "idmann509@gmail.com") + self.assertTrue(user.can("create-post")) + + def test_user_cannot(self): + user = User.find(1) + Gate.define("delete-post", lambda user: False) + self.assertTrue(user.cannot("delete-post")) + Gate.define("view-admin-panel", lambda user: user.email == "admin@gmail.com") + self.assertTrue(user.cannot("view-admin-panel")) diff --git a/tests/core/authorization/test_gate.py b/tests/core/authorization/test_gate.py new file mode 100644 index 000000000..dab57f0aa --- /dev/null +++ b/tests/core/authorization/test_gate.py @@ -0,0 +1,191 @@ +from tests import TestCase +from masoniteorm.models import Model + +from src.masonite.exceptions.exceptions import AuthorizationException, GateDoesNotExist +from src.masonite.authorization import AuthorizationResponse +from src.masonite.routes import Route + + +class User(Model): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class Post(Model): + __fillable__ = ["user_id", "name"] + + +class TestGate(TestCase): + def setUp(self): + super().setUp() + self.gate = self.application.make("gate") + self.make_request() + self.make_response() + self.addRoutes( + Route.get("/not-authorized", "WelcomeController@not_authorized"), + Route.get( + "/authorize-helper", "WelcomeController@use_authorization_helper" + ), + Route.get("/authorizations", "WelcomeController@authorizations"), + ) + + def tearDown(self): + super().tearDown() + self.gate.permissions = {} + self.gate.before_callbacks = [] + self.gate.after_callbacks = [] + + def test_can_define_gates(self): + self.gate.define( + "create-post", lambda user: user.email == "idmann509@gmail.com" + ) + self.gate.define("view-post", lambda user: True) + self.assertEqual( + list(self.gate.permissions.keys()), ["create-post", "view-post"] + ) + + def test_can_use_for_user(self): + given_user = User() + user = self.gate.for_user(given_user)._get_user() + self.assertEqual(user, given_user) + + def test_gate_user_the_current_authenticated_request_user(self): + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + user = self.gate._get_user() + self.assertEqual(user.email, "idmann509@gmail.com") + + def test_can_use_gate_facade(self): + from src.masonite.facades import Gate + + self.assertEqual(self.gate.define, Gate.define) + + def test_denies_guests_users(self): + self.gate.define("create-post", lambda user: True) + # above permission should always been allowed but user is not authenticated + self.assertTrue(self.gate.denies("create-post")) + + def test_allows_guests_users_if_specified(self): + # it can be specified by allowing user to be optional + self.gate.define("create-post", lambda user=None: True) + # above permission should always been allowed even for guests users + self.assertTrue(self.gate.allows("create-post")) + + def test_allows_and_denies(self): + self.gate.define( + "create-post", lambda user: user.email == "idmann509@gmail.com" + ) + self.gate.define("view-post", lambda user: False) + # authenticate user + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + + self.assertTrue(self.gate.allows("create-post")) + self.assertFalse(self.gate.denies("create-post")) + self.assertFalse(self.gate.allows("view-post")) + self.assertTrue(self.gate.denies("view-post")) + + def test_allows_with_arg(self): + self.gate.define("update-post", lambda user, post: post.user_id == user.id) + # authenticate user + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + # create a post for the user 1 + post = Post() + post.user_id = 1 + post2 = Post() + post2.user_id = 3 + self.assertTrue(self.gate.allows("update-post", post)) + self.assertFalse(self.gate.allows("update-post", post2)) + + def test_gate_has_permission(self): + self.gate.define("display-admin", lambda user: False) + self.assertTrue(self.gate.has("display-admin")) + self.assertFalse(self.gate.has("view-user")) + + def test_authorize(self): + self.gate.define("display-admin", lambda user: False) + with self.assertRaises(AuthorizationException) as e: + self.gate.authorize("display-admin") + exception = e.exception + self.assertEqual(e.exception.message, "Action not authorized") + self.assertEqual(e.exception.status, 403) + + def test_authorize_in_controller(self): + self.withExceptionsHandling() # this will allow exception to be handled and rendered + self.gate.define("display-admin", lambda user: False) + self.get("/not-authorized").assertForbidden().assertContains( + "Action not authorized" + ) + + def test_inspect(self): + self.gate.define("display-admin", lambda user: False) + response = self.gate.inspect("display-admin") + self.assertIsInstance(response, AuthorizationResponse) + self.assertFalse(response.allowed()) + + def test_define_gate_returning_response(self): + self.gate.define( + "display-admin", + lambda user: AuthorizationResponse.allow() + if user.email == "admin@masonite.com" + else AuthorizationResponse.deny("You shall not pass"), + ) + # authenticate user + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + response = self.gate.inspect("display-admin") + self.assertFalse(response.allowed()) + self.assertEqual(response.message(), "You shall not pass") + + def test_gate_before(self): + self.gate.before(lambda user, permission: user.email == "idmann509@gmail.com") + # a permission that is always False + self.gate.define("display-admin", lambda user: False) + # authenticate user + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + self.assertTrue(self.gate.allows("display-admin")) + + def test_gate_after(self): + self.gate.after(lambda user, permission, result: False) + # a permission that is always False + self.gate.define("display-admin", lambda user: True) + # authenticate user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + self.assertTrue(self.gate.denies("display-admin")) + + def test_any(self): + self.gate.define("delete-post", lambda user, post: True) + self.gate.define("update-post", lambda user, post: user.id == post.user_id) + # authenticate user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + post = Post() + post.user_id = 1 + self.assertTrue(self.gate.any(["update-post", "delete-post"], post)) + + def test_none(self): + self.gate.define("force-delete-post", lambda user, post: False) + self.gate.define( + "restore-post", lambda user, post: user.email == "admin@gmail.com" + ) + # authenticate user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + post = Post() + self.assertTrue(self.gate.none(["force-delete-post", "restore-post"], post)) + + def test_unknown_gate_raises_exception(self): + with self.assertRaises(GateDoesNotExist): + self.gate.allows("can-fly") + + def test_view_helpers(self): + self.gate.define("view-posts", lambda user=None: True) + self.gate.define( + "display-admin", lambda user: user.email == "idmann509@gmail.com" + ) + self.get("/authorizations").assertContains( + "User can view posts" + ).assertContains("User cannot display admin") + + # def test_can_use_authorize_helper_on_request(self): + # self.gate.define("display-admin", lambda user: True) + # # authenticate user : here it does not work => no user in request inside Gate... + # # something is going on + # self.application.make("auth").attempt("idmann509@gmail.com", "secret") + # self.get("/authorize-helper").assertOk() diff --git a/tests/core/authorization/test_policies.py b/tests/core/authorization/test_policies.py new file mode 100644 index 000000000..8cf83a00f --- /dev/null +++ b/tests/core/authorization/test_policies.py @@ -0,0 +1,82 @@ +from tests import TestCase +from masoniteorm.models import Model + +from tests.integrations.policies.PostPolicy import PostPolicy +from src.masonite.exceptions.exceptions import PolicyDoesNotExist + + +class User(Model): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class Post(Model): + __fillable__ = ["user_id", "name"] + + +class TestPolicies(TestCase): + def setUp(self): + super().setUp() + self.gate = self.application.make("gate") + self.make_request() + + def tearDown(self): + super().tearDown() + self.gate.policies = {} + self.gate.permissions = {} + + def test_can_register_policies(self): + self.gate.register_policies([(Post, PostPolicy)]) + self.assertEqual(self.gate.policies[Post], PostPolicy) + + def test_using_policies_with_argument(self): + self.gate.register_policies([(Post, PostPolicy)]) + post = Post() + post.user_id = 1 + # authenticates user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + self.assertTrue(self.gate.allows("update", post)) + + def test_using_policies_without_argument(self): + self.gate.register_policies([(Post, PostPolicy)]) + # authenticates user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + + self.assertTrue(self.gate.allows("create", Post)) + + def test_using_policy_returning_response(self): + self.gate.register_policies([(Post, PostPolicy)]) + # authenticates user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + post = Post() + post.user_id = 3 + response = self.gate.inspect("delete", post) + self.assertFalse(response.allowed()) + self.assertEqual(response.message(), "You do not own this post") + + def test_that_use_defined_gate_if_no_policy_match(self): + self.gate.define("update", lambda user, post: user.id == post.user_id) + post = Post() + post.user_id = 1 + # authenticates user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + # here no policy has been defined, the gate defined above will be used + self.assertTrue(self.gate.allows("update", post)) + + def test_that_policy_can_allow_guest_users(self): + self.gate.register_policies([(Post, PostPolicy)]) + self.assertTrue(self.gate.allows("view_any", Post)) + + def test_any_on_policy(self): + self.gate.register_policies([(Post, PostPolicy)]) + post = Post() + post.user_id = 1 + # authenticates user 1 + self.application.make("auth").attempt("idmann509@gmail.com", "secret") + self.assertTrue(self.gate.any(["update", "delete"], post)) + + def test_unknown_policy_method_raises_exception(self): + self.gate.register_policies([(Post, PostPolicy)]) + with self.assertRaises(PolicyDoesNotExist): + self.gate.allows("can-fly", Post) diff --git a/tests/core/configuration/test_config.py b/tests/core/configuration/test_config.py new file mode 100644 index 000000000..61ec463ff --- /dev/null +++ b/tests/core/configuration/test_config.py @@ -0,0 +1,68 @@ +from tests import TestCase +from src.masonite.facades import Config +from src.masonite.configuration import config +from src.masonite.exceptions import InvalidConfigurationSetup + + +PACKAGE_PARAM = 1 +OTHER_PARAM = 3 + + +class TestConfiguration(TestCase): + def test_config_is_loaded(self): + self.assertGreater(len(Config._config.keys()), 0) + + def test_base_configuration_files_can_be_accessed(self): + self.assertIsNotNone("config.application") + self.assertIsNotNone("config.auth") + self.assertIsNotNone("config.broadcast") + self.assertIsNotNone("config.cache") + self.assertIsNotNone("config.database") + self.assertIsNotNone("config.filesystem") + self.assertIsNotNone("config.mail") + self.assertIsNotNone("config.notification") + self.assertIsNotNone("config.providers") + self.assertIsNotNone("config.queue") + self.assertIsNotNone("config.session") + + def test_config_helper(self): + self.assertEqual(config("auth.guards.default"), "web") + + def test_config_facade(self): + self.assertEqual(Config.get("auth.guards.default"), "web") + + def test_config_use_default_if_not_exist(self): + self.assertEqual(config("some.app"), None) + self.assertEqual(config("some.app", 0), 0) + + def test_config_set_value(self): + original_value = config("cache.stores.redis.port") + Config.set("cache.stores.redis.port", 1000) + self.assertNotEqual(Config.get("cache.stores.redis.port"), original_value) + self.assertEqual(Config.get("cache.stores.redis.port"), 1000) + # reset to original value + Config.set("cache.stores.redis.port", original_value) + + def test_can_load_non_foundation_config_in_project(self): + self.assertEqual(config("package.package_param"), "package_value") + + def test_cannot_override_foundation_config(self): + with self.assertRaises(InvalidConfigurationSetup): + Config.merge_with("auth", {"key": "val"}) + + def test_can_merge_external_config_with_project_config(self): + # reset test package config for idempotent tests + Config.set("package", {"package_param": "package_value"}) + + package_default_config = {"PACKAGE_PARAM": "default_value", "OTHER_PARAM": 2} + Config.merge_with("package", package_default_config) + self.assertEqual(config("package.package_param"), "package_value") + self.assertEqual(config("package.other_param"), 2) + + def test_can_merge_external_config_using_path(self): + # reset test package config for idempotent tests + Config.set("package", {"package_param": "package_value"}) + + Config.merge_with("package", "tests/core/configuration/test_config.py") + self.assertEqual(config("package.package_param"), "package_value") + self.assertEqual(config("package.other_param"), 3) diff --git a/tests/core/test_cookies.py b/tests/core/cookies/test_cookies.py similarity index 51% rename from tests/core/test_cookies.py rename to tests/core/cookies/test_cookies.py index 9591644b1..cf836a547 100644 --- a/tests/core/test_cookies.py +++ b/tests/core/cookies/test_cookies.py @@ -1,15 +1,15 @@ import unittest from src.masonite.cookies import CookieJar -from src.masonite.helpers import cookie_expire_time +from src.masonite.utils.time import cookie_expire_time -class TestCookies(unittest.TestCase): +class TestCookies(unittest.TestCase): def test_cookies_can_get_set(self): cookiejar = CookieJar() cookiejar.add("cookie", "name") - self.assertEqual(cookiejar.get('cookie').value, "name") - self.assertEqual(cookiejar.get('cookie').name, "cookie") + self.assertEqual(cookiejar.get("cookie").value, "name") + self.assertEqual(cookiejar.get("cookie").name, "cookie") def test_cookies_can_put_to_dict(self): cookiejar = CookieJar() @@ -19,40 +19,53 @@ def test_cookies_can_put_to_dict(self): def test_cookie_jar_can_render_cookie_string(self): cookiejar = CookieJar() cookiejar.add("cookie", "name", http_only=False) - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', "cookie=name;")]) + self.assertEqual(cookiejar.render_response(), [("Set-Cookie", "cookie=name;Path=/;")]) def test_cookie_jar_can_render_multiple_cookies(self): cookiejar = CookieJar() cookiejar.add("cookie1", "name", http_only=False) cookiejar.add("cookie2", "name", http_only=False) - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', "cookie1=name;"), ('Set-Cookie', "cookie2=name;")]) + self.assertEqual( + cookiejar.render_response(), + [("Set-Cookie", "cookie1=name;Path=/;"), ("Set-Cookie", "cookie2=name;Path=/;")], + ) def test_cookie_jar_can_render_multiple_cookies_with_different_options(self): cookiejar = CookieJar() - cookiejar.add("cookie1", "name", path='/') - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', "cookie1=name;HttpOnly;Path=/;")]) + cookiejar.add("cookie1", "name", path="/") + self.assertEqual( + cookiejar.render_response(), + [("Set-Cookie", "cookie1=name;HttpOnly;Path=/;")], + ) def test_cookie_with_expires(self): cookiejar = CookieJar() time = cookie_expire_time("2 months") - cookiejar.add("cookie1", "name", path='/', expires=time, timezone="GMT") - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', f"cookie1=name;HttpOnly;Expires={time} GMT;Path=/;")]) + cookiejar.add("cookie1", "name", path="/", expires=time, timezone="GMT") + self.assertEqual( + cookiejar.render_response(), + [("Set-Cookie", f"cookie1=name;HttpOnly;Expires={time} GMT;Path=/;")], + ) def test_cookie_with_expired_already(self): cookiejar = CookieJar() time = cookie_expire_time("expired") - print(time) - cookiejar.add("cookie1", "name", path='/', expires=time, timezone="GMT") - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', f"cookie1=name;HttpOnly;Expires={time} GMT;Path=/;")]) + cookiejar.add("cookie1", "name", path="/", expires=time, timezone="GMT") + self.assertEqual( + cookiejar.render_response(), + [("Set-Cookie", f"cookie1=name;HttpOnly;Expires={time} GMT;Path=/;")], + ) def test_cookie_can_load(self): cookiejar = CookieJar() cookiejar.add("cookie1", "name", http_only=False) cookiejar.load("csrf_token=tok") - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', "cookie1=name;")]) - self.assertEqual(cookiejar.get('csrf_token').value, 'tok') + self.assertEqual(cookiejar.render_response(), [("Set-Cookie", "cookie1=name;Path=/;")]) + self.assertEqual(cookiejar.get("csrf_token").value, "tok") def test_cookie_can_make_secure_cookies(self): cookiejar = CookieJar() cookiejar.add("cookie1", "name", http_only=False, secure=True) - self.assertEqual(cookiejar.render_response(), [('Set-Cookie', "cookie1=name;Secure;")]) + self.assertEqual( + cookiejar.render_response(), [("Set-Cookie", "cookie1=name;Secure;Path=/;")] + ) diff --git a/tests/core/foundation/test_app_application.py b/tests/core/foundation/test_app_application.py new file mode 100644 index 000000000..894304951 --- /dev/null +++ b/tests/core/foundation/test_app_application.py @@ -0,0 +1,9 @@ +from tests import TestCase +from src.masonite.foundation import Application +import os + + +class TestAppApplication(TestCase): + def test_initialize_application(self): + app = Application(os.getcwd()) + self.assertTrue(app) diff --git a/tests/core/foundation/test_application.py b/tests/core/foundation/test_application.py new file mode 100644 index 000000000..fdc6c2872 --- /dev/null +++ b/tests/core/foundation/test_application.py @@ -0,0 +1,6 @@ +from tests import TestCase + + +class TestApplication(TestCase): + def test_is_running_tests(self): + self.assertTrue(self.application.is_running_tests()) diff --git a/tests/core/foundation/test_facades.py b/tests/core/foundation/test_facades.py new file mode 100644 index 000000000..7fb3ccf44 --- /dev/null +++ b/tests/core/foundation/test_facades.py @@ -0,0 +1,11 @@ +from tests import TestCase +from src.masonite.facades import Mail, View + + +class TestFacades(TestCase): + def test_mail_facade(self): + self.assertIsNone(Mail.get_config_options("mailgun")["domain"]) + + def test_view_facade(self): + View.add_location("tests/integrations/templates") + self.assertEqual("test", View.render("test", {"test": "test"}).get_content()) diff --git a/tests/core/headers/test_headers.py b/tests/core/headers/test_headers.py new file mode 100644 index 000000000..5da72d350 --- /dev/null +++ b/tests/core/headers/test_headers.py @@ -0,0 +1,30 @@ +import unittest +from src.masonite.headers import HeaderBag, Header + + +class TestHeaders(unittest.TestCase): + def test_headers_can_be_registered(self): + bag = HeaderBag() + bag.add(Header("Content-Type", "application/json")) + self.assertTrue(bag["CONTENT_TYPE"]) + + def test_headers_can_be_rendered(self): + bag = HeaderBag() + bag.add(Header("content-type", "application/json")) + bag.add(Header("x-forwarded-for", "127.0.0.1")) + self.assertEqual( + bag.render(), + [("Content-Type", "application/json"), ("X-Forwarded-For", "127.0.0.1")], + ) + + def test_headers_can_check_with_in(self): + bag = HeaderBag() + bag.add(Header("Content-Type", "application/json")) + + self.assertTrue("Content-Type" in bag) + + def test_headers_can_be_retrieved(self): + bag = HeaderBag() + bag.add(Header("Content-Type", "application/json")) + self.assertEqual(bag.get("content-type").value, "application/json") + self.assertEqual(bag.get("Content-Type").value, "application/json") diff --git a/tests/core/helpers/test_mix.py b/tests/core/helpers/test_mix.py new file mode 100644 index 000000000..dffa2163a --- /dev/null +++ b/tests/core/helpers/test_mix.py @@ -0,0 +1,54 @@ +import pathlib +import json +from tests import TestCase + +from src.masonite.helpers import MixHelper +from src.masonite.exceptions import MixFileNotFound, MixManifestNotFound +from src.masonite.facades import Config + + +class TestMix(TestCase): + def setUp(self): + super().setUp() + self.mix = MixHelper(self.application).url + self.manifest_file = "mix-manifest.json" + + def tearDown(self): + super().tearDown() + try: + # missing_ok=True is only available starting from Python3.8 + pathlib.Path(self.manifest_file).unlink() + except FileNotFoundError: + pass + + def _make_manifest(self): + manifest = {"/storage/app.css": "/static/app.css"} + with open("mix-manifest.json", "w") as f: + json.dump(manifest, f) + + def test_mix_without_manifest_raise_exception(self): + with self.assertRaises(MixManifestNotFound): + self.mix("/storage/app.css") + + def test_mix_with_wrong_path_raise_exception(self): + self._make_manifest() + with self.assertRaises(MixFileNotFound): + self.mix("/storage/wrong.css") + + def test_mix_url(self): + self._make_manifest() + self.assertEqual( + self.mix("/storage/app.css"), "http://localhost:8000/static/app.css" + ) + # also works if missing / prefix + self.assertEqual( + self.mix("storage/app.css"), "http://localhost:8000/static/app.css" + ) + + def test_mix_with_mix_base_url(self): + Config.set("application.mix_base_url", "https://some-cdn.com/") + self._make_manifest() + self.assertEqual( + self.mix("storage/app.css"), "https://some-cdn.com/static/app.css" + ) + Config.set("application.mix_base_url", None) diff --git a/tests/core/helpers/test_optional.py b/tests/core/helpers/test_optional.py new file mode 100644 index 000000000..af85b4aae --- /dev/null +++ b/tests/core/helpers/test_optional.py @@ -0,0 +1,28 @@ +from tests import TestCase + +from src.masonite.helpers import optional + + +class SomeClass: + + my_attr = 3 + + def my_method(self): + return 4 + + +class TestOptionalHelper(TestCase): + def test_optional_with_existing(self): + obj = SomeClass() + self.assertEqual(optional(obj).my_attr, 3) + self.assertEqual(optional(obj).my_method(), 4) + + def test_optional_with_undefined(self): + obj = SomeClass() + self.assertEqual(optional(obj).non_existing_attr, None) + self.assertEqual(optional(obj).non_existing_method(), None) + + def test_optional_with_default(self): + obj = SomeClass() + self.assertEqual(optional(obj, default=0).non_existing_attr, 0) + self.assertEqual(optional(obj, default=0).non_existing_method(), 0) diff --git a/tests/core/helpers/test_urls.py b/tests/core/helpers/test_urls.py new file mode 100644 index 000000000..7c1c1cccd --- /dev/null +++ b/tests/core/helpers/test_urls.py @@ -0,0 +1,25 @@ +from tests import TestCase + +from src.masonite.helpers import url + + +class TestUrlsHelper(TestCase): + def test_url(self): + self.assertEqual(url.url("about/us"), "http://localhost:8000/about/us") + self.assertEqual(url.url("/about/us"), "http://localhost:8000/about/us") + self.assertEqual(url.url(), "http://localhost:8000/") + + def test_route(self): + self.assertEqual(url.route("welcome"), "http://localhost:8000/") + self.assertEqual( + url.route("users.profile", {"id": 1}), "http://localhost:8000/users/1" + ) + self.assertEqual(url.route("upload"), "http://localhost:8000/upload") + self.assertEqual(url.route("upload", absolute=False), "/upload") + + def test_asset(self): + self.assertTrue( + url.asset("local", "test.jpg").endswith( + "storage/framework/filesystem/test.jpg" + ) + ) diff --git a/tests/core/middleware/test_encrypt_cookies.py b/tests/core/middleware/test_encrypt_cookies.py new file mode 100644 index 000000000..2f9014e4b --- /dev/null +++ b/tests/core/middleware/test_encrypt_cookies.py @@ -0,0 +1,17 @@ +from tests import TestCase +from src.masonite.middleware import EncryptCookies + + +class TestEncryptCookiesMiddleware(TestCase): + def test_encrypts_cookies(self): + request = self.make_request( + {"HTTP_COOKIE": f"test={self.application.make('sign').sign('value')}"} + ) + + response = self.make_response() + EncryptCookies().before(request, None) + self.assertEqual(request.cookie("test"), "value") + + response.cookie("test", "value") + EncryptCookies().after(request, response) + self.assertNotEqual(response.cookie("test"), "value") diff --git a/tests/core/middleware/test_middleware.py b/tests/core/middleware/test_middleware.py new file mode 100644 index 000000000..85eaef090 --- /dev/null +++ b/tests/core/middleware/test_middleware.py @@ -0,0 +1,100 @@ +from unittest.mock import MagicMock + +from src.masonite.middleware import MiddlewareCapsule +from tests import TestCase + + +class MockMiddleware: + def before(self, request, response, arg1): + return request + + def after(self, request, response): + + return request + + +class TestMiddleware(TestCase): + def test_can_create_capsule(self): + capsule = MiddlewareCapsule() + self.assertTrue(capsule) + + def test_can_add_middleware(self): + capsule = MiddlewareCapsule() + capsule.add({"mock": MockMiddleware}) + capsule.add([MockMiddleware]) + + self.assertTrue(len(capsule.route_middleware) == 1) + self.assertTrue(len(capsule.http_middleware) == 1) + + def test_can_add_and_remove_middleware(self): + capsule = MiddlewareCapsule() + capsule.add({"mock": MockMiddleware}) + capsule.add([MockMiddleware]) + capsule.remove(MockMiddleware) + + self.assertTrue(len(capsule.route_middleware) == 1) + self.assertTrue(len(capsule.http_middleware) == 0) + + def test_can_get_multiple_middleware(self): + capsule = MiddlewareCapsule() + capsule.add( + { + "mock": MockMiddleware, + "mock1": MockMiddleware, + "mock2": MockMiddleware, + "mock3": [MockMiddleware, MockMiddleware], + } + ) + capsule.add([MockMiddleware]) + capsule.remove(MockMiddleware) + + self.assertTrue( + len(capsule.get_route_middleware(["mock", "mock1", "mock2"])) == 3 + ) + self.assertTrue( + len(capsule.get_route_middleware(["mock", "mock1", "mock2", "mock3"])) == 5 + ) + + def test_can_run_middleware_with_args(self): + request = self.make_request() + response = self.make_response() + capsule = MiddlewareCapsule() + MockMiddleware.before = MagicMock(return_value=request) + capsule.add( + { + "mock": MockMiddleware, + } + ) + + capsule.run_route_middleware(["mock:arg1,arg2"], request, response) + MockMiddleware.before.assert_called_with(request, response, "arg1", "arg2") + + def test_can_use_request_inputs_as_args(self): + # this create a request with @user_id and @id as in input + request = self.make_request(query_string="user_id=3&id=1") + response = self.make_response() + capsule = MiddlewareCapsule() + MockMiddleware.before = MagicMock(return_value=request) + capsule.add( + { + "mock": MockMiddleware, + } + ) + + capsule.run_route_middleware(["mock:@user_id,@id"], request, response) + MockMiddleware.before.assert_called_with(request, response, "3", "1") + + def test_can_mix_args_and_request_inputs(self): + # this create a request with @user_id as in input + request = self.make_request(query_string="user_id=3") + response = self.make_response() + capsule = MiddlewareCapsule() + MockMiddleware.before = MagicMock(return_value=request) + capsule.add( + { + "mock": MockMiddleware, + } + ) + + capsule.run_route_middleware(["mock:@user_id,value"], request, response) + MockMiddleware.before.assert_called_with(request, response, "3", "value") diff --git a/tests/core/request/test_http_requests.py b/tests/core/request/test_http_requests.py new file mode 100644 index 000000000..c1cbd8465 --- /dev/null +++ b/tests/core/request/test_http_requests.py @@ -0,0 +1,7 @@ +from tests import TestCase + + +class TestHttpRequests(TestCase): + def test_csrf_request(self): + self.withoutCsrf() + return self.post("/").assertContains("Welcome") diff --git a/tests/core/request/test_input.py b/tests/core/request/test_input.py new file mode 100644 index 000000000..1f487bccc --- /dev/null +++ b/tests/core/request/test_input.py @@ -0,0 +1,104 @@ +from src.masonite.input import InputBag +from src.masonite.tests import MockInput +from tests import TestCase +import json, io + + +class TestInput(TestCase): + def setUp(self): + super().setUp() + self.post_data = MockInput( + '{"param": "hey", "foo": [9, 8, 7, 6], "bar": "baz"}' + ) + self.bytes_data = MockInput(b"jack=Daniels") + + def test_can_parse_query_string(self): + bag = InputBag() + bag.load({"QUERY_STRING": "hello=you&goodbye=me"}) + self.assertEqual(bag.get("hello"), "you") + self.assertEqual(bag.get("goodbye"), "me") + + def test_can_parse_post_data(self): + bag = InputBag() + bag.load( + { + "CONTENT_LENGTH": len(str(json.dumps({"__token": 1}))), + "wsgi.input": io.BytesIO(bytes(json.dumps({"__token": 1}), "utf-8")), + } + ) + self.assertEqual(bag.get("__token"), 1) + + def test_can_parse_duplicate_values(self): + bag = InputBag() + bag.load({"QUERY_STRING": "filter[name]=Joe&filter[last]=Bill"}) + """ + {"filter": [{}]} + """ + self.assertTrue("name" in bag.get("filter")) + self.assertTrue("last" in bag.get("filter")) + + def test_all_with_values(self): + bag = InputBag() + bag.load({"QUERY_STRING": "hello=you"}) + """ + {"filter": [{}]} + """ + self.assertEqual(bag.all_as_values(), {"hello": "you"}) + + def test_all_without_internal_values(self): + bag = InputBag() + bag.load({"QUERY_STRING": "hello=you&__token=tok"}) + """ + {"filter": [{}]} + """ + self.assertEqual(bag.all_as_values(internal_variables=False), {"hello": "you"}) + + def test_has(self): + bag = InputBag() + bag.load({"QUERY_STRING": "hello=you&goodbye=me"}) + self.assertTrue(bag.has("hello", "goodbye")) + + def test_only(self): + bag = InputBag() + bag.load({"QUERY_STRING": "hello=you&goodbye=me&name=Joe"}) + self.assertEqual(bag.only("hello", "name"), {"hello": "you", "name": "Joe"}) + + def test_only_array_based_inputs(self): + bag = InputBag() + bag.load({"QUERY_STRING": "user[]=user1&user[]=user2"}) + self.assertEqual(bag.get("user[]"), ["user1", "user2"]) + bag = InputBag() + bag.load({"QUERY_STRING": "user[user1]=value&user[user2]=value"}) + self.assertEqual(bag.get("user"), {"user1": "value", "user2": "value"}) + + def test_can_parse_post_params(self): + bag = InputBag() + bag.load({"wsgi.input": self.post_data, "CONTENT_TYPE": "application/json"}) + self.assertEqual(bag.get("param"), "hey") + + def test_can_parse_post_params_from_url_encoded(self): + bag = InputBag() + bag.load( + { + "wsgi.input": self.bytes_data, + "CONTENT_TYPE": "application/x-www-form-urlencoded", + } + ) + self.assertEqual(bag.get("jack"), "Daniels") + + def test_advanced_dict_parse(self): + bag = InputBag() + inputs = bag.parse_dict( + {"user[][name]": ["Joe"], "user[][email]": ["joe@masoniteproject.com"]} + ) + self.assertEqual( + inputs, {"user": [{"name": "Joe"}, {"email": "joe@masoniteproject.com"}]} + ) + inputs = bag.parse_dict( + {"user[name]": ["Joe"], "user[email]": ["joe@masoniteproject.com"]} + ) + self.assertEqual( + inputs, {"user": {"email": "joe@masoniteproject.com", "name": "Joe"}} + ) + # inputs = bag.parse_dict({'user[options][name]': ['Joe'], 'user[options][email]': ['joe@masoniteproject.com']}) + # self.assertEqual(inputs, {'user': {"options": {'email': 'joe@masoniteproject.com', 'name': 'Joe'}}} diff --git a/tests/core/request/test_request.py b/tests/core/request/test_request.py new file mode 100644 index 000000000..385452184 --- /dev/null +++ b/tests/core/request/test_request.py @@ -0,0 +1,24 @@ +from tests import TestCase +from src.masonite.request import Request +from src.masonite.utils.http import generate_wsgi +import os + + +class TestRequest(TestCase): + def test_request_can_get_path(self): + request = Request(generate_wsgi(path="/test")) + self.assertEqual(request.get_path(), "/test") + self.assertEqual(request.get_request_method(), "GET") + + def test_request_contains(self): + request = Request(generate_wsgi(path="/test")) + self.assertTrue(request.contains("/test")) + + request = Request(generate_wsgi(path="/test/user")) + self.assertTrue(request.contains("/test/*")) + + request = Request(generate_wsgi(path="/test/admin/user")) + self.assertTrue(request.contains("/test/*/user")) + + request = Request(generate_wsgi(path="/test/admin/user")) + self.assertTrue(request.contains("*")) diff --git a/tests/core/request/test_request_input.py b/tests/core/request/test_request_input.py new file mode 100644 index 000000000..af6003f78 --- /dev/null +++ b/tests/core/request/test_request_input.py @@ -0,0 +1,24 @@ +from tests import TestCase + +from src.masonite.utils.http import generate_wsgi +from src.masonite.request import Request + + +class TestRequest(TestCase): + def setUp(self): + self.request = Request(generate_wsgi(path="/test")) + + def test_request_no_input_returns_false(self): + self.assertEqual(self.request.input("notavailable"), False) + + def test_request_can_get_string_value(self): + self.request.input_bag.query_string = {"test": "value"} + self.assertEqual(self.request.input("test"), "value") + + def test_request_can_get_list_value(self): + self.request.input_bag.query_string = {"test": ["foo", "bar"]} + self.assertEqual(self.request.input("test"), ["foo", "bar"]) + + def test_request_can_get_float_value(self): + self.request.input_bag.query_string = {"test": 3.1415926} + self.assertEqual(self.request.input("test"), 3.1415926) diff --git a/tests/core/response/test_response_download.py b/tests/core/response/test_response_download.py new file mode 100644 index 000000000..3bff22c87 --- /dev/null +++ b/tests/core/response/test_response_download.py @@ -0,0 +1,20 @@ +from tests import TestCase +from src.masonite.foundation import Application +import os +from src.masonite.response import Response + + +class TestResponseRedirect(TestCase): + def setUp(self): + application = Application(os.getcwd()) + self.response = Response(application) + + def test_download(self): + self.response.download( + "invoice", "tests/integrations/storage/invoice.pdf", force=True + ) + self.assertTrue(self.response.header("Content-Disposition")) + self.assertEqual( + self.response.header("Content-Disposition"), + 'attachment; filename="invoice.pdf"', + ) diff --git a/tests/core/response/test_response_helpers.py b/tests/core/response/test_response_helpers.py new file mode 100644 index 000000000..522c9c1ba --- /dev/null +++ b/tests/core/response/test_response_helpers.py @@ -0,0 +1,21 @@ +from tests import TestCase +from src.masonite.routes import Route + + +class TestResponseHelpers(TestCase): + def setUp(self): + super().setUp() + self.setRoutes( + Route.get("/test-with-errors", "WelcomeController@with_errors"), + Route.get("/test-with-input", "WelcomeController@form_with_input"), + ) + + def test_with_input(self): + res = self.get("/test-with-input", {"name": "Sam"}).assertSessionHas( + "name", "Sam" + ) + + def test_with_errors(self): + self.get("/test-with-errors").assertSessionHasErrors().assertSessionHasErrors( + ["email"] + ) diff --git a/tests/core/response/test_response_redirections.py b/tests/core/response/test_response_redirections.py new file mode 100644 index 000000000..17e75c179 --- /dev/null +++ b/tests/core/response/test_response_redirections.py @@ -0,0 +1,27 @@ +from tests import TestCase +from src.masonite.foundation import Application +import os +from src.masonite.response import Response +from src.masonite.routes import Router, Route + + +class TestResponseRedirect(TestCase): + def setUp(self): + application = Application(os.getcwd()) + application.bind("router", Router(Route.get("/", None).name("home"))) + self.response = Response(application) + + def test_redirect(self): + self.response.redirect("/") + self.assertEqual(self.response.get_status(), 302) + self.assertEqual(self.response.header_bag.get("Location").value, "/") + + def test_redirect_to_route_named_route(self): + self.response.redirect(name="home") + self.assertEqual(self.response.get_status(), 302) + self.assertEqual(self.response.header_bag.get("Location").value, "/") + + def test_redirect_to_url(self): + self.response.redirect(url="/login") + self.assertEqual(self.response.get_status(), 302) + self.assertEqual(self.response.header_bag.get("Location").value, "/login") diff --git a/tests/core/test_app.py b/tests/core/test_app.py deleted file mode 100644 index f3cf1e68d..000000000 --- a/tests/core/test_app.py +++ /dev/null @@ -1,136 +0,0 @@ -from src.masonite.app import App -from src.masonite.request import Request -from src.masonite.testing import generate_wsgi -import unittest - -REQUEST = Request({}).load_environ(generate_wsgi()) - - -class MockMail: - - def __init__(self, request: Request): - self.request = request - -class Publisher: - """ - AMQP exchange publisher using aio_pika wrapper library - Messages have to bo instances of app.rmq.Messages.Message concretes - """ - __slots__ = ['username', 'password', 'host', 'exchange_name', 'exchange_type', 'routing_key'] - def __init__(self, exchange_name, exchange_type: str = 'direct', routing_key: str = ''): - self.exchange_name = exchange_name - self.exchange_type = exchange_type - self.routing_key = routing_key - -class TestApp(unittest.TestCase): - - def setUp(self): - self.app = App() - - def test_app_binds(self): - self.app.bind('test1', object) - self.app.bind('test2', object) - self.assertEqual(self.app.providers, {'test1': object, 'test2': object}) - - def test_app_makes(self): - self.app.bind('Request', REQUEST) - self.assertEqual(self.app.make('Request'), REQUEST) - - def test_app_makes_and_resolves(self): - self.app.bind('Request', REQUEST) - self.app.bind('MockMail', MockMail) - mockmail = self.app.make('MockMail') - self.assertIsInstance(mockmail.request, Request) - - def test_app_makes_different_instances(self): - self.app.bind('MockMail', MockMail) - self.app.bind('Request', REQUEST) - m1 = self.app.make('MockMail') - m2 = self.app.make('MockMail') - - self.assertNotEqual(id(m1), id(m2)) - - def test_app_makes_singleton_instance(self): - self.app.bind('Request', REQUEST) - self.app.singleton('MockMail', MockMail) - m1 = self.app.make('MockMail') - m2 = self.app.make('MockMail') - - self.assertEqual(id(m1), id(m2)) - self.assertEqual(id(m1.request), id(m2.request)) - - m1.request.test = 'test' - self.assertEqual(m2.request.test, 'test') - - def test_can_set_container_hook(self): - self.app.on_bind('Request', self._func_on_bind) - self.app.bind('Request', REQUEST) - self.assertEqual(self.app.make('Request').path, '/test/on/bind') - - def _func_on_bind(self, request, container): - request.path = '/test/on/bind' - - def test_can_set_container_hook_with_obj_binding(self): - self.app.on_bind(Request, self._func_on_bind_with_obj) - self.app.bind('Request', REQUEST) - self.assertEqual(self.app.make('Request').path, '/test/on/bind/obj') - - def _func_on_bind_with_obj(self, request, container): - request.path = '/test/on/bind/obj' - - def test_can_fire_container_hook_on_make(self): - self.app.on_make(Request, self._func_on_make) - self.app.bind('Request', REQUEST) - self.assertEqual(self.app.make('Request').path, '/test/on/make') - self.assertEqual(self.app.make('Request').path, '/test/on/make') - - def _func_on_make(self, request, container): - request.path = '/test/on/make' - - def test_can_fire_hook_on_resolve(self): - self.app.on_resolve(Request, self._func_on_resolve) - self.app.bind('Request', REQUEST) - self.assertEqual(self.app.resolve(self._resolve_request).path, '/on/resolve') - self.assertEqual(self.app.resolve(self._resolve_request).path, '/on/resolve') - - def test_can_fire_hook_on_resolve_class(self): - self.app.on_resolve(Request, self._func_on_resolve_class) - self.app.bind('Request', REQUEST) - self.assertEqual(self.app.resolve(self._resolve_reques_class).path, '/on/resolve/class') - self.assertEqual(self.app.resolve(self._resolve_reques_class).path, '/on/resolve/class') - - def test_can_resolve_parameter_with_keyword_argument_setting(self): - self.app.bind('Request', REQUEST) - self.app.resolve_parameters = True - self.assertEqual(self.app.resolve(self._resolve_parameter), REQUEST) - self.assertEqual(self.app.resolve(self._resolve_parameter), REQUEST) - - def test_can_resolve_parameter_with_typehint_arguments_with_minimal_passing(self): - self.app.bind('Publisher', Publisher) - publisher = self.app.make('Publisher', 'exchange') - self.assertEqual(publisher.exchange_name, 'exchange') - self.assertEqual(publisher.exchange_type, 'direct') - self.assertEqual(publisher.routing_key, '') - - def test_can_resolve_parameter_with_typehint_arguments_with_overriding(self): - self.app.bind('Publisher', Publisher) - publisher = self.app.make('Publisher', 'exchange', 'indirect') - self.assertEqual(publisher.exchange_name, 'exchange') - self.assertEqual(publisher.exchange_type, 'indirect') - self.assertEqual(publisher.routing_key, '') - - - def _func_on_resolve(self, request, container): - request.path = '/on/resolve' - - def _func_on_resolve_class(self, request, container): - request.path = '/on/resolve/class' - - def _resolve_request(self, request: Request): - return request - - def _resolve_parameter(self, Request): - return Request - - def _resolve_reques_class(self, request: Request): - return request diff --git a/tests/core/test_auth.py b/tests/core/test_auth.py deleted file mode 100644 index a177e1c95..000000000 --- a/tests/core/test_auth.py +++ /dev/null @@ -1,208 +0,0 @@ -import datetime -import time - -from config import application -from masoniteorm.models import Model -from src.masonite.app import App -from src.masonite.auth import Auth, MustVerifyEmail, Sign -from src.masonite.auth.guards import Guard, WebGuard -from src.masonite.drivers import AuthJwtDriver -from src.masonite.helpers import password as bcrypt_password -from src.masonite.routes import Get -from src.masonite.request import Request -from app.http.controllers.ConfirmController import \ - ConfirmController -from src.masonite.testing import TestCase -from src.masonite.testing import generate_wsgi -from src.masonite.view import View -import random - - -class User(Model, MustVerifyEmail): - __guarded__ = [] - - -class TestAuth(TestCase): - - """Start and rollback transactions for this test - """ - transactions = True - - def setUp(self): - super().setUp() - self.container = App() - self.app = self.container - self.app.bind('Container', self.app) - view = View(self.container) - self.request = Request(generate_wsgi()).load_environ(generate_wsgi()) - self.request.key(application.KEY) - self.app.bind('Request', self.request) - self.container.bind('View', view.render) - self.container.bind('ViewClass', view) - - - self.auth = Guard(self.app) - self.auth.register_guard('web', WebGuard) - self.auth.guard('web').register_driver('jwt', AuthJwtDriver) - self.auth.set('web') - - self.app.swap(Auth, self.auth) - self.request.load_app(self.app) - - def setUpFactories(self): - User.create({ - 'name': 'testuser123', - 'email': 'user@email.com', - 'password': bcrypt_password('secret'), - 'second_password': bcrypt_password('pass123'), - }) - - def test_auth(self): - self.assertTrue(self.auth) - - def test_login_user1(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertTrue(self.auth.login('user@email.com', 'secret')) - self.assertTrue(self.request.get_cookie('token')) - self.assertEqual(self.auth.user().name, 'testuser123') - - def test_login_with_no_password(self): - with self.assertRaises(TypeError): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.auth.driver = driver - self.assertTrue(self.auth.login('nopassword@email.com', None)) - - def test_guard_switches_guard(self): - self.assertIsInstance(self.auth.guard('web'), WebGuard) - - def test_login_user_with_list_auth_column(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.auth.auth_model.__auth__ = ['name', 'email'] - self.assertTrue(self.auth.login('testuser123', 'secret')) - self.assertTrue(self.request.get_cookie('token')) - - def test_can_register(self): - self.auth.register({ - 'name': 'Joe', - 'email': 'joe@email.com', - 'password': 'secret' - }) - - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertTrue(User.where('email', 'joe@email.com').first()) - self.assertNotEqual(User.where('email', 'joe@email.com').first().password, 'secret') - - def test_get_user(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertTrue(self.auth.login_by_id(1)) - self.assertTrue(self.request.user()) - - def test_get_user_returns_false_if_not_loggedin(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.auth.login('user@email.com', 'wrong_secret') - self.assertFalse(self.auth.user()) - - def test_logout_user(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.auth.login("user@email.com", 'secret') - self.assertTrue(self.request.get_cookie('token')) - self.assertTrue(self.auth.user()) - self.assertTrue(self.request.user()) - - self.auth.logout() - self.assertFalse(self.request.get_cookie('token')) - self.assertFalse(self.auth.user()) - self.assertFalse(self.request.user()) - - def test_login_user_fails(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertFalse(self.auth.login('user@email.com', 'bad_password')) - - def test_login_user_success(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertTrue(self.auth.login('user@email.com', 'secret')) - - def test_login_by_id(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertTrue(self.auth.login_by_id(1)) - self.assertTrue(self.request.get_cookie('token')) - self.assertFalse(self.auth.login_by_id(3)) - - def test_guard_can_register_new_drivers(self): - self.auth.guard('web').register_driver('api', AuthJwtDriver) - - self.assertIsInstance(self.auth.driver('api'), AuthJwtDriver) - - - def test_guard_can_register_new_guards(self): - self.auth.register_guard('api_guard', AuthJwtDriver) - - self.assertIsInstance(self.auth.guard('api_guard'), AuthJwtDriver) - - - def test_login_once_does_not_set_cookie(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - self.assertTrue(self.auth.once().login_by_id(1)) - self.assertIsNone(self.request.get_cookie('token')) - - def test_confirm_controller_success(self): - for driver in ('jwt', 'cookie'): - self.auth.driver(driver) - params = {'id': Sign().sign('{0}::{1}'.format(1, time.time()))} - self.request.set_params(params) - user = self.auth.once().login_by_id(1) - self.request.set_user(user) - - self.app.bind('Request', self.request) - self.app.make('Request').load_app(self.app) - - # Create the route - route = Get('/email/verify/@id', ConfirmController.confirm_email) - - ConfirmController.get_user = User - - # Resolve the controller constructor - controller = self.app.resolve(route.controller) - - # Resolve the method - response = self.app.resolve(getattr(controller, route.controller_method)) - - self.assertEqual(response.rendered_template, 'confirm') - self.refreshDatabase() - - def test_confirm_controller_failure(self): - for driver in ('cookie', 'jwt'): - self.auth.driver(driver) - timestamp_plus_11 = datetime.datetime.now() - datetime.timedelta(minutes=11) - - params = {'id': Sign().sign('{0}::{1}'.format(1, timestamp_plus_11.timestamp()))} - self.request.set_params(params) - user = self.auth.once().login_by_id(1) - self.request.set_user(user) - - self.app.bind('Request', self.request) - self.app.make('Request').load_app(self.app) - - # Create the route - route = Get('/email/verify/@id', ConfirmController.confirm_email) - - ConfirmController.get_user = User - - # Resolve the controller constructor - controller = self.app.resolve(route.controller) - - # Resolve the method - response = self.app.resolve(getattr(controller, route.controller_method)) - - self.assertEqual(response.rendered_template, 'error') diff --git a/tests/core/test_auth_middleware.py b/tests/core/test_auth_middleware.py deleted file mode 100644 index 6a0428992..000000000 --- a/tests/core/test_auth_middleware.py +++ /dev/null @@ -1,38 +0,0 @@ -from src.masonite.auth.guards import WebGuard - -from src.masonite.auth import Auth -from src.masonite.middleware import GuardMiddleware -from src.masonite.routes import Get -from src.masonite.testing import TestCase - -class MockApiGuard: - - def user(self): - return 'user' - -class MockController: - - def user(self, auth: Auth): - return auth.user() - -class TestAuthMiddleware(TestCase): - - def setUp(self): - super().setUp() - self.container.make(Auth).register_guard('api', MockApiGuard) - - self.withRouteMiddleware({ - 'guard': GuardMiddleware, - }) - - self.routes([ - Get('/guard/web', MockController.user).middleware('guard:web'), - Get('/guard/api', MockController.user).middleware('guard:api') - ]) - - def test_can_switch_guards(self): - self.get('/guard/web').assertContains('False') - self.assertIsInstance(self.container.make(Auth).get(), WebGuard) - - self.get('/guard/api').assertContains('user') - self.assertIsInstance(self.container.make(Auth).get(), WebGuard) \ No newline at end of file diff --git a/tests/core/test_autoload.py b/tests/core/test_autoload.py deleted file mode 100644 index 612e03d02..000000000 --- a/tests/core/test_autoload.py +++ /dev/null @@ -1,57 +0,0 @@ -import unittest - -from src.masonite.app import App -from src.masonite.autoload import Autoload -from src.masonite.exceptions import (AutoloadContainerOverwrite, ContainerError, - InvalidAutoloadPath) -from src.masonite.request import Request - - -class TestAutoload(unittest.TestCase): - - def setUp(self): - self.app = App() - - def test_autoload_loads_from_directories(self): - Autoload(self.app).load(['app/http/controllers']) - self.assertTrue(self.app.make('TestController')) - - def test_autoload_instantiates_classes(self): - classes = Autoload().collect(['app/http/test_controllers'], instantiate=True) - self.assertTrue(classes['TestController'].test) - - # def test_autoload_loads_from_directories_with_trailing_slash_raises_exception(self): - # with self.assertRaises(InvalidAutoloadPath): - # Autoload(self.app).load(['app/http/controllers/']) - - def test_autoload_raises_exception_with_no_container(self): - with self.assertRaises(ContainerError): - Autoload().load(['app/http/controllers/']) - - def test_autoload_collects_classes(self): - classes = Autoload().collect(['app/http/controllers']) - self.assertIn('TestController', classes) - self.assertNotIn('Command', classes) - - def test_autoload_loads_from_directories_and_instances(self): - classes = Autoload().instances(['app/http/controllers'], object) - self.assertIn('TestController', classes) - self.assertNotIn('Command', classes) - - def test_autoload_loads_not_only_from_app_from_directories_and_instances(self): - classes = Autoload().instances(['app/http/controllers'], object, only_app=False) - self.assertIn('TestController', classes) - - def test_autoload_does_not_instantiate_classes(self): - classes = Autoload().instances(['app/http/controllers'], object) - with self.assertRaises(AttributeError): - self.assertTrue(classes['TestController'].test, True) - - def test_collects_classes_only_in_app(self): - classes = Autoload().collect(['app/http/controllers'], only_app=False) - self.assertIn('TestController', classes) - - def test_autoload_throws_exception_when_binding_key_that_already_exists(self): - self.app.bind('Request', Request(None)) - with self.assertRaises(AutoloadContainerOverwrite): - Autoload(self.app).load(['app/http/test_controllers']) diff --git a/tests/core/test_cache.py b/tests/core/test_cache.py deleted file mode 100644 index b5924dde3..000000000 --- a/tests/core/test_cache.py +++ /dev/null @@ -1,122 +0,0 @@ -import glob -import os -import time -import unittest - -from src.masonite.app import App -from src.masonite.drivers import CacheDiskDriver, CacheRedisDriver -from src.masonite.environment import LoadEnvironment -from src.masonite.managers import CacheManager - -LoadEnvironment() - - -class TestCache(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Application', self.app) - self.app.bind('CacheDiskDriver', CacheDiskDriver) - self.app.bind('CacheRedisDriver', CacheRedisDriver) - self.app.bind('CacheManager', CacheManager(self.app)) - self.app.bind('Cache', self.app.make('CacheManager').driver('disk')) - self.drivers = ['disk'] - if os.environ.get('REDIS_CACHE_DRIVER'): - self.drivers.append('redis') - - def test_driver_disk_cache_store_for(self): - for driver in self.drivers: - self.app.bind('Cache', self.app.make( - 'CacheManager').driver(driver)) - key = "cache_driver_test" - key_store = self.app.make('Cache').store_for( - key, "macho", 5, "seconds") - - # This return one key like this: cache_driver_test:1519741028.5628147 - self.assertEqual(key, key_store[:len(key)]) - - content = self.app.make('Cache').get(key) - self.assertEqual(content, "macho") - assert self.app.make('Cache').exists(key) - assert self.app.make('Cache').is_valid(key) - self.app.make('Cache').delete(key) - - assert not self.app.make('Cache').is_valid("error") - - def test_driver_disk_cache_store(self): - for driver in self.drivers: - self.app.bind('Cache', self.app.make( - 'CacheManager').driver(driver)) - key = "forever_cache_driver_test" - key = self.app.make('Cache').store(key, "macho") - - # This return the same key because it's forever - self.assertEqual(key, key) - - content = self.app.make('Cache').get(key) - self.assertEqual(content, "macho") - self.assertTrue(self.app.make('Cache').exists(key)) - self.assertTrue(self.app.make('Cache').is_valid(key)) - self.app.make('Cache').delete(key) - - def test_get_cache(self): - for driver in self.drivers: - self.app.bind('Cache', self.app.make( - 'CacheManager').driver(driver)) - - cache_driver = self.app.make('Cache') - - cache_driver.store('key', 'value') - self.assertEqual(cache_driver.get('key'), 'value') - - cache_driver.store_for('key_time', 'key value', 4, 'seconds') - self.assertEqual(cache_driver.get('key_time'), 'key value') - - cache_driver.delete('key') - cache_driver.delete('key_time') - - def test_cache_expired_before_get(self): - for driver in self.drivers: - self.app.bind('Cache', self.app.make( - 'CacheManager').driver(driver)) - cache_driver = self.app.make('Cache') - - cache_driver.store_for('key_for_1_second', 'value', 1, 'second') - self.assertTrue(cache_driver.is_valid('key_for_1_second')) - self.assertEqual(cache_driver.get('key_for_1_second'), 'value') - - time.sleep(2) - - self.assertFalse(cache_driver.is_valid('key_for_1_second')) - self.assertIsNone(cache_driver.get('key_for_1_second')) - - for cache_file in glob.glob('bootstrap/cache/key*'): - os.remove(cache_file) - - def test_cache_sets_times(self): - for driver in self.drivers: - self.app.bind('Cache', self.app.make( - 'CacheManager').driver(driver)) - - cache_driver = self.app.make('Cache') - - cache_driver.store_for('key_for_1_minute', 'value', 1, 'minute') - cache_driver.store_for('key_for_1_hour', 'value', 1, 'hour') - cache_driver.store_for('key_for_1_day', 'value', 1, 'day') - cache_driver.store_for('key_for_1_month', 'value', 1, 'month') - cache_driver.store_for('key_for_1_year', 'value', 1, 'year') - - self.assertTrue(cache_driver.is_valid('key_for_1_minute')) - self.assertTrue(cache_driver.is_valid('key_for_1_hour')) - self.assertTrue(cache_driver.is_valid('key_for_1_day')) - self.assertTrue(cache_driver.is_valid('key_for_1_month')) - self.assertTrue(cache_driver.is_valid('key_for_1_year')) - - self.assertEqual(cache_driver.get('key_for_1_minute'), 'value') - self.assertEqual(cache_driver.get('key_for_1_hour'), 'value') - self.assertEqual(cache_driver.get('key_for_1_day'), 'value') - self.assertEqual(cache_driver.get('key_for_1_month'), 'value') - self.assertEqual(cache_driver.get('key_for_1_year'), 'value') - - for cache_file in glob.glob('bootstrap/cache/key*'): - os.remove(cache_file) diff --git a/tests/core/test_container.py b/tests/core/test_container.py deleted file mode 100644 index 8267c1280..000000000 --- a/tests/core/test_container.py +++ /dev/null @@ -1,226 +0,0 @@ -from src.masonite.app import App -from src.masonite.request import Request -from src.masonite.drivers import UploadDiskDriver -from src.masonite.contracts import UploadContract -from src.masonite.exceptions import ContainerError, StrictContainerException - -import unittest - - -class MockObject: - pass - - -class MockSelfObject: - - def __init__(self): - self.id = 1 - - def get_id(self): - return self.id - - -class GetObject(MockObject): - - def find(self): - return 1 - - -class GetAnotherObject(MockObject): - - def find(self): - return 2 - - -class MakeObject: - pass - - -class SubstituteThis: - pass - - -class TestContainer(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Request', Request(None)) - self.app.bind('MockObject', MockObject) - self.app.bind('GetObject', GetObject) - self.app.bind('Container', self.app) - - def test_container_gets_direct_class(self): - self.assertIsInstance(self.app.make('Request'), Request) - - def test_container_resolving_annotation(self): - self.assertIsInstance(self.app.resolve(self._function_test_annotation), MockObject) - - def _function_test_annotation(self, mock: MockObject): - return mock - - def test_container_resolving_instance_of_object(self): - self.app = App() - self.app.bind('Get', GetObject) - self.assertIsInstance(self.app.resolve(self._function_test_annotation), GetObject) - - def test_container_resolving_similiar_objects(self): - self.app.bind('GetAnotherObject', GetAnotherObject) - - obj = self.app.resolve(self._function_test_find_method_on_similiar_objects) - self.assertEqual(obj[0], 2) - self.assertEqual(obj[1], 1) - - def _function_test_find_method_on_similiar_objects(self, user: GetAnotherObject, country: GetObject): - return [user.find(), country.find()] - - def test_raises_error_when_getting_instances_of_classes(self): - with self.assertRaises(ContainerError): - self.assertTrue(self.app.resolve(self._function_test_find_method_on_similiar_objects)) - - def _function_test_double_annotations(self, mock: MockObject, request: Request): - return {'mock': mock, 'request': request} - - def test_container_resolving_multiple_annotations(self): - self.assertIsInstance(self.app.resolve(self._function_test_double_annotations)['mock'], MockObject) - self.assertIsInstance(self.app.resolve(self._function_test_double_annotations)['request'], Request) - - def test_container_contract_returns_upload_disk_driver(self): - self.app.bind('UploadDiskDriver', UploadDiskDriver) - self.assertIsInstance(self.app.resolve(self._function_test_contracts), UploadDiskDriver) - - def _function_test_contracts(self, upload: UploadContract): - return upload - - def _function_not_in_container(self, NotIn): - return NotIn - - def test_container_raises_value_error(self): - with self.assertRaises(ContainerError): - self.assertTrue(self.app.resolve(self._function_not_in_container)) - - def test_container_collects_correct_objects(self): - self.app.bind('ExceptionHook', object) - self.app.bind('SentryExceptionHook', object) - self.app.bind('ExceptionHandler', object) - - self.assertEqual(self.app.collect('*ExceptionHook'), {'ExceptionHook': object, 'SentryExceptionHook': object}) - self.assertEqual(self.app.collect('Exception*'), {'ExceptionHook': object, 'ExceptionHandler': object}) - self.assertEqual(self.app.collect('Sentry*Hook'), {'SentryExceptionHook': object}) - with self.assertRaises(AttributeError): - self.app.collect('Sentry') - - def test_container_collects_correct_subclasses_of_classes(self): - self.app.bind('GetAnotherObject', GetAnotherObject) - objects = self.app.collect(MockObject) - - self.assertIn('GetAnotherObject', objects) - self.assertIn('GetObject', objects) - - def test_container_collects_correct_subclasses_of_objects(self): - self.app.bind('GetAnotherObject', GetAnotherObject()) - objects = self.app.collect(MockObject) - - self.assertIn('GetAnotherObject', objects) - self.assertIn('GetObject', objects) - - def test_container_makes_from_class(self): - self.assertIsInstance(self.app.make(Request), Request) - - def test_container_can_bind_and_make_from_class_key(self): - self.app.bind(MakeObject, MakeObject) - self.assertIsInstance(self.app.make(MakeObject), MakeObject) - - def test_container_makes_from_base_class(self): - del self.app.providers['MockObject'] - self.assertIsInstance(self.app.make(MockObject), GetObject) - - def test_container_has_obj(self): - assert self.app.has('Request') - assert self.app.has(Request) - - def test_container_makes_from_contract(self): - self.app.bind('UploadDriver', UploadDiskDriver) - self.assertIsInstance(self.app.make(UploadContract), UploadDiskDriver) - - def test_strict_container_raises_exception(self): - self.app = App(strict=True) - - self.app.bind('Request', object) - - with self.assertRaises(StrictContainerException): - self.app.bind('Request', object) - - def test_override_container_does_not_override(self): - self.app = App(override=False) - - self.app.bind('Request', 'test') - self.app.bind('Request', 'override') - self.assertEqual(self.app.make('Request'), 'test') - - def test_app_simple_bind(self): - app = App() - app.simple(Request) - self.assertEqual(app.providers, {Request: Request}) - - def test_app_simple_bind_init(self): - app = App() - req = Request() - app.simple(req) - self.assertEqual(app.providers, {Request: req}) - - def test_app_make_after_simple_bind(self): - app = App() - req = Request() - app.simple(req) - self.assertEqual(app.make(Request), req) - - def test_can_pass_variables(self): - app = App() - req = Request() - app.bind('Request', req) - obj = app.resolve(self._test_resolves_variables, 'test1', 'test2') - self.assertEqual(obj[0], 'test1') - self.assertEqual(obj[1], req) - self.assertEqual(obj[2], 'test2') - - def _test_resolves_variables(self, var1, request: Request, var2): - return [var1, request, var2] - - def test_can_substitute(self): - app = App() - app.swap(SubstituteThis, self._substitute) - - self.assertEqual(app.resolve(self._test_substitute), 'test') - - def test_can_substitute_with_object(self): - app = App() - app.swap(SubstituteThis, MakeObject()) - - self.assertIsInstance(app.resolve(self._test_substitute), MakeObject) - - def test_instantiates_obj(self): - app = App() - app.bind('MockSelf', MockSelfObject) - - self.assertEqual(app.resolve(self._test_self_object).id, 1) - - def test_can_use_in_keyword(self): - app = App() - app.bind('test', 'value') - - self.assertIn('test', app) - - def test_can_substitute_with_make_object(self): - app = App() - app.swap(SubstituteThis, MakeObject()) - - self.assertIsInstance(app.make(SubstituteThis), MakeObject) - - def _substitute(self, _, __): - return 'test' - - def _test_substitute(self, test: SubstituteThis): - return test - - def _test_self_object(self, obj: MockSelfObject): - return obj diff --git a/tests/core/test_controllers.py b/tests/core/test_controllers.py deleted file mode 100644 index eab4ae4c0..000000000 --- a/tests/core/test_controllers.py +++ /dev/null @@ -1,44 +0,0 @@ -from src.masonite.app import App -from src.masonite.routes import Get -from app.http.controllers.ControllerTest import ControllerTest -from src.masonite.request import Request -import unittest - - -class TestController(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('object', object) - - def test_string_controller_constructor_resolves_container(self): - self.app.bind('Request', Request) - - # Create the route - route = Get('/url', 'ControllerTest@show') - - # Resolve the controller constructor - controller = self.app.resolve(route.controller) - - # Resolve the method - response = self.app.resolve(getattr(controller, route.controller_method)) - - self.assertIsInstance(route.controller, ControllerTest.__class__) - self.assertEqual(route.controller_method, 'show') - self.assertIsInstance(response, Request) - - def test_object_controller_constructor_resolves_container(self): - self.app.bind('Request', Request) - # Create the route - route = Get('/url', ControllerTest.show) - - # Resolve the controller constructor - controller = self.app.resolve(route.controller) - - # Resolve the method - response = self.app.resolve( - getattr(controller, route.controller_method)) - - self.assertIsInstance(route.controller, ControllerTest.__class__) - self.assertEqual(route.controller_method, 'show') - self.assertIsInstance(response, Request) diff --git a/tests/core/test_cookie_signing.py b/tests/core/test_cookie_signing.py deleted file mode 100644 index 95b581000..000000000 --- a/tests/core/test_cookie_signing.py +++ /dev/null @@ -1,49 +0,0 @@ -from src.masonite.request import Request -from src.masonite.testing import generate_wsgi -import unittest - - -class TestCookieSigning(unittest.TestCase): - - def setUp(self): - self.secret_key = 'pK1tLuZA8-upZGz-NiSCP_UVt-fxpxd796TaG6-dp8Y=' - self.request = Request(generate_wsgi()).key(self.secret_key) - - def test_set_and_get_cookie(self): - self.request.cookie('test', 'testvalue') - self.assertEqual(self.request.get_cookie('test'), 'testvalue') - - def test_set_and_get_multiple_cookies(self): - self.request.cookie('cookie1', 'cookie1value') - self.request.cookie('cookie2', 'cookie2value') - - self.assertEqual(self.request.get_cookie('cookie1'), 'cookie1value') - self.assertEqual(self.request.get_cookie('cookie2'), 'cookie2value') - - def test_set_cookie_without_encryption(self): - self.request.cookie('notencrypted', 'value', False) - - self.assertEqual(self.request.get_cookie('notencrypted', False), 'value') - - def test_set_and_get_cookie_with_no_existing_cookies(self): - self.request.environ['HTTP_COOKIE'] = '' - self.request.cookie('test', 'testvalue') - self.assertEqual(self.request.get_cookie('test'), 'testvalue') - - def test_set_and_get_cookie_with_existing_cookie(self): - self.request.environ['HTTP_COOKIE'] = 'cookie=true' - self.request.cookie('test', 'testvalue') - self.assertEqual(self.request.get_cookie('test'), 'testvalue') - - def test_set_and_get_cookie_with_http_only(self): - self.request.cookie('test', 'testvalue', encrypt=False) - self.assertEqual(self.request.get_cookie('test', decrypt=False), 'testvalue') - self.assertTrue(self.request.get_raw_cookie('test').http_only) - self.assertIn('/', self.request.get_raw_cookie('test').path) - self.assertIn('testvalue', self.request.get_raw_cookie('test').value) - - def test_set_and_get_cookie_without_http_only(self): - self.request.cookie('test', 'testvalue', http_only=False, encrypt=False) - self.assertEqual(self.request.get_cookie('test', decrypt=False), 'testvalue') - self.assertIn('/', self.request.get_raw_cookie('test').path) - self.assertIn('testvalue', self.request.get_raw_cookie('test').value) diff --git a/tests/core/test_csrf.py b/tests/core/test_csrf.py deleted file mode 100644 index 68d426872..000000000 --- a/tests/core/test_csrf.py +++ /dev/null @@ -1,40 +0,0 @@ -from src.masonite.middleware import CsrfMiddleware -from src.masonite.testing import TestCase -from src.masonite.routes import Get, Post - - -class TestCsrf(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[Get('/', 'ControllerTest@show').middleware('csrf'), Post('/test-route', 'ControllerTest@show').middleware('csrf')]) - csrf = CsrfMiddleware - csrf.exempt = ['/*'] - self.withRouteMiddleware({'csrf': csrf}) - - def test_middleware_sets_csrf_cookie(self): - self.assertTrue( - self.get('/').container.make('Request').get_cookie('csrf_token', decrypt=False) - ) - - def test_middleware_shares_view(self): - self.assertIn('csrf_field', self.get('/').container.make('ViewClass')._shared) - - def test_middleware_does_not_need_safe_filter(self): - self.assertNotIn('<', self.container.make('ViewClass').render('csrf_field').rendered_template) - - def test_verify_token(self): - token = self.get('/').container.make('Request').get_cookie('csrf_token', decrypt=False) - self.assertTrue(self.container.make('Csrf').verify_csrf_token(token)) - - def test_csrf_with_dashes(self): - (self.withCsrf() - .withoutHttpMiddleware() - .post('/test-route')) - - def test_csrf_can_use_header(self): - (self.withoutCsrf() - .withHeaders({ - 'X-CSRF-TOKEN': 'tok' - }) - .post('/test-route')) diff --git a/tests/core/test_download.py b/tests/core/test_download.py deleted file mode 100644 index dc9fa3897..000000000 --- a/tests/core/test_download.py +++ /dev/null @@ -1,35 +0,0 @@ -from src.masonite.testing import TestCase -from src.masonite.response import Download -from src.masonite.routes import Get - -class DownloadTestController: - - def show(self): - return Download('storage/static/profile.jpg') - - def force(self): - return Download('storage/static/profile.jpg', name="me").force() - -class TestDownload(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[ - Get('/download', DownloadTestController.show), - Get('/download/force', DownloadTestController.force), - ]) - - def test_can_show_download(self): - (self.get('/download') - .assertIsStatus(200) - .assertHasHeader('Content-Type') - .assertHeaderIs('Content-Type', 'image/jpeg') - .assertNotHasHeader('Content-Disposition') - ) - - def test_can_download_file(self): - (self.get('/download/force') - .assertIsStatus(200) - .assertHeaderIs('Content-Type', 'application/octet-stream') - .assertHeaderIs('Content-Disposition', 'attachment; filename="{}"'.format('me.jpg')) - ) diff --git a/tests/core/test_environment.py b/tests/core/test_environment.py deleted file mode 100644 index befe95b94..000000000 --- a/tests/core/test_environment.py +++ /dev/null @@ -1,55 +0,0 @@ - -from src.masonite.environment import LoadEnvironment -from src.masonite import env - -import os -import unittest - - -class TestEnvironment(unittest.TestCase): - - def test_environment_loads_custom_env(self): - LoadEnvironment('local') - self.assertIn('LOCAL', os.environ) - self.assertEqual(os.environ.get('LOCAL'), 'TEST') - - def test_environment_only_loads(self): - LoadEnvironment(only='local') - self.assertIn('LOCAL', os.environ) - self.assertEqual(os.environ.get('LOCAL'), 'TEST') - - -class TestEnv(unittest.TestCase): - - def test_env_returns_numeric(self): - os.environ["numeric"] = "1" - self.assertEqual(env('numeric'), 1) - - def test_env_returns_numeric_with_default(self): - os.environ["numeric"] = "1" - self.assertEqual(env('na', '1'), 1) - - def test_env_returns_bool(self): - os.environ["bool"] = "True" - self.assertTrue(env('bool')) - os.environ["bool"] = "true" - self.assertTrue(env('bool')) - os.environ["bool"] = "False" - self.assertFalse(env('bool')) - os.environ["bool"] = "false" - self.assertFalse(env('bool')) - - def test_env_returns_default(self): - os.environ["test"] = "1" - self.assertEqual(env('na', 'default'), 'default') - - def test_env_returns_false_on_blank_string(self): - os.environ["test"] = "" - self.assertEqual(env('test', 'default'), 'default') - - def test_env_returns_casted_value_on_blank_string(self): - os.environ["test"] = "" - self.assertEqual(env('test', '1234'), 1234) - - def test_env_works_with_none(self): - self.assertIsNone(env('na', None)) diff --git a/tests/core/test_exception.py b/tests/core/test_exception.py deleted file mode 100644 index c3a6fb5ba..000000000 --- a/tests/core/test_exception.py +++ /dev/null @@ -1,65 +0,0 @@ -from src.masonite.app import App -from src.masonite.exception_handler import ExceptionHandler -from src.masonite.hook import Hook -from src.masonite.request import Request -from src.masonite.response import Response -from src.masonite.testing import generate_wsgi -from src.masonite.view import View -import unittest - - -class ApplicationMock: - DEBUG = True - - -class StorageMock: - STATICFILES = {} - - -class MockExceptionHandler: - - def __init__(self, request: Request): - self.request = request - - def handle(self, _): - self.request.header('test', 'test') - - -class TestException(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Environ', generate_wsgi()) - self.app.bind('WebRoutes', []) - self.app.bind('View', View(self.app).render) - self.app.bind('Request', Request(generate_wsgi()).load_app(self.app)) - self.app.bind('Response', Response(self.app)) - self.app.bind('StatusCode', None) - self.app.bind('Storage', StorageMock) - self.app.bind('ExceptionHandler', ExceptionHandler(self.app)) - self.app.bind('HookHandler', Hook(self.app)) - self.app.bind('Request', Request(generate_wsgi()).load_app(self.app)) - self.app.bind('staticfiles', {}) - self.app.bind('ExceptionAttributeErrorHandler', MockExceptionHandler) - - def test_exception_renders_view(self): - self.app.make('ExceptionHandler').load_exception(OSError) - self.assertIsNotNone(self.app.make('Response')) - - def test_exception_uses_custom_exception(self): - try: - self.app.false() - except Exception as e: - self.app.make('ExceptionHandler').load_exception(e) - - self.assertEqual(self.app.make('Request').header('test'), 'test') - - def test_custom_exception_when_not_registered(self): - try: - self.app.false() - except Exception as e: - self.app.make('ExceptionHandler').load_exception(e) - - def test_exception_returns_none_when_debug_is_false(self): - # config('application.debug') = False - self.assertIsNone(self.app.make('ExceptionHandler').load_exception(KeyError)) diff --git a/tests/core/test_extends.py b/tests/core/test_extends.py deleted file mode 100644 index 0ba12cfd6..000000000 --- a/tests/core/test_extends.py +++ /dev/null @@ -1,136 +0,0 @@ -from src.masonite.app import App -from src.masonite.testing import generate_wsgi, MockWsgiInput -from src.masonite.routes import Route -from src.masonite.request import Request - -wsgi_request = generate_wsgi() -import unittest - - -class ExtendClass: - - path = None - - def get_path(self): - return self.path - - def get_another_path(self): - return self.path - - -class ExtendClass2: - - path = None - - def get_path2(self): - return self.path - - def get_another_path2(self): - return self.path - - -def get_third_path(self): - return self.path - - -class TestExtends(unittest.TestCase): - - def setUp(self): - self.app = App() - self.request = Request(wsgi_request) - self.app.bind('Request', self.request) - - def test_request_can_extend(self): - request = self.app.make('Request').load_app(self.app) - - request.extend('get_path', ExtendClass.get_path) - request.extend('get_another_path_test', ExtendClass.get_another_path) - request.extend('get_third_path_test', get_third_path) - - self.assertEqual(request.get_path(), '/') - self.assertEqual(request.get_another_path_test(), '/') - self.assertEqual(request.get_third_path_test(), '/') - - request.extend(ExtendClass2) - - self.assertEqual(request.get_path2(), '/') - self.assertEqual(request.get_another_path2(), '/') - - request.extend(get_third_path) - self.assertEqual(request.get_third_path(), '/') - - request.extend(ExtendClass.get_another_path) - self.assertEqual(request.get_another_path(), '/') - - def test_gets_input_and_query_with_get_request(self): - app = App() - wsgi_environ = generate_wsgi() - wsgi_environ['QUERY_STRING'] = 'param=1¶m=2¶m=3&foo=bar&q=yes' - wsgi_environ['wsgi.input'] = {'param': 'hey', 'foo': [9, 8, 7, 6], 'bar': 'baz'} - wsgi_environ['REQUEST_METHOD'] = 'GET' - - route_class = Route(wsgi_environ) - request_class = Request(wsgi_environ) - app.bind('Request', request_class) - app.bind('Route', route_class) - request = app.make('Request').load_app(app) - - self.assertEqual(request.query('param'), '1') - self.assertEqual(request.all_query()['param'], ['1', '2', '3']) - self.assertEqual(request.query('foo'), 'bar') - self.assertEqual(request.query('param', multi=True), ['1', '2', '3']) - self.assertEqual(request.query('not-exist', default=2), 2) - self.assertEqual(request.query('not-exist', default=2, multi=True), 2) - self.assertEqual(request.query('q', default='no'), 'yes') - - self.assertEqual(request.input('foo'), 'bar') - self.assertEqual(request.input('param'), '1') - self.assertEqual(request.input('q', default='no'), 'yes') - self.assertEqual(request.input('bar', default='default'), 'default') - - def test_gets_input_and_query_with_non_get_request(self): - app = App() - - for method in ['POST', 'PUT', 'DELETE']: - wsgi_environ = generate_wsgi() - wsgi_environ['REQUEST_METHOD'] = method - wsgi_environ['QUERY_STRING'] = 'param=1¶m=2¶m=3&foo=bar&q=yes' - wsgi_environ['wsgi.input'] = MockWsgiInput('{"param": "hey", "foo": [9, 8, 7, 6], "bar": "baz"}') - wsgi_environ['CONTENT_TYPE'] = 'application/json' - route_class = Route(wsgi_environ) - request_class = Request(wsgi_environ) - app.bind('Request', request_class) - app.bind('Route', route_class) - request = app.make('Request').load_app(app) - - self.assertEqual(request.input('foo'), [9, 8, 7, 6]) - self.assertEqual(request.input('param'), 'hey') - self.assertEqual(request.input('not-exist', default=2), 2) - self.assertEqual(request.input('q', default='default'), 'default') - self.assertEqual(request.input('bar', default='default'), 'baz') - self.assertEqual(request.query('foo'), 'bar') - self.assertEqual(request.query('param'), '1') - self.assertEqual(request.query('param', multi=True), ['1', '2', '3']) - self.assertEqual(request.query('not-exist', default=2), 2) - self.assertEqual(request.query('not-exist', default=2, multi=True), 2) - self.assertEqual(request.query('q', default='default'), 'yes') - - def test_hidden_form_request_method_changes_request_method(self): - app = App() - wsgi_request = generate_wsgi() - wsgi_request['POST_DATA'] = '__method=PUT' - request_class = Request(wsgi_request) - self.assertEqual(request_class.environ['REQUEST_METHOD'], 'PUT') - - def test_get_json_input(self): - json_wsgi = wsgi_request - json_wsgi['REQUEST_METHOD'] = 'POST' - json_wsgi['CONTENT_TYPE'] = 'application/json' - json_wsgi['POST_DATA'] = '' - json_wsgi['wsgi.input'] = MockWsgiInput('{"id": 1, "test": "testing"}') - Route(json_wsgi) - request_obj = Request(json_wsgi) - - self.assertIsInstance(request_obj.request_variables, dict) - self.assertEqual(request_obj.input('id'), 1) - self.assertEqual(request_obj.input('test'), 'testing') diff --git a/tests/core/test_headers.py b/tests/core/test_headers.py deleted file mode 100644 index 8868eadc9..000000000 --- a/tests/core/test_headers.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest -from src.masonite.headers import HeaderBag, Header - -class TestHeaders(unittest.TestCase): - - def test_headers_can_be_registered(self): - bag = HeaderBag() - bag.add(Header('Content-Type', 'application/json')) - self.assertTrue(bag['CONTENT_TYPE']) - - def test_headers_can_be_rendered(self): - bag = HeaderBag() - bag.add(Header('content-type', 'application/json')) - bag.add(Header('x-forwarded-for', '127.0.0.1')) - self.assertEqual(bag.render(), [('Content-Type', 'application/json'), ('X-Forwarded-For', '127.0.0.1')]) - - def test_headers_can_check_with_in(self): - bag = HeaderBag() - bag.add(Header('Content-Type', 'application/json')) - - self.assertTrue('Content-Type' in bag) - - def test_headers_can_be_retrieved(self): - bag = HeaderBag() - bag.add(Header('Content-Type', 'application/json')) - self.assertEqual(bag.get('content-type').value, 'application/json') - self.assertEqual(bag.get('Content-Type').value, 'application/json') \ No newline at end of file diff --git a/tests/core/test_hook.py b/tests/core/test_hook.py deleted file mode 100644 index 0c7b46e60..000000000 --- a/tests/core/test_hook.py +++ /dev/null @@ -1,19 +0,0 @@ -from src.masonite.app import App -from src.masonite.hook import Hook -import unittest - - -class SentryExceptionHookMock: - def load(self, _): - return 'loaded' - - -class TestFrameworkHooks(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('SentryExceptionHook', SentryExceptionHookMock()) - self.app.bind('HookHandler', Hook(self.app)) - - def test_exception_handler(self): - self.assertIsNone(self.app.make('HookHandler').fire('*ExceptionHook')) diff --git a/tests/core/test_mail_base_driver.py b/tests/core/test_mail_base_driver.py deleted file mode 100644 index 14443cb9c..000000000 --- a/tests/core/test_mail_base_driver.py +++ /dev/null @@ -1,49 +0,0 @@ -from src.masonite.app import App -from src.masonite.drivers import BaseMailDriver -from src.masonite.environment import LoadEnvironment -from src.masonite.managers.MailManager import MailManager -from src.masonite.view import View -import unittest - -LoadEnvironment() - - -text_content = 'Hi MasoniteTextTesting\nWelcome to MasoniteFramework!' -html_content = '
MasoniteHTMLTesting
' - - -class MyTestDriver(BaseMailDriver): - pass - - -class TestSMTPDriver(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Container', self.app) - self.app.bind('MailBaseDriver', MyTestDriver) - viewClass = View(self.app) - self.app.bind('ViewClass', viewClass) - self.app.bind('View', viewClass.render) - - def test_mail_renders_template_with_text_mimetype(self): - mail = MailManager(self.app).driver('base').to('idmann509@gmail.com').template( - 'mail/welcome.txt', {'to': 'MasoniteTextTesting'}, mimetype='plain') - self.assertEqual(mail.text_content, text_content) - self.assertEqual(mail.message_body, mail.text_content) - self.assertEqual(mail.html_content, None) - - def test_mail_renders_template_with_html_mimetype(self): - mail = MailManager(self.app).driver('base').to('idmann509@gmail.com').template( - 'mail/welcome.html', {'to': 'MasoniteHTMLTesting'}) - self.assertIn(html_content, mail.html_content) - self.assertEqual(mail.message_body, mail.html_content) - self.assertEqual(mail.text_content, None) - - def test_mail_renders_template_with_both_mimetypes(self): - mail = MailManager(self.app).driver('base').to('idmann509@gmail.com')\ - .template('mail/welcome.html', {'to': 'MasoniteHTMLTesting'}, mimetype='html')\ - .template('mail/welcome.txt', {'to': 'MasoniteTextTesting'}, mimetype='plain') - self.assertIn(html_content, mail.html_content) - self.assertEqual(mail.message_body, mail.html_content) - self.assertEqual(mail.text_content, text_content) diff --git a/tests/core/test_mail_log_drivers.py b/tests/core/test_mail_log_drivers.py deleted file mode 100644 index 8be043b4d..000000000 --- a/tests/core/test_mail_log_drivers.py +++ /dev/null @@ -1,94 +0,0 @@ - -import os -import sys -import unittest -from contextlib import contextmanager - -from _io import StringIO -from src.masonite.app import App -from src.masonite.drivers import MailLogDriver, MailTerminalDriver -from src.masonite.managers.MailManager import MailManager -from src.masonite.view import View - - -@contextmanager -def captured_output(): - new_out, new_err = StringIO(), StringIO() - old_out, old_err = sys.stdout, sys.stderr - try: - sys.stdout, sys.stderr = new_out, new_err - yield sys.stdout, sys.stderr - finally: - sys.stdout, sys.stderr = old_out, old_err - - -class UserMock: - pass - - -class TestMailLogDrivers(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app = self.app.bind('Container', self.app) - - self.app.bind('Test', object) - viewClass = View(self.app) - self.app.bind('ViewClass', viewClass) - self.app.bind('View', viewClass.render) - self.app.bind('MailLogDriver', MailLogDriver) - self.app.bind('MailTerminalDriver', MailTerminalDriver) - - def test_log_driver(self): - user = UserMock - user.email = 'test@email.com' - - self.assertEqual(MailManager(self.app).driver('log').to(user).to_addresses, ['test@email.com']) - - def test_log_mail_renders_template(self): - - self.assertIn('MasoniteTesting', MailManager(self.app).driver('log').to( - 'idmann509@gmail.com').template('mail/welcome', {'to': 'MasoniteTesting'}).message_body) - - def test_terminal_driver(self): - user = UserMock - user.email = ['test@email.com'] - - self.assertEqual(MailManager(self.app).driver('terminal').to(user).to_addresses, ['test@email.com']) - - def test_terminal_mail_renders_template(self): - - self.assertIn('MasoniteTesting', MailManager(self.app).driver('terminal').to( - 'idmann509@gmail.com').template('mail/welcome', {'to': 'MasoniteTesting'}).message_body) - - def test_log_driver_output(self): - user = UserMock - user.email = 'test@email.com' - - MailManager(self.app).driver('log').to(user).reply_to('reply-to@email.com').send('Masonite') - - filepath = '{0}/{1}'.format('bootstrap/mail', 'mail.log') - self.logfile = open(filepath, 'r') - file_string = self.logfile.read() - - self.assertIn('test@email.com', file_string) - self.assertIn('reply-to@email.com', file_string) - - def test_terminal_driver_output(self): - user = UserMock - user.email = 'test@email.com' - with captured_output() as (_, err): - MailManager(self.app).driver('terminal').to(user).reply_to('reply-to@email.com').send('Masonite') - - # This can go inside or outside the `with` block - error = err.getvalue().strip() - self.assertIn('test@email.com', error) - self.assertIn('reply-to@email.com', error) - - def tearDown(self): - if hasattr(self, 'logfile') and self.logfile: - self.logfile.close() - - filepath = '{0}/{1}'.format('bootstrap/mail', 'mail.log') - if os.path.isfile(filepath): - os.remove(filepath) diff --git a/tests/core/test_mail_smtp_driver.py b/tests/core/test_mail_smtp_driver.py deleted file mode 100644 index ac5e2b313..000000000 --- a/tests/core/test_mail_smtp_driver.py +++ /dev/null @@ -1,150 +0,0 @@ -from email.mime.multipart import MIMEMultipart - -from src.masonite import env -from src.masonite.app import App -from src.masonite.drivers import MailMailgunDriver as Mailgun, Mailable -from src.masonite.drivers import MailSmtpDriver -from src.masonite.environment import LoadEnvironment -from src.masonite.managers.MailManager import MailManager -from src.masonite.view import View -import unittest -from src.masonite.testing import TestCase - -LoadEnvironment() - - -class UserMock: - pass - - -class MailSmtpTestDriver(MailSmtpDriver): - def _send_mail(self, mail_from_header, to_addresses, message): - return mail_from_header, to_addresses, message.as_string() - - def _smtp_connect(self): - pass - - -text_content = 'Hi MasoniteTextTesting\nWelcome to MasoniteFramework!' -html_content = '
MasoniteHTMLTesting
' - - -class TestSMTPDriver(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Container', self.app) - - self.app.bind('Test', object) - self.app.bind('MailSmtpDriver', MailSmtpTestDriver) - viewClass = View(self.app) - self.app.bind('ViewClass', viewClass) - self.app.bind('View', viewClass.render) - - def test_smtp_driver(self): - user = UserMock - user.email = 'test@email.com' - - self.assertEqual(MailManager(self.app).driver('smtp').to(user).to_addresses, ['test@email.com']) - self.assertEqual(MailManager(self.app).driver('smtp').reply_to('reply_to@email.com').message_reply_to , 'reply_to@email.com') - - def test_mail_text_content(self): - mail = MailManager(self.app).driver('smtp').to('idmann509@gmail.com').text(text_content) - self.assertEqual(mail.text_content, text_content) - self.assertEqual(mail.message_body, text_content) - self.assertEqual(mail.html_content, None) - _, _, message_as_string = mail.send() - self.assertIn('Content-Type: text/plain', message_as_string) - self.assertNotIn('Content-Type: text/html', message_as_string) - - def test_mail_html_content(self): - mail = MailManager(self.app).driver('smtp').to('idmann509@gmail.com').html(html_content) - self.assertEqual(html_content, mail.html_content) - self.assertEqual(mail.message_body, mail.html_content) - self.assertEqual(mail.text_content, None) - _, _, message_as_string = mail.send() - self.assertIn('Content-Type: text/html', message_as_string) - self.assertNotIn('Content-Type: text/plain', message_as_string) - - def test_mail_text_and_html_content(self): - mail = MailManager(self.app).driver('smtp').to('idmann509@gmail.com').text(text_content).html(html_content) - self.assertIn(html_content, mail.html_content) - self.assertEqual(mail.message_body, mail.html_content) - self.assertEqual(mail.text_content, text_content) - _, _, message_as_string = mail.send() - self.assertIn('Content-Type: text/plain', message_as_string) - self.assertIn('Content-Type: text/html', message_as_string) - - def test_modified_message(self): - mail = MailManager(self.app).driver('smtp').to('idmann509@gmail.com').text(text_content).html(html_content) - message = mail.message() - message['Bcc'] = 'cc@example.com' - _, _, message_as_string = mail.send(message) - self.assertIn('Bcc: cc@example.com\n', message_as_string) - - def test_custom_message(self): - mail = MailManager(self.app).driver('smtp') - message = MIMEMultipart('alternative') - message['From'] = 'a@example.com' - message['Cc'] = 'b@example.com' - message.add_header('X-My-Custom-Header', 'my custom value') - _, _, message_as_string = mail.to('user@example.com').text('test text').send(message) - self.assertIn('From: a@example.com\n', message_as_string) - self.assertIn('Cc: b@example.com\n', message_as_string) - self.assertIn('X-My-Custom-Header: my custom value\n', message_as_string) - self.assertNotIn('Subject:', message_as_string) - self.assertNotIn('test text', message_as_string) - - def _assert_deprecated_send_method(self, message_as_string, warning): - self.assertIn('
Foo
', message_as_string) - self.assertNotIn('My Text', message_as_string) - self.assertNotIn('My HTML', message_as_string) - self.assertIn('Content-Type: text/html', message_as_string) - self.assertNotIn('Content-Type: text/plain', message_as_string) - self.assertEqual(warning.warnings[0].message.args[0], - 'Passing message_contents to .send() is a deprecated. Please use .text() and .html().') - - def test_deprecated_send_method_using_positional_arg(self): - with self.assertWarns(DeprecationWarning) as dw: - mail = MailManager(self.app).driver('smtp') - _, _, message_as_string = mail.to('user@example.com').text('My Text').html('My HTML').send('
Foo
') - self._assert_deprecated_send_method(message_as_string, dw) - - def test_deprecated_send_method_using_named_arg(self): - with self.assertWarns(DeprecationWarning) as dw: - mail = MailManager(self.app).driver('smtp') - _, _, message_as_string = mail.to('user@example.com').text('My Text').html('My HTML').send(message_contents='
Foo
') - self._assert_deprecated_send_method(message_as_string, dw) - - def test_mail_sends_with_queue_and_without_queue(self): - if env('RUN_MAIL'): - self.assertEqual(MailManager(self.app).driver('smtp').to('idmann509@gmail.com').send('test queue'), None) - self.assertEqual(MailManager(self.app).driver('smtp').queue().to('idmann509@gmail.com').send('test queue'), None) - - -class TestMailable(TestCase): - - def setUp(self): - super().setUp() - pass - - def test_works(self): - mailable = MailManager(self.container).driver('smtp').mailable(ForgotPasswordMailable()) - self.assertEqual(mailable.to_addresses, ['idmann509@gmail.com']) - self.assertEqual(mailable.from_address, 'admin@test.com') - self.assertEqual(mailable.message_subject, 'Forgot Password') - self.assertEqual(mailable.message_body, 'testing email') - self.assertEqual(mailable.message_reply_to, 'customer@email.com') - self.assertTrue(True) - -class ForgotPasswordMailable(Mailable): - - def build(self): - return (self - .to('idmann509@gmail.com') - .send_from('admin@test.com') - .view('emails.test') - .reply_to('customer@email.com') - .subject('Forgot Password')) - - diff --git a/tests/core/test_mailgun_driver.py b/tests/core/test_mailgun_driver.py deleted file mode 100644 index ee987872b..000000000 --- a/tests/core/test_mailgun_driver.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import unittest - -from src.masonite import env -from src.masonite.app import App -from src.masonite.drivers import MailMailgunDriver as Mailgun -from src.masonite.environment import LoadEnvironment -from src.masonite.managers.MailManager import MailManager -from src.masonite.view import View - -LoadEnvironment() - - -class MailgunTestDriver(Mailgun): - def _send_mail(self, data): - return data - - -if os.getenv('MAILGUN_SECRET'): - - class UserMock: - pass - - class TestMailgunDriver(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Container', self.app) - self.app.bind('Test', object) - self.app.bind('MailMailgunDriver', MailgunTestDriver) - viewClass = View(self.app) - self.app.bind('ViewClass', viewClass) - self.app.bind('View', viewClass.render) - - def test_mailgun_driver(self): - user = UserMock - user.email = 'test@email.com' - - self.assertEqual(MailManager(self.app).driver('mailgun').to(user).to_addresses, ['test@email.com']) - self.assertEqual(MailManager(self.app).driver('mailgun').reply_to('reply_to@email.com').message_reply_to , 'reply_to@email.com') - - def test_mail_renders_template(self): - self.assertIn('MasoniteTesting', MailManager(self.app).driver('mailgun').to( - 'idmann509@gmail.com').template('mail/welcome', {'to': 'MasoniteTesting'}).message_body) - - def test_html_together_with_text_content(self): - data = MailManager(self.app).driver('mailgun').to('user@example.com').html('
Hello
').text('hello').send() - self.assertEqual(data['text'], 'hello') - self.assertEqual(data['html'], '
Hello
') - - def test_html_content_only(self): - data = MailManager(self.app).driver('mailgun').to('user@example.com').html('
Hello
').send() - self.assertNotIn('text', data) - self.assertEqual(data['html'], '
Hello
') - - def test_text_content_only(self): - data = MailManager(self.app).driver('mailgun').to('user@example.com').text('hello').send() - self.assertNotIn('html', data) - self.assertEqual(data['text'], 'hello') - - def test_passing_message_to_send(self): - data = MailManager(self.app).driver('mailgun').to('user@example.com').text('hello').html('
hello
').send('Foo') - self.assertNotIn('text', data) - self.assertEqual(data['html'], 'Foo') - - def test_modified_message(self): - mail = MailManager(self.app).driver('mailgun').to('user@example.com').text('test text') - data = mail.message() - data['o:tag'] = ['Foo', 'Bar'] - data = mail.send(data) - self.assertEqual(data['o:tag'], ['Foo', 'Bar']) - - def test_custom_message(self): - data = { - 'from': 'other@example.com', - 'to': 'brother@example.com', - 'subject': 'Custom Message', - 'text': 'Custom Text', - } - self.assertEqual(MailManager(self.app).driver('mailgun').to('user@example.com').text('test text').send(data), data) - - def _assert_deprecated_send_method(self, data, warning): - self.assertEqual(data['html'], '
Foo
') - self.assertNotIn('text', data) - self.assertEqual(warning.warnings[0].message.args[0], - 'Passing message to .send() is deprecated. Please use .text() and .html().') - - def test_deprecated_send_method_using_positional_arg(self): - with self.assertWarns(DeprecationWarning) as dw: - mail = MailManager(self.app).driver('mailgun') - data = mail.to('user@example.com').text('My Text').html('My HTML').send('
Foo
') - self._assert_deprecated_send_method(data, dw) - - def test_deprecated_send_method_using_named_arg(self): - with self.assertWarns(DeprecationWarning) as dw: - mail = MailManager(self.app).driver('mailgun') - data = mail.to('user@example.com').text('My Text').html('My HTML').send(message='
Foo
') - self._assert_deprecated_send_method(data, dw) - - def test_mail_sends_with_queue_and_without_queue(self): - if env('RUN_MAIL'): - self.assertEqual(MailManager(self.app).driver('mailgun').to('idmann509@gmail.com').send('test queue'), None) - self.assertEqual(MailManager(self.app).driver('mailgun').queue().to('idmann509@gmail.com').send('test queue'), None) diff --git a/tests/core/test_managers_mail_manager.py b/tests/core/test_managers_mail_manager.py deleted file mode 100644 index fbfc5a3b1..000000000 --- a/tests/core/test_managers_mail_manager.py +++ /dev/null @@ -1,127 +0,0 @@ -from src.masonite.environment import LoadEnvironment - -LoadEnvironment() - -from src.masonite.app import App -from src.masonite.contracts import MailManagerContract -from src.masonite.drivers import MailMailgunDriver as Mailgun -from src.masonite.drivers import MailSmtpDriver as MailDriver -from src.masonite.exceptions import DriverNotFound -from src.masonite.managers import MailManager -from src.masonite.view import View -from src.masonite.contracts import MailContract -from src.masonite import env -import unittest - - -class MailSmtpDriver: - - def __init__(self, Test=None): - self.test = Test - - def send(self, message): - return message - - -class User: - pass - - -class TestMailManager(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app = self.app.bind('Container', self.app) - - self.app.bind('Test', object) - self.app.bind('MailSmtpDriver', object) - self.app.bind('View', View(self.app).render) - self.app.bind('ViewClass', View(self.app)) - - def test_mail_manager_loads_container(self): - mailManager = MailManager(self.app) - self.assertTrue(mailManager.load_container(self.app)) - - def test_mail_manager_resolves_from_contract(self): - self.app.singleton('MailManager', MailManager) - self.assertEqual(self.app.resolve(self._test_resolve), self.app.make('MailManager')) - - def _test_resolve(self, mail: MailManagerContract): - return mail - - def test_creates_driver(self): - mailManager = MailManager(self.app) - - self.assertIsInstance(mailManager.manage_driver, object) - - def test_does_not_create_driver_with_initilization_container(self): - - mailManager = MailManager(self.app) - - self.assertEqual(mailManager.manage_driver, None) - - def test_does_not_raise_drivernotfound_exception(self): - MailManager(self.app) - - def test_manager_sets_driver(self): - self.app.bind('MailMailtrapDriver', Mailgun) - MailManager(self.app).driver('mailtrap') - - def test_manager_sets_driver_throws_driver_not_found_exception(self): - with self.assertRaises(DriverNotFound): - MailManager(self.app).driver('mailtrap') - - def test_drivers_are_resolvable_by_container(self): - self.app.bind('MailSmtpDriver', MailDriver) - - self.assertIsInstance(MailManager(self.app).driver('smtp'), MailDriver) - - def test_driver_loads_template(self): - self.app.bind('MailSmtpDriver', MailDriver) - - driver = MailManager(self.app).driver('smtp') - - self.assertEqual(driver.template('test', {'test': 'test'}).message_body, 'test') - - def test_send_mail(self): - self.app.bind('MailSmtpDriver', MailDriver) - self.assertTrue(MailManager(self.app).driver('smtp').to('idmann509@gmail.com')) - - def test_send_mail_with_from(self): - self.app.bind('MailSmtpDriver', MailDriver) - - self.assertEqual(MailManager(self.app).driver('smtp').to('idmann509@gmail.com').send_from('masonite@masonite.com').from_address, 'masonite@masonite.com') - - def test_send_mail_sends(self): - if env('RUN_MAIL'): - self.app.bind('MailSmtpDriver', MailDriver) - - self.assertTrue(MailManager(self.app).driver('smtp').to('idmann509@gmail.com').send('hi')) - - def test_send_mail_sends_with_queue(self): - if env('RUN_MAIL'): - self.app.bind('MailSmtpDriver', MailDriver) - - self.assertEqual(MailManager(self.app).driver('smtp').to('idmann509@gmail.com').queue().send('hi'), None) - - def test_send_mail_with_subject(self): - self.app.bind('MailSmtpDriver', MailDriver) - - self.assertEqual(MailManager(self.app).driver('smtp').to('').subject('test').message_subject, 'test') - - def test_send_mail_with_callable(self): - self.app.bind('MailSmtpDriver', MailDriver) - user = User - user.email = 'email@email.com' - self.assertTrue(MailManager(self.app).driver('smtp').to(User)) - - def test_switch_mail_manager(self): - self.app.bind('MailSmtpDriver', MailDriver) - self.app.bind('MailTestDriver', Mailgun) - - mail_driver = MailManager(self.app).driver('smtp') - - self.assertIsInstance(mail_driver.driver('test'), Mailgun) - - def test_mail_helper_method_resolves_a_driver(self): - self.assertIsInstance(mail_helper(), MailContract) diff --git a/tests/core/test_middleware.py b/tests/core/test_middleware.py deleted file mode 100644 index 7081a1d77..000000000 --- a/tests/core/test_middleware.py +++ /dev/null @@ -1,55 +0,0 @@ -from src.masonite.request import Request -from src.masonite.routes import Get -from app.http.middleware.TestMiddleware import TestMiddleware as MiddlewareTest -from app.http.middleware.TestHttpMiddleware import TestHttpMiddleware as MiddlewareHttpTest -from src.masonite.testing import TestCase - - -class MiddlewareValueTest: - - def __init__(self, request: Request): - self.request = request - - def before(self, value1, value2): - self.request.value1 = value1 - self.request.value2 = value2 - -class ParameterMiddleware: - - def __init__(self, request: Request): - self.request = request - - def before(self, value1): - self.request.value1 = value1 - - -class TestMiddleware(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[ - Get().route('/', 'TestController@show').middleware('test'), - Get().route('/test', 'TestController@show').middleware('throttle:1,2'), - Get().route('/test/@param', 'TestController@show').middleware('params:@param'), - ]) - - self.withRouteMiddleware({ - 'test': MiddlewareTest, - 'throttle': MiddlewareValueTest, - 'params': ParameterMiddleware, - }).withHttpMiddleware([MiddlewareHttpTest]) - - def test_route_middleware_runs(self): - self.get('/').assertPathIs('/test/middleware') - - def test_http_middleware_runs(self): - self.get('/').assertPathIs('/test/middleware') - self.assertEqual(self.get('/').request.environ['HTTP_TEST'], 'test') - - def test_route_middleware_can_pass_values(self): - self.assertEqual(self.get('/test').request.value1, '1') - self.assertEqual(self.get('/test').request.value2, '2') - - def test_route_middleware_get_parameters(self): - request = self.get('/test/slug').request - self.assertEqual(request.value1, 'slug') diff --git a/tests/core/test_package.py b/tests/core/test_package.py deleted file mode 100644 index 247be2530..000000000 --- a/tests/core/test_package.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import unittest - -from src.masonite.packages import (create_controller, create_or_append_config) - -PACKAGE_DIRECTORY = os.getcwd() - - -class TestPackage(unittest.TestCase): - - def test_create_config(self): - create_or_append_config(os.path.join(PACKAGE_DIRECTORY, 'testpackage/test-config.py')) - self.assertTrue(os.path.exists('config/test-config.py')) - os.remove('config/test-config.py') - - def test_append_config(self): - create_or_append_config(os.path.join(PACKAGE_DIRECTORY, 'testpackage/test-config.py')) - create_or_append_config(os.path.join(PACKAGE_DIRECTORY, 'testpackage/test-config.py')) - self.assertTrue(os.path.exists('config/test-config.py')) - with open(os.path.join(PACKAGE_DIRECTORY, 'config/test-config.py')) as f: - self.assertIn('ROUTES = []', f.read()) - os.remove('config/test-config.py') - - def test_create_controller(self): - create_controller(os.path.join( - PACKAGE_DIRECTORY, 'testpackage/test-config.py')) - self.assertTrue(os.path.exists('app/http/controllers/test-config.py')) - os.remove('app/http/controllers/test-config.py') diff --git a/tests/core/test_providers.py b/tests/core/test_providers.py deleted file mode 100644 index b717ed696..000000000 --- a/tests/core/test_providers.py +++ /dev/null @@ -1,29 +0,0 @@ - -from src.masonite.helpers import config -from src.masonite.routes import Get -from src.masonite.testing import TestCase, generate_wsgi - - -class TestProviders(TestCase): - - def setUp(self): - super().setUp() - self.container.bind('Environ', generate_wsgi()) - - def test_providers_load_into_container(self): - for provider in config('providers.providers'): - provider().load_app(self.container).register() - - self.container.bind('Response', 'test') - self.container.bind('WebRoutes', [ - Get().route('url', 'TestController@show'), - Get().route('url/', 'TestController@show'), - Get().route('url/@firstname', 'TestController@show'), - ]) - - self.container.bind('Response', 'Route not found. Error 404') - - for provider in config('providers.providers'): - self.container.resolve(provider().load_app(self.container).boot) - - self.assertTrue(self.container.make('Request')) diff --git a/tests/core/test_request_routes.py b/tests/core/test_request_routes.py deleted file mode 100644 index 4588d8734..000000000 --- a/tests/core/test_request_routes.py +++ /dev/null @@ -1,127 +0,0 @@ -from src.masonite.routes import Get, Post -from src.masonite.testing import TestCase, generate_wsgi - - -class TestRequestRoutes(TestCase): - - def setUp(self): - super().setUp() - self.request = self.container.make('Request').load_environ(generate_wsgi()).key( - 'NCTpkICMlTXie5te9nJniMj9aVbPM6lsjeq5iDZ0dqY=') - - self.request.activate_subdomains() - - def test_get_initialized(self): - self.assertTrue(callable(Get)) - self.assertTrue(callable(Post)) - - def test_get_sets_route(self): - self.assertTrue(Get().route('test', None)) - - def test_sets_name(self): - get = Get().route('test', None).name('test') - - self.assertEqual(get.named_route, 'test') - - def test_loads_request(self): - get = Get().route('test', None).name('test').load_request('test') - - self.assertEqual(get.request, 'test') - - def test_loads_middleware(self): - get = Get().route('test', None).middleware('auth', 'middleware') - - self.assertEqual(get.list_middleware, ['auth', 'middleware']) - - def test_method_type(self): - self.assertEqual(Post().method_type, ['POST']) - self.assertEqual(Get().method_type, ['GET']) - - def test_method_type_sets_domain(self): - get = Get().domain('test') - post = Post().domain('test') - - self.assertEqual(get.required_domain, 'test') - self.assertEqual(post.required_domain, 'test') - - def test_method_type_has_required_subdomain(self): - get = Get().domain('test') - post = Get().domain('test') - - self.request.environ['HTTP_HOST'] = 'test.localhost:8000' - - get.request = post.request = self.request - - self.assertEqual(get.has_required_domain(), True) - self.assertEqual(post.has_required_domain(), True) - - def test_method_type_has_required_subdomain_with_asterick(self): - - - self.request.environ['HTTP_HOST'] = 'test.localhost:8000' - - self.request.activate_subdomains() - - get = Get().domain('*') - post = Get().domain('*') - - get.request = self.request - post.request = self.request - - self.assertEqual(get.has_required_domain(), True) - self.assertEqual(post.has_required_domain(), True) - - def test_request_sets_subdomain_on_get(self): - - - self.request.environ['HTTP_HOST'] = 'test.localhost:8000' - - self.request.activate_subdomains() - - get = Get().domain('*') - post = Get().domain('*') - - get.request = self.request - post.request = self.request - - get.has_required_domain() - self.assertEqual(self.request.param('subdomain'), 'test') - - def test_route_changes_module_location(self): - get = Get().module('app.test') - self.assertEqual(get.module_location, 'app.test') - -class TestRequestSubdomains(TestCase): - - def setUp(self): - super().setUp() - wsgi = generate_wsgi() - wsgi.update({"HTTP_HOST": "a.localhost"}) - self.request = self.container.make('Request').load_environ(wsgi).key( - 'NCTpkICMlTXie5te9nJniMj9aVbPM6lsjeq5iDZ0dqY=') - - self.request.activate_subdomains() - - self.routes(only=[ - Get('/', DomainAController.show).domain('a'), - Get('/', DomainBController.show).domain('b'), - ]) - - def test_can_get_correct_domain(self): - self.withWSGIOverride({ - 'HTTP_HOST': 'a.localhost.com' - }).get('/').assertContains('A') - - self.withWSGIOverride({ - 'HTTP_HOST': 'b.localhost.com' - }).get('/').assertContains('B') - -class DomainAController: - - def show(self): - return 'A' - -class DomainBController: - - def show(self): - return 'B' \ No newline at end of file diff --git a/tests/core/test_requests.py b/tests/core/test_requests.py deleted file mode 100644 index e2b690fac..000000000 --- a/tests/core/test_requests.py +++ /dev/null @@ -1,861 +0,0 @@ -import unittest -from cgi import MiniFieldStorage - -import pytest - -from app.http.test_controllers.TestController import TestController -from src.masonite.app import App -from src.masonite.exceptions import InvalidHTTPStatusCode, RouteException -from src.masonite.helpers import config -from src.masonite.helpers.routes import flatten_routes -from src.masonite.helpers.time import cookie_expire_time -from src.masonite.request import Request -from src.masonite.response import Response -from src.masonite.routes import Get, Route, RouteGroup -from src.masonite.testing import generate_wsgi, MockWsgiInput - -WEB_ROUTES = flatten_routes([ - Get('/test', 'Controller@show').name('test'), - RouteGroup([ - Get('/account', 'Controller@show').name('a_account'), - ], prefix='/a') -]) - -wsgi_request = generate_wsgi() - - -class TestRequest(unittest.TestCase): - - def setUp(self): - self.app = App() - self.request = Request(wsgi_request.copy()).key( - 'NCTpkICMlTXie5te9nJniMj9aVbPM6lsjeq5iDZ0dqY=').load_app(self.app) - self.app.bind('Request', self.request) - self.response = Response(self.app) - self.app.bind(Response, self.response) - self.app.simple(self.app) - self.app.simple(Response) - - def test_request_is_callable(self): - """ Request should be callable """ - self.assertIsInstance(self.request, object) - - def test_request_input_should_return_input_on_get_request(self): - self.assertEqual(self.request.input('application'), 'Masonite') - self.assertEqual(self.request.input('application', 'foo'), 'Masonite') - - def test_request_input_should_return_default_when_not_exists(self): - self.assertEqual(self.request.input('foo', 'bar'), 'bar') - - def test_request_all_should_return_params(self): - self.assertEqual(self.request.all(), {'application': 'Masonite'}) - - def test_request_all_without_internal_request_variables(self): - self.request.request_variables.update({'__token': 'testing', 'application': 'Masonite'}) - self.assertEqual(self.request.all(), {'__token': 'testing', 'application': 'Masonite'}) - self.assertEqual(self.request.all(internal_variables=False), {'application': 'Masonite'}) - - def test_request_has_should_return_bool(self): - self.assertEqual(self.request.has('application'), True) - self.assertEqual(self.request.has('shouldreturnfalse'), False) - - def test_request_has_should_accept_multiple_values(self): - self.request.request_variables.update({'__token': 'testing', 'application': 'Masonite'}) - self.assertEqual(self.request.has('application'), True) - self.assertEqual(self.request.has('shouldreturnfalse'), False) - self.assertEqual(self.request.has('__token'), True) - self.assertEqual(self.request.has('__token', 'shouldreturnfalse'), False) - self.assertEqual(self.request.has('__token', 'application'), True) - self.assertEqual(self.request.has('__token', 'application', 'shouldreturnfalse'), False) - - def test_request_set_params_should_return_self(self): - self.assertEqual(self.request.set_params({'value': 'new'}), self.request) - self.assertEqual(self.request.url_params, {'value': 'new'}) - - def test_request_param_returns_parameter_set_or_false(self): - self.request.set_params({'value': 'new'}) - self.assertEqual(self.request.param('value'), 'new') - self.assertEqual(self.request.param('nullvalue'), False) - - def test_request_input_can_get_dictionary_elements(self): - self.request.request_variables = { - "user": { - "address": [ - {"id": 1, 'street': 'A Street'}, - {"id": 2, 'street': 'B Street'} - ] - } - } - self.assertEqual(self.request.input('user.address.*.id'), [1, 2]) - self.assertEqual(self.request.input('user.address.*.street'), ['A Street', 'B Street']) - - def test_request_input_parses_query_string(self): - query_string = "filter=name" - self.request._set_standardized_request_variables(query_string) - self.request._set_standardized_request_variables(query_string) - self.assertEqual(self.request.input('filter'), 'name') - - query_string = "filter=name&user=Joe" - self.request._set_standardized_request_variables(query_string) - self.assertEqual(self.request.input('filter'), 'name') - self.assertEqual(self.request.input('user'), 'Joe') - - query_string = "filter[name]=Joe&filter[email]=user@email.com" - self.request._set_standardized_request_variables(query_string) - self.assertEqual(self.request.input('filter')['name'], 'Joe') - self.assertEqual(self.request.input('filter.name'), 'Joe') - self.assertEqual(self.request.input('filter')['email'], 'user@email.com') - self.assertEqual(self.request.input('filter.email'), 'user@email.com') - - def test_request_sets_and_gets_cookies(self): - self.request.cookie('setcookie', 'value', encrypt=False) - self.assertEqual(self.request.get_cookie('setcookie', decrypt=False), 'value') - - def test_request_sets_expiration_cookie_2_months(self): - self.request.cookie('setcookie_expiration', 'value', expires='2 months') - - time = cookie_expire_time('2 months') - - self.assertEqual(self.request.get_cookie('setcookie_expiration'), 'value') - self.assertEqual(self.request.get_raw_cookie('setcookie_expiration').expires, time) - - def test_delete_cookie(self): - self.request.cookie('delete_cookie', 'value') - - self.assertEqual(self.request.get_cookie('delete_cookie'), 'value') - self.request.delete_cookie('delete_cookie') - self.assertTrue('Expires' in self.request.cookie_jar.render_response()[0][1]) - self.assertFalse(self.request.get_cookie('delete_cookie')) - - def test_delete_cookie_with_wrong_key(self): - self.request.cookie('cookie', 'value') - self.request.key('wrongkey_TXie5te9nJniMj9aVbPM6lsjeq5iDZ0dqY=') - self.assertIsNone(self.request.get_cookie('cookie')) - - def test_redirect_returns_request(self): - self.assertEqual(self.request.redirect('newurl'), self.request) - self.assertEqual(self.request.redirect_url, '/newurl') - - def test_request_no_input_returns_false(self): - self.assertEqual(self.request.input('notavailable'), False) - - def test_request_mini_field_storage_returns_single_value(self): - storages = {'test': [MiniFieldStorage('key', '1')]} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test'), '1') - - def test_request_can_get_string_value(self): - storages = {'test': 'value'} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test'), 'value') - - def test_request_can_get_list_value(self): - storages = {'test': ['foo', 'bar']} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test'), ['foo', 'bar']) - - def test_request_mini_field_storage_doesnt_return_brackets(self): - storages = {'test[]': [MiniFieldStorage('key', '1')]} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test'), '1') - - def test_request_mini_field_storage_index(self): - storages = {'test[index]': [MiniFieldStorage('key', '1')]} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test[index]'), '1') - - def test_request_mini_field_storage_with_dot_notation(self): - storages = {'test[index]': [MiniFieldStorage('key', '1')]} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test.index'), '1') - - def test_request_mini_field_storage_returns_a_list(self): - storages = {'test': [MiniFieldStorage( - 'key', '1'), MiniFieldStorage('key', '2')]} - self.request._set_standardized_request_variables(storages) - self.assertEqual(self.request.input('test'), ['1', '2']) - - def test_request_get_cookies_returns_cookies(self): - self.assertEqual(self.request.get_cookies(), self.request.cookie_jar) - - def test_request_set_user_sets_object(self): - self.assertEqual(self.request.set_user(object), self.request) - self.assertEqual(self.request.user_model, object) - self.assertEqual(self.request.user(), object) - - def test_request_loads_app(self): - app = App() - app.bind('Request', self.request) - app.make('Request').load_app(app) - - self.assertEqual(self.request.app(), app) - self.assertEqual(app.make('Request').app(), app) - - def test_request_gets_input_from_container(self): - container = App() - container.bind('WSGI', object) - container.bind('Environ', wsgi_request) - - for provider in config('providers.providers'): - provider().load_app(container).register() - - container.bind('Response', 'test') - container.bind('WebRoutes', [ - Get().route('url', 'TestController@show'), - Get().route('url/', 'TestController@show'), - Get().route('url/@firstname', 'TestController@show'), - ]) - - container.bind('Response', 'Route not found. Error 404') - - for provider in config('providers.providers'): - located_provider = provider().load_app(container) - - container.resolve(located_provider.boot) - - self.assertEqual(container.make('Request').input('application'), 'Masonite') - self.assertEqual(container.make('Request').all(), {'application': 'Masonite'}) - container.make('Request').environ['REQUEST_METHOD'] = 'POST' - self.assertEqual(container.make('Request').environ['REQUEST_METHOD'], 'POST') - self.assertEqual(container.make('Request').input('application'), 'Masonite') - - def test_redirections_reset(self): - self.app.bind('WebRoutes', WEB_ROUTES) - request = self.app.make('Request') - - request.redirect('test') - - self.assertEqual(request.redirect_url, '/test') - - request.reset_redirections() - - self.assertFalse(request.redirect_url) - - request.redirect_to('test') - - self.assertEqual(request.redirect_url, '/test') - - request.reset_redirections() - - self.assertFalse(request.redirect_url) - - def test_redirect_to_throws_exception_when_no_routes_found(self): - self.app.bind('WebRoutes', WEB_ROUTES) - request = self.app.make('Request') - - request.redirect_to('test') - request.redirect(name='test') - - with pytest.raises(RouteException): - request.redirect_to('notavailable') - - with pytest.raises(RouteException): - request.redirect(name='notavailable') - - def test_request_has_subdomain_returns_bool(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - self.assertFalse(request.has_subdomain()) - self.assertIsNone(request.subdomain) - - request.environ['HTTP_HOST'] = 'test.localhost.com' - - request.header('TEST', 'set_this') - self.assertEqual(request.header('TEST'), 'set_this') - - request.header('TEST', 'set_this') - self.assertEqual(request.header('HTTP_TEST'), 'set_this') - - def test_redirect_compiles_url(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = '/test/url' - - self.assertEqual(request.compile_route_to_url(route), '/test/url') - - def test_redirect_compiles_url_with_1_slash(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = '/' - - self.assertEqual(request.compile_route_to_url(route), '/') - - def test_request_route_returns_url(self): - app = App() - app.bind('Request', self.request) - app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'TestController@show').name('test.id') - ]) - request = app.make('Request').load_app(app) - - self.assertEqual(request.route('test.url'), '/test/url') - self.assertEqual(request.route('test.id', {'id': 1}), '/test/url/1') - self.assertEqual(request.route('test.id', [1]), '/test/url/1') - - with self.assertRaises(RouteException): - self.assertTrue(request.route('not.exists', [1])) - - def test_request_route_returns_url_without_passing_args_with_current_param(self): - app = App() - app.bind('Request', self.request) - app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'TestController@show').name('test.id') - ]) - request = app.make('Request').load_app(app) - request.url_params = {'id': 1} - - assert request.route('test.id') == '/test/url/1' - - def test_request_redirection(self): - self.app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'TestController@testing').name('test.id'), - Get('/test/url/object', TestController.show).name('test.object') - ]) - - request = self.app.make('Request') - - self.assertEqual(request.redirect('/test/url/@id', {'id': 1}).redirect_url, '/test/url/1') - request.redirect_url = None - self.assertEqual(request.redirect(name='test.url').redirect_url, '/test/url') - request.redirect_url = None - self.assertEqual(request.redirect(name='test.id', params={'id': 1}).redirect_url, '/test/url/1') - request.redirect_url = None - self.assertEqual(request.redirect(controller='TestController@show').redirect_url, '/test/url') - request.redirect_url = None - self.assertEqual(request.redirect(controller=TestController.show).redirect_url, '/test/url/object') - request.redirect_url = None - self.assertEqual(request.redirect('some/url').redirect_url, '/some/url') - request.redirect_url = None - self.assertEqual(request.redirect(url='/some/url?with=querystring').redirect_url, '/some/url?with=querystring') - - def test_request_route_returns_full_url(self): - app = App() - app.bind('Request', self.request) - app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'TestController@show').name('test.id') - ]) - request = app.make('Request').load_app(app) - - self.assertEqual(request.route('test.url', full=True), 'http://localhost:8000/test/url') - - def test_redirect_compiles_url_with_multiple_slashes(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = 'test/url/here' - - self.assertEqual(request.compile_route_to_url(route), '/test/url/here') - - def test_redirect_compiles_url_with_trailing_slash(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = 'test/url/here/' - - self.assertEqual(request.compile_route_to_url(route), '/test/url/here/') - - def test_redirect_compiles_url_with_parameters(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = 'test/@id' - params = { - 'id': '1', - } - - self.assertEqual(request.compile_route_to_url(route, params), '/test/1') - - def test_redirect_compiles_url_with_list_parameters(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = 'test/@id' - params = ['1'] - - self.assertEqual(request.compile_route_to_url(route, params), '/test/1') - - route = 'test/@id/@user/test/@slug' - params = ['1', '2', '3'] - - self.assertEqual(request.compile_route_to_url(route, params), '/test/1/2/test/3') - - def test_redirect_compiles_url_with_multiple_parameters(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = 'test/@id/@test' - params = { - 'id': '1', - 'test': 'user', - } - self.assertEqual(request.compile_route_to_url(route, params), '/test/1/user') - - def test_request_compiles_custom_route_compiler(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = 'test/@id:signed' - params = { - 'id': '1', - } - self.assertEqual(request.compile_route_to_url(route, params), '/test/1') - - def test_redirect_compiles_url_with_http(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - route = "http://google.com" - - self.assertEqual(request.compile_route_to_url(route), 'http://google.com') - - def test_can_get_nully_value(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request._set_standardized_request_variables({ - "gateway": "RENDIMENTO", - "request": { - "user": "data" - }, - "response": None, - "description": "test only" - }) - - def test_can_get_nully_value_with_dictdot(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request._set_standardized_request_variables({ - "gateway": "RENDIMENTO", - "request": { - "user": "data", - "age": None, - }, - "response": None, - "description": "test only" - }) - - self.assertEqual(request.input('request.age'), None) - self.assertEqual(request.input('request.age', default=1), None) - self.assertEqual(request.input('request.salary', default=1), 1) - - def test_can_get_list_as_root_payload(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request._set_standardized_request_variables([{"key": "val"}, {"item2": "val2"}]) - - self.assertEqual(request.input(0)['key'], 'val') - self.assertEqual(request.input('0')['key'], 'val') - self.assertEqual(request.input(2), False) - - def test_can_get_list_as_root_payload_getting_all(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request._set_standardized_request_variables([{"key": "val"}, {"item2": "val2"}]) - - self.assertIsInstance(request.all(), list) - self.assertEqual(request.all()[0]['key'], 'val') - - def test_can_get_list_as_root_payload_as_dot_notation(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request._set_standardized_request_variables([{"key": "val"}, {"item2": "val2", "inner": {"value": "innervalue"}}, {"item3": [1, 2]}]) - - self.assertEqual(request.input('0.key'), 'val') - self.assertEqual(request.input('1.item2'), 'val2') - self.assertEqual(request.input('1.inner.value'), 'innervalue') - self.assertEqual(request.input('2.item3.0'), 1) - self.assertEqual(request.input('3.item3'), False) - - def test_list_as_root_payload_reset_between_requests(self): - app = App() - wsgi_environ = generate_wsgi() - wsgi_environ['REQUEST_METHOD'] = 'POST' - wsgi_environ['CONTENT_TYPE'] = 'application/json' - - route_class = Route() - request_class = Request() - app.bind('Request', request_class) - app.bind('Route', route_class) - route = app.make('Route') - request = app.make('Request').load_app(app) - - wsgi_environ['wsgi.input'] = MockWsgiInput('[1, 2]') - route.load_environ(wsgi_environ) - request.load_environ(wsgi_environ) - self.assertEqual(request.all(), [1, 2]) - - wsgi_environ['wsgi.input'] = MockWsgiInput('{"key": "val"}') - route.load_environ(wsgi_environ) - request.load_environ(wsgi_environ) - self.assertEqual(request.all(), {"key": "val"}) - - def test_request_gets_correct_header(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - self.assertEqual(request.header('HTTP_UPGRADE_INSECURE_REQUESTS'), '1') - # self.assertEqual(request.header('RAW_URI'), '/') - self.assertEqual(request.header('NOT_IN'), '') - self.assertFalse('text/html' in request.header('NOT_IN')) - - def test_request_sets_correct_header(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request.header('TEST', 'set_this') - self.assertEqual(request.header('TEST'), 'set_this') - - request.header('TEST', 'set_this') - self.assertEqual(request.header('HTTP_TEST'), 'set_this') - - def test_request_cant_set_multiple_headers(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request.header('TEST', 'test_this') - request.header('TEST', 'test_that') - - self.assertEqual(request.header('TEST'), 'test_that') - - def test_request_sets_headers_with_dictionary(self): - app = App() - app.bind('Request', self.request) - request = app.make('Request').load_app(app) - - request.header({ - 'test_dict': 'test_value', - 'test_dict1': 'test_value1' - }) - - self.assertEqual(request.header('test_dict'), 'test_value') - self.assertEqual(request.header('test_dict1'), 'test_value1') - - request.header({ - 'test_dict': 'test_value', - 'test_dict1': 'test_value1' - }) - - self.assertEqual(request.header('HTTP_test_dict'), 'test_value') - self.assertEqual(request.header('HTTP_test_dict1'), 'test_value1') - - def test_request_gets_all_headers(self): - - request = self.app.make('Request') - - request.header('TEST1', 'set_this_item') - self.assertEqual(request.get_headers(), [('Host', '127.0.0.1:8000'), ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), ('Upgrade-Insecure-Requests', '1'), ('Cookie', 'setcookie=value'), ('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7'), ('Accept-Language', 'en-us'), ('Accept-Encoding', 'gzip, deflate'), ('Connection', 'keep-alive'), ('Test1', 'set_this_item')]) - - def test_request_sets_str_status_code(self): - response = self.app.make(Response) - - response.status('200 OK') - self.assertEqual(response.get_status_code(), '200 OK') - - def test_request_sets_int_status_code(self): - - response = self.app.make(Response) - - response.status(500) - self.assertEqual(response.get_status_code(), '500 Internal Server Error') - - def test_request_gets_int_status(self): - response = self.app.make(Response) - - response.status(500) - self.assertEqual(response.get_status(), 500) - - def test_can_get_code_by_value(self): - response = self.app.make(Response) - - response.status(500) - self.assertEqual(response._get_status_code_by_value('500 Internal Server Error'), 500) - - def test_is_status_code(self): - response = self.app.make(Response) - - response.status(500) - self.assertEqual(response.is_status(500), True) - - def test_request_sets_invalid_int_status_code(self): - with self.assertRaises(InvalidHTTPStatusCode): - response = self.app.make(Response) - - response.status(600) - - def test_request_sets_request_method(self): - wsgi = generate_wsgi() - wsgi['QUERY_STRING'] = '__method=PUT' - request = Request(wsgi) - - assert request.has('__method') - self.assertEqual(request.input('__method'), 'PUT') - self.assertEqual(request.get_request_method(), 'PUT') - - def test_request_has_should_pop_variables_from_input(self): - self.request.request_variables.update({'key1': 'test', 'key2': 'test'}) - self.request.pop('key1', 'key2') - self.assertEqual(self.request.request_variables, {'application': 'Masonite'}) - self.request.pop('shouldnotexist') - self.assertEqual(self.request.request_variables, {'application': 'Masonite'}) - self.request.pop('application') - self.assertEqual(self.request.request_variables, {}) - - def test_is_named_route(self): - app = App() - app.bind('Request', self.request) - app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'TestController@show').name('test.id') - ]) - request = app.make('Request').load_app(app) - - request.path = '/test/url' - assert request.is_named_route('test.url') - - request.path = '/test/url/1' - assert request.is_named_route('test.id', {'id': 1}) - - def test_route_exists(self): - app = App() - app.bind('Request', self.request) - app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'TestController@show').name('test.id') - ]) - request = app.make('Request').load_app(app) - - self.assertEqual(request.route_exists('/test/url'), True) - self.assertEqual(request.route_exists('/test/Not'), False) - - def test_request_url_from_controller(self): - app = App() - app.bind('Request', self.request) - app.bind('WebRoutes', [ - Get('/test/url', 'TestController@show').name('test.url'), - Get('/test/url/@id', 'ControllerTest@show').name('test.id'), - Get('/test/url/controller/@id', TestController.show).name('test.controller'), - ]) - - request = app.make('Request').load_app(app) - - self.assertEqual(request.url_from_controller('TestController@show'), '/test/url') - self.assertEqual(request.url_from_controller('ControllerTest@show', {'id': 1}), '/test/url/1') - self.assertEqual(request.url_from_controller(TestController.show, {'id': 1}), '/test/url/controller/1') - - def test_contains_for_path_detection(self): - self.request.path = '/test/path' - self.assertTrue(self.request.contains('/test/*')) - self.assertTrue(self.request.contains('/test/path')) - self.assertFalse(self.request.contains('/test/wrong')) - - def test_contains_for_multiple_paths(self): - self.request.path = '/test/path/5' - self.assertTrue(self.request.contains('/test/*')) - - def test_contains_can_return_string(self): - self.request.path = '/test/path/5' - self.assertEqual(self.request.contains('/test/*', show='active'), 'active') - self.assertEqual(self.request.contains('/test/not', show='active'), '') - - def test_contains_for_path_with_digit(self): - self.request.path = '/test/path/1' - self.assertTrue(self.request.contains('/test/path/*')) - self.assertTrue(self.request.contains('/test/path/*:int')) - - def test_contains_for_path_with_digit_and_wrong_contains(self): - self.request.path = '/test/path/joe' - self.assertFalse(self.request.contains('/test/path/*:int')) - - def test_contains_for_path_with_alpha_contains(self): - self.request.path = '/test/path/joe' - self.assertTrue(self.request.contains('/test/path/*:string')) - - def test_contains_for_route_compilers(self): - self.request.path = '/test/path/joe' - self.assertTrue(self.request.contains('/test/path/*:signed')) - - def test_contains_multiple_asteriks(self): - self.request.path = '/dashboard/user/edit/1' - self.assertTrue(self.request.contains('/dashboard/user/*:string/*:int')) - - def test_request_can_get_input_as_properties(self): - self.request.request_variables = {'test': 'hey'} - self.assertEqual(self.request.test, 'hey') - self.assertEqual(self.request.input('test'), 'hey') - - def test_request_can_get_param_as_properties(self): - self.request.url_params = {'test': 'hey'} - self.assertEqual(self.request.test, 'hey') - self.assertEqual(self.request.param('test'), 'hey') - - def test_back_returns_correct_url(self): - self.request.path = '/dashboard/create' - self.request.back() - self.assertEqual(self.request.redirect_url, '/dashboard/create') - - self.request.back(default='/home') - self.assertEqual(self.request.redirect_url, '/home') - - self.request.request_variables = {'__back': '/login'} - self.request.back(default='/home') - self.assertEqual(self.request.redirect_url, '/login') - - def test_request_without(self): - self.request.request_variables.update({'__token': 'testing', 'application': 'Masonite'}) - self.assertEqual(self.request.without('__token'), {'application': 'Masonite'}) - - def test_request_only_returns_specified_values(self): - self.request.request_variables.update({'__token': 'testing', 'application': 'Masonite'}) - self.assertEqual(self.request.only('application'), {'application': 'Masonite'}) - self.assertEqual(self.request.only('__token'), {'__token': 'testing'}) - - def test_request_gets_only_clean_output(self): - self.request._set_standardized_request_variables({'key': '">', 'test': "awesome! 'this is a test'"}) - self.assertEqual(self.request.input('key', clean=True), '<img """><script>alert('hey')</script>">') - self.assertEqual(self.request.input('key', clean=False), '">') - self.assertEqual(self.request.input('test', clean=True, quote=False), "awesome! 'this is a test'") - - def test_request_cleans_all_optionally(self): - self.request._set_standardized_request_variables({'key': '">', 'test': "awesome! 'this is a test'"}) - self.assertEqual(self.request.all()['key'], '<img """><script>alert('hey')</script>">') - self.assertEqual(self.request.all(clean=False)['key'], '">') - self.assertEqual(self.request.all(quote=False)['test'], "awesome! 'this is a test'") - - def test_request_gets_input_with_dotdict(self): - self.request.request_variables = { - "key": { - "user": "1", - "name": "Joe", - "address": { - "street": "address 1" - } - } - } - - self.assertEqual(self.request.input('key')['address']['street'], 'address 1') - self.assertEqual(self.request.input('key.address.street'), 'address 1') - self.assertEqual(self.request.input('key.'), False) - self.assertEqual(self.request.input('key.user'), '1') - self.assertEqual(self.request.input('key.nothing'), False) - self.assertEqual(self.request.input('key.nothing', default='test'), 'test') - - def test_request_scheme(self): - self.request.environ['wsgi.url_scheme'] = 'http' - self.assertEqual(self.request.scheme(), 'http') - self.request.environ['wsgi.url_scheme'] = 'https' - self.assertEqual(self.request.scheme(), 'https') - - def test_request_host(self): - self.request.environ.update({'HTTP_HOST': 'www.masonite.com:80', 'SERVER_NAME': '127.0.0.1'}) - self.assertEqual(self.request.host(), 'www.masonite.com') - - self.request.environ.update({'HTTP_HOST': 'www.masonite.com:8000'}) - self.assertEqual(self.request.host(), 'www.masonite.com') - - self.request.environ.update({'HTTP_HOST': ''}) - self.assertEqual(self.request.host(), '127.0.0.1') - - def test_request_port(self): - self.request.environ.update({'SERVER_PORT': '443'}) - self.assertEqual(self.request.port(), '443') - - self.request.environ.update({'SERVER_PORT': '8000'}) - self.assertEqual(self.request.port(), '8000') - - def test_request_path(self): - def update_environ(**kwargs): - environ = self.request.environ - environ.update(kwargs) - self.request.load_environ(environ) - - update_environ(**{'SCRIPT_NAME': '', 'PATH_INFO': '/root/masonite'}) - self.assertEqual(self.request.full_path(), '/root/masonite') - self.assertEqual(self.request.path, '/root/masonite') - - update_environ(**{'SCRIPT_NAME': '/application', 'PATH_INFO': '/root/masonite'}) - self.assertEqual(self.request.full_path(), '/application/root/masonite') - self.assertEqual(self.request.path, '/root/masonite') - - update_environ(**{'SCRIPT_NAME': '/application', 'PATH_INFO': '/'}) - self.assertEqual(self.request.full_path(), '/application/') - self.assertEqual(self.request.path, '/') - - update_environ(**{'SCRIPT_NAME': '/application', 'PATH_INFO': '/masonite/hello world/'}) - self.assertEqual(self.request.full_path(), '/application/masonite/hello%20world/') - self.assertEqual(self.request.full_path(quoted=False), '/application/masonite/hello world/') - - def test_request_query_string(self): - self.assertEqual(self.request.query_string(), 'application=Masonite') - - def test_url(self): - self.request.environ.update({ - 'wsgi.url_scheme': 'https', - 'HTTP_HOST': 'www.masonite.com:443', - 'SERVER_PORT': '443', - 'SERVER_NAME': '127.0.0.1', - 'SCRIPT_NAME': '/application', - 'PATH_INFO': '/root/masonite is the best', - 'QUERY_STRING': 'Hello World', - }) - self.assertEqual(self.request.url(), 'https://www.masonite.com/application/root/masonite%20is%20the%20best') - self.assertEqual(self.request.url(include_standard_port=True), - 'https://www.masonite.com:443/application/root/masonite%20is%20the%20best') - - self.request.environ.update({ - 'HTTP_HOST': 'www.masonite.com:80', - 'SERVER_PORT': '80', - }) - self.assertEqual(self.request.url(), 'https://www.masonite.com:80/application/root/masonite%20is%20the%20best') - - self.request.environ['wsgi.url_scheme'] = 'http' - self.assertEqual(self.request.url(), 'http://www.masonite.com/application/root/masonite%20is%20the%20best') - - del self.request.environ['HTTP_HOST'] - self.assertEqual(self.request.url(), 'http://127.0.0.1/application/root/masonite%20is%20the%20best') - - self.request.environ['wsgi.url_scheme'] = 'https' - self.assertEqual(self.request.url(), 'https://127.0.0.1:80/application/root/masonite%20is%20the%20best') - - def test_full_url(self): - self.request.environ.update({ - 'wsgi.url_scheme': 'https', - 'HTTP_HOST': 'www.masonite.com:443', - 'SERVER_PORT': '443', - 'SERVER_NAME': '127.0.0.1', - 'SCRIPT_NAME': '/application', - 'PATH_INFO': '/root/masonite is the best', - 'QUERY_STRING': 'q=Hello&var=val', - }) - self.assertEqual( - self.request.full_url(), - 'https://www.masonite.com/application/root/masonite%20is%20the%20best?q=Hello&var=val' - ) diff --git a/tests/core/test_responsable.py b/tests/core/test_responsable.py deleted file mode 100644 index c485bcf59..000000000 --- a/tests/core/test_responsable.py +++ /dev/null @@ -1,14 +0,0 @@ -from src.masonite.testing import TestCase -from src.masonite.routes import Get - - -class TestResponsable(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[ - Get('/test/mail', 'TestController@mail') - ]) - - def test_mail_can_respond(self): - self.assertTrue(self.get('/test/mail').contains('mail')) diff --git a/tests/core/test_response.py b/tests/core/test_response.py deleted file mode 100644 index a3e5e2b9e..000000000 --- a/tests/core/test_response.py +++ /dev/null @@ -1,234 +0,0 @@ - -from masoniteorm.collection import Collection -from masoniteorm.models import Model - -from app.http.controllers.TestController import \ - TestController as ControllerTest -from app.User import User -from config.factories import factory -from src.masonite.response import Response -from src.masonite.request import Request -from src.masonite.routes import Get -from src.masonite.testing import TestCase -from src.masonite.view import View -from src.masonite.testing import generate_wsgi - - -class MockUser(Model): - __table__ = 'users' - - def all(self): - return Collection([ - {'name': 'TestUser', 'email': 'user@email.com'}, - {'name': 'TestUser', 'email': 'user@email.com'} - ]) - - def find(self, _): - self.name = 'TestUser' - self.email = 'user@email.com' - return self - -class MockController: - - def test_json(self, response: Response): - return response.json({'test': 'value'}) - - def redirect(self, response: Response): - return response.redirect('/some/test') - - def view(self, view: View): - return view.render('test', {'test': 'test'}) - - def response_int(self, response: Response): - return response.view(1) - - def all_users(self): - return MockUser().all() - - def paginate(self): - return MockUser.paginate(10) - - def single(self): - return User.find(1) - - def length_aware(self): - return LengthAwarePaginator(User.find(1), 1, 10) - - def change_response(self): - return 'created', 201 - - -class TestResponse(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[ - Get('/json', MockController.test_json), - Get('/redirect', MockController.redirect), - Get('/change/header', ControllerTest.change_header), - Get('/view', MockController.view), - Get('/int', MockController.response_int), - Get('/404', ControllerTest.change_404), - Get('/change/status', ControllerTest.change_status), - Get('/users', MockController.all_users), - Get('/paginate', MockController.paginate), - Get('/single', MockController.single), - Get('/length_aware', MockController.length_aware), - Get('/change/response', MockController.change_response), - ]) - - def setUpFactories(self): - factory(User, 50).create() - - def test_can_set_json(self): - ( - self.json('GET', '/json') - .assertIsStatus(200) - .assertHeaderIs('Content-Length', 17) - # .assertHeaderIs('Content-Type', 'application/json; charset=utf-8') - ) - - def test_redirect(self): - ( - self.get('/redirect') - .assertHeaderIs('Location', '/some/test') - .assertIsStatus(302) - ) - - def test_response_does_not_override_header_from_controller(self): - ( - self.get('/change/header') - .assertHeaderIs('Content-Type', 'application/xml') - ) - - def test_can_set_response_header(self): - self.container.bind('Request', Request(generate_wsgi())) - response = Response(self.container) - - response.header('Content-Length', 100) - - self.assertEqual(response.header('Content-Length').value, 100) - - response.header({'Content-Type': 100}) - - self.assertEqual(response.header('Content-Type').value, 100) - - - - def test_view(self): - ( - self.get('/view') - .assertContains('test') - .assertIsStatus(200) - ) - - def test_view_can_return_integer_as_string(self): - ( - self.get('/int') - .assertContains('1') - .assertIsStatus(200) - ) - - def test_view_can_set_own_status_code_to_404(self): - ( - self.get('/404') - .assertNotFound() - ) - - def test_view_can_set_own_status_code(self): - ( - self.get('/change/status') - .assertIsStatus(203) - ) - - def test_view_should_return_a_json_response_when_retrieve_a_user_from_model(self): - ( - self.json('GET', '/users') - .assertCount(2) - .assertJsonContains('name', 'TestUser') - .assertJsonContains('email', 'user@email.com') - ) - - - # def test_view_should_return_a_json_response_when_returning_length_aware_paginator_instance(self): - - # users = User.all() - - # # Page 1 - # ( - # self.get('/paginate') - # .assertHasJson('total', len(users)) - # .assertHasJson('count', 10) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 1) - # .assertHasJson('from', 1) - # .assertHasJson('to', 10) - # ) - - # # Page 2 - # ( - # self.get('/paginate', {'page': 2}) - # .assertHasJson('total', len(users)) - # .assertHasJson('count', 10) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 2) - # .assertHasJson('from', 11) - # .assertHasJson('to', 20) - # ) - - # factory(User).create() - # ( - # self.get('/length_aware') - # .assertHasJson('total', 1) - # .assertHasJson('count', 1) - # ) - - - # def test_view_should_return_a_json_response_when_returning_paginator_instance(self): - - # ( - # self.get('/paginator') - # .assertHasJson('count', 10) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 1) - # .assertHasJson('from', 1) - # .assertHasJson('to', 10) - # ) - - - # def test_can_correct_incorrect_pagination_page(self): - # users = User.all() - # ( - # self.get('/paginator') - # .assertHasJson('count', 10) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 1) - # .assertHasJson('from', 1) - # .assertHasJson('to', 10) - # ) - - # (self.get('/paginate', {'page': 'hey', 'page_size': 'hey'}) - # .assertHasJson('total', len(users)) - # .assertHasJson('count', 10) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 1) - # .assertHasJson('from', 1) - # .assertHasJson('to', 10)) - - # (self.get('/length_aware', {'page': 'hey', 'page_size': 'hey'}) - # # .assertHasJson('total', len(User.find(1))) - # # .assertHasJson('count', len(User.find(1))) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 1) - # .assertHasJson('from', 1) - # .assertHasJson('to', 10)) - - # (self.get('/single_paginator', {'page': 'hey', 'page_size': 'hey'}) - # .assertHasJson('count', 1) - # .assertHasJson('per_page', 10) - # .assertHasJson('current_page', 1) - # .assertHasJson('from', 1) - # .assertHasJson('to', 10)) - - def test_can_change_response_when_returning_tuple(self): - self.json('GET', '/change/response').assertContains('created').assertIsStatus(201) diff --git a/tests/core/test_routes.py b/tests/core/test_routes.py deleted file mode 100644 index 698c4ffbf..000000000 --- a/tests/core/test_routes.py +++ /dev/null @@ -1,353 +0,0 @@ - -import unittest - -from app.http.controllers.subdirectory.SubController import SubController -from app.http.controllers.subdirectory.deep.DeepController import DeepController -from src.masonite.app import App -from src.masonite.exceptions import (InvalidRouteCompileException, - RouteException) -from src.masonite.helpers.routes import create_matchurl, flatten_routes -from src.masonite.request import Request -from src.masonite.response import Response -from src.masonite.routes import (Connect, Delete, Get, Head, Match, Options, - Patch, Post, Put, Resource, Redirect, Route, RouteGroup, - Trace) -from src.masonite.testing import TestCase, generate_wsgi -from src.masonite.exceptions import RouteNotFoundException - -class TestRoutes(TestCase): - - def setUp(self): - super().setUp() - self.route = Route(generate_wsgi()) - - def test_route_is_callable(self): - self.assertTrue(callable(Get)) - self.assertTrue(callable(Head)) - self.assertTrue(callable(Post)) - self.assertTrue(callable(Match)) - self.assertTrue(callable(Put)) - self.assertTrue(callable(Patch)) - self.assertTrue(callable(Delete)) - self.assertTrue(callable(Connect)) - self.assertTrue(callable(Options)) - self.assertTrue(callable(Trace)) - - def test_route_prefixes_forward_slash(self): - self.assertEqual(Get('some/url', 'TestController@show').route_url, '/some/url') - - def test_route_is_not_post(self): - self.assertEqual(self.route.is_post(), False) - - def test_route_is_post(self): - self.route.environ['REQUEST_METHOD'] = 'POST' - self.assertEqual(self.route.is_post(), True) - - def test_compile_route_to_regex(self): - get_route = Get('test/route', '') - self.assertEqual(get_route.compile_route_to_regex(), r'^\/test\/route\/$') - - get_route = Get('test/@route', '') - self.assertEqual(get_route.compile_route_to_regex(), r'^\/test\/([\w.-]+)\/$') - - get_route = Get('test/@route:int', '') - self.assertEqual(get_route.compile_route_to_regex(), r'^\/test\/(\d+)\/$') - - get_route = Get('test/@route:string', '') - self.assertEqual(get_route.compile_route_to_regex(), r'^\/test\/([a-zA-Z]+)\/$') - - def test_route_can_add_compilers(self): - get_route = Get('test/@route:int', 'None') - self.assertEqual(get_route.compile_route_to_regex(), r'^\/test\/(\d+)\/$') - - self.route.compile('year', r'[0-9]{4}') - - get_route = Get('test/@route:year', '') - - self.assertEqual(get_route.compile_route_to_regex(), r'^\/test\/[0-9]{4}\/$') - - with self.assertRaises(InvalidRouteCompileException): - get_route = Get().route('test/@route:none', None) - get_route.request = self.container.make('Request') - create_matchurl('/test/1', get_route) - - def test_route_can_add_compilers_inside_route_group(self): - self.route.compile('year', r'[0-9]{4}') - group = RouteGroup([ - Get().route('/@route:year', 'TestController@show') - ], prefix="/test") - - self.assertEqual(group[0].compile_route_to_regex(), r'^\/test\/[0-9]{4}\/$') - - with self.assertRaises(InvalidRouteCompileException): - get_route = Get().route('test/@route:none', None) - get_route.request = self.container.make('Request') - create_matchurl('/test/1', get_route) - - def test_route_gets_controllers(self): - self.assertTrue(Get('test/url', 'TestController@show')) - self.assertTrue(Get('test/url', '/app.http.test_controllers.TestController@show')) - - def test_route_doesnt_break_on_incorrect_controller(self): - self.assertTrue(Get('test/url', 'BreakController@show')) - - - def test_route_can_pass_route_values_in_constructor_and_use_middleware(self): - route = Get('test/url', 'BreakController@show').middleware('auth') - self.assertEqual(route.route_url, '/test/url') - self.assertEqual(route.list_middleware, ['auth']) - - def test_route_gets_deeper_module_controller(self): - route = Get().route('test/url', 'subdirectory.SubController@show') - self.assertTrue(route.controller) - self.assertIsInstance(route.controller, SubController.__class__) - - def test_route_can_have_multiple_routes(self): - self.assertEqual(Match(['GET', 'POST']).route('test/url', 'TestController@show').method_type, ['GET', 'POST']) - - def test_match_routes_convert_lowercase_to_uppercase(self): - self.assertEqual(Match(['Get', 'Post']).route('test/url', 'TestController@show').method_type, ['GET', 'POST']) - - def test_match_routes_raises_exception_with_non_list_method_types(self): - with self.assertRaises(RouteException): - self.assertEqual(Match('get').route('test/url', 'TestController@show').method_type, ['GET', 'POST']) - - def test_group_route(self): - routes = RouteGroup([ - Get('/test/1', 'TestController@show'), - Get('/test/2', 'TestController@show') - ], prefix="/example") - - self.assertEqual(routes[0].route_url, '/example/test/1') - self.assertEqual(routes[1].route_url, '/example/test/2') - - def test_group_route_sets_middleware(self): - routes = RouteGroup([ - Get('/test/1', 'TestController@show').middleware('another'), - Get('/test/2', 'TestController@show'), - RouteGroup([ - Get('/test/3', 'TestController@show'), - Get('/test/4', 'TestController@show') - ], middleware=('test', 'test2')) - ], middleware=('auth', 'user')) - - self.assertIsInstance(routes, list) - self.assertEqual(['another', 'auth', 'user'], routes[0].list_middleware) - self.assertEqual(['auth', 'user'], routes[1].list_middleware) - self.assertEqual(['test', 'test2', 'auth', 'user'], routes[2].list_middleware) - - def test_group_route_namespace(self): - routes = RouteGroup([ - Get('/test/1', 'SubController@show'), - ], namespace='subdirectory.') - - self.assertIsInstance(routes, list) - self.assertEqual(SubController, routes[0].controller) - - def test_group_route_namespace_deep(self): - routes = RouteGroup([ - RouteGroup([ - Get('/test/1', 'DeepController@show'), - ], namespace='deep.') - ], namespace='subdirectory.') - - self.assertIsInstance(routes, list) - self.assertEqual(DeepController, routes[0].controller) - - def test_group_route_namespace_deep_using_route_values_in_constructor(self): - routes = RouteGroup([ - RouteGroup([ - Get('/test/1', 'DeepController@show'), - ], namespace='deep.') - ], namespace='subdirectory.') - - self.assertIsInstance(routes, list) - self.assertEqual(DeepController, routes[0].controller) - - def test_group_route_namespace_deep_no_dots(self): - routes = RouteGroup([ - RouteGroup([ - Get().route('/test/1', 'DeepController@show'), - ], namespace='deep') - ], namespace='subdirectory') - - self.assertIsInstance(routes, list) - self.assertEqual(DeepController, routes[0].controller) - - def test_group_route_sets_domain(self): - routes = RouteGroup([ - Get('/test/1', 'TestController@show'), - Get('/test/2', 'TestController@show') - ], domain=['www']) - - self.assertEqual(routes[0].required_domain, ['www']) - - def test_group_adds_methods(self): - routes = RouteGroup([ - Get('/test/1', 'TestController@show'), - Get('/test/2', 'TestController@show') - ], add_methods=['OPTIONS']) - - self.assertEqual(routes[0].method_type, ['GET', 'OPTIONS']) - - def test_group_route_sets_prefix(self): - routes = RouteGroup([ - Get().route('/test/1', 'TestController@show'), - Get('/test/2', 'TestController@show') - ], prefix='/dashboard') - - self.assertEqual(routes[0].route_url, '/dashboard/test/1') - - def test_group_route_sets_prefix_no_route(self): - routes = RouteGroup([ - Get('', 'TestController@show'), - ], prefix='/dashboard') - - self.assertEqual(routes[0].route_url, '/dashboard') - - def test_group_route_sets_name(self): - RouteGroup([ - Get('/test/1', 'TestController@show').name('create'), - Get('/test/2', 'TestController@show').name('edit') - ], name='post.') - - def test_group_route_sets_name_for_none_route(self): - routes = RouteGroup([ - Get('/test/1', 'TestController@show').name('create'), - Get('/test/2', 'TestController@show') - ], name='post.') - - self.assertEqual(routes[0].named_route, 'post.create') - self.assertEqual(routes[1].named_route, None) - - def test_flatten_flattens_multiple_lists(self): - routes = [ - Get('/test/1', 'TestController@show').name('create'), - RouteGroup([ - Get('/test/1', 'TestController@show').name('create'), - Get('/test/2', 'TestController@show').name('edit'), - RouteGroup([ - Get('/test/1', 'TestController@show').name('update'), - Get('/test/2', 'TestController@show').name('delete'), - RouteGroup([ - Get('/test/3', 'TestController@show').name('update'), - Get('/test/4', 'TestController@show').name('delete'), - ], middleware=('auth')), - ], name='post.') - ], prefix='/dashboard') - ] - - routes = flatten_routes(routes) - - self.assertEqual(routes[3].route_url, '/dashboard/test/1') - self.assertEqual(routes[3].named_route, 'post.update') - - def test_redirect_route(self): - route = Redirect('/test1', '/test2') - # request = Request(generate_wsgi()) - request = self.container.make('Request') - response = self.container.make(Response) - route.load_request(request) - request.load_app(self.container) - - route.get_response() - self.assertTrue(response.is_status(302)) - self.assertEqual(request.redirect_url, '/test2') - - def test_redirect_can_use_301(self): - request = self.container.make('Request') - response = self.container.make(Response) - route = Redirect('/test1', '/test3', status=301) - - route.load_request(request) - request.load_app(self.container) - route.get_response() - self.assertTrue(response.is_status(301)) - self.assertEqual(request.redirect_url, '/test3') - - def test_redirect_can_change_method_type(self): - route = Redirect('/test1', '/test3', methods=['POST', 'PUT']) - self.assertEqual(route.method_type, ['POST', 'PUT']) - - -class TestOptionalRoutes(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[ - Get('/user/?name', 'TestController@v').name('user.name'), - Get('/multiple/user/?name/?last', 'TestController@v').name('user.multiple'), - Get('/default/user/?name', 'TestController@v').default({'name': 'Joseph'}), - Get('/back/user/name?', 'TestController@v'), - Get('/back/default/user/?name', 'TestController@v').default({'name': 'Joseph'}), - Get('/optional/?name:int', 'TestController@v'), - ]) - - def test_can_get_name(self): - self.get('/user/john').assertParameterIs('name', 'john') - self.get('/user').assertParameterIs('name', None) - self.get('/default/user/Bill').assertParameterIs('name', 'Bill') - self.get('/default/user').assertParameterIs('name', 'Joseph') - - def test_can_get_optional_when_optional_is_in_back(self): - self.get('/back/user/john').assertParameterIs('name', 'john') - self.get('/back/user').assertParameterIs('name', None) - self.get('/back/default/user/Bill').assertParameterIs('name', 'Bill') - self.get('/back/default/user').assertParameterIs('name', 'Joseph') - - def test_can_get_optional_route_compilers(self): - self.get('/optional/1').assertParameterIs('name', '1') - - with self.assertRaises(RouteNotFoundException): - self.get('/optional/Joe') - - self.get('/optional').assertParameterIs('name', None) - - def test_cannot_get_longer_optional_parameter(self): - with self.assertRaises(RouteNotFoundException): - self.get('/user/john/settings').assertParameterIs('name', 'john') - - def test_route_helper_works(self): - request = self.get('/user/john').request - self.assertEqual(request.route('user.name'), '/user') - self.assertEqual(request.route('user.name', {'name': 'john'}), '/user/john') - self.assertEqual(request.route('user.multiple'), '/multiple/user') - self.assertEqual(request.route('user.multiple', {'name': 'john'}), '/multiple/user/john') - self.assertEqual(request.route('user.multiple', {'name': 'john', 'last': 'smith'}), '/multiple/user/john/smith') - self.assertEqual(request.route('user.multiple', {'last': 'smith'}), '/multiple/user/smith') - -class TestRouteResources(TestCase): - - def setUp(self): - super().setUp() - self.routes(only=[Resource('/user', 'UserResourceController', names={ - 'create': 'users.build' - })]) - - def test_has_correct_controllers(self): - self.get('/user').assertHasController('UserResourceController@index').assertIsNotNamed() - self.get('/user/create').assertHasController('UserResourceController@create').assertIsNamed('users.build') - self.post('/user').assertHasController('UserResourceController@store').assertIsNotNamed() - self.get('/user/1').assertHasController('UserResourceController@show').assertIsNotNamed() - self.get('/user/1/edit').assertHasController('UserResourceController@edit').assertIsNotNamed() - self.put('/user/1').assertHasController('UserResourceController@update').assertIsNotNamed() - self.delete('/user/1').assertHasController('UserResourceController@destroy').assertIsNotNamed() - - def test_has_correct_routes_names(self): - self.routes(only=[Resource('/user', 'UserResourceController', names={})]) - self.get('/user').assertHasController('UserResourceController@index').assertIsNamed('user.index') - self.get('/user/create').assertHasController('UserResourceController@create').assertIsNamed('user.create') - self.post('/user').assertHasController('UserResourceController@store').assertIsNamed('user.store') - self.get('/user/1').assertHasController('UserResourceController@show').assertIsNamed('user.show') - self.get('/user/1/edit').assertHasController('UserResourceController@edit').assertIsNamed('user.edit') - self.put('/user/1').assertHasController('UserResourceController@update').assertIsNamed('user.update') - self.delete('/user/1').assertHasController('UserResourceController@destroy').assertIsNamed('user.destroy') - -class WsgiInputTestClass: - - def load(self, byte): - self.byte = byte - return self - - def read(self, _): - return self.byte diff --git a/tests/core/test_service_provider.py b/tests/core/test_service_provider.py deleted file mode 100644 index 3c8643036..000000000 --- a/tests/core/test_service_provider.py +++ /dev/null @@ -1,139 +0,0 @@ -import os - -from src.masonite.provider import ServiceProvider -from src.masonite.request import Request -from src.masonite.routes import Get -from src.masonite.testing import TestCase, generate_wsgi - - -class ContainerTest(ServiceProvider): - - def boot(self, request: Request, get: Get): - return request - - def testboot(self, request: Request, get: Get): - return request - - -class ServiceProviderTest(ServiceProvider): - - def register(self): - self.container.bind('Request', object) - - -class Mock1Command: - pass - - -class Mock2Command: - pass - - -ROUTE1 = Get().route('/url/here', None) -ROUTE2 = Get().route('/test/url', None) - - -class LoadProvider(ServiceProvider): - - def boot(self): - self.routes([ - ROUTE1, - ROUTE2 - ]) - - self.http_middleware([ - object, - object - ]) - - self.route_middleware({ - 'route1': object, - 'route2': object, - }) - - self.migrations('directory/1', 'directory/2') - - self.assets({ - 'storage/static': '/some/location' - }) - - self.commands(Mock1Command(), Mock2Command()) - - -class TestServiceProvider(TestCase): - - def setUp(self): - super().setUp() - self.container.resolve_parameters = True - self.provider = ServiceProvider() - self.provider.load_app(self.container).register() - self.load_provider = LoadProvider() - self.load_provider.load_app(self.container).boot() - - def test_service_provider_loads_app(self): - self.assertEqual(self.provider.app, self.container) - - def test_can_call_container_with_self_parameter(self): - self.container.bind('Request', Request({})) - self.container.bind('Get', Get()) - - self.assertEqual(self.container.resolve(ContainerTest().boot), self.container.make('Request')) - - def test_can_call_container_with_annotations_from_variable(self): - request = Request(generate_wsgi()) - - self.container.bind('Request', request) - self.container.bind('Get', Get().route('url', None)) - - self.assertEqual(self.container.resolve(ContainerTest().testboot), self.container.make('Request')) - - def test_can_load_routes_into_container(self): - self.assertTrue(len(self.container.make('WebRoutes')) > 2) - self.assertEqual(self.container.make('WebRoutes')[-2:], [ROUTE1, ROUTE2]) - - def test_can_load_http_middleware_into_container(self): - self.assertEqual(self.container.make('HttpMiddleware')[-2:], [object, object]) - - def test_can_load_route_middleware_into_container(self): - self.assertEqual(self.container.make('RouteMiddleware')['route1'], object) - self.assertEqual(self.container.make('RouteMiddleware')['route2'], object) - - def test_can_load_migrations_into_container(self): - self.assertEqual(len(self.container.collect('*MigrationDirectory')), 12) - - def test_can_load_assets_into_container(self): - self.assertEqual(self.container.make('staticfiles')['storage/static'], '/some/location') - - def test_can_load_commands_into_container(self): - self.assertTrue(self.container.make('Mock1Command')) - self.assertTrue(self.container.make('Mock2Command')) - - def test_can_load_publishing(self): - self.load_provider.publishes({ - 'from/directory': 'to/directory' - }) - self.assertEqual(self.load_provider._publishes, {'from/directory': 'to/directory'}) - # self.assertTrue(self.container.make('Mock2Command')) - - def test_provider_can_publish_with_tags(self): - self.load_provider.publishes({ - 'from/directory': 'to/directory' - }, tag='config') - self.assertEqual(self.load_provider._publishes, {'from/directory': 'to/directory'}) - self.assertEqual(self.load_provider._publish_tags.get('config'), {'from/directory': 'to/directory'}) - - def test_provider_can_publish(self): - self.load_provider.publishes({ - os.path.join(os.getcwd(), 'storage/append_from.txt'): 'storage/append_to.txt' - }, tag='config') - - self.load_provider.publish() - os.remove(os.path.join(os.getcwd(), 'storage/append_to.txt')) - - def test_provider_can_publish_a_tag(self): - self.load_provider.publishes({ - os.path.join(os.getcwd(), 'storage/append_from.txt'): 'storage/append_to.txt' - }, tag='config') - - self.load_provider.publish(tag='config') - os.remove(os.path.join(os.getcwd(), 'storage/append_to.txt')) diff --git a/tests/core/test_session.py b/tests/core/test_session.py deleted file mode 100644 index 7caadf19d..000000000 --- a/tests/core/test_session.py +++ /dev/null @@ -1,185 +0,0 @@ - -from src.masonite.drivers import SessionCookieDriver, SessionMemoryDriver -from src.masonite.managers import SessionManager -from src.masonite.testing import generate_wsgi, TestCase - -class TestSession(TestCase): - - def setUp(self): - super().setUp() - self.container.make('Request').load_environ(generate_wsgi()).load_app(self.container) - self.container.bind('SessionMemoryDriver', SessionMemoryDriver) - self.container.bind('SessionCookieDriver', SessionCookieDriver) - self.container.bind('SessionManager', SessionManager(self.container)) - self.container.make('Request').session = self.container.make('SessionManager').driver('cookie') - self.container.bind('StatusCode', 200) - - def test_session_request(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session.set('username', 'pep') - session.set('password', 'secret') - self.assertEqual(session.get('username'), 'pep') - self.assertEqual(session.get('password'), 'secret') - - def test_session_has_no_data(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session._flash = {} - self.assertFalse(session.has('nodata')) - - def test_change_ip_address(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session.request.environ['REMOTE_ADDR'] = '111.222.33.44' - session.set('username', 'pep') - self.assertEqual(session.get('username'), 'pep') - - def test_session_get_all_data(self): - for driver in ('cookie', 'memory'): - session = self.container.make('SessionManager').driver(driver) - session.request.environ['REMOTE_ADDR'] = 'get.all.data' - session.set('username', 'pep') - session.flash('password', 'secret') - self.assertEqual(session.all(), {'username': 'pep', 'password': 'secret'}) - - def test_session_has_data(self): - for driver in ('cookie',): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session._flash = {} - session.set('username', 'pep') - self.assertTrue(session.has('username')) - self.assertFalse(session.has('has_password')) - - def test_session_helper(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session._flash = {} - helper = session.helper - - self.assertIsInstance(helper(), type(session)) - - def test_session_flash_data(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session.flash('flash_username', 'pep') - session.flash('flash_password', 'secret') - self.assertEqual(session.get('flash_username'), 'pep') - self.assertEqual(session.get('flash_password'), 'secret') - - def test_session_flash_error_messages(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session.flash('errors', {'password': ['password invalid']}) - self.assertEqual(session.get_error_messages(), ['password invalid']) - # assert session key is now deleted - self.assertEqual(session.get('errors'), None) - - def test_reset_flash_session_memory(self): - session = self.container.make('SessionManager').driver('memory') - session.flash('flash_', 'test_pep') - session.reset(flash_only=True) - self.assertIsNone(session.get('flash_')) - - def test_reset_flash_session_driver(self): - session = self.container.make('SessionManager').driver('cookie') - session.flash('flash_', 'test_pep') - - session.reset(flash_only=True) - self.assertIsNone(session.get('flash_')) - - def test_session_flash_data_serializes_dict(self): - for driver in ('cookie', 'memory'): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session.flash('flash_dict', {'id': 1}) - session.set('get_dict', {'id': 1}) - self.assertEqual(session.get('flash_dict'), {'id': 1}) - self.assertEqual(session.get('get_dict'), {'id': 1}) - - def test_session_flash_data_serializes_list(self): - for driver in ('cookie', 'memory'): - session = self.container.make('SessionManager').driver(driver) - session._session = {} - session.flash('flash_dict', [1, 2, 3]) - self.assertEqual(session.get('flash_dict'), [1, 2, 3]) - - def test_reset_serializes_dict(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session.set('flash_', 'test_pep') - session.reset() - self.assertIsNone(session.get('reset_username')) - - def test_delete_session(self): - for driver in ('memory', 'cookie'): - session = self.container.make('SessionManager').driver(driver) - session.set('test1', 'value') - session.set('test2', 'value') - self.assertTrue(session.delete('test1')) - self.assertFalse(session.has('test1')) - self.assertFalse(session.delete('test1')) - - def test_can_redirect_with_inputs(self): - for driver in ('memory', 'cookie'): - request = self.container.make('Request') - request.request_variables = { - 'key1': 'val1', - 'key2': 'val2', - } - session = self.container.make('SessionManager').driver(driver) - request.session = session - request.with_input() - self.assertTrue(session.has('key1')) - self.assertTrue(session.has('key2')) - - def test_can_redirect_with_bytes_inputs(self): - for driver in ('memory', 'cookie'): - - request = self.container.make('Request') - session = self.container.make('SessionManager').driver(driver) - request.request_variables = { - 'byte': 'val1'.encode('utf-8'), - 'key2': 'val2', - } - request.session = session - - request.with_input() - self.assertFalse(session.has('byte')) - self.assertTrue(session.has('key2')) - - def test_intended_returns_correct_url(self): - request = self.container.make('Request') - request.redirect('/dashboard') - self.assertEqual(request.redirect_url, '/dashboard') - - request.path = '/test' - - request.redirect('/dashboard').then_back() - self.assertEqual(request.session.get('__intend'), '/test') - - # Assert redirect intended method resets the redirection - request.redirect_intended() - self.assertEqual(request.session.get('__intend'), None) - - def test_with_flash(self): - request = self.container.make('Request') - request.redirect('/dashboard').with_flash('success', 'Ok') - self.assertEqual(request.session.get('success'), 'Ok') - request.redirect('/dashboard').with_flash('any_key', 'any_value') - self.assertEqual(request.session.get('any_key'), 'any_value') - - def test_with_errors(self): - request = self.container.make('Request') - request.redirect('/dashboard').with_errors('Form error') - self.assertEqual(request.session.get('errors'), 'Form error') - - def test_with_success(self): - request = self.container.make('Request') - request.redirect('/dashboard').with_success('Created !') - self.assertEqual(request.session.get('success'), 'Created !') diff --git a/tests/core/test_signing.py b/tests/core/test_signing.py deleted file mode 100644 index 197e67e91..000000000 --- a/tests/core/test_signing.py +++ /dev/null @@ -1,28 +0,0 @@ -import unittest - -from cryptography.fernet import Fernet - -from src.masonite.auth.Sign import Sign -from src.masonite.exceptions import InvalidSecretKey - - -class TestSigning(unittest.TestCase): - - def setUp(self): - self.secret_key = Fernet.generate_key() - self.signed = Sign(self.secret_key) - - def test_unsigning_returns_decrypted_value_with_parameter(self): - self.assertEqual(self.signed.unsign(self.signed.sign('value')), 'value') - - def test_unsigning_returns_decrypted_value_without_parameter(self): - self.assertEqual(self.signed.unsign(self.signed.sign('value')), 'value') - - def test_unsigning_without_value(self): - self.signed.sign('value') - self.assertEqual(self.signed.unsign(), 'value') - - def test_sign_incorrect_padding(self): - with self.assertRaises(InvalidSecretKey): - padded_secret_key = "AQAAQDhAAMAAQYS04MjQ2LWRkYzJkMmViYjQ2YQ===" - assert Sign(padded_secret_key).sign('value') diff --git a/tests/core/test_upload_manager.py b/tests/core/test_upload_manager.py deleted file mode 100644 index 19eb6d7a8..000000000 --- a/tests/core/test_upload_manager.py +++ /dev/null @@ -1,196 +0,0 @@ -import os -import shutil -import unittest - -from src.masonite.app import App -from src.masonite.drivers import UploadDiskDriver, UploadS3Driver -from src.masonite.environment import LoadEnvironment -from src.masonite.exceptions import (DriverNotFound, FileTypeException, - UnacceptableDriverType) -from src.masonite.helpers import static -from src.masonite.managers.UploadManager import UploadManager - -LoadEnvironment() - - -class TestStaticTemplateHelper(unittest.TestCase): - - def setUp(self): - self.static = static - - def test_static_gets_first_value_from_dictionary(self): - self.assertEqual(self.static('disk', 'profile.py'), 'uploads/profile.py') - - def test_static_gets_alias_with_dot_notation(self): - self.assertEqual(self.static('disk.uploading', 'profile.py'), 'uploads/profile.py') - - def test_static_gets_string_location(self): - self.assertEqual(self.static('s3', 'profile.py'), 'http://s3.amazon.com/bucket/profile.py') - - -class TestUploadManager(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Container', self.app) - self.app.bind('Test', object) - # self.app.bind('StorageConfig', storage) - self.app.bind('UploadDiskDriver', UploadDiskDriver) - self.app.bind('UploadS3Driver', UploadS3Driver) - self.app.bind('UploadManager', UploadManager) - - def test_upload_manager_grabs_drivers(self): - self.assertIsInstance(self.app.make('UploadManager').driver('disk'), UploadDiskDriver) - - def test_upload_manager_grabs_drivers_with_a_class(self): - self.assertIsInstance(self.app.make('UploadManager').driver(UploadDiskDriver), UploadDiskDriver) - - def test_upload_manager_throws_error_with_incorrect_file_type(self): - with self.assertRaises(UnacceptableDriverType): - self.app.make('UploadManager').driver(static) - - def test_disk_driver_creates_directory_if_not_exists(self): - self.app.make('UploadManager').driver('disk').store(ImageMock(), location="storage/temp") - self.assertTrue(os.path.exists('storage/temp')) - shutil.rmtree('storage/temp') - - def test_upload_manager_changes_accepted_files(self): - self.assertEqual(self.app.make('UploadManager').driver('disk').accept('yml').accept_file_types, ('yml',)) - self.assertEqual(self.app.make('UploadManager').driver('s3').accept('yml').accept_file_types, ('yml',)) - - def test_upload_manager_raises_driver_not_found_error(self): - self.app = App() - self.app.bind('Test', object) - # self.app.bind('StorageConfig', storage) - - with self.assertRaises(DriverNotFound): - self.assertIsNone(self.app.bind( - 'UploadManager', - UploadManager(self.app).load_container(self.app) - )) - - def test_upload_manager_switches_drivers(self): - self.app.bind('UploadTestDriver', UploadDiskDriver) - - self.assertIsInstance(self.app.make( - 'UploadManager').driver('disk'), UploadDiskDriver) - - self.assertIsInstance(self.app.make('UploadManager').driver('test'), UploadDiskDriver) - - def test_upload_file(self): - """ - This test is responsible for checking if you upload a file correctly. - """ - - self.assertTrue(UploadManager(self.app).driver('disk').store(ImageMock())) - - def test_upload_file_with_location(self): - """ - This test is responsible for checking if you upload a file correctly. - """ - - self.assertTrue(UploadManager(self.app).driver('disk').store(ImageMock(), location='uploads')) - - def test_upload_file_with_location_from_driver(self): - """ - This test is responsible for checking if you upload a file correctly. - """ - - self.assertTrue(UploadManager(self.app).driver('disk').store(ImageMock(), location='disk.uploading')) - - def test_upload_manage_accept_files(self): - """ - This test is responsible for checking if you upload - a file correctly with a valid extension. - """ - self.assertTrue(UploadManager(self.app).driver('disk').accept('jpg', 'png').store(ImageMock())) - - def test_upload_manage_accept_files_error(self): - """ - This test should return an error because it is an invalid extension. - """ - with self.assertRaises(FileTypeException): - UploadManager(self.app).driver('disk').accept('png').store(ImageMock()) - - def test_upload_manage_accept_all_extensions(self): - """ - This test should upload a file correctly by allowing all type files ( .accept('*') ) - """ - - image = ImageMock() - image.filename = 'file.pdf' - - self.assertTrue(UploadManager(self.app).driver('disk').accept('*').store(image)) - - def test_upload_manage_should_raise_exception_when_accept_all_extension_and_something_more(self): - """ - This test should raise an error when use something together with '*' when allowing all extensions ) - """ - with self.assertRaises(ValueError): - UploadManager(self.app).driver('disk').accept('*', 'png').store(ImageMock()) - - def test_upload_with_new_filename(self): - self.assertEqual(self.app.make('UploadManager').driver('disk').store(ImageMock(), filename='newname.jpg'), 'newname.jpg') - - def test_upload_manager_validates_file_ext(self): - """ - This test is responsible for checking if you upload - a file correctly with a valid extension. - """ - self.assertTrue(UploadManager(self.app).driver('disk').accept('jpg', 'png').validate_extension('test.png')) - - -class ImageMock: - """ - Image test for emulate upload file - """ - - filename = 'test.jpg' - - @property - def file(self): - return self - - def read(self): - return bytes('file read', 'utf-8') - - -if os.environ.get('S3_BUCKET'): - - class TestS3Upload(unittest.TestCase): - - def setUp(self): - self.app = App() - self.app.bind('Container', self.app) - - # self.app.bind('StorageConfig', storage) - self.app.bind('UploadDiskDriver', UploadDiskDriver) - self.app.bind('UploadManager', UploadManager(self.app)) - self.app.bind('Upload', UploadManager(self.app)) - self.app.bind('UploadS3Driver', UploadS3Driver) - - def test_upload_file_for_s3(self): - self.assertEqual(len(self.app.make('Upload').driver('s3').store(ImageMock())), 29) - - def test_upload_open_file_for_s3(self): - self.assertTrue(self.app.make('Upload').driver('s3').accept('yml').store(open('.travis.yml'))) - - def test_upload_manage_accept_files(self): - """ - This test is responsible for checking if you upload - a file correctly with a valid extension. - """ - self.assertTrue(UploadManager(self.app).driver('s3').accept('jpg', 'png').store(ImageMock())) - - def test_upload_manage_accept_files_error(self): - """ - This test should return an error because it is an invalid extension. - """ - with self.assertRaises(FileTypeException): - UploadManager(self.app).driver('s3').accept('png').store(ImageMock()) - - def test_upload_with_new_filename(self): - self.assertEqual(self.app.make('UploadManager').driver('s3').store(ImageMock(), filename='newname.jpg'), 'newname.jpg') - - def test_upload_with_new_filename_and_location_in_s3(self): - self.assertEqual(self.app.make('UploadManager').driver('s3').store(ImageMock(), filename='newname.jpg', location='3/2'), 'newname.jpg') diff --git a/tests/core/test_view.py b/tests/core/test_view.py deleted file mode 100644 index 27dedd4e8..000000000 --- a/tests/core/test_view.py +++ /dev/null @@ -1,272 +0,0 @@ -import glob -import time - -from jinja2 import FileSystemLoader - -from src.masonite.app import App -from src.masonite.drivers import CacheDiskDriver -from src.masonite.exceptions import RequiredContainerBindingNotFound, ViewException -from src.masonite.managers.CacheManager import CacheManager -from src.masonite.view import View -import unittest - - -class TestView(unittest.TestCase): - - def setUp(self): - self.container = App() - view = View(self.container) - - self.container.bind('View', view.render) - self.container.bind('ViewClass', view) - - def test_view_extends_dictionary(self): - view = self.container.make('View') - - self.assertEqual(view('test', {'test': 'test'}).rendered_template, 'test') - - def test_view_exists(self): - view = self.container.make('ViewClass') - - assert view.exists('index') - self.assertFalse(view.exists('not_available')) - - def test_view_render_does_not_keep_previous_variables(self): - view = self.container.make('ViewClass') - - view.render('test', {'var1': 'var1'}) - view.render('test', {'var2': 'var2'}) - - self.assertNotIn('var1', view.dictionary) - self.assertIn('var2', view.dictionary) - - def test_global_view_exists(self): - view = self.container.make('ViewClass') - - self.assertTrue(view.exists('/resources/templates/index')) - self.assertFalse(view.exists('/resources/templates/not_available')) - - def test_view_gets_global_template(self): - view = self.container.make('View') - self.assertEqual(view('/templates/test', {'test': 'test'}).rendered_template, 'test') - - def test_view_extends_without_dictionary_parameters(self): - view = self.container.make('ViewClass') - view.share({'test': 'test'}) - view = self.container.make('View') - - self.assertEqual(view('test').rendered_template, 'test') - - def test_render_from_container_as_view_class(self): - self.container.make('ViewClass').share({'test': 'test'}) - - view = self.container.make('View') - self.assertEqual(view('test').rendered_template, 'test') - - def test_composers(self): - self.container.make('ViewClass').composer('test', {'test': 'test'}) - view = self.container.make('View') - - self.assertEqual(self.container.make('ViewClass').composers, {'test': {'test': 'test'}}) - self.assertEqual(view('test').rendered_template, 'test') - - def test_composers_load_all_views_with_astericks(self): - - self.container.make('ViewClass').composer('*', {'test': 'test'}) - - self.assertEqual(self.container.make('ViewClass').composers, {'*': {'test': 'test'}}) - - view = self.container.make('View') - self.assertEqual(view('test').rendered_template, 'test') - - def test_composers_with_wildcard_base_view(self): - self.container.make('ViewClass').composer('mail*', {'to': 'test_user'}) - - self.assertEqual(self.container.make('ViewClass').composers, {'mail*': {'to': 'test_user'}}) - - view = self.container.make('View') - self.assertIn('test_user', view('mail/welcome').rendered_template) - - def test_composers_with_wildcard_base_view_route(self): - self.container.make('ViewClass').composer('mail*', {'to': 'test_user'}) - - self.assertEqual(self.container.make('ViewClass').composers, {'mail*': {'to': 'test_user'}}) - - view = self.container.make('View') - self.assertIn('test_user', view('mail/welcome').rendered_template) - - def test_render_deep_in_file_structure_with_package_loader(self): - - self.container.make('ViewClass').add_environment('storage') - - view = self.container.make('View') - self.assertEqual(view('/templates/tests/test', {'test': 'testing'}).rendered_template, 'testing') - - def test_composers_with_wildcard_lower_directory_view(self): - self.container.make('ViewClass').composer( - 'mail/welcome*', {'to': 'test_user'}) - - self.assertEqual(self.container.make('ViewClass').composers, {'mail/welcome*': {'to': 'test_user'}}) - - view = self.container.make('View') - self.assertIn('test_user', view('mail/welcome').rendered_template) - - def test_composers_with_wildcard_lower_directory_view_and_incorrect_shortend_wildcard(self): - self.container.make('ViewClass').composer( - 'mail/wel*', {'to': 'test_user'}) - - self.assertEqual(self.container.make('ViewClass').composers, {'mail/wel*': {'to': 'test_user'}}) - - view = self.container.make('View') - assert 'test_user' not in view('mail/welcome').rendered_template - - def test_composers_load_all_views_with_list(self): - self.container.make('ViewClass').composer( - ['home', 'test'], {'test': 'test'}) - - self.assertEqual(self.container.make('ViewClass').composers, {'home': {'test': 'test'}, 'test': {'test': 'test'}}) - - view = self.container.make('View') - self.assertEqual(view('test').rendered_template, 'test') - - def test_view_share_updates_dictionary_not_overwrite(self): - viewclass = self.container.make('ViewClass') - - viewclass.share({'test1': 'test1'}) - viewclass.share({'test2': 'test2'}) - - self.assertEqual(viewclass._shared, {'test1': 'test1', 'test2': 'test2'}) - viewclass.render('test', {'var1': 'var1'}) - self.assertEqual(viewclass.dictionary, {'test1': 'test1', 'test2': 'test2', 'var1': 'var1'}) - - def test_adding_environment(self): - viewclass = self.container.make('ViewClass') - - viewclass.add_environment('storage', loader=FileSystemLoader) - - self.assertEqual(viewclass.render('test_location', {'test': 'testing'}).rendered_template, 'testing') - - def test_view_throws_exception_without_cache_binding(self): - view = self.container.make('View') - - with self.assertRaises(RequiredContainerBindingNotFound): - view('test_cache').cache_for('5', 'seconds') - - def test_view_can_add_custom_filters(self): - view = self.container.make('ViewClass') - - view.filter('slug', self._filter_slug) - - self.assertEqual(view._filters, {'slug': self._filter_slug}) - self.assertEqual(view.render('filter', {'test': 'test slug'}).rendered_template, 'test-slug') - - @staticmethod - def _filter_slug(item): - return item.replace(' ', '-') - - def test_view_cache_caches_files(self): - - # self.container.bind('CacheConfig', cache) - self.container.bind('CacheDiskDriver', CacheDiskDriver) - self.container.bind('CacheManager', CacheManager(self.container)) - self.container.bind('Application', self.container) - self.container.bind('Cache', self.container.make( - 'CacheManager').driver('disk')) - - view = self.container.make('View') - - self.assertEqual(view('test_cache', {'test': 'test'}).cache_for(1, 'second').rendered_template, 'test') - - self.assertEqual(open(glob.glob('bootstrap/cache/test_cache:*')[0]).read(), 'test') - - time.sleep(2) - - self.assertEqual(view('test_cache', {'test': 'macho'}).cache_for(5, 'seconds').rendered_template, 'macho') - - time.sleep(2) - - self.assertEqual(open(glob.glob('bootstrap/cache/test_cache:*')[0]).read(), 'macho') - - self.assertEqual(view('test_cache', {'test': 'macho'}).cache_for(1, 'second').rendered_template, 'macho') - - time.sleep(1) - - self.assertEqual(open(glob.glob('bootstrap/cache/test_cache:*')[0]).read(), 'macho') - - self.assertEqual(view('test_cache', {'test': 'macho'}).cache_for('1', 'second').rendered_template, 'macho') - - def test_cache_throws_exception_with_incorrect_cache_type(self): - # self.container.bind('CacheConfig', cache) - self.container.bind('CacheDiskDriver', CacheDiskDriver) - self.container.bind('CacheManager', CacheManager(self.container)) - self.container.bind('Application', self.container) - self.container.bind('Cache', self.container.make( - 'CacheManager').driver('disk')) - - view = self.container.make('View') - - with self.assertRaises(ValueError): - view( - 'test_exception', {'test': 'test'} - ).cache_for(1, 'monthss') - - def test_view_can_change_template_splice(self): - self.container.make('ViewClass').set_splice('.') - - view = self.container.make('View') - self.container.make('ViewClass').composer( - 'mail/welcome', {'test': 'test'}) - self.container.make('ViewClass').share( - {'test': 'John'}) - - self.assertIn('John', view('mail.welcome', {'to': 'John'}).rendered_template) - self.assertEqual(view('mail.composers', {'test': 'John'}).rendered_template, 'John') - self.assertEqual(view('mail.share').rendered_template, 'John') - self.assertIn('John', view('mail/welcome', {'to': 'John'}).rendered_template) - - self.container.make('ViewClass').set_splice('@') - - self.assertIn('John', view('mail@welcome', {'to': 'John'}).rendered_template) - self.assertIn('John', view('mail@composers', {'test': 'John'}).rendered_template) - self.assertIn('John', view('mail/welcome', {'to': 'John'}).rendered_template) - - def test_can_add_tests_to_view(self): - view = self.container.make('ViewClass') - - view.test('admin', self._is_admin) - - self.assertEqual(view._tests, {'admin': self._is_admin}) - - user = MockAdminUser - self.assertEqual(view.render('admin_test', {'user': user}).rendered_template, 'True') - - user.admin = 0 - - self.assertEqual(view.render('admin_test', {'user': user}).rendered_template, 'False') - - def _is_admin(self, obj): - return obj.admin == 1 - - def test_can_render_pubjs(self): - view = self.container.make('ViewClass') - view.add_extension('pypugjs.ext.jinja.PyPugJSExtension') - self.assertEqual(view._jinja_extensions, ['jinja2.ext.loopcontrols', 'pypugjs.ext.jinja.PyPugJSExtension']) - - self.assertEqual(view.render('pug/hello.pug', {'name': 'Joe'}).rendered_template, '

hello Joe

') - - def test_throws_exception_on_incorrect_type(self): - view = self.container.make('ViewClass') - with self.assertRaises(ViewException): - assert view.render('test', {'', ''}) - - def test_can_use_dot_templates(self): - view = self.container.make('ViewClass') - self.assertEqual(view.render('mail.share', {'test': 'test'}).rendered_template, 'test') - - def test_can_use_at_line_statements(self): - view = self.container.make('ViewClass') - self.assertIn('test this string', view.render('line-statements', {'test': 'test this string'}).rendered_template) - - -class MockAdminUser: - admin = 1 diff --git a/tests/core/utils/test_location.py b/tests/core/utils/test_location.py new file mode 100644 index 000000000..4e52cfee2 --- /dev/null +++ b/tests/core/utils/test_location.py @@ -0,0 +1,106 @@ +import os +from tests import TestCase + +from src.masonite.utils.location import ( + base_path, + views_path, + controllers_path, + seeds_path, + migrations_path, + config_path, + jobs_path, + resources_path, +) + + +class TestLocation(TestCase): + def test_base_path(self): + base_dir = os.getcwd() + location = base_path() + self.assertEqual(base_dir, location) + location = base_path("tests/integrations") + self.assertEqual(os.path.join(base_dir, "tests/integrations"), location) + + def test_views_path(self): + location = views_path("app.html") + self.assertTrue(location.endswith("tests/integrations/templates/app.html")) + location = views_path("account/app.html") + self.assertTrue( + location.endswith("tests/integrations/templates/account/app.html") + ) + location = views_path("account/app.html", absolute=False) + self.assertEqual("tests/integrations/templates/account/app.html", location) + location = views_path(absolute=False) + self.assertEqual(location, "tests/integrations/templates/") + + def test_controllers_path(self): + location = controllers_path("MyController.py") + self.assertTrue( + location.endswith("tests/integrations/controllers/MyController.py") + ) + location = controllers_path("account/MyController.py") + self.assertTrue( + location.endswith("tests/integrations/controllers/account/MyController.py") + ) + location = controllers_path("MyController.py", absolute=False) + self.assertEqual("tests/integrations/controllers/MyController.py", location) + + def test_config_path(self): + location = config_path("app.py") + self.assertTrue(location.endswith("tests/integrations/config/app.py")) + location = config_path("package/base.py") + self.assertTrue(location.endswith("tests/integrations/config/package/base.py")) + location = config_path("app.py", absolute=False) + self.assertEqual("tests/integrations/config/app.py", location) + + def test_migrations_path(self): + location = migrations_path("create_users_table.py") + self.assertTrue( + location.endswith( + "tests/integrations/databases/migrations/create_users_table.py" + ) + ) + location = migrations_path("package/create_team_table.py") + self.assertTrue( + location.endswith( + "tests/integrations/databases/migrations/package/create_team_table.py" + ) + ) + location = migrations_path("create_users_table.py", absolute=False) + self.assertEqual( + "tests/integrations/databases/migrations/create_users_table.py", location + ) + + def test_seeds_path(self): + location = seeds_path("create_users.py") + self.assertTrue( + location.endswith("tests/integrations/databases/seeds/create_users.py") + ) + location = seeds_path("package/create_teams.py") + self.assertTrue( + location.endswith( + "tests/integrations/databases/seeds/package/create_teams.py" + ) + ) + location = seeds_path("create_users.py", absolute=False) + self.assertEqual("tests/integrations/databases/seeds/create_users.py", location) + + def test_jobs_path(self): + location = jobs_path("SomeTask.py") + self.assertTrue(location.endswith("tests/integrations/jobs/SomeTask.py")) + location = jobs_path("critical/SomeTask.py") + self.assertTrue( + location.endswith("tests/integrations/jobs/critical/SomeTask.py") + ) + location = jobs_path("critical/SomeTask.py", absolute=False) + self.assertEqual("tests/integrations/jobs/critical/SomeTask.py", location) + location = jobs_path(absolute=False) + self.assertEqual(location, "tests/integrations/jobs/") + + def test_resources_path(self): + location = resources_path("js/Home.vue") + self.assertTrue(location.endswith("tests/integrations/resources/js/Home.vue")) + location = resources_path("js/Home.vue", absolute=False) + self.assertEqual("tests/integrations/resources/js/Home.vue", location) + location = resources_path(absolute=False) + self.assertEqual(location, "tests/integrations/resources/") diff --git a/tests/core/utils/test_str.py b/tests/core/utils/test_str.py new file mode 100644 index 000000000..0c566e78e --- /dev/null +++ b/tests/core/utils/test_str.py @@ -0,0 +1,19 @@ +from tests import TestCase + +from src.masonite.utils.str import random_string, removeprefix, removesuffix + + +class TestStringsUtils(TestCase): + def test_random_string(self): + self.assertEqual(len(random_string()), 4) + self.assertEqual(len(random_string(10)), 10) + self.assertIsInstance(random_string(5), str) + self.assertNotEqual(random_string(), random_string()) + + def test_removesuffix(self): + self.assertEqual(removesuffix("test.com", ".com"), "test") + self.assertEqual(removesuffix("test", ".com"), "test") + + def test_removeprefix(self): + self.assertEqual(removeprefix("AppEvent", "App"), "Event") + self.assertEqual(removeprefix("Event", "App"), "Event") diff --git a/tests/core/utils/test_structures.py b/tests/core/utils/test_structures.py new file mode 100644 index 000000000..042d33293 --- /dev/null +++ b/tests/core/utils/test_structures.py @@ -0,0 +1,44 @@ +from tests import TestCase + +from src.masonite.utils.structures import data_get, data_set, data + + +class TestStructures(TestCase): + def test_data_get(self): + struct = {"key": "val", "a": {"b": "c", "nested": {"a": 1}}} + self.assertEqual(data_get(struct, "key"), "val") + + self.assertEqual(data_get(struct, "a.b"), "c") + self.assertEqual(data_get(struct, "a.nested.a"), 1) + + self.assertEqual(data_get(struct, "a.nested.unknown"), None) + self.assertEqual(data_get(struct, "a.nested.unknown", 0), 0) + + def test_data_set(self): + struct = {"key": "val", "a": {"b": "c", "nested": {"a": 1}}} + data_set(struct, "key", "val2") + self.assertEqual(struct.get("key"), "val2") + + data_set(struct, "a.nested.a", 3) + self.assertEqual(data_get(struct, "a.nested.a"), 3) + + data_set(struct, "a.unknown", "new") + self.assertEqual(data_get(struct, "a.unknown"), "new") + + def test_data_set_no_overwrite(self): + struct = {"key": "val", "a": {"b": "c", "nested": {"a": 1}}} + data_set(struct, "key", "val2", overwrite=False) + self.assertEqual(data_get(struct, "key"), "val") + + data_set(struct, "a.nested.a", "new", overwrite=False) + self.assertEqual(data_get(struct, "a.nested.a"), 1) + + data_set(struct, "unknown.key", "new", overwrite=False) + self.assertEqual(data_get(struct, "unknown.key"), "new") + + def test_data(self): + struct = {"key": "val"} + dotted_struct = data(struct) + dotted_struct["new_key.nested"] = 3 + self.assertEqual(dotted_struct.get("new_key.nested"), 3) + self.assertEqual(dotted_struct, {"key": "val", "new_key": {"nested": 3}}) diff --git a/tests/core/utils/test_time.py b/tests/core/utils/test_time.py new file mode 100644 index 000000000..affc08788 --- /dev/null +++ b/tests/core/utils/test_time.py @@ -0,0 +1,60 @@ +import pendulum +from tests import TestCase + +from src.masonite.utils.time import ( + migration_timestamp, + parse_human_time, + cookie_expire_time, +) + + +class TestTimeUtils(TestCase): + def tearDown(self): + super().tearDown() + self.restoreTime() + + def test_parse_human_time_now(self): + ref_time = pendulum.datetime(2021, 1, 1) + self.fakeTime(ref_time) + instance = parse_human_time("now") + self.assertEqual(ref_time, instance) + + def test_parse_human_time_expired(self): + self.fakeTime(pendulum.datetime(2021, 1, 1)) + instance = parse_human_time("expired") + self.assertEqual(pendulum.datetime(2001, 1, 1), instance) + + def test_parse_human_time(self): + self.fakeTime(pendulum.datetime(2021, 1, 1, 12, 0, 0)) + self.assertEqual( + pendulum.datetime(2021, 1, 1, 12, 0, 2), parse_human_time("2 seconds") + ) + self.assertEqual( + pendulum.datetime(2021, 1, 1, 12, 2, 0), parse_human_time("2 minutes") + ) + self.assertEqual( + pendulum.datetime(2021, 1, 1, 14, 0, 0), parse_human_time("2 hour") + ) + self.assertEqual( + pendulum.datetime(2021, 1, 2, 12, 0, 0), parse_human_time("1 day") + ) + self.assertEqual( + pendulum.datetime(2021, 1, 15, 12, 0, 0), parse_human_time("2 weeks") + ) + self.assertEqual( + pendulum.datetime(2021, 4, 1, 12, 0, 0), parse_human_time("3 months") + ) + self.assertEqual( + pendulum.datetime(2030, 1, 1, 12, 0, 0), parse_human_time("9 years") + ) + + self.assertEqual(None, parse_human_time("10 nanoseconds")) + + def test_cookie_expire_time(self): + self.fakeTime(pendulum.datetime(2021, 1, 21, 7, 28, 0)) + expiration_time_str = cookie_expire_time("7 days") + self.assertEqual(expiration_time_str, "Thu, 28 Jan 2021 07:28:00") + + def test_migration_timestamp(self): + self.fakeTime(pendulum.datetime(2021, 10, 25, 8, 12, 54)) + self.assertEqual(migration_timestamp(), "2021_10_25_081254") diff --git a/tests/core/views/test_view.py b/tests/core/views/test_view.py new file mode 100644 index 000000000..803d40b08 --- /dev/null +++ b/tests/core/views/test_view.py @@ -0,0 +1,115 @@ +from src.masonite.configuration import config +from src.masonite.helpers import url +from tests import TestCase + + +class TestView(TestCase): + def setUp(self): + super().setUp() + # keep this to have a "fresh view instance" for each test + self.view = self.application.make("view") + self.view.loaders = [] + self.view.composers = {} + self.view.add_location("tests/integrations/templates") + + def test_can_pass_dict(self): + self.assertIn("test", self.view.render("test", {"test": "test"}).get_content()) + + def test_view_exists(self): + self.assertTrue(self.view.exists("welcome")) + self.assertFalse(self.view.exists("not_available")) + + def test_view_render_does_not_keep_previous_variables(self): + self.view.render("test", {"var1": "var1"}) + self.view.render("test", {"var2": "var2"}) + + self.assertNotIn("var1", self.view.dictionary) + self.assertIn("var2", self.view.dictionary) + + def test_global_view_exists(self): + self.assertTrue(self.view.exists("/tests/integrations/templates/welcome")) + self.assertFalse( + self.view.exists("/tests/integrations/templates/not_available") + ) + + def test_view_gets_global_template(self): + self.assertEqual( + self.view.render( + "/tests/integrations/templates/test", {"test": "test"} + ).get_content(), + "test", + ) + + def test_view_extends_without_dictionary_parameters(self): + self.view.share({"test": "test"}) + self.assertEqual(self.view.render("test").get_content(), "test") + + def test_composers(self): + view = self.view.composer("test", {"test": "test"}) + + self.assertEqual(view.composers, {"test": {"test": "test"}}) + self.assertEqual(view.render("test").rendered_template, "test") + + def test_composers_load_all_views_with_asterisks(self): + + self.view.composer("*", {"test": "test"}) + + self.assertEqual(self.view.composers, {"*": {"test": "test"}}) + + self.assertEqual(self.view.render("test").get_content(), "test") + + def test_composers_with_wildcard_base_view(self): + self.view.composer("mail*", {"to": "test_user"}) + + self.assertEqual(self.view.composers, {"mail*": {"to": "test_user"}}) + + self.assertIn("test_user", self.view.render("mail/welcome").get_content()) + + def test_composers_with_wildcard_base_view_route(self): + self.view.composer("mail*", {"to": "test_user"}) + + self.assertEqual(self.view.composers, {"mail*": {"to": "test_user"}}) + + self.assertIn("test_user", self.view.render("mail/welcome").get_content()) + + def test_composers_with_wildcard_lower_directory_view_and_incorrect_shortend_wildcard( + self, + ): + self.view.composer("mail/wel*", {"to": "test_user"}) + + self.assertEqual(self.view.composers, {"mail/wel*": {"to": "test_user"}}) + + assert "test_user" not in self.view.render("mail/welcome").get_content() + + def test_composers_load_all_views_with_list(self): + self.view.composer(["home", "test"], {"test": "test"}) + + self.assertEqual( + self.view.composers, {"home": {"test": "test"}, "test": {"test": "test"}} + ) + + self.assertEqual(self.view.render("test").rendered_template, "test") + + def test_view_share_updates_dictionary_not_overwrite(self): + self.view.share({"test1": "test1"}) + self.view.share({"test2": "test2"}) + + self.assertEqual(self.view._shared["test1"], "test1") + self.assertEqual(self.view._shared["test2"], "test2") + self.view.render("test", {"var1": "var1"}) + self.assertIn("test1", self.view.dictionary.keys()) + self.assertIn("test2", self.view.dictionary.keys()) + self.assertIn("var1", self.view.dictionary.keys()) + + def test_can_use_namespaced_view(self): + self.view.add_namespaced_location("auth", "tests/integrations/templates/auth") + self.assertIn("Welcome", self.view.render("auth:home").get_content()) + + def test_can_access_shared_helpers(self): + content = self.view.render("test_helpers").get_content() + self.assertIn( + config("application.app_url"), + content, + ) + self.assertIn(url.asset("local", "avatar.jpg"), content) + self.assertIn(url.url("welcome"), content) diff --git a/tests/database/test_user.py b/tests/database/test_user.py deleted file mode 100644 index 56f9d4bfc..000000000 --- a/tests/database/test_user.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Example Database Testcase.""" - -from src.masonite.testing import TestCase - -from app.User import User -from config.factories import factory - - -class TestUser(TestCase): - - def setUp(self): - """Anytime you override the setUp method you must call the setUp method - on the parent class like below. - """ - super().setUp() - - def setUpFactories(self): - """This runs when the test class first starts up. - This does not run before every test case. - """ - factory(User, 1).create() - - def test_created_user(self): - self.assertTrue(User.find(1)) diff --git a/tests/feature/__init__.py b/tests/feature/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/feature/test_feature_works.py b/tests/feature/test_feature_works.py deleted file mode 100644 index 87295714b..000000000 --- a/tests/feature/test_feature_works.py +++ /dev/null @@ -1,7 +0,0 @@ -import unittest - - -class TestFeature(unittest.TestCase): - - def test_feature(self): - self.assertTrue(True) diff --git a/tests/features/broadcasting/test_pusher.py b/tests/features/broadcasting/test_pusher.py new file mode 100644 index 000000000..d74524c28 --- /dev/null +++ b/tests/features/broadcasting/test_pusher.py @@ -0,0 +1,35 @@ +from tests import TestCase +import os +import time +from src.masonite.broadcasting import Channel, PrivateChannel +import pytest + + +class CanBroadcast: + def broadcast_on(self): + return Channel(f"order.{self.order_id}") + + def broadcast_with(self): + return vars(self) + + def broadcast_as(self): + return self.__class__.__name__ + + +class OrderProcessed(CanBroadcast): + def __init__(self): + self.order_id = 1 + + +@pytest.mark.integrations +class TestFileCache(TestCase): + def setUp(self): + super().setUp() + self.application.make("cache") + self.driver = self.application.make("broadcast") + + def test_can_get_file_driver(self): + print(self.driver.channel("order.1", "status", {"status": "processed"})) + + def test_can_fire_class(self): + print(self.driver.channel(OrderProcessed())) diff --git a/tests/features/cache/test_file_cache.py b/tests/features/cache/test_file_cache.py new file mode 100644 index 000000000..794834b3b --- /dev/null +++ b/tests/features/cache/test_file_cache.py @@ -0,0 +1,61 @@ +from tests import TestCase +import os +import time + + +class TestFileCache(TestCase): + def setUp(self): + super().setUp() + self.application.make("cache") + self.driver = self.application.make("cache").store() + + def test_can_get_file_driver(self): + self.driver.put("key", "value") + self.assertEqual(self.driver.get("key"), "value") + self.assertTrue(self.driver.has("key"), "value") + + def test_can_add_file_driver(self): + self.assertEqual(self.driver.add("add_key", "value"), "value") + + def test_can_increment(self): + self.driver.put("count", "1") + self.assertEqual(self.driver.get("count"), "1") + self.driver.increment("count") + self.assertEqual(self.driver.get("count"), "2") + self.driver.decrement("count") + self.assertEqual(self.driver.get("count"), "1") + + def test_will_not_get_expired(self): + self.driver.put("expire", "1", 1) + + time.sleep(2) + self.assertEqual(self.driver.get("expire"), None) + + def test_will_get_not_expired(self): + self.driver.put("expire", "1", 20) + self.assertEqual(self.driver.get("expire"), "1") + + def test_forget(self): + self.driver.put("forget", "1") + self.assertEqual(self.driver.get("forget"), "1") + self.driver.forget("forget") + self.assertEqual(self.driver.get("forget"), None) + + def test_remember(self): + self.driver.remember("remember", lambda cache: (cache.put("remember", "1", 10))) + self.assertEqual(self.driver.get("remember"), "1") + + def test_remember_datatypes(self): + self.driver.remember( + "dic", lambda cache: (cache.put("dic", {"id": 1, "name": "Joe"}, 10)) + ) + self.assertIsInstance(self.driver.get("dic"), dict) + self.driver.remember("list", lambda cache: (cache.put("list", [1, 2, 3], 10))) + self.assertIsInstance(self.driver.get("list"), list) + + def test_flush(self): + self.driver.remember( + "dic", lambda cache: (cache.put("dic", {"id": 1, "name": "Joe"}, 10)) + ) + self.driver.flush() + self.assertIsNone(self.driver.get("dic")) diff --git a/tests/features/cache/test_memcache_cache.py b/tests/features/cache/test_memcache_cache.py new file mode 100644 index 000000000..674438e84 --- /dev/null +++ b/tests/features/cache/test_memcache_cache.py @@ -0,0 +1,63 @@ +from tests import TestCase +import os +import time +import pytest + + +@pytest.mark.integrations +class TestMemcacheCache(TestCase): + def setUp(self): + super().setUp() + self.application.make("cache") + self.driver = self.application.make("cache").store("memcache") + + def test_can_add_file_driver(self): + self.assertEqual(self.driver.add("add_key", "value"), "value") + + def test_can_get_driver(self): + self.driver.put("key", "value") + self.assertEqual(self.driver.get("key"), "value") + self.assertTrue(self.driver.has("key"), "value") + + def test_can_increment(self): + self.driver.put("count", "1") + self.assertEqual(self.driver.get("count"), "1") + self.driver.increment("count") + self.assertEqual(self.driver.get("count"), "2") + self.driver.decrement("count") + self.assertEqual(self.driver.get("count"), "1") + + def test_will_not_get_expired(self): + self.driver.put("expire", "1", 1) + + time.sleep(2) + self.assertEqual(self.driver.get("expire"), None) + + def test_will_get_not_yet_expired(self): + self.driver.put("expire", "1", 20) + self.assertEqual(self.driver.get("expire"), "1") + + def test_forget(self): + self.driver.put("forget", "1") + self.assertEqual(self.driver.get("forget"), "1") + self.driver.forget("forget") + self.assertEqual(self.driver.get("forget"), None) + + def test_remember(self): + self.driver.remember("remember", lambda cache: (cache.put("remember", "1", 10))) + self.assertEqual(self.driver.get("remember"), "1") + + def test_remember_datatypes(self): + self.driver.remember( + "dic", lambda cache: (cache.put("dic", {"id": 1, "name": "Joe"}, 10)) + ) + self.assertIsInstance(self.driver.get("dic"), dict) + self.driver.remember("list", lambda cache: (cache.put("list", [1, 2, 3], 10))) + self.assertIsInstance(self.driver.get("list"), list) + + def test_flush(self): + self.driver.remember( + "dic", lambda cache: (cache.put("dic", {"id": 1, "name": "Joe"}, 10)) + ) + self.driver.flush() + self.assertIsNone(self.driver.get("dic")) diff --git a/tests/features/cache/test_redis_cache.py b/tests/features/cache/test_redis_cache.py new file mode 100644 index 000000000..61e69c2ab --- /dev/null +++ b/tests/features/cache/test_redis_cache.py @@ -0,0 +1,63 @@ +from tests import TestCase +import os +import time +import pytest + + +@pytest.mark.integrations +class TestRedisCache(TestCase): + def setUp(self): + super().setUp() + self.application.make("cache") + self.driver = self.application.make("cache").store("redis") + + def test_can_add_file_driver(self): + self.assertEqual(self.driver.add("add_key", "value"), "value") + + def test_can_get_driver(self): + self.driver.put("key", "value") + self.assertEqual(self.driver.get("key"), "value") + self.assertTrue(self.driver.has("key"), "value") + + def test_can_increment(self): + self.driver.put("count", "1") + self.assertEqual(self.driver.get("count"), "1") + self.driver.increment("count") + self.assertEqual(self.driver.get("count"), "2") + self.driver.decrement("count") + self.assertEqual(self.driver.get("count"), "1") + + def test_will_not_get_expired(self): + self.driver.put("expire", "1", 1) + + time.sleep(2) + self.assertEqual(self.driver.get("expire"), None) + + def test_will_get_not_yet_expired(self): + self.driver.put("expire", "1", 20) + self.assertEqual(self.driver.get("expire"), "1") + + def test_forget(self): + self.driver.put("forget", "1") + self.assertEqual(self.driver.get("forget"), "1") + self.driver.forget("forget") + self.assertEqual(self.driver.get("forget"), None) + + def test_remember(self): + self.driver.remember("remember", lambda cache: (cache.put("remember", "1", 10))) + self.assertEqual(self.driver.get("remember"), "1") + + def test_remember_datatypes(self): + self.driver.remember( + "dic", lambda cache: (cache.put("dic", {"id": 1, "name": "Joe"}, 10)) + ) + self.assertIsInstance(self.driver.get("dic"), dict) + self.driver.remember("list", lambda cache: (cache.put("list", [1, 2, 3], 10))) + self.assertIsInstance(self.driver.get("list"), list) + + def test_flush(self): + self.driver.remember( + "dic", lambda cache: (cache.put("dic", {"id": 1, "name": "Joe"}, 10)) + ) + self.driver.flush() + self.assertIsNone(self.driver.get("dic")) diff --git a/tests/features/event/test_event.py b/tests/features/event/test_event.py new file mode 100644 index 000000000..779422356 --- /dev/null +++ b/tests/features/event/test_event.py @@ -0,0 +1,92 @@ +import time + +import pytest + +from src.masonite.events import Event +from tests import TestCase + + +class UserAddedEvent(Event): + def __init__(self): + pass + + def handle(self): + pass + + +class NewUserEvent(Event): + def __init__(self): + pass + + def handle(self): + pass + + +class SendEmailListener: + def handle(self, event): + pass + + +class UpdateAdminListener: + def handle(self, event, user): + pass + + +class SendAlert: + def handle(self, event): + pass + + +class AdminNotificationListener: + def handle(self): + pass + + +class Subscriber: + def handle(self, event): + pass + + def subscribe(self, event): + event.listen("masonite.event_handled", [self.__class__]) + + +class TestEvent(TestCase): + def setUp(self): + super().setUp() + self.event = self.application.make("event") + self.event.listen(UserAddedEvent, [SendEmailListener]) + + def test_events_registered(self): + self.assertEqual(len(self.event.get_events().get(UserAddedEvent)), 2) + self.event.listen(UserAddedEvent, [AdminNotificationListener]) + self.assertEqual(len(self.event.get_events().get(UserAddedEvent)), 3) + + def test_fire_event_class(self): + self.event.fire(UserAddedEvent) + + def test_fire_event_string(self): + self.event.listen("masonite.*.booted", [SendEmailListener]) + self.event.listen("masonite.commands", [SendEmailListener]) + self.event.listen("view.*", [SendEmailListener]) + self.event.listen("masonite.exception.*", [SendEmailListener]) + self.event.listen("user.added", [UpdateAdminListener]) + self.assertEqual(self.event.fire("masonite.commands"), ["masonite.commands"]) + self.assertEqual(self.event.fire("masonite.orm.booted"), ["masonite.*.booted"]) + self.assertEqual(self.event.fire("masonite.orm"), []) + self.assertEqual(self.event.fire("masonite.command"), []) + self.assertEqual( + self.event.fire("masonite.exception.ZeroDivisionError"), + ["masonite.exception.*"], + ) + self.assertEqual(self.event.fire("view.rendered"), ["view.*"]) + self.assertEqual(self.event.fire("user.added", 1), ["user.added"]) + + def test_fire_event_class(self): + self.event.listen(NewUserEvent, [SendAlert]) + self.event.fire(NewUserEvent()) + + def test_can_subscribe(self): + self.event.subscribe(Subscriber()) + self.assertEqual( + self.event.fire("masonite.event_handled"), ["masonite.event_handled"] + ) diff --git a/tests/features/hashid/test_hashid_middleware.py b/tests/features/hashid/test_hashid_middleware.py new file mode 100644 index 000000000..e931329cd --- /dev/null +++ b/tests/features/hashid/test_hashid_middleware.py @@ -0,0 +1,34 @@ +from src.masonite.essentials.middleware import HashIDMiddleware +from src.masonite.essentials.helpers.hashid import hashid +from tests import TestCase + + +class TestHashID(TestCase): + def test_hashid_hashes_integer(self): + assert hashid(10) == "l9avmeG" + + def test_hashid_hashes_several_integers(self): + assert hashid(10, 20, 30) == "dB1I1uo" + + def test_hashid_decodes_several_integers(self): + assert hashid("B1I1uo", decode=True) == (10, 20, 30) + + def test_hashid_decodes_non_encoded_value_is_falsey(self): + assert not hashid("B8I6ub", decode=True) + + def test_hashid_can_decode_dictionary(self): + assert ( + hashid( + { + "id": "l9avmeG", + "name": "Joe", + }, + decode=True, + ) + == {"id": 10, "name": "Joe"} + ) + + def test_middleware(self): + request = self.make_request(query_string="id=l9avmeG&name=Joe") + HashIDMiddleware().before(request, None) + assert request.all() == {"id": 10, "name": "Joe"} diff --git a/tests/features/hashing/test_hashers.py b/tests/features/hashing/test_hashers.py new file mode 100644 index 000000000..b49fb83c9 --- /dev/null +++ b/tests/features/hashing/test_hashers.py @@ -0,0 +1,33 @@ +from tests import TestCase +from src.masonite.facades import Hash + + +class TestHashers(TestCase): + def test_bcrypt_hasher(self): + hashed = Hash.make("masonite") + assert hashed != "masonite" + assert Hash.check("masonite", hashed) + + def test_bcrypt_needs_rehash(self): + hashed = Hash.make("masonite", options={"rounds": 5}) + # here no options is given so default rounds will be used (10 in tests config) + assert Hash.needs_rehash(hashed) + + def test_argon2_hasher(self): + hashed = Hash.make("masonite", driver="argon2") + assert hashed != "masonite" + assert Hash.check("masonite", hashed, driver="argon2") + + def test_argon2_needs_rehash(self): + hashed = Hash.make( + "masonite", + driver="argon2", + options={ + "memory": 512, + "threads": 8, + "time": 2, + }, + ) + # Here argon2 method is invoked without custom options and will + # use default argon2 configuration, so rehash will be needed + assert Hash.needs_rehash(hashed, driver="argon2") diff --git a/tests/features/loader/test_loader.py b/tests/features/loader/test_loader.py new file mode 100644 index 000000000..18277ea0e --- /dev/null +++ b/tests/features/loader/test_loader.py @@ -0,0 +1,55 @@ +from tests import TestCase + +from src.masonite.loader.Loader import Loader +from src.masonite.exceptions import LoaderNotFound + +OBJ_1 = "test" + + +class TestLoader(TestCase): + def setUp(self): + super().setUp() + self.loader = Loader() + + def test_get_objects(self): + objects = self.loader.get_objects("tests.features.loader.test_loader") + self.assertIsInstance(objects, dict) + self.assertEqual(objects.get("OBJ_1"), "test") + self.assertEqual(objects.get("Loader"), Loader) + + def test_get_objects_for_unexisting_path(self): + objects = self.loader.get_objects("test.not.existing.path") + self.assertIsNone(objects) + + def test_get_parameters(self): + objects = self.loader.get_parameters("tests.features.loader.test_loader") + self.assertIsInstance(objects, dict) + self.assertEqual(objects.get("OBJ_1"), "test") + self.assertEqual(len(objects.keys()), 1) + + def test_find_all(self): + from masoniteorm.models import Model + from tests.integrations.app.User import User + + objects = self.loader.find_all(Model, "tests.integrations.app") + self.assertIsInstance(objects, dict) + self.assertEqual(objects.get("User"), User) + self.assertEqual(len(objects.keys()), 1) + + def test_find(self): + from masoniteorm.models import Model + from tests.integrations.app.User import User + + obj = self.loader.find(Model, "tests.integrations.app", "User") + self.assertEqual(obj, User) + + def test_find_methods_raise_exception_if_specified(self): + from masoniteorm.models import Model + + with self.assertRaises(LoaderNotFound): + self.loader.find( + Model, "test.not.existing.path", "User", raise_exception=True + ) + + with self.assertRaises(LoaderNotFound): + self.loader.find_all(Model, "test.not.existing.path", raise_exception=True) diff --git a/tests/features/mail/test_mailable.py b/tests/features/mail/test_mailable.py new file mode 100644 index 000000000..a713a7be7 --- /dev/null +++ b/tests/features/mail/test_mailable.py @@ -0,0 +1,90 @@ +from tests import TestCase +from src.masonite.mail import Mailable +from src.masonite.mail.Recipient import Recipient + + +class Welcome(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + .driver("smtp") + ) + + +class ViewMailable(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .view("mailables.welcome", {}) + ) + + +class TestMailable(TestCase): + def setUp(self): + super().setUp() + self.application.make("mail") + + def test_build_mail(self): + mailable = Welcome().build().get_options() + self.assertEqual(mailable.get("to"), "idmann509@gmail.com") + self.assertEqual(mailable.get("from"), "joe@masoniteproject.com") + self.assertEqual(mailable.get("subject"), "Masonite 4") + self.assertEqual(mailable.get("text_content"), "Hello from Masonite!") + self.assertEqual(mailable.get("html_content"), "

Hello from Masonite!

") + self.assertEqual(mailable.get("reply_to"), "") + self.assertEqual(mailable.get("driver"), "smtp") + + def test_build_mailable_view(self): + mailable = ( + ViewMailable().set_application(self.application).build().get_options() + ) + self.assertEqual(mailable.get("html_content"), "

Welcome Email

") + mailable = ViewMailable().set_application(self.application).get_response() + self.assertEqual(mailable, "

Welcome Email

") + + def test_attach(self): + self.assertTrue( + len( + Welcome() + .attach("invoice", "tests/integrations/storage/invoice.pdf") + .build() + .get_options() + .get("attachments") + ) + == 1 + ) + + def test_recipient(self): + to = Recipient("idmann509@gmail.com, joe@masoniteproject.com") + self.assertEqual( + to.header(), ", " + ) + to = Recipient("Joseph Mancuso , joe@masoniteproject.com") + self.assertEqual( + to.header(), + "Joseph Mancuso , ", + ) + + def test_recipient(self): + to = Recipient("idmann509@gmail.com, joe@masoniteproject.com") + self.assertEqual( + to.header(), ", " + ) + to = Recipient("Joseph Mancuso , joe@masoniteproject.com") + self.assertEqual( + to.header(), + "Joseph Mancuso , ", + ) + + to = Recipient( + ["Joseph Mancuso ", "joe@masoniteproject.com"] + ) + self.assertEqual( + to.header(), + "Joseph Mancuso , ", + ) diff --git a/tests/features/mail/test_mailgun_driver.py b/tests/features/mail/test_mailgun_driver.py new file mode 100644 index 000000000..0f6f20d47 --- /dev/null +++ b/tests/features/mail/test_mailgun_driver.py @@ -0,0 +1,22 @@ +import pytest +from tests import TestCase +from src.masonite.mail import Mailable + + +class Welcome(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + ) + + +@pytest.mark.integrations +class TestMailgunDriver(TestCase): + def test_send_mailable(self): + self.application.make("mail").mailable( + Welcome().attach("invoice", "tests/integrations/storage/invoice.pdf") + ) diff --git a/tests/features/mail/test_mock_mail.py b/tests/features/mail/test_mock_mail.py new file mode 100644 index 000000000..04c96eeec --- /dev/null +++ b/tests/features/mail/test_mock_mail.py @@ -0,0 +1,42 @@ +from tests import TestCase +from src.masonite.mail import Mailable + + +class Welcome(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("text from Masonite!") + .html("

Hello from Masonite!

") + ) + + +class TestSMTPDriver(TestCase): + def setUp(self): + super().setUp() + self.fake("mail") + + def tearDown(self): + super().tearDown() + self.restore("mail") + + def test_mock_mail(self): + self.fake("mail") + welcome_email = self.application.make("mail").mailable(Welcome()).send() + ( + welcome_email.seeEmailCc("") + .seeEmailBcc("") + .seeEmailContains("Hello from Masonite!") + .seeEmailContains("text from Masonite!") + .seeEmailFrom("joe@masoniteproject.com") + .seeEmailCountEquals(1) + .send() + .seeEmailCountEquals(2) + ) + + def test_mock_mail_sending(self): + self.fake("mail") + welcome_email = self.application.make("mail").mailable(Welcome()) + (welcome_email.seeEmailWasNotSent().send().seeEmailWasSent()) diff --git a/tests/features/mail/test_smtp_driver.py b/tests/features/mail/test_smtp_driver.py new file mode 100644 index 000000000..9f66f94f4 --- /dev/null +++ b/tests/features/mail/test_smtp_driver.py @@ -0,0 +1,20 @@ +from tests import TestCase +from src.masonite.mail import Mailable +import pytest + + +class Welcome(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + ) + + +@pytest.mark.integrations +class TestSMTPDriver(TestCase): + def test_send_mailable(self): + self.application.make("mail").mailable(Welcome()).send() diff --git a/tests/features/mail/test_terminal.py b/tests/features/mail/test_terminal.py new file mode 100644 index 000000000..dbf3d8940 --- /dev/null +++ b/tests/features/mail/test_terminal.py @@ -0,0 +1,37 @@ +from tests import TestCase +from src.masonite.mail import Mailable + + +class Welcome(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + ) + + +class Other(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Other") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + .driver("terminal") + ) + + +class TestTerminalDriver(TestCase): + def test_send_mailable(self): + self.application.make("mail").mailable( + Welcome().attach("invoice", "tests/integrations/storage/invoice.pdf") + ).send(driver="terminal") + + def test_define_driver_with_mailable(self): + self.application.make("mail").mailable( + Other().attach("invoice", "tests/integrations/storage/invoice.pdf") + ).send() diff --git a/tests/features/notification/test_anonymous_notifiable.py b/tests/features/notification/test_anonymous_notifiable.py new file mode 100644 index 000000000..1558e6035 --- /dev/null +++ b/tests/features/notification/test_anonymous_notifiable.py @@ -0,0 +1,54 @@ +from tests import TestCase + +from src.masonite.notification import Notification, AnonymousNotifiable +from src.masonite.mail import Mailable + + +class WelcomeNotification(Notification): + def to_mail(self, notifiable): + return Mailable().text("Welcome") + + def via(self, notifiable): + return ["mail"] + + +class TestAnonymousNotifiable(TestCase): + def test_one_routing(self): + notifiable = AnonymousNotifiable(self.application).route( + "mail", "user@example.com" + ) + self.assertDictEqual({"mail": "user@example.com"}, notifiable._routes) + + def test_multiple_routing(self): + notifiable = ( + AnonymousNotifiable(self.application) + .route("mail", "user@example.com") + .route("slack", "#general") + ) + self.assertDictEqual( + {"mail": "user@example.com", "slack": "#general"}, notifiable._routes + ) + + def test_sending_notification(self): + self.application.make("notification").route("mail", "user@example.com").send( + WelcomeNotification() + ) + + def test_can_override_dry_when_sending(self): + AnonymousNotifiable(self.application).route("mail", "user@example.com").send( + WelcomeNotification(), dry=True + ) + self.application.make("notification").dry_notifications.keys() == 1 + + def test_can_override_fail_silently_when_sending(self): + class FailingNotification(Notification): + def to_slack(self, notifiable): + raise Exception("Error") + + def via(self, notifiable): + return ["slack"] + + AnonymousNotifiable(self.application).route("slack", "#general").send( + FailingNotification(), fail_silently=True + ) + # no assertion raised :) diff --git a/tests/features/notification/test_broadcast_driver.py b/tests/features/notification/test_broadcast_driver.py new file mode 100644 index 000000000..761799cc9 --- /dev/null +++ b/tests/features/notification/test_broadcast_driver.py @@ -0,0 +1,35 @@ +import pytest +from tests import TestCase +from src.masonite.notification import Notification, Notifiable +from masoniteorm.models import Model + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + def route_notification_for_broadcast(self): + return f"user.{self.id}" + + +class WelcomeNotification(Notification): + def to_broadcast(self, notifiable): + return {"data": "Welcome"} + + def via(self, notifiable): + return ["broadcast"] + + +@pytest.mark.integrations +class TestBroadcastDriver(TestCase): + def setUp(self): + super().setUp() + self.notification = self.application.make("notification") + + def test_send_to_anonymous(self): + self.notification.route("broadcast", "all").send(WelcomeNotification()) + + def test_send_to_notifiable(self): + user = User.find(1) + user.notify(WelcomeNotification()) diff --git a/tests/features/notification/test_database_driver.py b/tests/features/notification/test_database_driver.py new file mode 100644 index 000000000..440f651e8 --- /dev/null +++ b/tests/features/notification/test_database_driver.py @@ -0,0 +1,132 @@ +from masoniteorm import connections +from masoniteorm.models import Model +import pendulum + +from tests import TestCase +from src.masonite.tests import DatabaseTransactions +from src.masonite.notification import Notification, Notifiable +from src.masonite.notification import DatabaseNotification + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class WelcomeNotification(Notification): + def to_database(self, notifiable): + return {"data": "Welcome {0}!".format(notifiable.name)} + + def via(self, notifiable): + return ["database"] + + +notif_data = { + "id": "1234", + "read_at": None, + "type": "TestNotification", + "data": "test", + "notifiable_id": 1, + "notifiable_type": "users", +} + + +class TestDatabaseDriver(TestCase, DatabaseTransactions): + connection = None + + def setUp(self): + super().setUp() + self.notification = self.application.make("notification") + + def test_send_to_notifiable(self): + user = User.find(1) + count = user.notifications.count() + user.notify(WelcomeNotification()) + assert user.notifications.count() == count + 1 + + def test_database_notification_is_created_correctly(self): + user = User.find(1) + notification = user.notify(WelcomeNotification()) + assert notification["id"] + assert not notification["read_at"] + assert notification["data"] == '{"data": "Welcome Joe!"}' + assert notification["notifiable_id"] == user.id + assert notification["notifiable_type"] == "users" + + def test_notify_multiple_users(self): + User.create({"name": "sam", "email": "sam@test.com", "password": "secret"}) + users = User.all() # == 2 users + self.notification.send(users, WelcomeNotification()) + assert users[0].notifications.count() == 1 + assert users[1].notifications.count() == 1 + + +class TestDatabaseNotification(TestCase, DatabaseTransactions): + connection = None + + def test_database_notification_read_state(self): + notification = DatabaseNotification.create( + { + **notif_data, + "read_at": pendulum.now().to_datetime_string(), + } + ) + self.assertTrue(notification.is_read) + notification.read_at = None + self.assertFalse(notification.is_read) + + def test_database_notification_unread_state(self): + notification = DatabaseNotification.create( + { + **notif_data, + "read_at": pendulum.yesterday().to_datetime_string(), + } + ) + self.assertFalse(notification.is_unread) + notification.read_at = None + self.assertTrue(notification.is_unread) + + def test_database_notification_mark_as_read(self): + notification = DatabaseNotification.create(notif_data) + notification.mark_as_read() + self.assertNotEqual(None, notification.read_at) + + def test_database_notification_mark_as_unread(self): + notification = DatabaseNotification.create( + { + **notif_data, + "read_at": pendulum.now().to_datetime_string(), + } + ) + notification.mark_as_unread() + self.assertEqual(None, notification.read_at) + + def test_notifiable_get_notifications(self): + user = User.find(1) + self.assertEqual(0, user.notifications.count()) + user.notify(WelcomeNotification()) + self.assertEqual(1, user.notifications.count()) + + def test_notifiable_get_read_notifications(self): + user = User.find(1) + self.assertEqual(0, user.read_notifications.count()) + DatabaseNotification.create( + { + **notif_data, + "read_at": pendulum.yesterday().to_datetime_string(), + "notifiable_id": user.id, + } + ) + self.assertEqual(1, user.read_notifications.count()) + + def test_notifiable_get_unread_notifications(self): + user = User.find(1) + self.assertEqual(0, user.unread_notifications.count()) + DatabaseNotification.create( + { + **notif_data, + "notifiable_id": user.id, + } + ) + self.assertEqual(1, user.unread_notifications.count()) diff --git a/tests/features/notification/test_integrations.py b/tests/features/notification/test_integrations.py new file mode 100644 index 000000000..36b20466b --- /dev/null +++ b/tests/features/notification/test_integrations.py @@ -0,0 +1,86 @@ +from tests.features.notification.test_anonymous_notifiable import WelcomeNotification +from tests import TestCase +from src.masonite.mail import Mailable +from src.masonite.notification import Notification, SlackMessage, Sms, Notifiable +from masoniteorm.models import Model + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + def route_notification_for_broadcast(self): + return f"user.{self.id}" + + def route_notification_for_slack(self): + return "#bot" + + def route_notification_for_vonage(self): + return "+33123456789" + + def route_notification_for_mail(self): + return self.email + + +class WelcomeNotification(Notification): + def to_mail(self, notifiable): + return ( + Mailable() + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + ) + + def to_database(self, notifiable): + return {"data": "Welcome {0}!".format(notifiable.name)} + + def to_broadcast(self, notifiable): + return {"data": "Welcome"} + + def to_slack(self, notifiable): + return SlackMessage().text("Welcome !").from_("test-bot") + + def to_vonage(self, notifiable): + return Sms().text("Welcome !").from_("123456") + + def via(self, notifiable): + return ["mail", "database", "slack", "vonage", "broadcast"] + + +class TestIntegrationsNotifications(TestCase): + def setUp(self): + super().setUp() + self.fake("notification") + + def tearDown(self): + super().tearDown() + self.restore("notification") + + def test_all_drivers_with_anonymous(self): + notif = ( + self.application.make("notification") + .route("mail", "user@example.com") + .route("slack", "#general") + .route("broadcast", "all") + .route("vonage", "+33456789012") + ) + notif.send(WelcomeNotification()).assertSentTo( + "user@example.com", WelcomeNotification + ).assertSentTo("#general", WelcomeNotification).assertSentTo( + "all", WelcomeNotification + ).assertSentTo( + "+33456789012", WelcomeNotification + ) + + def test_all_drivers_with_notifiable(self): + self.application.make("notification").assertNothingSent() + user = User.find(1) + user.notify(WelcomeNotification()) + self.application.make("notification").assertSentTo( + "user@example.com", WelcomeNotification + ).assertSentTo("#general", WelcomeNotification).assertSentTo( + "all", WelcomeNotification + ).assertSentTo( + "+33456789012", WelcomeNotification + ) diff --git a/tests/features/notification/test_mail_driver.py b/tests/features/notification/test_mail_driver.py new file mode 100644 index 000000000..4a4688059 --- /dev/null +++ b/tests/features/notification/test_mail_driver.py @@ -0,0 +1,59 @@ +from tests import TestCase +from src.masonite.notification import Notification, Notifiable +from src.masonite.mail import Mailable +from masoniteorm.models import Model + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class WelcomeUserNotification(Notification): + def to_mail(self, notifiable): + return ( + Mailable() + .to(notifiable.email) + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text(f"Hello {notifiable.name}") + ) + + def via(self, notifiable): + return ["mail"] + + +class WelcomeNotification(Notification): + def to_mail(self, notifiable): + return ( + Mailable() + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + ) + + def via(self, notifiable): + return ["mail"] + + +class TestMailDriver(TestCase): + def setUp(self): + super().setUp() + self.notification = self.application.make("notification") + + def test_send_to_anonymous(self): + self.notification.route("mail", "test@mail.com").send(WelcomeNotification()) + + def test_send_to_notifiable(self): + user = User.find(1) + user.notify(WelcomeUserNotification()) + + def test_send_and_override_driver(self): + # TODO: but I don't really know how to proceed as driver can't be defined anymore + # in the Mailable + # Some API solutions: + # self.notification.route("mail", "test@mail.com").send(WelcomeNotification()).driver("log") + # self.notification.route("mail", "test@mail.com").send(WelcomeNotification(), driver="log") + # self.notification.route("mail", "test@mail.com", driver="log").send(WelcomeNotification()) + pass diff --git a/tests/features/notification/test_mock_notification.py b/tests/features/notification/test_mock_notification.py new file mode 100644 index 000000000..b53f07c04 --- /dev/null +++ b/tests/features/notification/test_mock_notification.py @@ -0,0 +1,166 @@ +from tests import TestCase + +from src.masonite.notification import Notification, Notifiable, SlackMessage +from src.masonite.mail import Mailable +from masoniteorm.models import Model + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class WelcomeNotification(Notification): + def to_mail(self, notifiable): + return ( + Mailable() + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + ) + + def via(self, notifiable): + return ["mail"] + + +class OtherNotification(Notification): + def to_mail(self, notifiable): + return ( + Mailable() + .subject("Other") + .from_("sam@masoniteproject.com") + .text("Hello again!") + ) + + def via(self, notifiable): + return ["mail"] + + +class OrderNotification(Notification): + def __init__(self, order_id): + self.order_id = order_id + + def to_mail(self, notifiable): + return ( + Mailable() + .subject(f"Order {self.order_id} shipped !") + .from_("sam@masoniteproject.com") + .text(f"{notifiable.name}, your order has been shipped") + ) + + def to_slack(self, notifiable): + return SlackMessage().text(f"Order {self.order_id} has been shipped !") + + def via(self, notifiable): + return ["mail", "slack"] + + +class TestMockNotification(TestCase): + def setUp(self): + super().setUp() + self.fake("notification") + + def tearDown(self): + super().tearDown() + self.restore("notification") + + def test_assert_nothing_sent(self): + notification = self.application.make("notification") + notification.assertNothingSent() + + def test_assert_count(self): + notification = self.application.make("notification") + notification.assertCount(0) + notification.route("mail", "test@mail.com").send(WelcomeNotification()) + notification.assertCount(1) + notification.route("mail", "test2@mail.com").send(WelcomeNotification()) + notification.assertCount(2) + + def test_reset_count(self): + notification = self.application.make("notification") + notification.assertNothingSent() + notification.route("mail", "test@mail.com").send(WelcomeNotification()) + notification.resetCount() + notification.assertNothingSent() + + def test_assert_sent_to_with_anonymous(self): + notification = self.application.make("notification") + notification.route("mail", "test@mail.com").send(WelcomeNotification()) + notification.assertSentTo("test@mail.com", WelcomeNotification) + + notification.route("vonage", "123456").route("slack", "#general").send( + WelcomeNotification() + ) + notification.assertSentTo("123456", WelcomeNotification) + notification.assertSentTo("#general", WelcomeNotification) + + def test_assert_not_sent_to(self): + notification = self.application.make("notification") + notification.resetCount() + notification.assertNotSentTo("test@mail.com", WelcomeNotification) + notification.route("vonage", "123456").send(OtherNotification()) + notification.assertNotSentTo("123456", WelcomeNotification) + notification.assertNotSentTo("test@mail.com", OtherNotification) + + def test_assert_sent_to_with_notifiable(self): + notification = self.application.make("notification") + user = User.find(1) + user.notify(WelcomeNotification()) + notification.assertSentTo(user, WelcomeNotification) + user.notify(OtherNotification()) + notification.assertSentTo(user, OtherNotification) + notification.assertCount(2) + + def test_assert_sent_to_with_count(self): + notification = self.application.make("notification") + user = User.find(1) + user.notify(WelcomeNotification()) + user.notify(WelcomeNotification()) + notification.assertSentTo(user, WelcomeNotification, count=2) + + user.notify(OtherNotification()) + user.notify(OtherNotification()) + with self.assertRaises(AssertionError): + notification.assertSentTo(user, OtherNotification, count=1) + + def test_assert_with_assertions_on_notification(self): + user = User.find(1) + user.notify(OrderNotification(6)) + self.application.make("notification").assertSentTo( + user, + OrderNotification, + lambda user, notification: ( + notification.assertSentVia("mail", "slack") + .assertEqual(notification.order_id, 6) + .assertEqual( + notification.to_mail(user).get_options().get("subject"), + "Order 6 shipped !", + ) + .assertIn( + user.name, + notification.to_mail(user).get_options().get("text_content"), + ) + ), + ) + + def test_last_notification(self): + notification = self.application.make("notification") + message = WelcomeNotification() + notification.route("mail", "test@mail.com").send(message) + assert message == notification.last() + + def test_assert_last(self): + self.application.make("notification").route("mail", "test@mail.com").route( + "slack", "#general" + ).send(OrderNotification(10)) + self.application.make("notification").assertLast( + lambda user, notif: ( + notif.assertSentVia("mail") + .assertEqual(notif.order_id, 10) + .assertEqual( + notif.to_slack(user).get_options().get("text"), + "Order 10 has been shipped !", + ) + ) + ) diff --git a/tests/features/notification/test_notification.py b/tests/features/notification/test_notification.py new file mode 100644 index 000000000..d31c7d445 --- /dev/null +++ b/tests/features/notification/test_notification.py @@ -0,0 +1,63 @@ +from tests import TestCase + +from src.masonite.notification import Notification, Notifiable +from src.masonite.mail import Mailable +from masoniteorm.models import Model + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password"] + + +class WelcomeNotification(Notification): + def to_mail(self, notifiable): + return ( + Mailable() + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + ) + + def via(self, notifiable): + return ["mail"] + + +class TestNotification(TestCase): + def test_should_send(self): + notification = WelcomeNotification() + self.assertTrue(notification.should_send()) + + def test_ignore_errors(self): + notification = WelcomeNotification() + self.assertFalse(notification.ignore_errors()) + + def test_notification_type(self): + self.assertEqual("WelcomeNotification", WelcomeNotification().type()) + + +DRY = True + + +class TestNotificationManager(TestCase): + def test_dry_mode(self): + # locally when sending to anonymous or notifiable + self.assertEqual( + self.application.make("notification") + .route("mail", "test@mail.com") + .send(WelcomeNotification(), dry=True), + None, + ) + + user = User.find(1) + user.notify(WelcomeNotification(), dry=True) + + # globally + # override settings for testing purposes + self.assertEqual( + self.application.make("notification") + .route("mail", "test@mail.com") + .send(WelcomeNotification(), dry=True), + None, + ) diff --git a/tests/features/notification/test_slack_driver.py b/tests/features/notification/test_slack_driver.py new file mode 100644 index 000000000..b4031ede9 --- /dev/null +++ b/tests/features/notification/test_slack_driver.py @@ -0,0 +1,169 @@ +import pytest +import responses +from tests import TestCase +from src.masonite.notification import Notification, Notifiable, SlackMessage +from src.masonite.exceptions import NotificationException + +from masoniteorm.models import Model + +# fake webhook for tests +webhook_url = "https://hooks.slack.com/services/X/Y/Z" +webhook_url_2 = "https://hooks.slack.com/services/A/B/C" + + +def route_for_slack(self): + return "#bot" + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password", "phone"] + + def route_notification_for_slack(self): + return route_for_slack(self) + + +class WelcomeUserNotification(Notification): + def to_slack(self, notifiable): + return SlackMessage().text(f"Welcome {notifiable.name}!").from_("test-bot") + + def via(self, notifiable): + return ["slack"] + + +class WelcomeNotification(Notification): + def to_slack(self, notifiable): + return SlackMessage().text("Welcome !").from_("test-bot") + + def via(self, notifiable): + return ["slack"] + + +class OtherNotification(Notification): + def to_slack(self, notifiable): + return ( + SlackMessage().to(["#general", "#news"]).text("Welcome !").from_("test-bot") + ) + + def via(self, notifiable): + return ["slack"] + + +class TestSlackWebhookDriver(TestCase): + def setUp(self): + super().setUp() + self.notification = self.application.make("notification") + + @responses.activate + def test_sending_to_anonymous(self): + responses.add(responses.POST, webhook_url, body=b"ok") + self.notification.route("slack", webhook_url).notify(WelcomeNotification()) + self.assertTrue(responses.assert_call_count(webhook_url, 1)) + + @responses.activate + def test_sending_to_notifiable(self): + responses.add(responses.POST, webhook_url, body=b"ok") + User.route_notification_for_slack = lambda notifiable: webhook_url + user = User.find(1) + user.notify(WelcomeNotification()) + self.assertTrue(responses.assert_call_count(webhook_url, 1)) + User.route_notification_for_slack = route_for_slack + + @responses.activate + def test_sending_to_multiple_webhooks(self): + responses.add(responses.POST, webhook_url, body=b"ok") + responses.add(responses.POST, webhook_url_2, body=b"ok") + User.route_notification_for_slack = lambda notifiable: [ + webhook_url, + webhook_url_2, + ] + user = User.find(1) + user.notify(WelcomeNotification()) + self.assertTrue(responses.assert_call_count(webhook_url, 1)) + self.assertTrue(responses.assert_call_count(webhook_url_2, 1)) + User.route_notification_for_slack = route_for_slack + + +class TestSlackAPIDriver(TestCase): + url = "https://slack.com/api/chat.postMessage" + channel_url = "https://slack.com/api/conversations.list" + + def setUp(self): + super().setUp() + self.notification = self.application.make("notification") + + def test_sending_without_credentials(self): + with self.assertRaises(NotificationException) as e: + self.notification.route("slack", "123456").notify(WelcomeNotification()) + self.assertIn("not_authed", str(e.exception)) + + @responses.activate + def test_sending_to_anonymous(self): + responses.add( + responses.POST, + self.url, + body=b'{"ok": "True"}', + ) + responses.add( + responses.POST, + self.channel_url, + body=b'{"channels": [{"name": "bot", "id": "123"}]}', + ) + self.notification.route("slack", "#bot").notify(WelcomeNotification()) + # to convert #bot to Channel ID + self.assertTrue(responses.assert_call_count(self.channel_url, 1)) + self.assertTrue(responses.assert_call_count(self.url, 1)) + + @responses.activate + def test_sending_to_notifiable(self): + user = User.find(1) + responses.add( + responses.POST, + self.url, + body=b'{"ok": "True"}', + ) + responses.add( + responses.POST, + self.channel_url, + body=b'{"channels": [{"name": "bot", "id": "123"}]}', + ) + user.notify(WelcomeUserNotification()) + self.assertTrue(responses.assert_call_count(self.url, 1)) + + @responses.activate + @pytest.mark.skip( + reason="Failing because user defined routing takes precedence. What should be the behaviour ?" + ) + def test_sending_to_multiple_channels(self): + user = User.find(1) + responses.add( + responses.POST, + self.url, + body=b'{"ok": "True"}', + ) + responses.add( + responses.POST, + self.channel_url, + body=b'{"channels": [{"name": "bot", "id": "123"}, {"name": "general", "id": "456"}]}', + ) + user.notify(OtherNotification()) + self.assertTrue(responses.assert_call_count(self.channel_url, 2)) + self.assertTrue(responses.assert_call_count(self.url, 2)) + + @responses.activate + def test_convert_channel(self): + channel_id = self.notification.get_driver("slack").convert_channel( + "123456", "token" + ) + self.assertEqual(channel_id, "123456") + + responses.add( + responses.POST, + self.channel_url, + body=b'{"channels": [{"name": "general", "id": "654321"}]}', + ) + channel_id = self.notification.get_driver("slack").convert_channel( + "#general", "token" + ) + self.assertEqual(channel_id, "654321") diff --git a/tests/features/notification/test_slack_message.py b/tests/features/notification/test_slack_message.py new file mode 100644 index 000000000..83d42f48d --- /dev/null +++ b/tests/features/notification/test_slack_message.py @@ -0,0 +1,64 @@ +from tests import TestCase +from src.masonite.notification import SlackMessage + + +class WelcomeToSlack(SlackMessage): + def build(self): + return ( + self.to("#general") + .from_("sam") + .text("Hello from Masonite!") + .link_names() + .unfurl_links() + .without_markdown() + .can_reply() + .mode(2) # API MODE + ) + + +class TestSlackMessage(TestCase): + def test_build_message(self): + slack_message = WelcomeToSlack().build().get_options() + self.assertEqual(slack_message.get("channel"), "#general") + self.assertEqual(slack_message.get("username"), "sam") + self.assertEqual(slack_message.get("text"), "Hello from Masonite!") + self.assertEqual(slack_message.get("link_names"), True) + self.assertEqual(slack_message.get("unfurl_links"), True) + self.assertEqual(slack_message.get("unfurl_media"), True) + self.assertEqual(slack_message.get("mrkdwn"), False) + self.assertEqual(slack_message.get("reply_broadcast"), True) + self.assertEqual(slack_message.get("as_user"), False) + self.assertEqual(slack_message.get("blocks"), "[]") + + def test_from_options(self): + slack_message = SlackMessage().from_("sam", icon=":ghost").build().get_options() + self.assertEqual(slack_message.get("username"), "sam") + self.assertEqual(slack_message.get("icon_emoji"), ":ghost") + self.assertEqual(slack_message.get("icon_url"), "") + slack_message = SlackMessage().from_("sam", url="#").build().get_options() + self.assertEqual(slack_message.get("username"), "sam") + self.assertEqual(slack_message.get("icon_url"), "#") + self.assertEqual(slack_message.get("icon_emoji"), "") + + def test_build_with_blocks(self): + from slackblocks import DividerBlock, HeaderBlock + + slack_message = ( + SlackMessage() + .from_("Sam") + .text("Hello") + .block(HeaderBlock("Header title", block_id="1")) + .block(DividerBlock(block_id="2")) + .build() + .get_options() + ) + self.assertEqual( + slack_message.get("blocks"), + '[{"type": "header", "block_id": "1", "text": {"type": "plain_text", "text": "Header title"}}, {"type": "divider", "block_id": "2"}]', + ) + + def test_api_mode_options(self): + slack_message = SlackMessage().as_current_user().token("123456") + self.assertEqual(slack_message._token, "123456") + slack_message_options = slack_message.build().get_options() + self.assertEqual(slack_message_options.get("as_user"), True) diff --git a/tests/features/notification/test_sms.py b/tests/features/notification/test_sms.py new file mode 100644 index 000000000..e8e2b49a7 --- /dev/null +++ b/tests/features/notification/test_sms.py @@ -0,0 +1,24 @@ +from tests import TestCase +from src.masonite.notification import Sms + + +class Welcome(Sms): + def build(self): + return self.to("+33612345678").from_("+44123456789").text("Masonite 4") + + +class TestSms(TestCase): + def test_build_sms(self): + sms = Welcome().build().get_options() + self.assertEqual(sms.get("to"), "+33612345678") + self.assertEqual(sms.get("from"), "+44123456789") + self.assertEqual(sms.get("text"), "Masonite 4") + self.assertEqual(sms.get("type"), "text") + + def test_set_unicode(self): + sms = Welcome().set_unicode().build().get_options() + self.assertEqual(sms.get("type"), "unicode") + + def test_adding_client_ref(self): + sms = Welcome().client_ref("ABCD").build().get_options() + self.assertEqual(sms.get("client-ref"), "ABCD") diff --git a/tests/features/notification/test_vonage_driver.py b/tests/features/notification/test_vonage_driver.py new file mode 100644 index 000000000..190252302 --- /dev/null +++ b/tests/features/notification/test_vonage_driver.py @@ -0,0 +1,101 @@ +from tests import TestCase +from unittest.mock import patch +from src.masonite.notification import Notification, Notifiable, Sms, Textable +from src.masonite.exceptions import NotificationException + +from masoniteorm.models import Model + + +class User(Model, Notifiable): + """User Model""" + + __fillable__ = ["name", "email", "password", "phone"] + + def route_notification_for_vonage(self): + return "+33123456789" + + +class WelcomeUserNotification(Notification, Textable): + def to_vonage(self, notifiable): + return self.text_message("Welcome !").from_("123456") + + def via(self, notifiable): + return ["vonage"] + + +class WelcomeNotification(Notification): + def to_vonage(self, notifiable): + return Sms().text("Welcome !").from_("123456") + + def via(self, notifiable): + return ["vonage"] + + def should_send(self): + return True + + +class OtherNotification(Notification): + def to_vonage(self, notifiable): + return Sms().text("Welcome !") + + def via(self, notifiable): + return ["vonage"] + + +class VonageAPIMock(object): + @staticmethod + def send_success(): + return {"hoho": "hihi", "message-count": 1, "messages": [{"status": "0"}]} + + @staticmethod + def send_error(error="Missing api_key", status=2): + return { + "message-count": 1, + "messages": [{"status": str(status), "error-text": error}], + } + + +class TestVonageDriver(TestCase): + def setUp(self): + super().setUp() + self.notification = self.application.make("notification") + + def test_sending_without_credentials(self): + with self.assertRaises(NotificationException) as e: + self.notification.route("vonage", "+33123456789").send( + WelcomeNotification() + ) + error_message = str(e.exception) + self.assertIn("Code [2]", error_message) + + def test_send_to_anonymous(self): + with patch("vonage.sms.Sms") as MockSmsClass: + MockSmsClass.return_value.send_message.return_value = ( + VonageAPIMock().send_success() + ) + self.notification.route("vonage", "+33123456789").send( + WelcomeNotification() + ) + + def test_send_to_notifiable(self): + with patch("vonage.sms.Sms") as MockSmsClass: + MockSmsClass.return_value.send_message.return_value = ( + VonageAPIMock().send_success() + ) + user = User.find(1) + user.notify(WelcomeUserNotification()) + + def test_send_to_notifiable_with_route_notification_for(self): + with patch("vonage.sms.Sms") as MockSmsClass: + MockSmsClass.return_value.send_message.return_value = ( + VonageAPIMock().send_success() + ) + user = User.find(1) + user.notify(WelcomeNotification()) + + def test_global_send_from_is_used_when_not_specified(self): + notifiable = self.notification.route("vonage", "+33123456789") + sms = self.notification.get_driver("vonage").build( + notifiable, OtherNotification() + ) + self.assertEqual(sms._from, "+33000000000") diff --git a/tests/features/packages/test_package_provider.py b/tests/features/packages/test_package_provider.py new file mode 100644 index 000000000..80a3e0d50 --- /dev/null +++ b/tests/features/packages/test_package_provider.py @@ -0,0 +1,43 @@ +from src.masonite.configuration import config + +from tests import TestCase + + +class TestPackageProvider(TestCase): + def test_config_is_loaded(self): + self.assertEqual(config("test_package.param_2"), 1) + + def test_config_is_merged(self): + self.assertEqual(config("test_package.param_1"), 0) + + # def test_package_config_can_be_published(self): + # pp = self.application.providers[-1] + # import pdb + + # pdb.set_trace() + + def test_views_are_registered(self): + self.application.make("view").exists("test_package:package") + self.application.make("view").exists("test_package:admin.settings") + # this one has been published in project and overriden + # check that the project view is used and not the package view + self.assertEqual( + self.application.make("view") + .render("test_package:admin.settings") + .rendered_template, + "overriden", + ) + + def test_commands_are_registered(self): + self.craft("test_package:command1").assertSuccess() + self.craft("test_package:command2").assertSuccess() + + # def test_routes_are_registered(self): + # # nb = len(self.application.make("router").routes) + # # import pdb + + # # pdb.set_trace() + # # for r in self.application.make("router").routes: + # # print(f"{r.url} -> {r._name}") + # self.get("/package/test/").assertContains("index") + # self.get("/api/package/test/").assertCreated() diff --git a/src/masonite/snippets/scaffold/view.html b/tests/features/packages/test_publish.py similarity index 100% rename from src/masonite/snippets/scaffold/view.html rename to tests/features/packages/test_publish.py diff --git a/tests/features/queues/test_async_driver.py b/tests/features/queues/test_async_driver.py new file mode 100644 index 000000000..8264fd4fe --- /dev/null +++ b/tests/features/queues/test_async_driver.py @@ -0,0 +1,12 @@ +from tests import TestCase +from src.masonite.queues import Queueable +import os +import time + + +from tests.integrations.app.SayHi import SayHello + + +class TestAsyncDriver(TestCase): + def test_async_push(self): + self.application.make("queue").push(SayHello(), driver="async") diff --git a/tests/features/scheduling/test_handler.py b/tests/features/scheduling/test_handler.py new file mode 100644 index 000000000..a8a0e9d3e --- /dev/null +++ b/tests/features/scheduling/test_handler.py @@ -0,0 +1,27 @@ +import pytest +import pendulum +from src.masonite.scheduling import TaskHandler, Task +from tests import TestCase + + +class MockTask1(Task): + run_every = "1 minutes" + timezone = "America/New_York" + + def handle(self): + print("hello 1") + + +class MockTask2(Task): + run_every = "1 minutes" + timezone = "America/New_York" + + def handle(self): + print("hello 2") + + +class TestHandler(TestCase): + def test_handler_adds_and_runs_tasks(self): + self.handler = TaskHandler(self.application) + self.handler.add(MockTask1, MockTask2()) + self.handler.run() diff --git a/tests/features/scheduling/test_scheduling.py b/tests/features/scheduling/test_scheduling.py new file mode 100644 index 000000000..44c77ed2c --- /dev/null +++ b/tests/features/scheduling/test_scheduling.py @@ -0,0 +1,161 @@ +import pytest +import pendulum +from src.masonite.scheduling import Task + + +class MockTask(Task): + run_every = "5 minutes" + timezone = "America/New_York" + + +class TestScheduler: + def setup_method(self): + self.task = MockTask() + + def test_scheduler_should_run(self): + assert self.task.run_every == "5 minutes" + time = pendulum.now().on(2018, 5, 21).at(22, 5, 5) + self.task._date = time + assert self.task.should_run(time) == True + + time = pendulum.now().on(2018, 5, 21).at(22, 6, 5) + self.task._date = time + assert self.task.should_run(time) == False + + def test_scheduler_should_run_every_minute(self): + self.task.run_every = "1 minute" + time = pendulum.now().on(2018, 5, 21).at(22, 5, 5) + self.task._date = time + assert self.task.should_run(time) == True + + time = pendulum.now().on(2018, 5, 21).at(22, 6, 5) + self.task._date = time + assert self.task.should_run(time) == True + + def test_scheduler_should_run_every_2_minutes(self): + self.task.run_every = "2 minutes" + time = pendulum.now().on(2018, 5, 21).at(14, 56, 5) + self.task._date = time + assert self.task.should_run(time) == True + + time = pendulum.now().on(2018, 5, 21).at(14, 58, 5) + self.task._date = time + assert self.task.should_run(time) == True + + def test_scheduler_should_run_every_hour(self): + self.task.run_every = "1 hour" + time = pendulum.now().on(2018, 5, 21).at(2, 0, 1) + self.task._date = time + assert self.task.should_run(time) == True + + time = pendulum.now().on(2018, 5, 21).at(3, 0, 1) + self.task._date = time + assert self.task.should_run(time) == True + + self.task.run_every = "2 hours" + time = pendulum.now().on(2018, 5, 21).at(2, 0, 1) + self.task._date = time + assert self.task.should_run(time) == True + + self.task.run_every = "2 hours" + time = pendulum.now().on(2018, 5, 21).at(3, 0, 1) + self.task._date = time + assert self.task.should_run(time) == False + + time = pendulum.now().on(2018, 5, 21).at(4, 0, 1) + self.task._date = time + assert self.task.should_run(time) == True + + def test_scheduler_should_run_every_days(self): + self.task.run_every = "2 days" + time = pendulum.now().on(2018, 5, 21).at(0, 0, 1) + self.task._date = time + assert self.task.should_run(time) == False + + time = pendulum.now().on(2018, 5, 23).at(0, 0, 1) + self.task._date = time + assert self.task.should_run(time) == False + + self.task.run_at = "5:30" + time = pendulum.now().on(2018, 5, 22).at(5, 30, 0) + self.task._date = time + assert self.task.should_run(time) == True + + self.task.run_at = "5:35" + time = pendulum.now().on(2018, 5, 22).at(5, 30, 0) + self.task._date = time + assert self.task.should_run(time) == False + + def test_scheduler_should_run_every_months(self): + self.task.run_every = "2 months" + time = pendulum.now().on(2018, 1, 1).at(0, 0, 1) + self.task._date = time + assert self.task.should_run(time) == False + + time = pendulum.now().on(2018, 2, 1).at(0, 0, 1) + self.task._date = time + assert self.task.should_run(time) == True + + time = pendulum.now().on(2018, 2, 1).at(10, 0, 1) + self.task._date = time + assert self.task.should_run(time) == False + + self.task.run_at = "5:30" + time = pendulum.now().on(2018, 2, 1).at(5, 30, 0) + self.task._date = time + assert self.task.should_run(time) == False + + def test_twice_daily_at_correct_time(self): + time = pendulum.now().on(2018, 1, 1).at(1, 20, 5) + self.task.run_every = "" + self.task.twice_daily = (1, 13) + self.task._date = time + + assert self.task.should_run() + + time = pendulum.now().on(2018, 1, 1).at(13, 20, 5) + self.task._date = time + assert self.task.should_run() + + def test_twice_daily_at_incorrect_time(self): + time = pendulum.now().on(2018, 1, 1).at(12, 20, 5) + self.task.run_every = "" + self.task.twice_daily = (1, 13) + self.task._date = time + + assert self.task.should_run() is False + + def test_run_at(self): + self.task.run_every = "" + self.task.run_at = "13:00" + + time = pendulum.now().on(2018, 1, 1).at(13, 0, 5) + self.task._date = time + + self.task.run_at = "13:05" + + time = pendulum.now().on(2018, 1, 1).at(13, 5, 5) + self.task._date = time + + assert self.task.should_run() is True + + time = pendulum.now().on(2018, 1, 1).at(13, 6, 5) + self.task._date = time + + assert self.task.should_run() is False + + def test_method_calls(self): + task = MockTask() + task.at("13:00") + + time = pendulum.now().on(2018, 1, 1).at(13, 0, 5) + task._date = time + + assert task.should_run(time) == True + + task = MockTask() + task.every_minute() + + time = pendulum.now().on(2018, 5, 21).at(22, 5, 5) + task._date = time + assert task.should_run(time) == True diff --git a/tests/features/session/test_cookie_session.py b/tests/features/session/test_cookie_session.py new file mode 100644 index 000000000..4b6a0a8ff --- /dev/null +++ b/tests/features/session/test_cookie_session.py @@ -0,0 +1,119 @@ +from tests import TestCase + + +class TestCookieSession(TestCase): + def test_can_start_session(self): + request = self.make_request() + session = self.application.make("session") + request.cookie("s_hello", "test") + session.start() + self.assertEqual(session.get("hello"), "test") + + def test_can_get_session_dict(self): + request = self.make_request() + session = self.application.make("session") + request.cookie("s_hello", '{"hello": "test"}') + session.start() + self.assertEqual(type(session.get("hello")), dict) + + def test_can_set_and_get_session_dict(self): + request = self.make_request() + session = self.application.make("session") + session.start() + session.set("key1", {"hello": "test"}) + self.assertEqual(type(session.get("key1")), dict) + self.assertEqual(session.get("key1")["hello"], "test") + + def test_can_set_and_get_session_dict(self): + request = self.make_request() + session = self.application.make("session") + session.start() + session.flash("key1", {"hello": "test"}) + self.assertEqual(session.get("key1")["hello"], "test") + + def test_can_set_and_get_session(self): + self.make_request() + session = self.application.make("session") + session.start() + session.set("key1", "test1") + self.assertEqual(session.get("key1"), "test1") + + def test_can_increment_and_decrement_session(self): + self.make_request() + session = self.application.make("session") + session.start() + session.set("key1", "1") + session.set("key5", "5") + session.increment("key1") + session.decrement("key5") + self.assertEqual(session.get("key1"), "2") + self.assertEqual(session.get("key5"), "4") + + def test_can_save_session(self): + self.make_request() + response = self.make_response() + session = self.application.make("session") + session.start() + session.set("key1", "test1") + session.save() + self.assertEqual(response.cookie("s_key1"), "test1") + + def test_can_delete_session(self): + request = self.make_request() + response = self.make_response() + session = self.application.make("session") + request.cookie("s_key", "test") + session.start() + + self.assertEqual(session.get("key"), "test") + + session.delete("key") + self.assertEqual(session.get("key"), None) + + session.save() + self.assertEqual(response.cookie("s_key"), None) + self.assertTrue("s_key" in response.cookie_jar.deleted_cookies) + + def test_can_pull_session(self): + request = self.make_request() + response = self.make_response() + session = self.application.make("session") + request.cookie("s_key", "test") + session.start() + + self.assertEqual(session.get("key"), "test") + + key = session.pull("key") + self.assertEqual(key, "test") + self.assertEqual(session.get("key"), None) + + session.save() + self.assertEqual(response.cookie("s_key"), None) + self.assertTrue("s_key" in response.cookie_jar.deleted_cookies) + + def test_can_flush_session(self): + request = self.make_request() + response = self.make_response() + session = self.application.make("session") + request.cookie("s_key", "test") + session.start() + + self.assertEqual(session.get("key"), "test") + + session.flush() + self.assertEqual(session.get("key"), None) + session.save() + self.assertEqual(response.cookie("s_key"), None) + self.assertTrue("s_key" in response.cookie_jar.deleted_cookies) + + def test_can_flash(self): + request = self.make_request() + response = self.make_response() + session = self.application.make("session") + session.start() + session.flash("key", "test") + self.assertEqual(session.get("key"), "test") + self.assertEqual(session.get("key"), None) + + session.save() + self.assertEqual(response.cookie("f_key"), None) diff --git a/tests/features/storage/test_local_storage.py b/tests/features/storage/test_local_storage.py new file mode 100644 index 000000000..33430e51f --- /dev/null +++ b/tests/features/storage/test_local_storage.py @@ -0,0 +1,38 @@ +from tests import TestCase +import os +import time +from src.masonite.filesystem import File + + +class TestLocalStorage(TestCase): + def setUp(self): + super().setUp() + self.application.make("storage") + self.driver = self.application.make("storage").disk() + + def test_can_get_file_driver(self): + self.driver.put("key.log", "value") + self.assertEqual(self.driver.get("key.log"), "value") + self.assertTrue(self.driver.exists("key.log")) + + def test_can_move(self): + self.driver.move("key.log", "logs/key.log") + + def test_can_stream(self): + self.driver.put("key.log", "value") + stream = self.driver.stream("key.log") + self.assertEqual(stream.name(), "key.log") + self.assertEqual(stream.extension(), ".log") + + def test_can_delete(self): + self.driver.put("delete.log", "value") + self.assertEqual(self.driver.get("delete.log"), "value") + stream = self.driver.delete("delete.log") + self.assertEqual(self.driver.get("delete.log"), None) + + def test_can_store(self): + self.driver.store(File(b"hello.log", "hello-world")) + + def test_can_append_file(self): + self.driver.append("world.log", "hello") + self.driver.prepend("world.log", "world") diff --git a/tests/features/storage/test_s3_storage.py b/tests/features/storage/test_s3_storage.py new file mode 100644 index 000000000..f1c88542d --- /dev/null +++ b/tests/features/storage/test_s3_storage.py @@ -0,0 +1,41 @@ +from tests import TestCase +import os +import time +from src.masonite.filesystem import File +import pytest + + +@pytest.mark.integrations +class TestLocalStorage(TestCase): + def setUp(self): + super().setUp() + self.application.make("storage") + self.driver = self.application.make("storage").disk("s3") + + def test_can_get_file_driver(self): + self.driver.put("key.log", "value") + self.assertEqual(self.driver.get("key.log"), "value") + self.assertEqual(self.driver.get("key.logs"), None) + self.assertTrue(self.driver.exists("key.log")) + + def test_can_move(self): + self.driver.move("key.log", "logs/key.log") + + def test_can_stream(self): + self.driver.put("key.log", "value") + stream = self.driver.stream("key.log") + self.assertEqual(stream.name(), "key.log") + self.assertEqual(stream.extension(), ".log") + + def test_can_delete(self): + self.driver.put("delete.log", "value") + self.assertEqual(self.driver.get("delete.log"), "value") + stream = self.driver.delete("delete.log") + self.assertEqual(self.driver.get("delete.log"), None) + + def test_can_store(self): + self.driver.store(File(b"hello.log", "hello-world")) + + def test_can_append_file(self): + self.driver.append("world.log", "hello") + self.driver.prepend("world.log", "world") diff --git a/tests/features/validation/test_messagebag.py b/tests/features/validation/test_messagebag.py new file mode 100644 index 000000000..3e67e00a6 --- /dev/null +++ b/tests/features/validation/test_messagebag.py @@ -0,0 +1,116 @@ +import unittest +from src.masonite.validation import MessageBag + + +class TestMessageBag(unittest.TestCase): + def setUp(self): + self.bag = MessageBag() + + def test_message_bag_can_add_errors_and_messages(self): + self.bag.add("email", "Your email is invalid") + self.assertEqual(self.bag.items, {"email": ["Your email is invalid"]}) + + def test_message_bag_can_add_several_errors_and_messages(self): + self.bag.add("email", "Your email is invalid") + self.bag.add("email", "Your email is invalid") + + def test_message_bag_can_get_all_errors_and_messages(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.assertEqual(self.bag.all(), {"email": ["Your email is invalid"]}) + + def test_message_bag_has_any_errors(self): + self.bag.reset() + self.assertFalse(self.bag.any()) + self.bag.add("email", "Your email is invalid") + self.assertTrue(self.bag.any()) + + def test_message_bag_has_any_errors(self): + self.bag.reset() + self.assertTrue(self.bag.empty()) + self.bag.add("email", "Your email is invalid") + self.assertFalse(self.bag.empty()) + + def test_message_bag_can_get_first_error(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.bag.add("email", "Your email is too short") + self.bag.add("username", "Your username is invalid") + self.assertEqual(self.bag.first("email"), "Your email is invalid") + self.assertEqual(self.bag.first("username"), "Your username is invalid") + + def test_amount_of_messages(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.bag.add("email", "Your email too short") + self.assertEqual(self.bag.amount("email"), 2) + + def test_has_message(self): + self.bag.reset() + self.assertFalse(self.bag.has("email")) + self.assertFalse(self.bag.has("username")) + self.bag.add("email", "Your email is invalid") + self.bag.add("email", "Your email too short") + self.assertTrue(self.bag.has("email")) + self.assertFalse(self.bag.has("username")) + + def test_get_messages_with_same_keys(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.bag.add("email", "Your email too short") + self.assertIn( + "Your email is invalid", + self.bag.get("email"), + ) + self.assertIn( + "Your email too short", + self.bag.get("email"), + ) + + def test_get_errors(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.bag.add("email", "Your email too short") + self.assertEqual(self.bag.errors(), ["email"]) + + def test_get_messages(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.bag.add("username", "Your username too short") + self.assertIn( + "Your email is invalid", + self.bag.messages(), + ) + + self.assertIn( + "Your username too short", + self.bag.messages(), + ) + + def test_can_convert_to_json(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.assertEqual(self.bag.json(), '{"email": ["Your email is invalid"]}') + + def test_can_merge(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + self.bag.merge({"username": ["username is too short"]}) + self.assertEqual(self.bag.count(), 2) + + def test_can_work_with_if_statements_and_full(self): + self.bag.reset() + self.bag.add("email", "Your email is invalid") + + if self.bag: + pass + else: + raise AssertionError("Should assert true as a boolean") + + def test_can_work_with_if_statements_and_false(self): + self.bag.reset() + + if self.bag: + raise AssertionError("Should not raise when not full") + else: + pass diff --git a/tests/features/validation/test_messagebag_view.py b/tests/features/validation/test_messagebag_view.py new file mode 100644 index 000000000..4ba4accb7 --- /dev/null +++ b/tests/features/validation/test_messagebag_view.py @@ -0,0 +1,16 @@ +import unittest +from src.masonite.validation import MessageBag + + +class TestMessageBag(unittest.TestCase): + def setUp(self): + self.errors = MessageBag.view_helper( + {"email": ["email is required", "email must be a valid email"]} + ) + + def test_get_errors(self): + self.assertTrue(self.errors.any()) + + def test_get_messages(self): + self.assertIn("email is required", self.errors.messages()) + self.assertIn("email must be a valid email", self.errors.messages()) diff --git a/tests/features/validation/test_request_validation.py b/tests/features/validation/test_request_validation.py new file mode 100644 index 000000000..0db23efba --- /dev/null +++ b/tests/features/validation/test_request_validation.py @@ -0,0 +1,25 @@ +from tests import TestCase + + +class TestValidation(TestCase): + def test_can_validate_request(self): + request = self.make_request( + data={"QUERY_STRING": "email=joe@masoniteproject.com"} + ) + validation = request.validate( + { + "email": "required", + } + ) + + self.assertEqual(validation.all(), {}) + + def test_can_validate_request(self): + request = self.make_request(query_string="") + validation = request.validate( + { + "email": "required", + } + ) + + self.assertEqual(validation.all(), {"email": ["The email field is required."]}) diff --git a/tests/features/validation/test_validation.py b/tests/features/validation/test_validation.py new file mode 100644 index 000000000..c7bdbf38d --- /dev/null +++ b/tests/features/validation/test_validation.py @@ -0,0 +1,1755 @@ +import json +import unittest +import pytest +import platform +import pendulum +from uuid import uuid1, uuid3, uuid4, uuid5 + +# from masonite.drivers import SessionCookieDriver +from tests import TestCase + +from src.masonite.validation import RuleEnclosure +from src.masonite.validation.providers import ValidationProvider +from src.masonite.validation import ( + ValidationFactory, + Validator, + accepted, + active_domain, + after_today, + before_today, + confirmed, + contains, + date, + different, + distinct, + does_not, + email, + equals, + exists, + file, + greater_than, + image, + in_range, + ip, + is_future, + is_list, + is_in, + is_past, + isnt, + postal_code, + strong, + regex, + uuid, + video, +) +from src.masonite.validation.Validator import json as vjson +from src.masonite.validation.Validator import ( + length, + less_than, + matches, + none, + numeric, + one_of, + phone, + required, + required_if, + required_with, + string, + timezone, + truthy, + when, +) + + +class TestValidation(unittest.TestCase): + def setUp(self): + pass + + def test_required(self): + validate = Validator().validate({"test": 1}, required(["user", "email"])) + + self.assertEqual(validate.get("user"), ["The user field is required."]) + self.assertEqual(validate.get("email"), ["The email field is required."]) + + validate = Validator().validate({"test": 1}, required(["test"])) + + self.assertEqual(len(validate), 0) + + def test_required_with_non_truthy_values(self): + for falsy_value in [[], {}, "", False, 0]: + validate = Validator().validate({"user": falsy_value}, required(["user"])) + self.assertEqual(validate.get("user"), ["The user field is required."]) + + def test_can_validate_null_values(self): + validate = Validator().validate({"test": None}, length(["test"], min=2, max=5)) + + self.assertEqual(len(validate), 0) + + def test_extendable(self): + v = Validator() + v.extend("numeric", numeric) + + validate = v.validate({"test": 1}, v.numeric(["test"])) + + self.assertEqual(len(validate), 0) + + def test_email(self): + validate = Validator().validate({"email": "user@example.com"}, email(["email"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"email": "user"}, email(["email"])) + + self.assertEqual( + validate.all(), {"email": ["The email must be a valid email address."]} + ) + + def test_email_with_one_letter_username(self): + validate = Validator().validate({"email": "u@example.com"}, email(["email"])) + self.assertEqual(len(validate), 0) + + def test_matches(self): + validate = Validator().validate( + { + "password": "secret", + "confirm": "secret", + }, + matches("password", "confirm"), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "password": "secret", + "confirm": "no-secret", + }, + matches("password", "confirm"), + ) + + self.assertEqual( + validate.all(), {"password": ["The password must match confirm."]} + ) + + def test_active_domain(self): + validate = Validator().validate( + { + "domain1": "google.com", + "domain2": "http://google.com", + "domain3": "https://www.google.com", + "email": "admin@gmail.com", + }, + active_domain(["domain1", "domain2", "domain3", "email"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "domain1": "domain", + }, + active_domain(["domain1"]), + ) + + self.assertEqual( + validate.all(), {"domain1": ["The domain1 must be an active domain name."]} + ) + + def test_phone(self): + validate = Validator().validate( + {"phone": "876-182-1921"}, phone("phone", pattern="123-456-7890") + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"phone": "(876)182-1921"}, phone("phone", pattern="123-456-7890") + ) + + self.assertEqual( + validate.all(), {"phone": ["The phone must be in the format XXX-XXX-XXXX."]} + ) + + def test_accepted(self): + validate = Validator().validate({"terms": "on"}, accepted(["terms"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"terms": "test"}, accepted(["terms"])) + + self.assertEqual(validate.all(), {"terms": ["The terms must be accepted."]}) + + def test_ip(self): + validate = Validator().validate({"ip": "192.168.1.1"}, ip(["ip"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"ip": "test"}, ip(["ip"])) + + self.assertEqual( + validate.all(), {"ip": ["The ip must be a valid ipv4 address."]} + ) + + def test_timezone(self): + validate = Validator().validate( + {"timezone": "America/New_York"}, timezone(["timezone"]) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"timezone": "test"}, timezone(["timezone"])) + + self.assertEqual( + validate.all(), {"timezone": ["The timezone must be a valid timezone."]} + ) + + def test_exists(self): + validate = Validator().validate( + { + "terms": "on", + "user": "here", + }, + exists(["user"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"terms": "test"}, exists(["user"])) + + self.assertEqual(validate.all(), {"user": ["The user must exist."]}) + + def test_date(self): + validate = Validator().validate( + { + "date": "1975-05-21T22:00:00", + }, + date(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": "woop", + }, + date(["date"]), + ) + + self.assertEqual(validate.all(), {"date": ["The date must be a valid date."]}) + + def test_before_today(self): + validate = Validator().validate( + { + "date": "1975-05-21T22:00:00", + }, + before_today(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": pendulum.now().subtract(days=2).to_datetime_string(), + }, + before_today(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": "2030-05-21T22:00:00", + }, + before_today(["date"]), + ) + + self.assertEqual( + validate.all(), {"date": ["The date must be a date before today."]} + ) + + def test_after_today(self): + validate = Validator().validate( + { + "date": "2030-05-21T22:00:00", + }, + after_today(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": pendulum.tomorrow().to_datetime_string(), + }, + after_today(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": "1975-05-21T22:00:00", + }, + after_today(["date"]), + ) + + self.assertEqual( + validate.all(), {"date": ["The date must be a date after today."]} + ) + + def test_is_past(self): + validate = Validator().validate( + { + "date": "1950-05-21T22:00:00", + }, + is_past(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": pendulum.yesterday().to_datetime_string(), + }, + is_past(["date"], tz="America/New_York"), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": pendulum.tomorrow().to_datetime_string(), + }, + is_past("date", tz="America/New_York"), + ) + + self.assertEqual( + validate.all(), {"date": ["The date must be a time in the past."]} + ) + + def test_is_future(self): + validate = Validator().validate( + { + "date": "2030-05-21T22:00:00", + }, + is_future(["date"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": pendulum.tomorrow().to_datetime_string(), + }, + is_future(["date"], tz="America/New_York"), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "date": pendulum.yesterday().to_datetime_string(), + }, + is_future(["date"]), + ) + + self.assertEqual( + validate.all(), {"date": ["The date must be a time in the past."]} + ) + + def test_exception(self): + with self.assertRaises(AttributeError) as e: + validate = Validator().validate( + { + "terms": "on", + }, + required(["user"], raises={"user": AttributeError}), + ) + + try: + validate = Validator().validate( + { + "terms": "on", + }, + required(["user"], raises={"user": AttributeError}), + ) + except AttributeError as e: + self.assertEqual(str(e), "The user field is required.") + + try: + validate = Validator().validate( + { + "terms": "on", + }, + required(["user"], raises=True), + ) + except ValueError as e: + self.assertEqual(str(e), "The user field is required.") + + def test_conditional(self): + validate = Validator().validate( + {"terms": "on"}, when(accepted(["terms"])).then(required(["user"])) + ) + + self.assertEqual(validate.all(), {"user": ["The user field is required."]}) + + validate = Validator().validate({"terms": "test"}, accepted(["terms"])) + + self.assertEqual(validate.all(), {"terms": ["The terms must be accepted."]}) + + def test_error_message_required(self): + validate = Validator().validate( + {"test": 1}, + required( + ["user", "email"], messages={"user": "there must be a user value"} + ), + ) + + self.assertEqual(validate.get("user"), ["there must be a user value"]) + self.assertEqual(validate.get("email"), ["The email field is required."]) + + validate = Validator().validate( + {"test": 1}, + required( + ["user", "email"], messages={"email": "there must be an email value"} + ), + ) + + self.assertEqual(validate.get("user"), ["The user field is required."]) + self.assertEqual(validate.get("email"), ["there must be an email value"]) + + def test_numeric(self): + validate = Validator().validate({"test": 1}, numeric(["test"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"test": "hey"}, numeric(["test"])) + + self.assertEqual(validate.all(), {"test": ["The test must be a numeric."]}) + + def test_several_tests(self): + validate = Validator().validate( + {"test": "hey"}, required(["notin"]), numeric(["notin"]) + ) + + self.assertEqual( + validate.all(), + {"notin": ["The notin field is required.", "The notin must be a numeric."]}, + ) + + def test_json(self): + validate = Validator().validate({"json": "hey"}, vjson(["json"])) + + self.assertEqual(validate.all(), {"json": ["The json must be a valid JSON."]}) + + validate = Validator().validate( + {"json": json.dumps({"test": "key"})}, vjson(["json"]) + ) + + self.assertEqual(len(validate), 0) + + def test_length(self): + validate = Validator().validate( + {"json": "hey"}, length(["json"], min=1, max=10) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"json": "hey"}, length(["json"], "1..10")) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"json": "this is a really long string"}, length(["json"], min=1, max=10) + ) + + self.assertEqual( + validate.all(), {"json": ["The json length must be between 1 and 10."]} + ) + + # test when only min given + validate = Validator().validate({"json": "hoh"}, length(["json"], min=6)) + + self.assertEqual( + validate.all(), {"json": ["The json must be at least 6 characters."]} + ) + + # passing test when only min given + validate = Validator().validate( + {"json": "string which is long enough"}, length(["json"], min=6) + ) + self.assertEqual(len(validate), 0) + + # test when only max given + validate = Validator().validate( + {"json": "this is a string too long"}, length(["json"], max=10) + ) + + self.assertEqual( + validate.all(), {"json": ["The json length must be between 0 and 10."]} + ) + + # test that empty strings validates maximum length + validate = Validator().validate({"json": ""}, length(["json"], max=10)) + self.assertEqual(len(validate), 0) + + def test_string(self): + validate = Validator().validate({"text": "hey"}, string(["text"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"text": ["string1", "string2"]}, string(["text"]) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": 1}, string(["text"])) + + self.assertEqual(validate.all(), {"text": ["The text must be a string."]}) + + def test_none(self): + validate = Validator().validate({"text": None}, none(["text"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": 1}, none(["text"])) + + self.assertEqual(validate.all(), {"text": ["The text must be None."]}) + + def test_equals(self): + validate = Validator().validate({"text": "test1"}, equals(["text"], "test1")) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": "test2"}, equals(["text"], "test1")) + + self.assertEqual(validate.all(), {"text": ["The text must be equal to test1."]}) + + def test_truthy(self): + validate = Validator().validate({"text": "value"}, truthy(["text"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": 1}, truthy(["text"])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": False}, truthy(["text"])) + + self.assertEqual(validate.all(), {"text": ["The text must be a truthy value."]}) + + def test_in_range(self): + validate = Validator().validate( + {"text": 52}, in_range(["text"], min=25, max=72) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"text": "1"}, in_range(["text"], min=1, max=10) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": 1}, in_range(["text"], min=1, max=10)) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"text": "hello"}, in_range(["text"], min=1, max=10) + ) + + self.assertEqual(validate.get("text"), ["The text must be between 1 and 10."]) + + validate = Validator().validate( + {"text": "1.5"}, in_range(["text"], min=1.5, max=5.5) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"text": 101}, in_range(["text"], min=25, max=72) + ) + + self.assertEqual( + validate.all(), {"text": ["The text must be between 25 and 72."]} + ) + + def test_greater_than(self): + validate = Validator().validate({"text": 52}, greater_than(["text"], 25)) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": 101}, greater_than(["text"], 150)) + + self.assertEqual( + validate.all(), {"text": ["The text must be greater than 150."]} + ) + + def test_less_than(self): + validate = Validator().validate({"text": 10}, less_than(["text"], 25)) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"text": 101}, less_than(["text"], 75)) + + self.assertEqual(validate.all(), {"text": ["The text must be less than 75."]}) + + def test_isnt(self): + validate = Validator().validate( + {"test": 50}, isnt(in_range(["test"], min=10, max=20)) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"test": 15}, isnt(in_range(["test"], min=10, max=20)) + ) + + self.assertEqual( + validate.all(), {"test": ["The test must not be between 10 and 20."]} + ) + + def test_isnt_equals(self): + validate = Validator().validate( + {"test": "test"}, + isnt(equals(["test"], "test"), length(["test"], min=10, max=20)), + ) + + self.assertEqual( + validate.all(), {"test": ["The test must not be equal to test."]} + ) + + def test_contains(self): + validate = Validator().validate( + {"test": "this is a sentence"}, contains(["test"], "this") + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"test": "this is a not sentence"}, contains(["test"], "test") + ) + + self.assertEqual(validate.all(), {"test": ["The test must contain test."]}) + + def test_is_in(self): + validate = Validator().validate({"test": 1}, is_in(["test"], [1, 2, 3])) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate({"test": 1}, is_in(["test"], [4, 2, 3])) + + self.assertEqual( + validate.all(), {"test": ["The test must contain an element in [4, 2, 3]."]} + ) + + def test_when(self): + validate = Validator().validate( + {"email": "user@example.com", "phone": "123-456-7890"}, + when(isnt(required("email"))).then(required("phone")), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"email": "user@example.com"}, when(exists("email")).then(required("phone")) + ) + + self.assertEqual(validate.get("phone"), ["The phone field is required."]) + + validate = Validator().validate( + {"user": "user"}, when(exists("email")).then(required("phone")) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "email": "user@example.com", + }, + when(does_not(exists("email"))).then(required("phone")), + ) + + self.assertEqual(len(validate), 0) + + def test_does_not(self): + validate = Validator().validate( + {"phone": "123-456-7890"}, does_not(exists("email")).then(required("phone")) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"email": "user@example.com", "phone": "123-456-7890"}, + does_not(exists("email")).then(required("phone")), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"user": "Joe"}, does_not(exists("email")).then(required("phone")) + ) + + self.assertEqual(validate.get("phone"), ["The phone field is required."]) + + def test_one_of(self): + validate = Validator().validate( + {"email": "user@example.com", "phone": "123-456-7890"}, + one_of(["email", "phone"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"accepted": "on", "user": "Joe"}, one_of(["email", "phone"]) + ) + + self.assertEqual(validate.get("email"), ["The email or phone is required."]) + self.assertEqual(validate.get("phone"), ["The email or phone is required."]) + + validate = Validator().validate( + {"accepted": "on", "user": "Joe"}, one_of(["email", "phone", "password"]) + ) + + self.assertEqual( + validate.get("email"), ["The email, phone, password is required."] + ) + + validate = Validator().validate( + {"accepted": "on", "user": "Joe"}, + one_of(["email", "phone", "password", "user"]), + ) + + self.assertEqual(len(validate), 0) + + def test_regex(self): + validate = Validator().validate( + { + "username": "masonite_user_1", + }, + regex(["username"], "^[a-z0-9_-]{3,16}$"), + ) + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"username": "Masonite User 2"}, regex(["username"], "^[a-z0-9_-]{3,16}$") + ) + self.assertEqual( + validate.get("username"), + ["The username does not match pattern ^[a-z0-9_-]{3,16}$ ."], + ) + + def test_list_validation(self): + validate = Validator().validate( + {"name": "Joe", "discounts_ref": [1, 2, 3]}, + required(["name", "discounts_ref"]), + numeric(["discounts_ref.*"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"name": "Joe", "discounts_ref": [1, 2, 3]}, + required(["name", "discounts_ref"]), + length(["discounts_ref.*"], min=1, max=2), + ) + + self.assertEqual(len(validate), 0) + + def test_list_validation(self): + validate = Validator().validate( + {"name": "Joe", "discounts_ref": [1, 2, 3]}, + is_list(["discounts_ref.*"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"name": "Joe", "discounts_ref": {1: 2}}, + is_list(["discounts_ref.*"]), + ) + + self.assertEqual(len(validate), 1) + + def test_postal_code(self): + validate = Validator().validate( + { + "postal_code": "not a post code", + }, + postal_code(["postal_code"], "FR"), + ) + self.assertEqual( + validate.get("postal_code"), + ["The postal_code is not a valid FR postal code. Valid example is 33380."], + ) + + validate = Validator().validate( + { + "postal_code": "44000", + }, + postal_code(["postal_code"], "FR"), + ) + self.assertEqual(len(validate), 0) + + def test_multiple_countries_for_postal_code(self): + valid_postal_codes = ["EC1Y 8SY", "44000", "87832"] # gb, fr, us + for code in valid_postal_codes: + validate = Validator().validate( + { + "postal_code": code, + }, + postal_code(["postal_code"], "FR,GB,US"), + ) + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "postal_code": "4430", + }, + postal_code(["postal_code"], "FR,GB,US"), + ) + self.assertEqual( + validate.get("postal_code"), + [ + "The postal_code is not a valid FR,GB,US postal code. Valid examples are 33380,EC1Y 8SY,95014." + ], + ) + + def test_not_implemented_country_postal_code(self): + try: + validate = Validator().validate( + { + "postal_code": "90988", + }, + postal_code(["postal_code"], "XX"), + ) + except NotImplementedError as e: + self.assertEqual( + str(e), + "Unsupported country code XX. Check that it is a ISO 3166-1 country code or open a PR to require support of this country code.", + ) + + def test_file_validation(self): + validate = Validator().validate( + { + "document": "a string", + }, + file(["document"]), + ) + + self.assertEqual( + validate.get("document"), ["The document is not a valid file."] + ) + import os + + test_file = os.path.abspath(__file__) + validate = Validator().validate({"document": test_file}, file(["document"])) + self.assertEqual(len(validate), 0) + + def test_file_size_validation(self): + import os + + # check that max size is 100 bytes + test_file = os.path.abspath(__file__) + validate = Validator().validate( + {"document": test_file}, file(["document"], size=100) + ) + self.assertEqual( + validate.get("document"), ["The document file size exceeds 100 bytes."] + ) + + validate = Validator().validate( + {"document": test_file}, file(["document"], size="2MB") + ) + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"document": test_file}, file(["document"], size="4K") + ) + self.assertEqual( + validate.get("document"), ["The document file size exceeds 4 KB."] + ) + + def test_file_mime_types_validation(self): + import os + + test_file = os.path.abspath(__file__) + validate = Validator().validate( + {"document": test_file}, + file( + ["document"], + mimes=[ + "jpg", + "png", + ], + ), + ) + self.assertEqual( + validate.get("document"), + ["The document mime type is not valid. Allowed formats are jpg,png."], + ) + + validate = Validator().validate( + {"document": test_file}, + file( + ["document"], + mimes=[ + "py", + ], + ), + ) + self.assertEqual(len(validate), 0) + + def test_multiple_file_validations(self): + import os + + test_file = os.path.abspath(__file__) + validate = Validator().validate( + {"document": test_file}, + file( + ["document"], + size=100, + mimes=[ + "jpg", + "png", + ], + ), + ) + self.assertEqual( + validate.get("document"), + [ + "The document file size exceeds 100 bytes.", + "The document mime type is not valid. Allowed formats are jpg,png.", + ], + ) + + def test_image_validation(self): + validate = Validator().validate( + { + "avatar": "a string", + }, + image(["avatar"]), + ) + + self.assertEqual(validate.get("avatar"), ["The avatar is not a valid file."]) + + import mimetypes + + image_extensions = [ + ext for ext, mt in mimetypes.types_map.items() if mt.startswith("image") + ] + + import os + + test_file = os.path.abspath(__file__) # python file + validate = Validator().validate( + { + "avatar": test_file, + }, + image(["avatar"]), + ) + self.assertEqual( + validate.get("avatar"), + [ + "The avatar file is not a valid image. Allowed formats are {}.".format( + ",".join(image_extensions) + ) + ], + ) + + import tempfile + + with tempfile.NamedTemporaryFile(dir="/tmp", suffix=".png") as tmpfile: + test_image = tmpfile.name + validate = Validator().validate({"avatar": test_image}, image(["avatar"])) + + self.assertEqual(len(validate), 0) + + def test_image_size_validation(self): + import tempfile + import os + + with tempfile.NamedTemporaryFile(dir="/tmp", suffix=".png") as tmpfile: + test_image = tmpfile.name + tmpfile.write(b"dummy content to get a size around 40 bytes") + tmpfile.flush() + validate = Validator().validate( + {"avatar": test_image}, image(["avatar"], size="2MB") + ) + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"avatar": test_image}, image(["avatar"], size="20b") + ) + self.assertEqual( + validate.get("avatar"), ["The avatar file size exceeds 20 bytes."] + ) + + def test_video_validation(self): + validate = Validator().validate( + { + "document": "a string", + }, + video(["document"]), + ) + + self.assertEqual( + validate.get("document"), ["The document is not a valid file."] + ) + + import mimetypes + + video_extensions = [ + ext for ext, mt in mimetypes.types_map.items() if mt.startswith("video") + ] + + import os + + test_file = os.path.abspath(__file__) # python file + validate = Validator().validate( + { + "document": test_file, + }, + video(["document"]), + ) + self.assertEqual( + validate.get("document"), + [ + "The document file is not a valid video. Allowed formats are {}.".format( + ",".join(video_extensions) + ) + ], + ) + + import tempfile + + with tempfile.NamedTemporaryFile(dir="/tmp", suffix=".mp4") as tmpfile: + test_video = tmpfile.name + validate = Validator().validate( + {"document": test_video}, video(["document"]) + ) + + self.assertEqual(len(validate), 0) + + def test_different(self): + validate = Validator().validate( + {"field_1": "value_1", "field_2": "value_2"}, + different(["field_1"], "field_2"), + ) + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"field_1": "value_1", "field_2": "value_1"}, + different(["field_1"], "field_2"), + ) + self.assertEqual( + validate.get("field_1"), + ["The field_1 value must be different than field_2 value."], + ) + + validate = Validator().validate( + {"field_1": None, "field_2": None}, different(["field_1"], "field_2") + ) + self.assertEqual( + validate.get("field_1"), + ["The field_1 value must be different than field_2 value."], + ) + + def test_that_default_uuid_must_be_uuid4(self): + from uuid import NAMESPACE_DNS + + u3 = uuid3(NAMESPACE_DNS, "domain.com") + u5 = uuid5(NAMESPACE_DNS, "domain.com") + for uuid_value in [uuid1(), u3, u5]: + validate = Validator().validate( + { + "document_id": uuid_value, + }, + uuid(["document_id"]), + ) + self.assertEqual( + validate.get("document_id"), + ["The document_id value must be a valid UUID 4."], + ) + + validate = Validator().validate( + { + "document_id": uuid4(), + }, + uuid(["document_id"], 4), + ) + self.assertEqual(len(validate), 0) + + def test_invalid_uuid_values(self): + for uuid_value in [None, [], True, "", "uuid", {"uuid": "nope"}, 3, ()]: + validate = Validator().validate( + { + "document_id": uuid_value, + }, + uuid(["document_id"]), + ) + self.assertEqual( + validate.get("document_id"), + ["The document_id value must be a valid UUID 4."], + ) + + def test_uuid_rule_with_specified_versions(self): + from uuid import NAMESPACE_DNS + + u3 = uuid3(NAMESPACE_DNS, "domain.com") + u5 = uuid5(NAMESPACE_DNS, "domain.com") + for version, uuid_value in [(1, uuid1()), (3, u3), (4, uuid4()), (5, u5)]: + validate = Validator().validate( + { + "document_id": uuid_value, + }, + uuid(["document_id"], version), + ) + self.assertEqual(len(validate), 0) + + def test_invalid_uuid_rule_with_specified_versions(self): + for version in [1, 2, 3, 5]: + validate = Validator().validate( + { + "document_id": uuid4(), + }, + uuid(["document_id"], version), + ) + self.assertEqual( + validate.get("document_id"), + ["The document_id value must be a valid UUID {0}.".format(version)], + ) + + def test_uuid_version_can_be_str_or_int(self): + uuid_value = uuid4() + for version in [4, "4"]: + validate = Validator().validate( + { + "document_id": uuid_value, + }, + uuid(["document_id"], version), + ) + self.assertEqual(len(validate), 0) + for version in [3, "3"]: + validate = Validator().validate( + { + "document_id": uuid_value, + }, + uuid(["document_id"], version), + ) + self.assertEqual( + validate.get("document_id"), + ["The document_id value must be a valid UUID 3."], + ) + + def test_required_if_rule_when_other_field_is_present(self): + validate = Validator().validate( + {"first_name": "Sam", "last_name": "Gamji"}, + required_if(["last_name"], "first_name", "Sam"), + ) + self.assertEqual(len(validate), 0) + validate = Validator().validate( + {"first_name": "Sam", "last_name": ""}, + required_if(["last_name"], "first_name", "Sam"), + ) + self.assertEqual( + validate.get("last_name"), + ["The last_name is required because first_name=Sam."], + ) + validate = Validator().validate( + {"first_name": "Sam", "last_name": ""}, + required_if(["last_name"], "first_name", "Joe"), + ) + self.assertEqual(len(validate), 0) + + def test_required_if_rule_when_other_field_is_not_present(self): + validate = Validator().validate( + { + "first_name": "Sam", + }, + required_if(["last_name"], "first_name", "Sam"), + ) + self.assertEqual( + validate.get("last_name"), + ["The last_name is required because first_name=Sam."], + ) + validate = Validator().validate( + { + "first_name": "Sam", + }, + required_if(["last_name"], "first_name", "Joe"), + ) + self.assertEqual(len(validate), 0) + + def test_required_with_rule(self): + validate = Validator().validate( + {"first_name": "Sam", "last_name": "Gamji", "email": "samgamji@loftr.com"}, + required_with(["email"], ["first_name", "last_name" "nick_name"]), + ) + self.assertEqual(len(validate), 0) + validate = Validator().validate( + {"first_name": "Sam", "email": "samgamji@loftr.com"}, + required_with(["email"], "first_name"), + ) + self.assertEqual(len(validate), 0) + validate = Validator().validate( + {"first_name": "Sam", "email": ""}, required_with(["email"], "first_name") + ) + self.assertEqual( + validate.get("email"), + ["The email is required because first_name is present."], + ) + validate = Validator().validate( + {"first_name": "Sam", "email": ""}, + required_with(["email"], "first_name,nick_name"), + ) + self.assertEqual( + validate.get("email"), + ["The email is required because one in first_name,nick_name is present."], + ) + + def test_required_with_rule_with_comma_separated_fields(self): + validate = Validator().validate( + {"nick_name": "Sam", "email": "samgamji@loftr.com"}, + required_with(["email"], "first_name,last_name,nick_name"), + ) + self.assertEqual(len(validate), 0) + validate = Validator().validate( + { + "nick_name": "Sam", + }, + required_with(["email"], "first_name,nick_name"), + ) + self.assertEqual( + validate.get("email"), + ["The email is required because one in first_name,nick_name is present."], + ) + + def test_distinct(self): + validate = Validator().validate( + { + "users": [ + { + "first_name": "John", + "last_name": "Masonite", + }, + { + "first_name": "Joe", + "last_name": "Masonite", + }, + ] + }, + distinct(["users.*.last_name"]), + ) + self.assertEqual( + validate.get("users.*.last_name"), + ["The users.*.last_name field has duplicate values."], + ) + validate = Validator().validate( + { + "users": [ + { + "id": 1, + "name": "John", + }, + { + "id": 2, + "name": "Nick", + }, + ] + }, + distinct(["users.*.id"]), + ) + self.assertEqual(len(validate), 0) + + def test_distinct_with_simple_list(self): + validate = Validator().validate( + {"emails": ["john@masonite.com", "joe@masonite.com", "john@masonite.com"]}, + distinct(["emails"]), + ) + self.assertEqual( + validate.get("emails"), ["The emails field has duplicate values."] + ) + + +class TestDotNotationValidation(unittest.TestCase): + def setUp(self): + pass + + def test_dot_required(self): + validate = Validator().validate( + {"user": {"email": "user@example.com"}}, required(["user.id"]) + ) + + self.assertEqual( + validate.all(), {"user.id": ["The user.id field is required."]} + ) + + validate = Validator().validate({"user": {"id": 1}}, required(["user.id"])) + + self.assertEqual(len(validate), 0) + + def test_dot_numeric(self): + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com"}}, numeric(["user.id"]) + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com"}}, numeric(["user.email"]) + ) + + self.assertEqual( + validate.all(), {"user.email": ["The user.email must be a numeric."]} + ) + + def test_dot_several_tests(self): + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com"}}, + required(["user.id", "user.email"]), + numeric(["user.id"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com"}}, + required(["user.id", "user.email"]), + numeric(["user.email"]), + ) + + self.assertEqual( + validate.all(), {"user.email": ["The user.email must be a numeric."]} + ) + + def test_dot_json(self): + validate = Validator().validate( + {"user": {"id": "hey", "email": "user@example.com"}}, vjson(["user.id"]) + ) + + self.assertEqual( + validate.all(), {"user.id": ["The user.id must be a valid JSON."]} + ) + + validate = Validator().validate( + { + "user": { + "id": 1, + "email": "user@example.com", + "payload": json.dumps({"test": "key"}), + } + }, + vjson(["user.payload"]), + ) + + self.assertEqual(len(validate), 0) + + def test_dot_length(self): + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com"}}, + length(["user.id"], min=1, max=10), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "user": { + "id": 1, + "email": "user@example.com", + "description": "this is a really long description", + } + }, + length(["user.id"], "1..10"), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "user": { + "id": 1, + "email": "user@example.com", + "description": "this is a really long description", + } + }, + length(["user.description"], min=1, max=10), + ) + + self.assertEqual( + validate.all(), + { + "user.description": [ + "The user.description length must be between 1 and 10." + ] + }, + ) + + def test_dot_in_range(self): + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com", "age": 25}}, + in_range(["user.age"], min=25, max=72), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com", "age": 25}}, + in_range(["user.age"], min=27, max=72), + ) + + self.assertEqual( + validate.all(), {"user.age": ["The user.age must be between 27 and 72."]} + ) + + validate = Validator().validate( + {"data": {"value": "1.5"}}, + in_range(["data.value"], min=2, max=2.5), + ) + + self.assertEqual( + validate.all(), + {"data.value": ["The data.value must be between 2 and 2.5."]}, + ) + + def test_dot_equals(self): + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com", "age": 25}}, + equals(["user.age"], 25), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com", "age": 25}}, + equals(["user.age"], "test1"), + ) + + self.assertEqual( + validate.all(), {"user.age": ["The user.age must be equal to test1."]} + ) + + def test_can_use_asterisk(self): + validate = Validator().validate( + { + "user": { + "id": 1, + "addresses": [ + {"id": 1, "street": "A Street"}, + {"id": 2, "street": "B Street"}, + {"id": 3, "street": "C Street"}, + ], + "age": 25, + } + }, + required(["user.addresses.*.id"]), + equals("user.addresses.*.id", [1, 2, 3]), + ) + + self.assertEqual(len(validate), 0, validate) + + validate = Validator().validate( + { + "user": { + "id": 1, + "addresses": [ + {"id": 1, "street": "A Street"}, + {"id": 2, "street": "B Street"}, + {"id": 3, "street": "C Street"}, + ], + "age": 25, + } + }, + required(["user.addresses.*.house"]), + ) + + self.assertEqual( + validate.all(), + { + "user.addresses.*.house": [ + "The user.addresses.*.house field is required." + ] + }, + ) + + validate = Validator().validate( + {"user": {"id": 1, "addresses": [], "age": 25}}, + required(["user.addresses.*.id"]), + ) + + self.assertEqual( + validate.all(), + {"user.addresses.*.id": ["The user.addresses.*.id field is required."]}, + ) + + def test_dot_error_message_required(self): + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com", "age": 25}}, + required( + ["user.description"], + messages={"user.description": "You are missing a description"}, + ), + ) + + self.assertEqual( + validate.all(), {"user.description": ["You are missing a description"]} + ) + + validate = Validator().validate( + {"user": {"id": 1, "email": "user@example.com"}}, + required( + ["user.id", "user.email", "user.age"], + messages={"user.age": "You are missing a user age"}, + ), + ) + + self.assertEqual(validate.all(), {"user.age": ["You are missing a user age"]}) + + +class TestValidationFactory(unittest.TestCase): + def test_can_register(self): + factory = ValidationFactory() + factory.register(required) + self.assertEqual(factory.registry["required"], required) + + +class TestValidationProvider(TestCase): + def setUp(self): + super().setUp() + self.provider = ValidationProvider(self.application) + self.application.resolve(self.provider.boot) + + def test_loaded_validator_class(self): + self.assertIsInstance(self.application.make(Validator), Validator) + + def test_loaded_registry(self): + self.assertTrue(self.application.make(Validator).numeric) + + def test_request_validation(self): + request = self.make_request(query_string="id=1&name=Joe") + validate = self.application.make("validator") + + validated = request.validate(validate.required(["id", "name"])) + + self.assertEqual(len(validated), 0) + + validated = request.validate(validate.required(["user"])) + + self.assertEqual(validated.all(), {"user": ["The user field is required."]}) + + # def test_request_validation_redirects_back_with_session(self): + # wsgi = generate_wsgi() + # self.application.bind("Application", self.application) + # self.application.bind("SessionCookieDriver", SessionCookieDriver) + # self.application.bind("Environ", wsgi) + + # request = self.application.make("Request") + # request.load_environ(wsgi) + + # request.request_variables = {"id": 1, "name": "Joe"} + + # errors = request.validate(required("user")) + + # request.session = SessionManager(self.app).driver("cookie") + # request.key("UKLAdrye6pZG4psVRPZytukJo2-A_Zxbo0VaqR5oig8=") + # self.assertEqual( + # request.redirect("/login").with_errors(errors).redirect_url, "/login" + # ) + # self.assertEqual( + # request.redirect("/login").with_errors(errors).session.get("errors"), + # {"user": ["The user field is required."]}, + # ) + + def test_confirmed(self): + validate = Validator().validate( + { + "password": "secret", + "password_confirmation": "secret", + }, + confirmed(["password"]), + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + { + "password": "secret", + }, + confirmed(["password"]), + ) + + self.assertEqual( + validate.all(), {"password": ["The password confirmation does not match."]} + ) + + validate = Validator().validate({}, confirmed(["password"])) + + self.assertEqual( + validate.all(), {"password": ["The password confirmation does not match."]} + ) + + validate = Validator().validate( + { + "password": "secret", + "password_confirmation": "foo", + }, + confirmed(["password"]), + ) + + self.assertEqual( + validate.all(), {"password": ["The password confirmation does not match."]} + ) + + def test_strong(self): + validate = Validator().validate( + { + "password": "secret", + }, + strong(["password"], uppercase=0, special=0, numbers=0), + ) + + self.assertEqual( + validate.all(), + {"password": ["The password field must be 8 characters in length"]}, + ) + + validate = Validator().validate( + { + "password": "Secret", + }, + strong(["password"], length=5, uppercase=2, special=0, numbers=0), + ) + + self.assertEqual( + validate.all(), + {"password": ["The password field must have 2 uppercase letters"]}, + ) + + validate = Validator().validate( + { + "password": "secret!", + }, + strong(["password"], length=5, uppercase=0, special=2, numbers=0), + ) + + self.assertEqual( + validate.all(), + {"password": ["The password field must have 2 special characters"]}, + ) + + validate = Validator().validate( + { + "password": "secret!", + }, + strong(["password"], length=5, uppercase=0, special=0, numbers=2), + ) + + self.assertEqual( + validate.all(), {"password": ["The password field must have 2 numbers"]} + ) + + validate = Validator().validate( + { + "password": "secret!", + }, + strong(["password"], length=8, uppercase=2, special=2, numbers=2), + ) + + password_validation = validate.get("password") + self.assertIn("The password field must have 2 numbers", password_validation) + self.assertIn( + "The password field must be 8 characters in length", password_validation + ) + + self.assertIn( + "The password field must have 2 uppercase letters", password_validation + ) + + self.assertIn( + "The password field must have 2 special characters", password_validation + ) + + validate = Validator().validate( + { + "password": "secret!!", + }, + strong(["password"], length=8, uppercase=0, special=2, numbers=0), + ) + + self.assertEqual( + len(validate.all()), + 0, + ) + + def test_strong_breach(self): + validate = Validator().validate( + { + "password": "secret", + }, + strong(["password"], breach=True), + ) + + password_validation = validate.get("password") + self.assertIn( + "The password field has been breached in the past. Try another password", + password_validation, + ) + + +class MockRuleEnclosure(RuleEnclosure): + def rules(self): + return [required(["username", "email"]), accepted("terms")] + + +class TestRuleEnclosure(unittest.TestCase): + def test_enclosure_can_encapsulate_rules(self): + validate = Validator().validate( + {"username": "user123", "email": "user@example.com", "terms": "on"}, + MockRuleEnclosure, + ) + + self.assertEqual(len(validate), 0) + + validate = Validator().validate( + {"email": "user@example.com", "terms": "on"}, MockRuleEnclosure + ) + + self.assertEqual(len(validate), 1) + + +class TestDictValidation(unittest.TestCase): + def test_dictionary(self): + validate = Validator().validate( + {"test": 1, "terms": "on", "name": "Joe", "age": "25"}, + { + "test": "required|truthy", + "terms": "accepted", + "name": "required|equals:Joe", + "age": "required|greater_than:18", + }, + ) + + self.assertEqual(len(validate), 0) + + def test_required_with_string_validation(self): + validate = Validator().validate( + {"first_name": "Sam", "email": "samgamji@loftr.com"}, + {"email": "required_with:first_name,last_name"}, + ) + self.assertEqual(len(validate), 0) + # with one argument + validate = Validator().validate( + {"email": ""}, {"email": "required_with:first_name"} + ) + self.assertEqual(len(validate), 0) + validate = Validator().validate( + {"first_name": "Sam", "email": ""}, + {"email": "required_with:first_name,nick_name"}, + ) + self.assertIn( + "The email is required because one in first_name,nick_name is present.", + validate.get("email"), + ) diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/helpers/test_clean_request_input.py b/tests/helpers/test_clean_request_input.py deleted file mode 100644 index 573d2a114..000000000 --- a/tests/helpers/test_clean_request_input.py +++ /dev/null @@ -1,64 +0,0 @@ -import cgi -import unittest - -from src.masonite.helpers import clean_request_input - - -class TestCleanRequestInput(unittest.TestCase): - - def test_can_clean_string(self): - self.assertEqual(clean_request_input('">'), '<img """><script>alert('hey')</script>">') - - def test_can_clean_list(self): - self.assertEqual(clean_request_input( - ['">', '">'] - ), [ - '<img """><script>alert('hey')</script>">', - '<img """><script>alert('hey')</script>">' - ]) - - def test_can_clean_dictionary(self): - self.assertEqual(clean_request_input( - {'key': '">'} - ), {'key': '<img """><script>alert('hey')</script>">'}) - - def test_can_clean_multiple_dictionary(self): - self.assertEqual(clean_request_input( - { - "conta_corrente": { - "ocultar": False, - "visao_geral": True, - "extrato": True - } - } - ), { - "conta_corrente": { - "ocultar": False, - "visao_geral": True, - "extrato": True - } - }) - - def test_does_not_clean_field_storage_objects(self): - fieldstorage = FieldStorageTest() - self.assertEqual(clean_request_input(fieldstorage), fieldstorage) - - def test_does_not_clean_bytes_objects(self): - obj = b'test' - self.assertEqual(clean_request_input(obj), obj) - - def test_does_not_clean_bytes_object_list(self): - obj = [b'test', b'test'] - self.assertEqual(clean_request_input(obj), obj) - - def test_does_not_clean_bytes_objects_with_dicts(self): - obj = {'x': b'test'} - self.assertEqual(clean_request_input(obj), obj) - - def test_does_not_clean_quotes(self): - obj = {'content': "awesome! 'i love you'"} - self.assertEqual(clean_request_input(obj, quote=False)['content'], "awesome! 'i love you'") - - -class FieldStorageTest(cgi.FieldStorage): - pass diff --git a/tests/helpers/test_collect.py b/tests/helpers/test_collect.py deleted file mode 100644 index e8a8c7843..000000000 --- a/tests/helpers/test_collect.py +++ /dev/null @@ -1,7 +0,0 @@ -from src.masonite.helpers import collect - - -class TestCollect: - - def test_collect_helper(self): - assert collect([1, 2]).first() == 1 diff --git a/tests/helpers/test_compact.py b/tests/helpers/test_compact.py deleted file mode 100644 index 522d96356..000000000 --- a/tests/helpers/test_compact.py +++ /dev/null @@ -1,38 +0,0 @@ -from src.masonite.helpers import compact -from src.masonite.request import Request -from src.masonite.exceptions import AmbiguousError - -import unittest - - -class TestCompact(unittest.TestCase): - - def test_compact_returns_dict_of_local_variable(self): - x = 'hello' - self.assertEqual(compact(x), {'x': 'hello'}) - - def test_works_with_several_variables(self): - x = 'hello' - y = 'world' - self.assertEqual(compact(x, y), {'x': 'hello', 'y': 'world'}) - - def test_can_contain_dict(self): - x = 'hello' - y = 'world' - self.assertEqual(compact(x, y, {'z': 'foo'}), {'x': 'hello', 'y': 'world', 'z': 'foo'}) - - def test_exception_on_too_many(self): - x = 'hello' - y = 'world' - with self.assertRaises(ValueError): - compact(x, y, 'z') - - def test_compact_throws_exceptions(self): - r = Request(None) - request = r - with self.assertRaises(AmbiguousError): - compact(request) - - def test_works_with_classes(self): - request = Request(None) - self.assertIn('request', compact(request)) diff --git a/tests/helpers/test_config.py b/tests/helpers/test_config.py deleted file mode 100644 index 0dbf7f611..000000000 --- a/tests/helpers/test_config.py +++ /dev/null @@ -1,60 +0,0 @@ - -import unittest - -from src.masonite.helpers import Dot, config - -from config import database - - -class TestConfig(unittest.TestCase): - - def setUp(self): - self.config = config - - def test_config_can_get_value_from_file(self): - self.assertEqual(self.config('application.DEBUG'), True) - - def test_config_can_get_value_from_file_that_is_false(self): - self.assertEqual(self.config('application.FALSY'), False) - - def test_config_can_get_dict_value_lowercase(self): - self.assertEqual(self.config('application.debug'), True) - - def test_config_can_get_dict_default(self): - self.assertEqual(self.config('sdff.na', 'default'), 'default') - - def test_config_not_found_returns_default(self): - self.assertEqual(self.config('application.nothere', 'default'), 'default') - - def test_dict_dot_returns_value(self): - self.assertEqual(Dot().dict_dot('s3.test', {'s3': {'test': 'value'}}, ''), 'value') - - def test_config_can_get_dict_value_inside_dict(self): - self.assertEqual(self.config('database.DATABASES.default'), database.DATABASES['default']) - - def test_config_can_get_dict_value_inside_dict_with_lowercase(self): - self.assertEqual(self.config('database.databases.default'), database.DATABASES['default']) - - def test_config_can_get_dict_inside_dict_inside_dict(self): - self.assertIsInstance(self.config('database.databases.sqlite'), dict) - - def test_config_can_get_dict_inside_dict_inside_another_dict(self): - self.assertEqual(self.config('storage.DRIVERS.s3.test_locations.test'), 'value') - - def test_dot_dict(self): - self.assertEqual(Dot().dict_dot('async.driver', {'async': {'driver': 'me'}}, 'you'), 'me') - - def test_dict_dot_works_for_deep_dictionaries(self): - dictionary = { - 'storage': { - 'drivers': { - 'disk': { - 'location': { - 'uploading': 'uploads/' - } - } - } - } - } - - self.assertEqual(Dot().dict_dot('storage.drivers.disk.location', dictionary)['uploading'], 'uploads/') diff --git a/tests/helpers/test_dot_notation.py b/tests/helpers/test_dot_notation.py deleted file mode 100644 index 7b9589a92..000000000 --- a/tests/helpers/test_dot_notation.py +++ /dev/null @@ -1,37 +0,0 @@ -import unittest - -from src.masonite.helpers import dot, Dot as DictDot - - -class TestDot(unittest.TestCase): - - def test_dot(self): - self.assertEqual(dot('hey.dot', compile_to="{1}[{.}]"), "hey[dot]") - self.assertEqual(dot('hey.dot.another', compile_to="{1}[{.}]"), "hey[dot][another]") - self.assertEqual(dot('hey.dot.another.and.another', compile_to="{1}[{.}]"), "hey[dot][another][and][another]") - self.assertEqual(dot('hey.dot.another.and.another', compile_to="/{1}[{.}]"), "/hey[dot][another][and][another]") - self.assertEqual(dot('hey.dot', compile_to="{1}/{.}"), "hey/dot") - self.assertEqual(dot('hey.dot.another', compile_to="{1}/{.}"), "hey/dot/another") - self.assertEqual(dot('hey.dot.another', compile_to="{1}/{.}"), "hey/dot/another") - self.assertEqual(dot('hey.dot.another', compile_to="/{1}/{.}"), "/hey/dot/another") - with self.assertRaises(ValueError): - self.assertEqual(dot('hey.dot.another', compile_to="{1}//{.}"), "hey/dot/another") - - def test_dict_dot(self): - self.assertEqual(DictDot().dot('key', {'key': 'value'}), 'value') - self.assertEqual(DictDot().dot('key.test', {'key': {'test': 'value'}}), 'value') - self.assertEqual(DictDot().dot('key.test.layer', {'key': {'test': {'layer': 'value'}}}), 'value') - self.assertEqual(DictDot().dot('key.none', {'key': {'test': {'layer': 'value'}}}), None) - self.assertEqual(DictDot().dot('key', {'key': {'test': {'layer': 'value'}}}), {'test': {'layer': 'value'}}) - self.assertEqual(DictDot().dot('key.test.none', {'key': 'value'}), None) - self.assertEqual(DictDot().flatten({'key': [1,2]}), {'key.0': 1, 'key.1': 2}) - self.assertEqual(DictDot().dot('key.0', {'key': [1,2]}), 1) - - def test_dict_dot_asterisk(self): - payload = { - "username": "someone@mail.com", - "address": [{"id": "street1", "street": "some street"}, {"id": "street2", "street": "a street"}] - } - self.assertEqual(DictDot().dot('address.*.id', payload), ['street1', 'street2']) - self.assertEqual(DictDot().dot('address.*.street', payload), ['some street', 'a street']) - self.assertEqual(DictDot().dot('user.*.street', payload), []) diff --git a/tests/helpers/test_filesystem.py b/tests/helpers/test_filesystem.py deleted file mode 100644 index f42df0c72..000000000 --- a/tests/helpers/test_filesystem.py +++ /dev/null @@ -1,18 +0,0 @@ -import shutil - -from src.masonite.helpers.filesystem import make_directory - -import unittest - - -class TestFilesystem(unittest.TestCase): - - def test_make_directory(self): - dir_path = 'storage/uploads/test-dir' - file_path = 'storage/uploads/test-dir/test.py' - self.assertTrue(make_directory(dir_path)) - self.assertTrue(make_directory(file_path)) - with open(file_path, "w+"): - pass - self.assertFalse(make_directory(file_path)) - shutil.rmtree(dir_path) diff --git a/tests/helpers/test_instead_of.py b/tests/helpers/test_instead_of.py deleted file mode 100644 index 3c8ede614..000000000 --- a/tests/helpers/test_instead_of.py +++ /dev/null @@ -1,34 +0,0 @@ -from src.masonite.request import Request -import unittest - - -class MockUser: - pass - - -class InsteadOf: - - def __init__(self, cls, method): - self.cls = cls - self.method = method - - def _return(self, value): - setattr(self.cls, self.method, value) - return self.cls - - -class TestInsteadOf(unittest.TestCase): - - def test_instead_of_attribute(self): - request = Request() - - InsteadOf(request, 'user')._return('awesome') - - self.assertEqual(request.user, 'awesome') - - def test_instead_of_with_method(self): - request = Request() - - InsteadOf(request, 'user')._return(MockUser) - - self.assertIsInstance(request.user(), MockUser) diff --git a/tests/helpers/test_optional.py b/tests/helpers/test_optional.py deleted file mode 100644 index abbf85fc4..000000000 --- a/tests/helpers/test_optional.py +++ /dev/null @@ -1,26 +0,0 @@ -from src.masonite.helpers import optional -import unittest - - -class MockUser: - id = 1 - - -class CallThis: - - def method(self, var): - self.test = var - return self - - -class TestOptional(unittest.TestCase): - - def test_optional_returns_object_id(self): - self.assertEqual(optional(MockUser).id, 1) - self.assertTrue(optional(object).id) # It's a class - self.assertTrue(optional(None).id) # It's a class - self.assertEqual(optional(object).instance(), object) - - def test_optional_can_handle_method_calls(self): - self.assertFalse(optional(MockUser).method()) - self.assertEqual(optional(CallThis()).method('test').test, 'test') diff --git a/tests/helpers/test_password.py b/tests/helpers/test_password.py deleted file mode 100644 index 97188a3f0..000000000 --- a/tests/helpers/test_password.py +++ /dev/null @@ -1,9 +0,0 @@ -from src.masonite.helpers import password -import unittest - - -class TestPassword(unittest.TestCase): - - def test_password_returns_bcrypted_password(self): - self.assertNotEqual(password('secret'), 'secret') - self.assertIsInstance(password('secret'), str) diff --git a/tests/helpers/test_view_helpers.py b/tests/helpers/test_view_helpers.py deleted file mode 100644 index 2b9f7c0a3..000000000 --- a/tests/helpers/test_view_helpers.py +++ /dev/null @@ -1,64 +0,0 @@ - -import unittest - -from src.masonite.app import App -from src.masonite.providers import HelpersProvider, RequestHelpersProvider -from src.masonite.request import Request -from src.masonite.testing import generate_wsgi -from src.masonite.view import View - - -class TestViewHelpers(unittest.TestCase): - - def setUp(self): - self.app = App() - self.view = View(self.app) - self.request = Request(generate_wsgi()).load_app(self.app) - self.provider = HelpersProvider() - self.provider.load_app(self.app).boot(self.view) - RequestHelpersProvider().load_app(self.app).boot(self.view, self.request) - self.provider.load_app(self.app).boot(self.view) - - def test_boot_added_view_shares(self): - self.assertGreater(len(self.view._shared), 1) - - def test_request_view_helper_is_view_class(self): - self.assertIsInstance(self.view._shared['request'](), Request) - - def test_auth_returns_user_and_none(self): - self.assertIsNone(self.view._shared['auth']()) - self.request.set_user(MockUser) - self.assertEqual(self.view._shared['auth']().id, 1) - - def test_request_method_returns_hidden_input(self): - self.assertEqual(self.view._shared['request_method']('PUT'), "") - - def test_can_sign_and_encrypt(self): - self.assertNotEqual(self.view._shared['sign']('secret'), 'secret') - self.assertGreater(len(self.view._shared['sign']('secret')), 10) - - self.assertNotEqual(self.view._shared['encrypt']('secret'), 'secret') - self.assertGreater(len(self.view._shared['encrypt']('secret')), 10) - - def test_can_unsign_and_decrypt(self): - signed = self.view._shared['sign']('secret') - self.assertEqual(self.view._shared['decrypt'](signed), 'secret') - self.assertEqual(self.view._shared['unsign'](signed), 'secret') - - def test_can_get_config(self): - self.assertEqual(self.view._shared['config']('cache.driver'), 'disk') - - def test_optional(self): - self.assertEqual(self.view._shared['optional'](MockUser).id, 1) - self.assertNotEqual(self.view._shared['optional'](MockUser).test, 1) - - def test_cookie(self): - self.request.cookie('test', 'value') - self.assertEqual(self.view._shared['cookie']('test'), 'value') - - def test_hidden(self): - self.assertEqual(self.view._shared['hidden']('test', name='form1'), "") - - -class MockUser: - id = 1 diff --git a/tests/integrations/api.py b/tests/integrations/api.py new file mode 100644 index 000000000..1940373b0 --- /dev/null +++ b/tests/integrations/api.py @@ -0,0 +1,6 @@ +from src.masonite.routes import Route + + +ROUTES = [ + Route.get("/try", "WelcomeController@show").name("welcome"), +] diff --git a/tests/integrations/app/Kernel/Kernel.py b/tests/integrations/app/Kernel/Kernel.py new file mode 100644 index 000000000..af0d776f4 --- /dev/null +++ b/tests/integrations/app/Kernel/Kernel.py @@ -0,0 +1,116 @@ +import os + +from src.masonite.auth import Sign +from src.masonite.foundation import response_handler +from src.masonite.storage import StorageCapsule +from src.masonite.environment import LoadEnvironment +from src.masonite.configuration import Configuration, config +from src.masonite.middleware import ( + VerifyCsrfToken, + SessionMiddleware, + EncryptCookies, + LoadUserMiddleware, +) +from src.masonite.routes import Route +from src.masonite.utils.structures import load +from src.masonite.utils.location import base_path + + +class Kernel: + + http_middleware = [EncryptCookies] + route_middleware = {"web": [SessionMiddleware, LoadUserMiddleware, VerifyCsrfToken]} + + def __init__(self, app): + self.application = app + + def register(self): + self.load_environment() + self.register_configurations() + self.register_middleware() + self.register_routes() + self.register_database() + self.register_templates() + self.register_storage() + + def load_environment(self): + LoadEnvironment() + + def register_configurations(self): + # load configuration + self.application.bind("config.location", "tests/integrations/config") + configuration = Configuration(self.application) + configuration.load() + self.application.bind("config", configuration) + key = config("application.key") + self.application.bind("key", key) + self.application.bind("sign", Sign(key)) + + # set locations + self.application.bind("controllers.location", "tests/integrations/controllers") + self.application.bind("jobs.location", "tests/integrations/jobs") + self.application.bind("mailables.location", "tests/integrations/mailables") + self.application.bind("providers.location", "tests/integrations/providers") + self.application.bind("listeners.location", "tests/integrations/listeners") + self.application.bind("validation.location", "tests/integrations/validation") + self.application.bind("tasks.location", "tests/integrations/tasks") + self.application.bind("events.location", "tests/integrations/events") + self.application.bind("policies.location", "tests/integrations/policies") + self.application.bind( + "notifications.location", "tests/integrations/notifications" + ) + self.application.bind("resources.location", "tests/integrations/resources") + + self.application.bind( + "server.runner", "src.masonite.commands.ServeCommand.main" + ) + + def register_middleware(self): + self.application.make("middleware").add(self.route_middleware).add( + self.http_middleware + ) + + def register_routes(self): + Route.set_controller_locations(self.application.make("controllers.location")) + + self.application.bind("routes.location", "tests/integrations/web") + self.application.make("router").add( + Route.group( + load(self.application.make("routes.location"), "ROUTES", []), + middleware=["web"], + ) + ) + + def register_templates(self): + self.application.bind("views.location", "tests/integrations/templates") + + def register_database(self): + from masoniteorm.query import QueryBuilder + + self.application.bind( + "builder", + QueryBuilder(connection_details=config("database.databases")), + ) + + self.application.bind( + "migrations.location", "tests/integrations/databases/migrations" + ) + self.application.bind("seeds.location", "tests/integrations/databases/seeds") + + self.application.bind("resolver", config("database.db")) + + def register_storage(self): + storage = StorageCapsule() + storage.add_storage_assets( + { + # folder # template alias + "tests/integrations/storage/static": "static/", + "tests/integrations/storage/compiled": "static/", + "tests/integrations/storage/uploads": "static/", + "tests/integrations/storage/public": "/", + } + ) + self.application.bind("storage_capsule", storage) + + self.application.set_response_handler(response_handler) + self.application.use_storage_path(base_path("tests/integrations/storage")) diff --git a/tests/integrations/app/Kernel/__init__.py b/tests/integrations/app/Kernel/__init__.py new file mode 100644 index 000000000..24f65111e --- /dev/null +++ b/tests/integrations/app/Kernel/__init__.py @@ -0,0 +1 @@ +from .Kernel import Kernel diff --git a/tests/integrations/app/SayHi.py b/tests/integrations/app/SayHi.py new file mode 100644 index 000000000..8a783b947 --- /dev/null +++ b/tests/integrations/app/SayHi.py @@ -0,0 +1,6 @@ +from src.masonite.queues import Queueable + + +class SayHello(Queueable): + def handle(self): + print("hello there") diff --git a/tests/integrations/app/User.py b/tests/integrations/app/User.py new file mode 100644 index 000000000..28e1d050d --- /dev/null +++ b/tests/integrations/app/User.py @@ -0,0 +1,8 @@ +from masoniteorm.models import Model +from src.masonite.authentication import Authenticates +from src.masonite.authorization import Authorizes +from src.masonite.notification import Notifiable + + +class User(Model, Authenticates, Authorizes, Notifiable): + __fillable__ = ["name", "password", "email", "phone"] diff --git a/tests/integrations/config/application.py b/tests/integrations/config/application.py new file mode 100644 index 000000000..d62e8c070 --- /dev/null +++ b/tests/integrations/config/application.py @@ -0,0 +1,14 @@ +import os + +KEY = os.getenv("APP_KEY", "-RkDOqXojJIlsF_I8wWiUq_KRZ0PtGWTOZ676u5HtLg=") + + +HASHING = { + "default": "bcrypt", + "bcrypt": {"rounds": 10}, + "argon2": {"memory": 1024, "threads": 2, "time": 2}, +} + +APP_URL = os.getenv("APP_URL", "http://localhost:8000/") + +MIX_BASE_URL = os.getenv("MIX_BASE_URL", None) diff --git a/tests/integrations/config/auth.py b/tests/integrations/config/auth.py new file mode 100644 index 000000000..831b99f98 --- /dev/null +++ b/tests/integrations/config/auth.py @@ -0,0 +1,9 @@ +import os +from ..app.User import User + +GUARDS = { + "default": "web", + "web": {"model": User}, + "password_reset_table": "password_resets", + "password_reset_expiration": 1440, # in minutes. 24 hours. None if disabled +} diff --git a/tests/integrations/config/broadcast.py b/tests/integrations/config/broadcast.py new file mode 100644 index 000000000..ce745fe96 --- /dev/null +++ b/tests/integrations/config/broadcast.py @@ -0,0 +1,15 @@ +"""Cache Config""" + +import os + +BROADCASTS = { + "default": "pusher", + "pusher": { + "driver": "pusher", + "client": os.getenv("PUSHER_CLIENT"), + "app_id": os.getenv("PUSHER_APP_ID"), + "secret": os.getenv("PUSHER_SECRET"), + "cluster": os.getenv("PUSHER_CLUSTER"), + "ssl": False, + }, +} diff --git a/tests/integrations/config/cache.py b/tests/integrations/config/cache.py new file mode 100644 index 000000000..b371f04f8 --- /dev/null +++ b/tests/integrations/config/cache.py @@ -0,0 +1,24 @@ +"""Cache Config""" + +STORES = { + "default": "local", + "local": { + "driver": "file", + "location": "storage/framework/cache" + # + }, + "redis": { + "driver": "redis", + "host": "127.0.0.1", + "port": "6379", + "password": "", + "name": "masonite4", + }, + "memcache": { + "driver": "memcache", + "host": "127.0.0.1", + "port": "11211", + "password": "", + "name": "masonite4", + }, +} diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py new file mode 100644 index 000000000..3fc92d99d --- /dev/null +++ b/tests/integrations/config/database.py @@ -0,0 +1,11 @@ +from masoniteorm.connections import ConnectionResolver + +DATABASES = { + "default": "sqlite", + "sqlite": { + "driver": "sqlite", + "database": "database.sqlite3", + }, +} + +DB = ConnectionResolver().set_connection_details(DATABASES) diff --git a/tests/integrations/config/exceptions.py b/tests/integrations/config/exceptions.py new file mode 100644 index 000000000..006d09157 --- /dev/null +++ b/tests/integrations/config/exceptions.py @@ -0,0 +1 @@ +HANDLERS = {"stack_overflow": True, "solutions": True} diff --git a/tests/integrations/config/filesystem.py b/tests/integrations/config/filesystem.py new file mode 100644 index 000000000..d01870ac0 --- /dev/null +++ b/tests/integrations/config/filesystem.py @@ -0,0 +1,17 @@ +"""Cache Config""" +import os +from src.masonite.utils.location import base_path + +DISKS = { + "default": "local", + "local": { + "driver": "file", + "path": base_path("storage/framework/filesystem"), + }, + "s3": { + "driver": "s3", + "client": os.getenv("AWS_CLIENT"), + "secret": os.getenv("AWS_SECRET"), + "bucket": os.getenv("AWS_BUCKET"), + }, +} diff --git a/tests/integrations/config/mail.py b/tests/integrations/config/mail.py new file mode 100644 index 000000000..ef560a0ed --- /dev/null +++ b/tests/integrations/config/mail.py @@ -0,0 +1,16 @@ +import os + + +DRIVERS = { + "default": "smtp", + "smtp": { + "host": os.getenv("MAIL_HOST"), + "port": os.getenv("MAIL_PORT"), + "username": os.getenv("MAIL_USERNAME"), + "password": os.getenv("MAIL_PASSWORD"), + }, + "mailgun": { + "domain": os.getenv("MAILGUN_DOMAIN"), + "secret": os.getenv("MAILGUN_SECRET"), + }, +} diff --git a/tests/integrations/config/notification.py b/tests/integrations/config/notification.py new file mode 100644 index 000000000..72cc691df --- /dev/null +++ b/tests/integrations/config/notification.py @@ -0,0 +1,21 @@ +"""Notifications Settings.""" +import os + +DRIVERS = { + "slack": { + "token": os.getenv("SLACK_TOKEN", ""), # used for API mode + "webhook": os.getenv("SLACK_WEBHOOK", ""), # used for webhook mode + # "mode": os.getenv("SLACK_MODE", "webhook"), # webhook or api + }, + "vonage": { + "key": os.getenv("VONAGE_KEY", ""), + "secret": os.getenv("VONAGE_SECRET", ""), + "sms_from": os.getenv("VONAGE_SMS_FROM", "+33000000000"), + }, + "database": { + "connection": "sqlite", + "table": "notifications", + }, +} + +DRY = False diff --git a/tests/integrations/config/package.py b/tests/integrations/config/package.py new file mode 100644 index 000000000..aced64295 --- /dev/null +++ b/tests/integrations/config/package.py @@ -0,0 +1,3 @@ +"""External package settings for tests.""" + +PACKAGE_PARAM = "package_value" diff --git a/tests/integrations/config/providers.py b/tests/integrations/config/providers.py new file mode 100644 index 000000000..552c81f7b --- /dev/null +++ b/tests/integrations/config/providers.py @@ -0,0 +1,50 @@ +from src.masonite.providers import ( + RouteProvider, + FrameworkProvider, + ViewProvider, + WhitenoiseProvider, + ExceptionProvider, + MailProvider, + SessionProvider, + QueueProvider, + CacheProvider, + EventProvider, + StorageProvider, + HelpersProvider, + BroadcastProvider, + AuthenticationProvider, + AuthorizationProvider, + HashServiceProvider, +) + + +from src.masonite.scheduling.providers import ScheduleProvider +from src.masonite.notification.providers import NotificationProvider +from src.masonite.validation.providers.ValidationProvider import ValidationProvider +from ..test_package import MyTestPackageProvider + +from tests.integrations.providers import AppProvider + +PROVIDERS = [ + FrameworkProvider, + HelpersProvider, + RouteProvider, + ViewProvider, + WhitenoiseProvider, + ExceptionProvider, + MailProvider, + NotificationProvider, + SessionProvider, + CacheProvider, + QueueProvider, + ScheduleProvider, + EventProvider, + StorageProvider, + BroadcastProvider, + HashServiceProvider, + AuthenticationProvider, + AuthorizationProvider, + ValidationProvider, + MyTestPackageProvider, + AppProvider, +] diff --git a/tests/integrations/config/queue.py b/tests/integrations/config/queue.py new file mode 100644 index 000000000..291ea06d8 --- /dev/null +++ b/tests/integrations/config/queue.py @@ -0,0 +1,30 @@ +DRIVERS = { + "default": "async", + "database": { + "connection": "sqlite", + "table": "jobs", + "failed_table": "failed_jobs", + "attempts": 3, + "poll": 5, + "tz": "UTC", + }, + "redis": { + # + }, + "amqp": { + "username": "guest", + "password": "guest", + "port": "5672", + "vhost": "", + "host": "localhost", + "channel": "default", + "queue": "masonite4", + "tz": "UTC", + }, + "async": { + "blocking": True, + "callback": "handle", + "mode": "threading", + "workers": 1, + }, +} diff --git a/tests/integrations/config/session.py b/tests/integrations/config/session.py new file mode 100644 index 000000000..620c5957f --- /dev/null +++ b/tests/integrations/config/session.py @@ -0,0 +1,7 @@ +import os + + +DRIVERS = { + "default": "cookie", + "cookie": {}, +} diff --git a/tests/integrations/config/test_package.py b/tests/integrations/config/test_package.py new file mode 100644 index 000000000..af1768db5 --- /dev/null +++ b/tests/integrations/config/test_package.py @@ -0,0 +1 @@ +PARAM_1 = 0 diff --git a/tests/integrations/controllers/HelloController.py b/tests/integrations/controllers/HelloController.py new file mode 100644 index 000000000..cb48af1d3 --- /dev/null +++ b/tests/integrations/controllers/HelloController.py @@ -0,0 +1,7 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View + + +class HelloController(Controller): + def show(self, view: View): + return view.render("") diff --git a/tests/integrations/controllers/MailableController.py b/tests/integrations/controllers/MailableController.py new file mode 100644 index 000000000..0062113ed --- /dev/null +++ b/tests/integrations/controllers/MailableController.py @@ -0,0 +1,19 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View +from src.masonite.mail import Mailable, Mail + + +class Welcome(Mailable): + def build(self): + return ( + self.to("idmann509@gmail.com") + .subject("Masonite 4") + .from_("joe@masoniteproject.com") + .text("Hello from Masonite!") + .html("

Hello from Masonite!

") + ) + + +class MailableController(Controller): + def view(self, mail: Mail): + mail.mailable(Welcome()).send(driver="mailgun") diff --git a/tests/integrations/controllers/WelcomeController.py b/tests/integrations/controllers/WelcomeController.py new file mode 100644 index 000000000..f4365080a --- /dev/null +++ b/tests/integrations/controllers/WelcomeController.py @@ -0,0 +1,149 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View +from src.masonite.response.response import Response +from src.masonite.request import Request +from src.masonite.filesystem import Storage +from src.masonite.broadcasting import Broadcast, Channel +from src.masonite.facades import Session, Config, Gate + + +class CanBroadcast: + def broadcast_on(self): + return Channel(f"private-shipped") + + def broadcast_with(self): + return vars(self) + + def broadcast_as(self): + return self.__class__.__name__ + + +class OrderProcessed(CanBroadcast): + def __init__(self): + self.order_id = 1 + + +class WelcomeController(Controller): + def play_with_session(self, request: Request, view: View): + # Session.flash("test", "hello flashed") + # Session.set("test_persisted", "hello persisted in session") + # request.app.make("session").set("test_persisted", "hello persisted") + return view.render("welcome") + + def show(self, request: Request, view: View): + request.app.make("session").flash("test", "value") + return view.render("welcome") + + def flash_data(self, request: Request, response: Response, view: View): + request.app.make("session").flash("test", "value") + return response.with_input().redirect("/sessions") + + def form_with_input(self, request: Request, response: Response, view: View): + return response.redirect("/sessions").with_input() + + def test(self): + return "test" + + def api(self): + return {"key": "value"} + + def emit(self, broadcast: Broadcast): + broadcast.channel("private-orders", OrderProcessed()) + return "emitted" + + def view(self, view: View): + return view.render("welcome") + + def upload(self, request: Request, storage: Storage): + return storage.disk("s3").store(request.input("profile")) + + def create(self): + return "user created", 201 + + def not_found(self): + return "not found", 404 + + def unauthorized(self): + return "unauthorized", 401 + + def input(self, request: Request): + return request.all() + return "input" + + def forbidden(self): + return "forbidden", 403 + + def empty(self): + return "", 204 + + def redirect_url(self, response: Response): + return response.redirect("/") + + def redirect_route(self, response: Response): + return response.redirect(name="test") + + def redirect_route_params(self, response: Response): + return response.redirect(name="test_params", params={"id": 1}) + + def response_with_headers(self, response: Response): + response.header("TEST", "value") + response.header("TEST2", "value2") + return "" + + def view_with_context(self, view: View): + return view.render( + "welcome", + {"count": 1, "users": ["John", "Joe"], "other_key": {"nested": 1}}, + ) + + def json(self, response: Response): + return response.json( + { + "key": "value", + "key2": [1, 2], + "other_key": { + "nested": 1, + "nested_again": {"a": 1, "b": 2}, + }, + } + ) + + def session(self, request: Request): + request.app.make("session").flash("key", "value") + return "session" + + def session_with_errors(self, request: Request, response: Response): + request.app.make("session").flash("key", "value") + request.app.make("session").flash( + "errors", + {"email": "Email required", "password": "Password too short", "name": ""}, + ) + return "session" + + def with_errors(self, response: Response): + return response.back().with_errors( + {"email": "Email required", "password": "Password too short", "name": ""} + ) + + def session2(self, request: Request): + request.app.make("session").flash( + "key", {"nested": 1, "nested_again": {"key2": "value2"}} + ) + return "session2" + + def with_params(self): + return "" + + def auth(self, request: Request): + request.app.make("auth").guard("web").attempt("idmann509@gmail.com", "secret") + return "logged in" + + def not_authorized(self): + # if current user not authorized it will raise an exception + Gate.authorize("display-admin") + + def use_authorization_helper(self, request: Request): + request.authorize("display-admin") + + def authorizations(self, view: View): + return view.render("authorizations") diff --git a/tests/integrations/controllers/api/TestController.py b/tests/integrations/controllers/api/TestController.py new file mode 100644 index 000000000..b42f9b2a5 --- /dev/null +++ b/tests/integrations/controllers/api/TestController.py @@ -0,0 +1,6 @@ +from src.masonite.controllers import Controller + + +class TestController(Controller): + def show(self): + return "welcome" diff --git a/tests/integrations/controllers/auth/HomeController.py b/tests/integrations/controllers/auth/HomeController.py new file mode 100644 index 000000000..474572b71 --- /dev/null +++ b/tests/integrations/controllers/auth/HomeController.py @@ -0,0 +1,7 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View + + +class HomeController(Controller): + def show(self, view: View): + return view.render("auth.home") diff --git a/tests/integrations/controllers/auth/LoginController.py b/tests/integrations/controllers/auth/LoginController.py new file mode 100644 index 000000000..e43f6c1b2 --- /dev/null +++ b/tests/integrations/controllers/auth/LoginController.py @@ -0,0 +1,19 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View +from src.masonite.request import Request +from src.masonite.response import Response +from src.masonite.authentication import Auth + + +class LoginController(Controller): + def show(self, view: View): + return view.render("auth.login") + + def store(self, view: View, request: Request, auth: Auth, response: Response): + login = auth.attempt(request.input("username"), request.input("password")) + + if login: + return response.redirect(name="home") + + # Go back to login page + return response.redirect(name="login") diff --git a/tests/integrations/controllers/auth/PasswordResetController.py b/tests/integrations/controllers/auth/PasswordResetController.py new file mode 100644 index 000000000..09100541c --- /dev/null +++ b/tests/integrations/controllers/auth/PasswordResetController.py @@ -0,0 +1,29 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View +from tests.integrations.app.User import User +from src.masonite.request import Request +from src.masonite.response import Response +from src.masonite.authentication import Auth + + +class PasswordResetController(Controller): + def show(self, view: View): # Show password_reset page + return view.render("auth.password_reset") + + def store( + self, auth: Auth, request: Request, response: Response + ): # store password_reset record + auth.password_reset(request.input("email")) + return "event fired" + + def change_password(self, view: View): # store password_reset record + return view.render("auth.change_password") + + def store_changed_password( + self, auth: Auth, request: Request, response: Response + ): # store password_reset record + auth.reset_password(request.input("password"), request.input("token")) + + # Need to validate?? + # Redirect back? + return response.back() diff --git a/tests/integrations/controllers/auth/RegisterController.py b/tests/integrations/controllers/auth/RegisterController.py new file mode 100644 index 000000000..2325a0e31 --- /dev/null +++ b/tests/integrations/controllers/auth/RegisterController.py @@ -0,0 +1,21 @@ +from src.masonite.controllers import Controller +from src.masonite.views import View +from tests.integrations.app.User import User +from src.masonite.request import Request +from src.masonite.response import Response +from src.masonite.authentication import Auth + + +class RegisterController(Controller): + def show(self, view: View): # Show register page + return view.render("auth.register") + + def store( + self, auth: Auth, request: Request, response: Response + ): # store register user + user = auth.register(request.only("name", "email", "password")) + + if not user: + return response.redirect("/register") + + return response.redirect("/home") diff --git a/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py b/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py new file mode 100644 index 000000000..4fa611ff2 --- /dev/null +++ b/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py @@ -0,0 +1,15 @@ +from masoniteorm.migrations import Migration + + +class CreatePasswordResetTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("password_resets") as table: + table.string("email").unique() + table.string("token") + table.datetime("expires_at").nullable() + table.datetime("created_at") + + def down(self): + """Revert the migrations.""" + self.schema.drop("password_resets") diff --git a/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py b/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py new file mode 100644 index 000000000..a8da836cc --- /dev/null +++ b/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py @@ -0,0 +1,20 @@ +from masoniteorm.migrations import Migration + + +class CreateUsersTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("users") as table: + table.increments("id") + table.string("name") + table.string("email").unique() + table.string("password") + table.string("second_password").nullable() + table.string("remember_token").nullable() + table.string("phone").nullable() + table.timestamp("verified_at").nullable() + table.timestamps() + + def down(self): + """Revert the migrations.""" + self.schema.drop("users") diff --git a/tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py b/tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py new file mode 100644 index 000000000..af83657ca --- /dev/null +++ b/tests/integrations/databases/migrations/2021_03_18_190410_create_notifications_table.py @@ -0,0 +1,17 @@ +from masoniteorm.migrations import Migration + + +class CreateNotificationsTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("notifications") as table: + table.big_increments("id").primary() + table.string("type") + table.text("data") + table.morphs("notifiable") + table.datetime("read_at").nullable() + table.timestamps() + + def down(self): + """Revert the migrations.""" + self.schema.drop("notifications") diff --git a/storage/uploads/__init__.py b/tests/integrations/databases/seeds/__init__.py similarity index 100% rename from storage/uploads/__init__.py rename to tests/integrations/databases/seeds/__init__.py diff --git a/databases/seeds/database_seeder.py b/tests/integrations/databases/seeds/database_seeder.py similarity index 84% rename from databases/seeds/database_seeder.py rename to tests/integrations/databases/seeds/database_seeder.py index b6e1be121..57592e071 100644 --- a/databases/seeds/database_seeder.py +++ b/tests/integrations/databases/seeds/database_seeder.py @@ -1,11 +1,10 @@ """Base Database Seeder Module.""" -from orator.seeds import Seeder +from masoniteorm.seeds import Seeder from .user_table_seeder import UserTableSeeder class DatabaseSeeder(Seeder): - def run(self): """Run the database seeds.""" self.call(UserTableSeeder) diff --git a/tests/integrations/databases/seeds/user_table_seeder.py b/tests/integrations/databases/seeds/user_table_seeder.py new file mode 100644 index 000000000..daf3d427e --- /dev/null +++ b/tests/integrations/databases/seeds/user_table_seeder.py @@ -0,0 +1,17 @@ +"""UserTableSeeder Seeder.""" + +from masoniteorm.seeds import Seeder +from tests.integrations.app.User import User + + +class UserTableSeeder(Seeder): + def run(self): + """Run the database seeds.""" + User.create( + { + "name": "idmann509", + "email": "idmann509@gmail.com", + "password": "secret", + "phone": "+123456789", + } + ) diff --git a/tests/integrations/notifications/OneTimePassword.py b/tests/integrations/notifications/OneTimePassword.py new file mode 100644 index 000000000..9a49c8e77 --- /dev/null +++ b/tests/integrations/notifications/OneTimePassword.py @@ -0,0 +1,19 @@ +from src.masonite.notification import Notification, Textable +from src.masonite.mail import Mailable +from src.masonite.mail import Mailable + + +class OneTimePassword(Notification, Mailable, Textable): + def to_mail(self, notifiable): + return ( + self.to(notifiable.email) + .subject("Masonite 4") + .from_("hello@email.com") + .text(f"Hello {notifiable.name}") + ) + + def to_vonage(self, notifiable): + return self.text_message("Welcome !").to("6314870798").from_("33123456789") + + def via(self, notifiable): + return ["vonage"] diff --git a/tests/integrations/policies/PostPolicy.py b/tests/integrations/policies/PostPolicy.py new file mode 100644 index 000000000..42c3c67f4 --- /dev/null +++ b/tests/integrations/policies/PostPolicy.py @@ -0,0 +1,29 @@ +from src.masonite.authorization import Policy + + +class PostPolicy(Policy): + def create(self, user): + return user.email == "idmann509@gmail.com" + + def view_any(self, user=None): + """Here we allow guests (non-authenticated) users by declaring the user optional else + only authenticated users would have been allowed.""" + return True + + def view(self, user, instance): + return False + + def update(self, user, instance): + return user.id == instance.user_id + + def delete(self, user, instance): + if user.id != instance.user_id: + return self.deny("You do not own this post") + else: + return self.allow() + + def force_delete(self, user, instance): + return False + + def restore(self, user, instance): + return False diff --git a/tests/integrations/providers/AppProvider.py b/tests/integrations/providers/AppProvider.py new file mode 100644 index 000000000..7d2292626 --- /dev/null +++ b/tests/integrations/providers/AppProvider.py @@ -0,0 +1,15 @@ +from src.masonite.providers import Provider +from src.masonite.facades import Gate + + +class AppProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + # Everyone (guests and authenticated users can view posts) + Gate.define("view-posts", lambda user=None: True) + Gate.define("display-admin", lambda user: user.email == "admin@gmail.com") + + def boot(self): + pass diff --git a/tests/integrations/providers/__init__.py b/tests/integrations/providers/__init__.py new file mode 100644 index 000000000..b98133027 --- /dev/null +++ b/tests/integrations/providers/__init__.py @@ -0,0 +1 @@ +from .AppProvider import AppProvider diff --git a/tests/integrations/storage/invoice.pdf b/tests/integrations/storage/invoice.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fc3d58a76b9507cb2831a04487beaadcdeebd15c GIT binary patch literal 43627 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�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 G4&*=CjAW|- literal 0 HcmV?d00001 diff --git a/storage/public/favicon.ico b/tests/integrations/storage/public/favicon.ico similarity index 100% rename from storage/public/favicon.ico rename to tests/integrations/storage/public/favicon.ico diff --git a/tests/integrations/storage/static/main.css b/tests/integrations/storage/static/main.css new file mode 100644 index 000000000..be3d2461c --- /dev/null +++ b/tests/integrations/storage/static/main.css @@ -0,0 +1,3 @@ +html { + background-color: #ccc; +} \ No newline at end of file diff --git a/templates/__init__.py b/tests/integrations/templates/__init__.py similarity index 100% rename from templates/__init__.py rename to tests/integrations/templates/__init__.py diff --git a/tests/integrations/templates/auth/base.html b/tests/integrations/templates/auth/base.html new file mode 100644 index 000000000..015d2dee8 --- /dev/null +++ b/tests/integrations/templates/auth/base.html @@ -0,0 +1,13 @@ + + + + + + + Masonite 4 + + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/tests/integrations/templates/auth/change_password.html b/tests/integrations/templates/auth/change_password.html new file mode 100644 index 000000000..158fd756b --- /dev/null +++ b/tests/integrations/templates/auth/change_password.html @@ -0,0 +1,49 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +

+ +
+ +
+ + +

+ Password Reset Request +

+ +
+ + + + + + + + + + +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/tests/integrations/templates/auth/home.html b/tests/integrations/templates/auth/home.html new file mode 100644 index 000000000..51c6eef7b --- /dev/null +++ b/tests/integrations/templates/auth/home.html @@ -0,0 +1,17 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ Welcome! +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/tests/integrations/templates/auth/login.html b/tests/integrations/templates/auth/login.html new file mode 100644 index 000000000..342cdf0af --- /dev/null +++ b/tests/integrations/templates/auth/login.html @@ -0,0 +1,64 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +{% endblock %} \ No newline at end of file diff --git a/tests/integrations/templates/auth/password_reset.html b/tests/integrations/templates/auth/password_reset.html new file mode 100644 index 000000000..9de6964be --- /dev/null +++ b/tests/integrations/templates/auth/password_reset.html @@ -0,0 +1,40 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ + +

+ Password Reset Request +

+ +
+ + + + + + +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/tests/integrations/templates/auth/register.html b/tests/integrations/templates/auth/register.html new file mode 100644 index 000000000..a03f122eb --- /dev/null +++ b/tests/integrations/templates/auth/register.html @@ -0,0 +1,72 @@ + +{% extends 'auth/base.html' %} + +{% block content %} +
+ +
+ +
+ + +

+ New Account +

+ +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/tests/integrations/templates/authorizations.html b/tests/integrations/templates/authorizations.html new file mode 100644 index 000000000..e93dcf6c3 --- /dev/null +++ b/tests/integrations/templates/authorizations.html @@ -0,0 +1,21 @@ + + + + + + + + Authorizations + + + +

Authorizations

+ + {% if can("view-posts") %} +

User can view posts

+ {% endif %} + {% if cannot("display-admin") %} +

User cannot display admin

+ {% endif %} + + \ No newline at end of file diff --git a/tests/integrations/templates/mail/welcome.html b/tests/integrations/templates/mail/welcome.html new file mode 100644 index 000000000..8b5794bc2 --- /dev/null +++ b/tests/integrations/templates/mail/welcome.html @@ -0,0 +1 @@ +to: {{ to }} \ No newline at end of file diff --git a/tests/integrations/templates/mailables/welcome.html b/tests/integrations/templates/mailables/welcome.html new file mode 100644 index 000000000..38e3455b8 --- /dev/null +++ b/tests/integrations/templates/mailables/welcome.html @@ -0,0 +1 @@ +

Welcome Email

\ No newline at end of file diff --git a/resources/templates/test.html b/tests/integrations/templates/test.html similarity index 100% rename from resources/templates/test.html rename to tests/integrations/templates/test.html diff --git a/tests/integrations/templates/test_helpers.html b/tests/integrations/templates/test_helpers.html new file mode 100644 index 000000000..e1c4f7929 --- /dev/null +++ b/tests/integrations/templates/test_helpers.html @@ -0,0 +1,3 @@ +{{ config("application.app_url") }} +{{ asset("local", "avatar.jpg") }} +{{ url("welcome") }} \ No newline at end of file diff --git a/tests/integrations/templates/vendor/test_package/admin/settings.html b/tests/integrations/templates/vendor/test_package/admin/settings.html new file mode 100644 index 000000000..495a3bc5b --- /dev/null +++ b/tests/integrations/templates/vendor/test_package/admin/settings.html @@ -0,0 +1 @@ +overriden \ No newline at end of file diff --git a/tests/integrations/templates/welcome.html b/tests/integrations/templates/welcome.html new file mode 100644 index 000000000..88b525b49 --- /dev/null +++ b/tests/integrations/templates/welcome.html @@ -0,0 +1,35 @@ + +Hello. Setup chat connection here + + + + + + + Document + + + + +

Welcome

+
+

flash messages

+

{{ session().get("test") }}

+ step 2 + + + +
+ + + \ No newline at end of file diff --git a/tests/integrations/test_package/__init__.py b/tests/integrations/test_package/__init__.py new file mode 100644 index 000000000..9643aaabd --- /dev/null +++ b/tests/integrations/test_package/__init__.py @@ -0,0 +1 @@ +from .providers.MyTestPackageProvider import MyTestPackageProvider diff --git a/storage/public/robots.txt b/tests/integrations/test_package/assets/folder/test.pdf similarity index 100% rename from storage/public/robots.txt rename to tests/integrations/test_package/assets/folder/test.pdf diff --git a/tests/broadcasts/__init__.py b/tests/integrations/test_package/assets/test.js similarity index 100% rename from tests/broadcasts/__init__.py rename to tests/integrations/test_package/assets/test.js diff --git a/tests/integrations/test_package/commands/Command1.py b/tests/integrations/test_package/commands/Command1.py new file mode 100644 index 000000000..f3638db65 --- /dev/null +++ b/tests/integrations/test_package/commands/Command1.py @@ -0,0 +1,12 @@ +from cleo.commands import Command + + +class Command1(Command): + """ + Test Command1 for test_package + + test_package:command1 + """ + + def handle(self): + pass diff --git a/tests/integrations/test_package/commands/Command2.py b/tests/integrations/test_package/commands/Command2.py new file mode 100644 index 000000000..edbcfcede --- /dev/null +++ b/tests/integrations/test_package/commands/Command2.py @@ -0,0 +1,12 @@ +from cleo.commands import Command + + +class Command2(Command): + """ + Test Command2 for test_package + + test_package:command2 + """ + + def handle(self): + pass diff --git a/tests/integrations/test_package/config/test.py b/tests/integrations/test_package/config/test.py new file mode 100644 index 000000000..1523cb68f --- /dev/null +++ b/tests/integrations/test_package/config/test.py @@ -0,0 +1,2 @@ +PARAM_1 = "test" +PARAM_2 = 1 diff --git a/tests/integrations/test_package/controllers/PackageController.py b/tests/integrations/test_package/controllers/PackageController.py new file mode 100644 index 000000000..0f54f0c1c --- /dev/null +++ b/tests/integrations/test_package/controllers/PackageController.py @@ -0,0 +1,9 @@ +from src.masonite.controllers import Controller + + +class PackageController(Controller): + def api(self): + return {"data": "ok"}, 201 + + def index(self): + return "index" diff --git a/tests/commands/__init__.py b/tests/integrations/test_package/migrations/create_some_table.py similarity index 100% rename from tests/commands/__init__.py rename to tests/integrations/test_package/migrations/create_some_table.py diff --git a/tests/integrations/test_package/providers/MyTestPackageProvider.py b/tests/integrations/test_package/providers/MyTestPackageProvider.py new file mode 100644 index 000000000..627b9a0d6 --- /dev/null +++ b/tests/integrations/test_package/providers/MyTestPackageProvider.py @@ -0,0 +1,27 @@ +from src.masonite.packages.providers import PackageProvider +from tests.integrations.test_package.commands.Command1 import Command1 +from tests.integrations.test_package.commands.Command2 import Command2 + + +""" +.../mypackage/templates/ + - admin.html + - test.html + +../myproject/templates/ +""" + + +class MyTestPackageProvider(PackageProvider): + def configure(self): + ( + self.root("tests/integrations/test_package") + .name("test_package") + .config("config/test.py", publish=True) + .views("templates", publish=True) + .commands(Command1(), Command2()) + .migrations("migrations/create_some_table.py") + .assets("assets") + .controllers("controllers") # ensure this one is done before routes() + # .routes("routes/api.py", "routes/web.py") + ) diff --git a/tests/integrations/test_package/routes/api.py b/tests/integrations/test_package/routes/api.py new file mode 100644 index 000000000..5c8fd1c10 --- /dev/null +++ b/tests/integrations/test_package/routes/api.py @@ -0,0 +1,5 @@ +from src.masonite.routes import Route + +ROUTES = [ + Route.get("/api/package/test/", "PackageController@api"), +] diff --git a/tests/integrations/test_package/routes/web.py b/tests/integrations/test_package/routes/web.py new file mode 100644 index 000000000..28fbca1fe --- /dev/null +++ b/tests/integrations/test_package/routes/web.py @@ -0,0 +1,5 @@ +from src.masonite.routes import Route + +ROUTES = [ + Route.get("/package/test/", "PackageController@index"), +] diff --git a/tests/integrations/test_package/templates/admin/settings.html b/tests/integrations/test_package/templates/admin/settings.html new file mode 100644 index 000000000..0b4040828 --- /dev/null +++ b/tests/integrations/test_package/templates/admin/settings.html @@ -0,0 +1 @@ +package settings \ No newline at end of file diff --git a/tests/integrations/test_package/templates/package.html b/tests/integrations/test_package/templates/package.html new file mode 100644 index 000000000..972a19655 --- /dev/null +++ b/tests/integrations/test_package/templates/package.html @@ -0,0 +1,5 @@ +{% extends "package_base.html" %} + +{% block content %} +

Test package

+{% endblock %} \ No newline at end of file diff --git a/tests/integrations/test_package/templates/package_base.html b/tests/integrations/test_package/templates/package_base.html new file mode 100644 index 000000000..4b3770356 --- /dev/null +++ b/tests/integrations/test_package/templates/package_base.html @@ -0,0 +1,13 @@ + + + + + + + Package Title + + + {% block content %} + {% endblock %} + + \ No newline at end of file diff --git a/tests/integrations/web.py b/tests/integrations/web.py new file mode 100644 index 000000000..758deeffe --- /dev/null +++ b/tests/integrations/web.py @@ -0,0 +1,22 @@ +from src.masonite.routes import Route +from src.masonite.broadcasting import Broadcast +from src.masonite.authentication import Auth + +ROUTES = [ + Route.get("/", "WelcomeController@show").name("welcome"), + Route.get("/flash_data", "WelcomeController@flash_data"), + Route.get("/sessions", "WelcomeController@play_with_session").name( + "play_with_session" + ), + Route.post("/", "WelcomeController@show"), + Route.post("/input", "WelcomeController@input"), + Route.post("/upload", "WelcomeController@upload").name("upload"), + Route.get("/test", "WelcomeController@test"), + Route.get("/emit", "WelcomeController@emit"), + Route.get("/view", "WelcomeController@view"), + Route.get("/mail", "MailableController@view"), + Route.get("/users/@id", "WelcomeController@test").name("users.profile"), +] + +Broadcast.routes() +Auth.routes() diff --git a/tests/listeners/test_exception_listener.py b/tests/listeners/test_exception_listener.py deleted file mode 100644 index d8579aefa..000000000 --- a/tests/listeners/test_exception_listener.py +++ /dev/null @@ -1,47 +0,0 @@ -from src.masonite.testing import TestCase -from src.masonite.request import Request -from src.masonite.listeners import BaseExceptionListener -from routes.web import ROUTES -class ExceptionListener(BaseExceptionListener): - - listens = [ - ZeroDivisionError - ] - - def __init__(self, request: Request): - self.request = request - - def handle(self, exception, file, line): - self.request.error_thrown = True - -class ExceptionAllListener(BaseExceptionListener): - - listens = ['*'] - - def __init__(self, request: Request): - self.request = request - - def handle(self, exception, file, line): - self.request.error_thrown = True - - -class TestExceptionListener(TestCase): - - def setUp(self): - super().setUp() - self.routes(ROUTES) - self.container.simple(ExceptionListener) - - def test_listener_fires(self): - self.withExceptionHandling() - self.assertEqual(self.get('/bad').request.error_thrown, True) - - def test_listener_doesnt_fire(self): - self.withExceptionHandling() - with self.assertRaises(AttributeError): - self.assertEqual(self.get('/keyerror').request.error_thrown, True) - - def test_listener_fires_for_all(self): - self.withExceptionHandling() - self.container.simple(ExceptionAllListener) - self.assertEqual(self.get('/keyerror').request.error_thrown, True) diff --git a/tests/middleware/__init__.py b/tests/middleware/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/middleware/test_cors_middleware.py b/tests/middleware/test_cors_middleware.py deleted file mode 100644 index 2adb18991..000000000 --- a/tests/middleware/test_cors_middleware.py +++ /dev/null @@ -1,21 +0,0 @@ -from src.masonite.middleware import CorsMiddleware -from src.masonite.routes import Get -from src.masonite.testing import TestCase - -class TestCorsMiddleware(TestCase): - - def setUp(self): - super().setUp() - self.buildOwnContainer() - self.middleware = CorsMiddleware - self.middleware.CORS = {"Access-Control-Allow-Origin": "*"} - self.withHttpMiddleware([ - self.middleware, - ]) - self.routes(only=[ - Get('/', 'TestController@show'), - ]) - - def test_cors_middleware(self): - mock = self.get('/') - mock.assertHeaderIs('Access-Control-Allow-Origin', '*') diff --git a/tests/middleware/test_csrf_middleware.py b/tests/middleware/test_csrf_middleware.py deleted file mode 100644 index b99115c4b..000000000 --- a/tests/middleware/test_csrf_middleware.py +++ /dev/null @@ -1,71 +0,0 @@ -from src.masonite.middleware import CsrfMiddleware -from src.masonite.exceptions import InvalidCSRFToken -from src.masonite.routes import Get, Post -from src.masonite.testing import TestCase - - -class TestCSRFMiddleware(TestCase): - - def setUp(self): - super().setUp() - self.buildOwnContainer() - self.middleware = CsrfMiddleware - self.withHttpMiddleware([ - self.middleware, - ]) - self.routes(only=[ - Post('/test/@route', 'TestController@show'), - Get('/test/10', 'TestController@show'), - Post('/', 'TestController@show'), - ]) - - self.withCsrf() - - def test_middleware_shares_correct_input(self): - container = self.get('/test/10').container - self.assertIn('csrf_field', container.make('ViewClass')._shared) - self.assertTrue(container.make('ViewClass')._shared['csrf_field'].startswith("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 Tu3uEazv%UJ2VbsM|IGXs3;Bod diff --git a/tests/storage/__init__.py b/tests/storage/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/storage/test_storage_manager.py b/tests/storage/test_storage_manager.py deleted file mode 100644 index 29aa75e10..000000000 --- a/tests/storage/test_storage_manager.py +++ /dev/null @@ -1,82 +0,0 @@ -import os - -from src.masonite.testing import TestCase -from src.masonite.managers import StorageManager -from src.masonite.drivers import StorageDiskDriver -from config import storage - - -class TestStorage(TestCase): - - def setUp(self): - super().setUp() - self.container.bind('StorageDiskDriver', StorageDiskDriver) - self.container.bind('StorageManager', StorageManager(self.container)) - self.container.bind('Storage', StorageManager(self.container).driver(storage.DRIVER)) - self.manager = self.container.make('Storage') - - def test_storage_creates_disk_driver(self): - self.assertIsInstance(self.manager.driver('disk'), StorageDiskDriver) - - def test_storage_puts_file(self): - driver = self.manager.driver('disk') - driver.put('storage/some_file.txt', 'HI') - self.assertEqual(driver.get('storage/some_file.txt'), 'HI') - - def test_storage_appends_file(self): - driver = self.manager.driver('disk') - driver.append('storage/some_file.txt', 'HI') - self.assertEqual(driver.get('storage/some_file.txt'), 'HIHI') - - def test_storage_deletes_file(self): - driver = self.manager.driver('disk') - driver.put('storage/some_file.txt', 'HI') - self.assertTrue(driver.delete('storage/some_file.txt')) - - def test_storage_file_exists(self): - driver = self.manager.driver('disk') - driver.put('storage/some_file.txt', 'HI') - self.assertTrue(driver.exists('storage/some_file.txt')) - self.assertTrue(driver.delete('storage/some_file.txt')) - self.assertFalse(driver.exists('storage/some_file.txt')) - - def test_storage_file_gets_size(self): - driver = self.manager.driver('disk') - self.assertEqual(driver.size('storage/not_exists.txt'), 0) - driver.put('storage/file.txt', 'HI') - self.assertEqual(driver.size('storage/file.txt'), 2) - self.assertTrue(driver.delete('storage/file.txt')) - - def test_storage_get_extension(self): - driver = self.manager.driver('disk') - driver.put('storage/file.txt', 'HI') - self.assertEqual(driver.extension('storage/file.txt'), 'txt') - - def test_storage_upload(self): - driver = self.manager.driver('disk') - driver.upload(ImageMock()) - - def test_storage_make_directory(self): - driver = self.manager.driver('disk') - self.assertTrue(driver.make_directory('storage/some_directory')) - self.assertTrue(os.path.isdir('storage/some_directory')) - - def test_storage_delete_directory(self): - driver = self.manager.driver('disk') - self.assertTrue(driver.delete_directory('storage/some_directory')) - self.assertFalse(os.path.isdir('storage/some_directory')) - - -class ImageMock(): - """ - Image test for emulate upload file - """ - - filename = 'test.jpg' - - @property - def file(self): - return self - - def read(self): - return bytes('file read', 'utf-8') diff --git a/tests/testing/__init__.py b/tests/testing/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/testing/test_database_tests.py b/tests/testing/test_database_tests.py deleted file mode 100644 index 4702eb11d..000000000 --- a/tests/testing/test_database_tests.py +++ /dev/null @@ -1,27 +0,0 @@ - -from src.masonite.testing import TestCase - -from masoniteorm.models import Model -from src.masonite import env - - -class User(Model): - pass - - -if env('RUN_DATABASE'): - class TestDatabase(TestCase): - - def setUp(self): - super().setUp() - self.make(User, self.users_factory, 20) - - def users_factory(self, faker): - return { - 'name': faker.name(), - 'email': faker.email(), - 'password': '$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk/8/aNbVMpD1dR.Et83vfL8WAu', # == 'secret' - } - - def test_has_records(self): - self.assertGreater(User.all().count(), 0) diff --git a/tests/testing/test_route_tests.py b/tests/testing/test_route_tests.py deleted file mode 100644 index 5c0aca795..000000000 --- a/tests/testing/test_route_tests.py +++ /dev/null @@ -1,175 +0,0 @@ -from routes import web -from src.masonite.testing import TestCase -from app.User import User -from src.masonite.exceptions import InvalidCSRFToken - - -class TestUnitTest(TestCase): - - def setUp(self): - super().setUp() - self.routes(web.ROUTES) - - def setUpFactories(self): - User.create({ - 'name': 'Joe', - 'email': 'user@example.com', - 'password': 'secret' - }) - - def test_can_get_route(self): - self.assertTrue(self.get('/unit/test/get').ok()) - - self.assertTrue(self.get('/unit/test/get').canView()) - - def test_can_post_route(self): - self.assertTrue(self.post('/unit/test/post').ok()) - self.assertTrue(self.post('/unit/test/post').contains('posted')) - - def test_can_send_post_parameters(self): - self.assertTrue( - self.post('/unit/test/params', {'test': 'test this'}).contains('test this') - ) - - def test_can_send_get_parameters(self): - self.assertTrue( - self.get('/unit/test/get/params', {'test': 'test this'}).contains('test this') - ) - - def test_can_test_route_name(self): - self.assertTrue( - self.get('/unit/test/get/params').isNamed('get.params') - ) - - def test_has_middleware(self): - self.assertTrue( - self.post('/unit/test/post').hasMiddleware('test') - ) - - def test_can_get_route_param(self): - self.assertTrue( - self.get('/unit/test/param/1').contains('1') - ) - - def test_can_have_user(self): - - self.assertTrue( - self.actingAs(User.find(1)).post('/unit/test/user').contains('Joe') - ) - - def test_json(self): - self.assertTrue(self.json('POST', '/unit/test/json', {'test': 'testing'}).contains('testing')) - - def test_json_response(self): - self.assertTrue(self.json('GET', '/unit/test/json/response').hasJson('count', 5)) - - def test_json_response_dictionary(self): - self.assertTrue(self.json('GET', '/unit/test/json/response').hasJson({ - 'count': 5 - })) - - self.assertFalse(self.json('GET', '/unit/test/json/response').hasJson({ - 'count': 10 - })) - - def test_multi_json_response(self): - self.assertTrue(self.json('GET', '/unit/test/json/multi').hasJson({ - 'author.name': 'Joe' - })) - - self.assertTrue(self.json('GET', '/unit/test/json/multi').hasJson('author.name', 'Joe')) - self.assertFalse(self.json('GET', '/unit/test/json/multi_count').hasJson('count.foo', 'foo')) - - def test_as_dictionary(self): - dictionary = self.json('GET', '/unit/test/json/multi').asDictionary() - self.assertEqual(dictionary['author']['name'], 'Joe') - - with self.assertRaises(ValueError): - dictionary = self.json('GET', '/login').asDictionary() - - def test_count(self): - self.assertTrue(self.json('GET', '/unit/test/json/response').count(2)) - self.assertFalse(self.json('GET', '/unit/test/json/response').count(1)) - - self.assertTrue(self.json('GET', '/unit/test/json/response').amount(2)) - self.assertFalse(self.json('GET', '/unit/test/json/response').amount(1)) - - def test_has_amount(self): - self.assertTrue(self.json('GET', '/unit/test/json/response').hasAmount('iterable', 3)) - self.assertFalse(self.json('GET', '/unit/test/json/response').hasAmount('iterable', 2)) - - self.json('GET', '/unit/test/json/response').assertHasAmount('iterable', 3) - self.json('GET', '/unit/test/json/response').assertNotHasAmount('iterable', 2) - - def test_patch(self): - self.assertTrue(self.patch('/unit/test/patch', {'test': 'testing'}).contains('testing')) - - def test_csrf(self): - self.withCsrf() - with self.assertRaises(InvalidCSRFToken): - self.assertTrue(self.post('/unit/test/json', {'test': 'testing'}).contains('testing')) - - def test_database_has(self): - self.assertDatabaseHas('users.email', 'user@example.com') - self.assertDatabaseNotHas('users.email', 'joe@example.com') - - def test_acting_as_none(self): - with self.assertRaises(TypeError): - self.actingAs(User.find(10)).get('/helloworld') - - def test_capture_output(self): - with self.captureOutput() as output: - print('hello', end='') - - self.assertEqual(output.getvalue(), 'hello') - - def test_assert_view_is(self): - self.get("/v").assertViewIs("test") - - def test_assert_view_has(self): - self.get("/test/view").assertViewHas("count") - self.get("/test/view").assertViewHas("count", 1) - self.get("/test/view").assertViewHas("users", ["John", "Joe"]) - - with self.assertRaises(AssertionError): - self.get("/test/view").assertViewHas("not_in_view") - with self.assertRaises(AssertionError): - self.get("/test/view").assertViewHas("not_in_view", 3) - - def test_assert_view_helpers_raise_error_if_not_rendering_a_view(self): - # json response - with self.assertRaises(ValueError): - self.get("/json_response").assertViewIs("test") - # string response - with self.assertRaises(ValueError): - self.get("/example/test/1").assertViewIs("test") - # - with self.assertRaises(ValueError): - self.get("/json_response").assertViewHas("not_in_view") - - with self.assertRaises(ValueError): - self.get("/json_response").assertViewHasAll(["test"]) - - with self.assertRaises(ValueError): - self.get("/example/test/1").assertViewMissing(["not in data"]) - - def test_assert_view_has_all(self): - self.get("/test/view").assertViewHasAll(["users", "count"]) - self.get("/test/view").assertViewHasAll({"count": 1, "users": ["John", "Joe"]}) - - with self.assertRaises(AssertionError): - self.get("/test/view").assertViewHasAll(["users", "count", "not in data"]) - - with self.assertRaises(AssertionError): - self.get("/test/view").assertViewHasAll({"count": 1}) - - def test_assert_view_missing(self): - self.get("/test/view").assertViewMissing("not in data") - - with self.assertRaises(AssertionError): - self.get("/test/view").assertViewMissing("users") - - def test_assert_redirect(self): - self.get("/test/redirect").assertRedirect("/v") - with self.assertRaises(AssertionError): - self.get("/test/view").assertRedirect("v") diff --git a/tests/tests/test_commands.py b/tests/tests/test_commands.py new file mode 100644 index 000000000..3877e070e --- /dev/null +++ b/tests/tests/test_commands.py @@ -0,0 +1,47 @@ +from cleo import Command +from cleo import Application as CommandApplication +from src.masonite.commands import CommandCapsule +from tests import TestCase + + +class FakeTestCommand(Command): + """ + Command for testing command assertions only. + fake_test_command + {--f|--fail : Make command fail} + """ + + def handle(self): + self.info("Command success !") + + +class TestCommandsAssertions(TestCase): + def setUp(self): + super().setUp() + self.original_commands = self.application.make("commands") + command_app = CommandCapsule(CommandApplication("Masonite Version:", "tests")) + self.application.bind("commands", command_app) + self.application.make("commands").add(FakeTestCommand()) + + def tearDown(self): + super().tearDown() + self.application.bind("commands", self.original_commands) + + def test_running_command_during_tests(self): + self.craft("fake_test_command") + + def test_assert_output(self): + self.craft("fake_test_command").assertOutputContains("Command") + self.craft("fake_test_command").assertExactOutput("Command success !\n") + + def test_assert_output_missing(self): + self.craft("fake_test_command").assertOutputMissing( + "This is not in the command" + ) + + def test_assert_errors(self): + with self.assertRaises(AssertionError): + self.craft("fake_test_command").assertHasErrors() + + def test_assert_success(self): + self.craft("fake_test_command").assertSuccess() diff --git a/tests/tests/test_mock.py b/tests/tests/test_mock.py new file mode 100644 index 000000000..214b096a9 --- /dev/null +++ b/tests/tests/test_mock.py @@ -0,0 +1,19 @@ +from tests import TestCase +from src.masonite.mail import Mail, MockMail + + +class CustomMockMail: + def __init__(self, application): + self.application = application + + +class TestMocking(TestCase): + def test_fake_service(self): + mocked_mail = self.fake("mail") + + assert isinstance(self.application.make("mail"), MockMail) + assert isinstance(mocked_mail, MockMail) + + self.restore("mail") + + assert isinstance(self.application.make("mail"), Mail) diff --git a/tests/tests/test_testcase.py b/tests/tests/test_testcase.py new file mode 100644 index 000000000..89bfe3797 --- /dev/null +++ b/tests/tests/test_testcase.py @@ -0,0 +1,325 @@ +import pendulum + +from tests import TestCase +from tests.integrations.controllers.WelcomeController import WelcomeController +from masoniteorm.models import Model +from src.masonite.routes import Route +from src.masonite.authentication import Authenticates + + +class User(Model, Authenticates): + pass + + +class CustomTestResponse: + def assertCustom(self): + assert 1 + return self + + +class OtherCustomTestResponse: + def assertOtherCustom(self): + assert 2 + return self + + +class TestTestCase(TestCase): + def setUp(self): + super().setUp() + self.setRoutes( + Route.get("/", "WelcomeController@show").name("home"), + ) + + def tearDown(self): + super().tearDown() + self.restoreTime() + + def test_add_routes(self): + self.assertEqual(len(self.application.make("router").routes), 1) + self.addRoutes( + Route.get("/test", "WelcomeController@show").name("test"), + ) + self.assertEqual(len(self.application.make("router").routes), 2) + + def test_use_custom_test_response(self): + self.application.make("tests.response").add( + CustomTestResponse, OtherCustomTestResponse + ) + # can use default assertions and custom from different classes + self.get("/").assertContains("Welcome").assertCustom().assertOtherCustom() + + def test_fake_time(self): + given_date = pendulum.datetime(2015, 2, 5) + self.fakeTime(given_date) + self.assertEqual(pendulum.now(), given_date) + self.restoreTime() + self.assertNotEqual(pendulum.now(), given_date) + + def test_fake_time_tomorrow(self): + tomorrow = pendulum.tomorrow() + self.fakeTimeTomorrow() + self.assertEqual(pendulum.now(), tomorrow) + + def test_fake_time_yesterday(self): + yesterday = pendulum.yesterday() + self.fakeTimeYesterday() + self.assertEqual(pendulum.now(), yesterday) + + def test_fake_time_in_future(self): + real_now = pendulum.now() + self.fakeTimeInFuture(10) + self.assertEqual(pendulum.now().diff(real_now).in_days(), 10) + self.assertGreater(pendulum.now(), real_now) + + self.fakeTimeInFuture(1, "months") + self.assertEqual(pendulum.now().diff(real_now).in_months(), 1) + + # def test_fake_time_in_past(self): + # real_now = pendulum.now() + # self.fakeTimeInPast(10) + # self.assertEqual(pendulum.now().diff(real_now).in_days(), 10) + # self.assertLess(pendulum.now(), real_now) + + # self.fakeTimeInPast(3, "hours") + # self.assertEqual(real_now.hour - pendulum.now().hour, 3) + + +class TestTestingAssertions(TestCase): + def setUp(self): + super().setUp() + self.setRoutes( + Route.get("/", "WelcomeController@show").name("home"), + Route.get("/test", "WelcomeController@show").name("test"), + Route.get("/view", "WelcomeController@view").name("view"), + Route.get("/view-context", "WelcomeController@view_with_context").name( + "view_with_context" + ), + Route.get("/test-404", "WelcomeController@not_found").name("not_found"), + Route.get("/test-creation", "WelcomeController@create").name("create"), + Route.get("/test-unauthorized", "WelcomeController@unauthorized").name( + "unauthorized" + ), + Route.get("/test-forbidden", "WelcomeController@forbidden").name( + "forbidden" + ), + Route.get("/test-empty", "WelcomeController@empty").name("empty"), + Route.get( + "/test-response-header", "WelcomeController@response_with_headers" + ), + Route.get("/test-redirect-1", "WelcomeController@redirect_url"), + Route.get("/test-redirect-2", "WelcomeController@redirect_route"), + Route.get("/test-redirect-3", "WelcomeController@redirect_route_params"), + Route.get("/test/@id", "WelcomeController@with_params").name("test_params"), + Route.get("/test-json", "WelcomeController@json").name("json"), + Route.get("/test-session", "WelcomeController@session").name("session"), + Route.get( + "/test-session-errors", "WelcomeController@session_with_errors" + ).name("session"), + Route.get("/test-session-2", "WelcomeController@session2").name("session2"), + Route.get("/test-authenticates", "WelcomeController@auth").name("auth"), + ) + + def test_assert_contains(self): + self.get("/").assertContains("Welcome") + self.get("/").assertNotContains("hello") + + def test_assert_is_named(self): + self.get("/test").assertIsNamed("test") + self.get("/test").assertIsNotNamed("welcome") + + def test_assert_not_found(self): + self.get("/test-404").assertNotFound() + + def test_assert_is_status(self): + self.get("/test").assertIsStatus(200) + + def test_assert_ok(self): + self.get("/test").assertOk() + + def test_assert_created(self): + self.get("/test-creation").assertCreated() + + def test_assert_unauthorized(self): + self.get("/test-unauthorized").assertUnauthorized() + + def test_assert_forbidden(self): + self.get("/test-forbidden").assertForbidden() + + def test_assert_no_content(self): + self.get("/test-empty").assertNoContent() + + def test_assert_cookie(self): + self.withCookies({"test": "value"}).get("/").assertCookie("test") + + def test_assert_cookie_value(self): + self.withCookies({"test": "value"}).get("/").assertCookie("test", "value") + + def test_assert_cookie_missing(self): + self.get("/").assertCookieMissing("test") + + def test_assert_plain_cookie(self): + self.withCookies({"test": "value"}).get("/").assertPlainCookie("test") + + def test_assert_has_header(self): + self.get("/test-response-header").assertHasHeader("TEST") + self.get("/test-response-header").assertHasHeader("TEST", "value") + + def test_assert_header_missing(self): + self.get("/").assertHeaderMissing("X-Test") + + def test_assert_request_with_headers(self): + request = self.withHeaders({"X-TEST": "value"}).get("/").request + assert request.header("X-Test") == "value" + + def test_assert_redirect_to_url(self): + self.get("/test-redirect-1").assertRedirect("/") + + def test_assert_redirect_to_route(self): + self.get("/test-redirect-2").assertRedirect(name="test") + self.get("/test-redirect-3").assertRedirect( + name="test_params", params={"id": 1} + ) + + def test_assert_session_has(self): + self.get("/test-session").assertSessionHas("key") + self.get("/test-session").assertSessionHas("key", "value") + + def test_assert_session_has_errors(self): + self.get("/test-session-errors").assertSessionHasErrors() + self.get("/test-session-errors").assertSessionHasErrors(["email"]) + self.get("/test-session-errors").assertSessionHasErrors(["email", "password"]) + + def test_assert_session_has_no_errors(self): + self.get("/test-session").assertSessionHasNoErrors() + self.get("/test-session-errors").assertSessionHasNoErrors(["name"]) + + def test_assert_session_missing(self): + self.get("/").assertSessionMissing("some_test_key") + + def test_assert_view_is(self): + self.get("/view").assertViewIs("welcome") + + def test_assert_view_has(self): + self.get("/view-context").assertViewHas("count") + self.get("/view-context").assertViewHas("count", 1) + self.get("/view-context").assertViewHas("users", ["John", "Joe"]) + self.get("/view-context").assertViewHas("other_key.nested") + self.get("/view-context").assertViewHas("other_key.nested", 1) + + with self.assertRaises(AssertionError): + self.get("/view-context").assertViewHas("not_in_view") + with self.assertRaises(AssertionError): + self.get("/view-context").assertViewHas("not_in_view", 3) + + def test_assert_view_helpers_raise_error_if_not_rendering_a_view(self): + # json response + with self.assertRaises(ValueError): + self.get("/test-json").assertViewIs("test") + # string response + self.get("/test").assertViewIs("welcome") + + def test_assert_view_has_exact(self): + self.get("/view-context").assertViewHasExact(["users", "count", "other_key"]) + self.get("/view-context").assertViewHasExact( + {"count": 1, "users": ["John", "Joe"], "other_key": {"nested": 1}} + ) + + with self.assertRaises(AssertionError): + self.get("/view-context").assertViewHasExact( + ["users", "count", "not in data"] + ) + + with self.assertRaises(AssertionError): + self.get("/view-context").assertViewHasExact({"count": 1}) + + def test_assert_view_missing(self): + self.get("/view-context").assertViewMissing("not in data") + + with self.assertRaises(AssertionError): + self.get("/view-context").assertViewMissing("users") + + def test_assert_guest(self): + self.get("/test").assertGuest() + + # def test_assert_authenticated(self): + # self.get("/test-authenticates").assertAuthenticated() + + def test_assert_authenticated_as(self): + self.make_request() + self.application.make("auth").guard("web").attempt( + "idmann509@gmail.com", "secret" + ) + user = User.find(1) + self.get("/test").assertAuthenticatedAs(user) + + def test_assert_has_controller(self): + self.get("/test").assertHasController("WelcomeController@show") + self.get("/test").assertHasController(WelcomeController) + + def test_assert_route_has_parameter(self): + self.get("/test/3").assertRouteHasParameter("id") + with self.assertRaises(AssertionError): + self.get("/test/3").assertRouteHasParameter("key") + # self.get("/test/3").assertRouteHasParameter("id", 3) + # with self.assertRaises(AssertionError): + # self.get("/test/3").assertRouteHasParameter("id", 4) + + def test_assert_has_route_middleware(self): + self.get("/test").assertHasRouteMiddleware("web") + + def test_assert_has_http_middleware(self): + # TODO: add one for testing purposes + # self.get("/test").assertHasHttpMiddleware() + pass + + def test_assert_json(self): + self.get("/test-json").assertJson({"key": "value"}) + # works also in a nested path + self.get("/test-json").assertJson( + {"other_key": {"nested": 1, "nested_again": {"a": 1, "b": 2}}} + ) + + def test_json_assertions_fail_when_response_not_json(self): + with self.assertRaises(ValueError): + self.get("/view").assertJson({"key": "value"}) + + def test_assert_json_path(self): + self.get("/test-json").assertJsonPath("key2", [1, 2]) + self.get("/test-json").assertJsonPath("other_key.nested", 1) + self.get("/test-json").assertJsonPath("other_key.nested_again.b", 2) + self.get("/test-json").assertJsonPath( + "other_key.nested_again", {"a": 1, "b": 2} + ) + + def test_assert_json_count(self): + self.get("/test-json").assertJsonCount(3) + self.get("/test-json").assertJsonCount(2, key="other_key") + + def test_assert_json_exact(self): + self.get("/test-json").assertJsonExact( + { + "key": "value", + "key2": [1, 2], + "other_key": { + "nested": 1, + "nested_again": {"a": 1, "b": 2}, + }, + } + ) + + def test_assert_json_missing(self): + self.get("/test-json").assertJsonMissing("key3") + self.get("/test-json").assertJsonMissing("some_key.nested") + with self.assertRaises(AssertionError): + self.get("/test-json").assertJsonMissing("other_key.nested") + + def test_assert_database_count(self): + self.assertDatabaseCount("users", 1) + + def test_assert_database_has(self): + self.assertDatabaseHas("users", {"name": "Joe"}) + + def test_assert_database_missing(self): + self.assertDatabaseMissing( + "users", {"name": "John", "email": "john@example.com"} + ) diff --git a/tests/tests/test_transactions.py b/tests/tests/test_transactions.py new file mode 100644 index 000000000..263c149a8 --- /dev/null +++ b/tests/tests/test_transactions.py @@ -0,0 +1,19 @@ +from tests import TestCase +from tests.integrations.app.User import User +from src.masonite.tests import DatabaseTransactions + + +class TestDatabase(TestCase, DatabaseTransactions): + + connection = None + + def setUp(self): + super().setUp() + + def test_can_use_transactions(self): + User.create({"name": "john", "email": "john6", "password": "secret"}) + + # def test_assert_deleted(self): + # user = User.find(1) + # user.delete() + # self.assertDeleted(user) diff --git a/tests/unit/test_works.py b/tests/unit/test_works.py deleted file mode 100644 index bbc085318..000000000 --- a/tests/unit/test_works.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_unit(): - assert True diff --git a/trigger_build.py b/trigger_build.py deleted file mode 100644 index 0e2f03ac9..000000000 --- a/trigger_build.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -This is a script to easily execute circle CI jobs from other repository builds. -Useful if you have a dependent build that you need to run to ensure the latest code is up to date with the parent build. - -This does not work for PR forks because a Circle Access token is required which is insecure for forks. - -This script will: - - - Fire a build in another circle CI repository - - Loop and call the build_url to continuously get the status - - Wait until the job finishes - - If the job fails this script will exit 1 which will fail the job. - - Else it will exit 0 which will pass the job. - -**You will need to download this script and put it in the base of your repository** - -Usage: - - python trigger_build.py --repo user/repo --branch branch_name --token CIRCLE_TOKEN --build ENV_VARIABLE=value --build ENV_VARIABLE2=value2 - -Example: - - python trigger_build.py --repo masoniteframework/validation --branch circle-ci --token $CIRCLE_TOKEN --build MASONITE_BRANCH=$CIRCLE_BRANCH - -NOTE: You can get your CircleCI access token via your dashboard. If no token is specified it will use the CIRCLE_TOKEN in your job dashboard. -So if you don't want to pass in the token then put it in the environment variable in your job's dashboard. - -Example config: -version: 2 -jobs: - Masonite Validation: - docker: - - image: circleci/python:3.6 - steps: - - checkout - - run: python trigger_build.py --repo masoniteframework/validation --branch circle-ci --build MASONITE_DEPENDENT_BRANCH=$CIRCLE_BRANCH -""" - -import requests -import time -import os -import argparse -parser = argparse.ArgumentParser() - -parser.add_argument("-r", "--repo", help="Repository name") -parser.add_argument("-b", "--branch", help="Branch name") -parser.add_argument("-t", "--token", help="Circle CI Token") -parser.add_argument('-a', '--build', action='append', help='Set Build Arguments') -parser.add_argument('-p', '--poll', help='How long the script should sleep before checking status') -args = parser.parse_args() - -repo = args.repo -branch = args.branch or 'master' -token = args.token or os.getenv('CIRCLE_TOKEN') -current_repo = os.getenv('CIRCLE_PROJECT_REPONAME') -current_user = os.getenv('CIRCLE_PROJECT_USERNAME') -poll = args.poll or 5 - -if os.getenv('CIRCLE_PR_NUMBER'): - print('Cannot Build On PR Forks.') - exit(0) - -if not token: - print('No token found.') - exit(1) - -parameters = {} -for argument in args.build or []: - if '=' not in argument: - print("ERROR: '--build' argument must contain a '=' sign. Got '{}'".format(argument)) - exit(1) - key = argument.split('=')[0] - value = argument.split('=')[1] - - parameters.update({key: value}) - - -endpoint = 'https://circleci.com/api/v2/project/gh/{}/pipeline?circle-token={}'.format(repo, token) -headers = {'Circle-Token': f'{token}', 'Accept': 'application/json'} -r = requests.post(endpoint, json={'branch': branch, 'parameters': parameters}, headers=headers) -if 'number' not in r.json(): - print('ERROR: Could not find repository {} or with the branch {}'.format(repo, branch)) - print(r.json()) - exit(1) - -print('Building: ', r.json()['number']) - -status = requests.get('https://circleci.com/api/v2/pipeline/{}?circle-token={}'.format(r.json()['id'], token)) -print('Build Status: ', status.json()['state']) -workflow_id = status.json()['workflows'][0]['id'] -workflow = requests.get('https://circleci.com/api/v2/workflow/{}?circle-token={}'.format(workflow_id, token)) -while workflow.json()['status'] not in ('error', 'failed', 'success'): - time.sleep(poll) - print('Build Status: ', workflow.json()['status']) - workflow = requests.get('https://circleci.com/api/v2/workflow/{}?circle-token={}'.format(workflow_id, token)) - -print('Finished with status: ', workflow.json()['status']) -if workflow.json()['status'] in ('error', 'failed'): - exit(1) diff --git a/uploads/.gitkeep b/uploads/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/wsgi.py b/wsgi.py index c8bf14e02..beab0b112 100644 --- a/wsgi.py +++ b/wsgi.py @@ -1,50 +1,18 @@ -"""First Entry For The WSGI Server.""" +from src.masonite.foundation import Application, Kernel +from src.masonite.utils.location import base_path +from tests.integrations.config.providers import PROVIDERS +from tests.integrations.app.Kernel import Kernel as ApplicationKernel -from src.masonite.app import App -from src.masonite.wsgi import response_handler, package_response_handler -from src.masonite.helpers import config -from src.masonite.environment import LoadEnvironment +# here the project base path is tests/integrations +application = Application(base_path("tests/integrations")) -"""Instantiate Container And Perform Important Bindings -Some Service providers need important bindings like the WSGI application -and the application configuration file before they boot. +"""First Bind important providers needed to start the server """ -LoadEnvironment() +application.register_providers( + Kernel, + ApplicationKernel, +) -container = App() - -container.bind('WSGI', package_response_handler) -container.bind('Container', container) - -container.bind('Providers', []) -container.bind('WSGIProviders', []) - -"""Bind all service providers -Let's register everything into the Service Container. Once everything is -in the container we can run through all the boot methods. For reasons -some providers don't need to execute with every request and should -only run once when the server is started. Providers will be ran -once if the wsgi attribute on a provider is False. -""" - -for provider in config('providers.providers'): - located_provider = provider() - located_provider.load_app(container).register() - if located_provider.wsgi: - container.make('WSGIProviders').append(located_provider) - else: - container.make('Providers').append(located_provider) - -for provider in container.make('Providers'): - container.resolve(provider.boot) - -"""Get the application from the container -Some providers may change the WSGI Server like wrapping the WSGI server -in a Whitenoise container for an example. Let's get a WSGI instance -from the container and pass it to the application variable. This -will allow WSGI servers to pick it up from the command line -""" - -application = container.make('WSGI') +application.add_providers(*PROVIDERS)