diff --git a/Pipfile.lock b/Pipfile.lock index 3578eebe..5d64a8b5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -91,11 +91,11 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", + "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" ], "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.6.2" }, "cffi": { "hashes": [ @@ -287,41 +287,41 @@ }, "cryptography": { "hashes": [ - "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", - "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", - "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", - "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", - "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", - "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", - "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", - "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", - "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", - "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", - "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", - "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", - "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", - "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", - "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", - "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", - "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", - "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", - "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", - "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", - "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", - "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", - "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", - "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", - "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", - "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", - "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", - "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", - "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", - "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", - "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", - "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" + "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", + "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", + "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", + "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", + "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", + "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", + "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", + "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", + "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", + "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", + "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", + "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", + "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", + "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", + "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", + "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", + "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", + "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", + "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", + "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", + "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", + "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", + "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", + "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", + "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", + "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", + "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", + "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", + "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", + "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", + "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", + "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" ], "markers": "python_version >= '3.7'", - "version": "==42.0.7" + "version": "==42.0.8" }, "daphne": { "hashes": [ @@ -507,19 +507,19 @@ }, "redis": { "hashes": [ - "sha256:7adc2835c7a9b5033b7ad8f8918d09b7344188228809c98df07af226d39dec91", - "sha256:ec31f2ed9675cc54c21ba854cfe0462e6faf1d83c8ce5944709db8a4700b9c61" + "sha256:38473cd7c6389ad3e44a91f4c3eaf6bcb8a9f746007f29bf4fb20824ff0b2197", + "sha256:c0d6d990850c627bbf7be01c5c4cbaadf67b48593e913bb71c9819c30df37eee" ], "markers": "python_version >= '3.7'", - "version": "==5.0.4" + "version": "==5.0.6" }, "requests": { "hashes": [ - "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", - "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "markers": "python_version >= '3.8'", - "version": "==2.32.2" + "version": "==2.32.3" }, "service-identity": { "hashes": [ @@ -574,11 +574,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", - "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version < '3.12'", - "version": "==4.12.0" + "version": "==4.12.2" }, "tzdata": { "hashes": [ @@ -599,12 +599,12 @@ }, "uvicorn": { "hashes": [ - "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", - "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0" + "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81", + "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.29.0" + "version": "==0.30.1" }, "zope-interface": { "hashes": [ @@ -660,11 +660,11 @@ }, "packaging": { "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" ], - "markers": "python_version >= '3.7'", - "version": "==24.0" + "markers": "python_version >= '3.8'", + "version": "==24.1" }, "pluggy": { "hashes": [ @@ -676,12 +676,12 @@ }, "pytest": { "hashes": [ - "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", - "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==8.2.1" + "version": "==8.2.2" }, "pytest-asyncio": { "hashes": [ diff --git a/mesh/CONTRIBUTING.md b/mesh/CONTRIBUTING.md new file mode 100644 index 00000000..6a64b578 --- /dev/null +++ b/mesh/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to Let's Mesh Backend (API) + +Thank you for considering contributing to Let's Mesh! Here are some guidelines to help you get started. + +## Table of Contents + +1. [Issue Tracking and Management](#issue-tracking-and-management) +2. [Backend Structure](#backend-structure) +3. [Writing tests](#writing-tests) +4. [Communication and Support](#communication-and-support) + +## Issue Tracking and Management + +We use GitHub Projects to manage and track issues for the Let's Mesh project. You can find our [project board here](https://github.com/orgs/LetsMesh/projects/2). + +### How to Report an Issue + +1. **Check Existing Issues**: Before creating a new issue (bug/task), please check if the issue has already been reported to avoid duplications. +2. **Open a New Issue**: If the issue does not exist, open a new issue and provide as much detail as possible. For bug reporting, please provide the steps to reproduce the bug. +3. **Link to Project Board**: Ensure your issue is linked to the project board for better tracking. + +### How to Work on an Issue + +1. **Assign Yourself**: If you are interested in working on an issue, assign it to yourself. +2. **Create a Branch**: Create a new branch from `dev` with a descriptive name related to the issue. +3. **Submit a Pull Request**: Once you have completed your work, submit a pull request (PR) and link it to the issue. + +## Backend Structure + +Our backend is structured to support both HTTP API requests and WebSocket connections using Django's ASGI and WSGI capabilities. Here’s an overview of our project structure: + +- **`mesh/`**: backend + - **`helpers/`**: Custom middlewares & helpers for request/response processing. + - **`utils/`**: Utility functions. + - **`exceptions/`**: Custom exception handling. + - **`tests/`**: Test cases for API and WebSocket endpoints. + - **`asgi.py`**: ASGI configuration for handling asynchronous requests. + - **`routing.py`**: Routing configuration for WebSocket connections. + - **`settings.py`**: Django project settings. + - **`urls.py`**: URL routing configuration. + - **`wsgi.py`**: WSGI configuration for handling synchronous requests. + - Other folders (`accounts/`, `accountSettings/`, `auth`, `profiles/`, etc.): Each folder contains its own URLs and models to handle related operations. +- **`meshapp/`**: frontend + +### Getting Started + +1. **Determine if you need a new API/app**: If an API already exists for your use case, continue development there. Otherwise, create a new app. +2. **Create a new app**: Ensure you are in the `Site/mesh` directory and run `django-admin startapp api_name`. +3. **Add `urls.py` to the new app**: Follow the example in `Site/mesh/`urls.py`` to add URLs and create a new API. + +### Learn by Example + +Explore the example available in the project: + +- **Files to Check**: + + - `Site/mesh/exampleapi/`urls.py`` + - `Site/mesh/exampleapi/`views.py`` + - `Site/mesh/`urls.py`` + +- **Steps to Test**: + 1. Run `python `manage.py` runserver` in `Site/`. + 2. Open a browser and go to [http://127.0.0.1:8000/example/](http://127.0.0.1:8000/example/). You should see "You Got Home". + 3. Check the `index` function in `Site/mesh/exampleapi/`views.py``. + 4. Check the URL definitions in `Site/mesh/`urls.py`` and `Site/mesh/exampleapi/`urls.py``. + +Visit [http://127.0.0.1:8000/example/helloworld/](http://127.0.0.1:8000/example/helloworld/) to see the "Hello World" example. + +## Writing Tests + +Here are some guidelines for writing tests for our API endpoints in Django: + +- Our Django setup has `APPEND_SLASH=true` by default, which appends a trailing slash to URLs and permanently redirects requests. + +### Incorrect Way to Send Test Requests (without trailing slash): + +```python +from django.test import TestCase, Client + +class YourTestClass(TestCase): + def setUp(self): + self.client = Client() + + def your_test_case(self): + response = self.client.get("/accounts") + self.assertEqual(response.status_code, 200) # This will not pass ❌ +``` + +This will fail as `response.status_code` is 301 instead of 200, due to the permanent redirect. + +### Correct Way to Send Test Requests: + +```python +from django.test import TestCase, Client + +class YourTestClass(TestCase): + def setUp(self): + self.client = Client() + + def your_test_case(self): + response = self.client.get("/accounts", follow=True) + self.assertEqual(response.status_code, 200) # This will pass ✅ + + response = self.client.get("/accounts/") + self.assertEqual(response.status_code, 200) # This will pass ✅ +``` + +## Communication and Support + +For easier communication and support, we invite all contributors to join our Discord server. Our project Discord server is beginner-friendly and a great place to ask questions, get feedback, and stay updated on the latest project developments. [Join our Discord server here](https://discord.gg/eUDKr8u55u). + +We appreciate your contributions and look forward to collaborating with you on Let's Mesh! + +--- diff --git a/mesh/accountSettings/urls.py b/mesh/accountSettings/urls.py index 2a7fe003..c10f0813 100644 --- a/mesh/accountSettings/urls.py +++ b/mesh/accountSettings/urls.py @@ -2,7 +2,6 @@ from . import views as accountsettings_views urlpatterns = [ - path("displayTheme/", accountsettings_views.display_theme, name="display_theme"), - path("", accountsettings_views.SettingsView.as_view(), name = "settings"), - path("/", accountsettings_views.SettingsDetailView.as_view(), name = "specific_settings"), + path('', accountsettings_views.SettingsView.as_view(), name = 'settings'), + path('/', accountsettings_views.SettingsDetailView.as_view(), name = 'specific_settings'), ] diff --git a/mesh/accountSettings/views.py b/mesh/accountSettings/views.py index 2f82b7bb..393ce9f7 100644 --- a/mesh/accountSettings/views.py +++ b/mesh/accountSettings/views.py @@ -1,10 +1,16 @@ +# Django from django.http import JsonResponse, HttpResponse from django.views import View from django.core.serializers import serialize from django.core.exceptions import ValidationError, ObjectDoesNotExist + +# Libraries import json +# Models from .models import Settings, Account + +# Utils from ..utils.validate_data import validate_json_and_required_fields def display_theme(request, account_id): @@ -87,17 +93,30 @@ def post(self, request): ) class SettingsDetailView(View): + valid_fields = ['isVerified', 'verificationToken', 'hasContentFilterEnabled', 'displayTheme', 'is2FAEnabled'] + def get(self, request, account_id, *args, **kwargs): """ Handles GET requests when the client fetches for a specific account settings """ try: + fields = request.GET.get('fields') + if fields: + fields = [field for field in fields.split(',') if field in self.valid_fields] + else: + fields = self.valid_fields + if len(fields) == 0: + return JsonResponse({'error': 'Invalid fields'}, status=400) + settings = Settings.objects.get(accountID= account_id) - settings_detail = serialize('json', [settings]) + settings_detail = { field: getattr(settings, field, None) for field in fields } + # Add accountID to the response data + settings_detail['accountID'] = account_id + return JsonResponse(settings_detail, safe = False, status=200) except Settings.DoesNotExist: - return JsonResponse({'error': 'Settings for this account do not exist'}, status=404) + return JsonResponse({'error': 'Setting for account not found'}, status=404) def patch(self, request, account_id): """ @@ -105,16 +124,19 @@ def patch(self, request, account_id): """ try: setting = Settings.objects.get(accountID=account_id) + data = json.loads(request.body) + + for field, value in data.items(): + if field in self.valid_fields: + setattr(setting, field, value) + + # After updating, save the setting + setting.save() + return HttpResponse(status=204) except Settings.DoesNotExist: - return JsonResponse({'error': 'Settings do not exist'}, status=404) - - data = json.loads(request.body) - # Here, update the setting's attributes based on the data received - setting.isVerified = data.get('isVerified', setting.isVerified) - setting.verificationToken = data.get('verificationToken', setting.verificationToken) - setting.hasContentFilterEnabled = data.get('hasContentFilterEnabled', setting.hasContentFilterEnabled) - setting.displayTheme = data.get('displayTheme', setting.displayTheme) - setting.is2FAEnabled = data.get('is2FAEnabled', setting.is2FAEnabled) - # After updating, save the setting - setting.save() - return HttpResponse(status=204) + return JsonResponse({'error': 'Setting for account not found'}, status=404) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON format."}, status=400) + except KeyError: + return JsonResponse({"error": "Invalid fields in the request."}, status=400) + diff --git a/mesh/accounts/urls.py b/mesh/accounts/urls.py index f0286b3e..f599658e 100644 --- a/mesh/accounts/urls.py +++ b/mesh/accounts/urls.py @@ -1,6 +1,6 @@ # in accounts folder: urls.py (accounts.urls) -from django.urls import path +from django.urls import path, include from . import views as accounts_views from .views import * from ..conversation.views import get_account_conversations @@ -12,8 +12,10 @@ # GET /accounts/:account_id - get account with id account_id # PATCH /accounts/:account_id - update (patch) account with id account_id # DELETE /accounts/:account_id - delete account with id account_id - path('/', accounts_views.SingleAccountView.as_view(), name='single_account'), - path('/conversations/', get_account_conversations, name='account_conversations'), + path('/', include([ + path('', accounts_views.SingleAccountView.as_view(), name='single_account'), + path('conversations/', get_account_conversations, name='account_conversations'), + ])), # PATCH /accounts/change-password - update account password path('change-password/', change_password, name = "change_password"), # POST /accounts/check-password diff --git a/mesh/accounts/views.py b/mesh/accounts/views.py index 80745d7c..9f7093f6 100644 --- a/mesh/accounts/views.py +++ b/mesh/accounts/views.py @@ -1,16 +1,20 @@ +# Django from django.http import JsonResponse, HttpResponse from django.core.exceptions import ValidationError from django.core.serializers import serialize -from .models import * +from django.views import View + +# Libraries import bcrypt import os +import json -from django.views import View +# Models +from .models import * +from ..accountSettings.models import Settings -from .models import Account +# Utils from ..utils.validate_data import validate_json_and_required_fields -from ..accountSettings.models import Settings -import json from mesh.accounts.services import ( get_OTP_validity_service, @@ -210,7 +214,7 @@ def delete(self, request, account_id): account.delete() return JsonResponse({'message': f'successfully deleted Account with account_id: {account_id}'}, status=204) -def check_password(request): +def check_password(request, *args, **kwargs): """ Handles a POST request to authenticate a user's credentials. @@ -251,7 +255,7 @@ def check_password(request): else: return JsonResponse({"error": "Method not allowed"}, status=405) -def change_password(request): +def change_password(request, *args, **kwargs): """ Handles a PATCH request to change a user's password. diff --git a/mesh/auth/views.py b/mesh/auth/views.py index e0b229f6..db670609 100644 --- a/mesh/auth/views.py +++ b/mesh/auth/views.py @@ -1,10 +1,15 @@ # in auth folder: views.py (auth.views) -import json +# Django from django.http import JsonResponse, HttpResponseRedirect from django.contrib.auth import login, logout from django.views.decorators.http import require_POST,require_GET from django.views.decorators.csrf import ensure_csrf_cookie + +# Libraries +import json + +# Models from mesh.accounts.models import Account from .backend import AccountAuthenticationBackend diff --git a/mesh/confirmation/urls.py b/mesh/confirmation/urls.py index a286d6d2..d8053cb7 100644 --- a/mesh/confirmation/urls.py +++ b/mesh/confirmation/urls.py @@ -2,6 +2,8 @@ from . import views as confirmation_views urlpatterns = [ - path('/', confirmation_views.email_confirmation, name="email_confirmation"), - path('//', confirmation_views.confirm_token, name="confirm_token"), + path('/', include([ + path('', confirmation_views.email_confirmation, name="email_confirmation"), + path('/', confirmation_views.confirm_token, name="confirm_token"), + ])), ] \ No newline at end of file diff --git a/mesh/confirmation/views.py b/mesh/confirmation/views.py index 704c148b..4c437dc1 100644 --- a/mesh/confirmation/views.py +++ b/mesh/confirmation/views.py @@ -1,10 +1,15 @@ +# Django from django.http import HttpResponse from django.core.mail import send_mail from django.template.loader import render_to_string from django.utils.html import strip_tags + +# Models from ..accounts.models import Account from ..accountSettings.models import Settings from ..profiles.models import Profile + +# Libraries import secrets import os import time diff --git a/mesh/conversation/urls.py b/mesh/conversation/urls.py index 2f892e2e..b9556768 100644 --- a/mesh/conversation/urls.py +++ b/mesh/conversation/urls.py @@ -1,14 +1,16 @@ # mesh/conversation/urls.py -from django.urls import path +from django.urls import path, include from .views import * urlpatterns = [ # POST /conversations path('', ConversationsView.as_view(), name='conversations'), - # GET /conversation// - # todo: PATCH & DELETE - path('/', SingleConversationView.as_view(), name='single_conversation'), - # get conversation's messages with conversation_messages - path('/messages/', conversation_messages, name='conversation_messages'), + path('/', include([ + # GET /conversation// + # todo: PATCH & DELETE + path('', SingleConversationView.as_view(), name='single_conversation'), + # get conversation's messages with conversation_messages + path('messages/', conversation_messages, name='conversation_messages'), + ])), ] diff --git a/mesh/conversation/views.py b/mesh/conversation/views.py index 077847a1..6bd60c4c 100644 --- a/mesh/conversation/views.py +++ b/mesh/conversation/views.py @@ -1,17 +1,20 @@ # mesh/conversation/views.py -from django.shortcuts import get_object_or_404 - -import json -from django.http import JsonResponse, HttpResponseBadRequest +# Django +from django.http import JsonResponse from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from django.views.decorators.http import require_GET from django.views import View +from django.core.exceptions import ValidationError +# Libraries +import json + +# Models from .models import Conversation, Message, ConversationParticipant from ..accounts.models import Account -from ..profiles.models import Profile + +# Utils from ..utils.validate_data import validate_json_and_required_fields -from django.core.exceptions import ValidationError class ConversationsView(View): def post(self, request): diff --git a/mesh/education/urls.py b/mesh/education/urls.py index 7efc22fe..dfa191c8 100644 --- a/mesh/education/urls.py +++ b/mesh/education/urls.py @@ -8,6 +8,5 @@ path('', education_views.EducationView.as_view(), name="education"), # GET /educations/:account_id/ - path("/", education_views.EducationsDetailView.as_view(), - name="user_educations") + path("/", education_views.EducationsDetailView.as_view(), name="user_educations") ] \ No newline at end of file diff --git a/mesh/education/views.py b/mesh/education/views.py index cf466e23..2f06c1bc 100644 --- a/mesh/education/views.py +++ b/mesh/education/views.py @@ -13,7 +13,6 @@ from mesh.exceptions.MissingRequiredFields import MissingRequiredFields from mesh.exceptions.InvalidJsonFormat import InvalidJsonFormat - class EducationView(View): def get(self, request, *args, **kwargs): """ diff --git a/mesh/exampleapi/urls.py b/mesh/exampleapi/urls.py index 500600a3..a6a4baf5 100644 --- a/mesh/exampleapi/urls.py +++ b/mesh/exampleapi/urls.py @@ -1,4 +1,4 @@ -from django.urls import path, include +from django.urls import path from . import views as exampleapi_views urlpatterns = [ diff --git a/mesh/exampleapi/views.py b/mesh/exampleapi/views.py index 4304b044..818c7c3c 100644 --- a/mesh/exampleapi/views.py +++ b/mesh/exampleapi/views.py @@ -1,7 +1,5 @@ from django.shortcuts import render -from django.http import HttpResponse - - +from django.http import HttpResponse, JsonResponse def index(request): if request.method == "GET": @@ -12,4 +10,36 @@ def index(request): def hello_world(request): if request.method == "GET": print(request) - return HttpResponse("Hello World!") \ No newline at end of file + return HttpResponse("Hello World!") + +# This if for testing purposes and cleanup saved images from the testing requests +# warning: only use this if you know what you're doing with backblaze buckets +def check_buckets(request): + if request.method == "GET": + try: + from mesh.profiles.views import initialize_backblaze_client + backblaze_api, backblaze_bucket = initialize_backblaze_client() + cleanup = request.GET.get('cleanup', 'false').lower() == 'true' + + files = [] + + try: + for file_version, folder_name in backblaze_bucket.ls(): + files.append({ + "file_id": file_version.id_, + "file_name": file_version.file_name, + "folder_name": folder_name + }) + if cleanup: + backblaze_bucket.delete_file_version(file_version.id_, file_version.file_name) + + except Exception as e: + + # Handle the case where there are no files in the bucket + files = [f'error: {e}'] + + return JsonResponse({"files": files, "message": "these files were cleaned up" if cleanup else "bucket file list"}, status=200) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + else: + return JsonResponse({"error": "Invalid request method."}, status=405) diff --git a/mesh/occupations/urls.py b/mesh/occupations/urls.py index 812a9c97..d53e2aaa 100644 --- a/mesh/occupations/urls.py +++ b/mesh/occupations/urls.py @@ -9,6 +9,5 @@ path("", occupation_views.OccupationsView.as_view(), name="occupations"), # GET /occupations/:account_id - path("/", occupation_views.OccupationsDetailView.as_view(), - name="user_occupations") + path("/", occupation_views.OccupationsDetailView.as_view(), name="user_occupations") ] \ No newline at end of file diff --git a/mesh/profiles/urls.py b/mesh/profiles/urls.py index a326d276..77ad11c3 100644 --- a/mesh/profiles/urls.py +++ b/mesh/profiles/urls.py @@ -1,31 +1,18 @@ -from django.urls import path +from django.urls import include, path from . import views as profile_views urlpatterns = [ - # GET /profiles/profile-picture - # POST /profiles/profile-picture - # PATCH /profiles/profile-picture - # DELETE /profiles/profile-picture - path("profile-picture", profile_views.ProfilePicturesView.as_view(), name="profile-picture"), - - # GET /profiles/biography/:account_id - # POST /profiles/biography/:account_id - path("biography/", profile_views.BiographyView.as_view(), name='biography'), - - # GET /profiles/profile-picture/:account_id - path("profile-picture/", profile_views.ProfilePictureDetailsView.as_view(), name="profile-picture-details"), + path('/', include([ + # GET /profiles/:account-id + # GET /profiles/:account-id?fields= + # PATCH /profiles/:account-id + path('', profile_views.ProfileDetailsView.as_view(), name='profile-details'), - # GET /profiles/user-name/:account_id - # POST /profiles/user-name/:account_id - path("user-name/", profile_views.UserNamesView.as_view(), name="user-name"), - - # GET /profiles/preferred-name/:account_id - # POST /profiles/preferred-name/:account_id - path("preferred-name/", profile_views.PreferredNamesView.as_view(), name="preferred-name"), - - # GET /profiles/preferred-pronouns/:account_id - # POST /profiles/preferred-pronouns/:account_id - path("preferred-pronouns/", profile_views.PreferredPronounsView.as_view(), name="preferred-pronouns"), + # GET /profiles/:account-id/profile-picture + # POST /profiles/:account-id/profile-picture + # DELETE /profiles/:account-id/profile-picture + path('profile-picture/', profile_views.ProfilePicturesView.as_view(), name="profile-picture"), + ])), ] \ No newline at end of file diff --git a/mesh/profiles/views.py b/mesh/profiles/views.py index c64b1873..6d286bf5 100644 --- a/mesh/profiles/views.py +++ b/mesh/profiles/views.py @@ -22,41 +22,57 @@ from requests.exceptions import HTTPError from b2sdk.v1 import B2Api, InMemoryAccountInfo, UploadSourceBytes - -class BiographyView(View): +class ProfileDetailsView(View): """ - View for getting an biography by accountId or updating an biography by accountID. + View for getting or updating profile details by account ID. """ + valid_fields = ['biography', 'userName', 'preferredName', 'preferredPronouns', 'profilePicture'] def get(self, request, account_id, *args, **kwargs): """ Handle GET requests. - Retrieves a biography of the listed profile in JSON format. - - Returns a JSON response containing biography of specified profile through id. + Retrieves specified fields of the profile in JSON format. """ + fields = request.GET.get('fields') + if fields: + fields = [field for field in fields.split(',') if field in self.valid_fields] + else: + fields = self.valid_fields + + if len(fields) == 0: + return JsonResponse({'error': 'Invalid fields'}, status=400) + try: profile = Profile.objects.get(accountID=account_id) - profile_biography = profile.biography - return JsonResponse({"biography": profile_biography}, status=200) - except Account.DoesNotExist: - return JsonResponse( - {"error": "Invalid request. Account does not exist"}, status=404 - ) + profile_data = { field: getattr(profile, field, None) for field in fields } + return JsonResponse({"profile": profile_data}, status=200) except Profile.DoesNotExist: - return JsonResponse({'error': 'Invalid request. Profile does not exist'}, status=404) - - def post(self, request, account_id, *args, **kwargs): - """ - Handle POST requests. + return JsonResponse({"error": "Profile not found."}, status=404) - Updates the biography of the listed profile. + def patch(self, request, account_id, *args, **kwargs): + """ + Handle PATCH requests. - Returns a JSON response containing the saved biography of the specified profile through id. + Updates specified fields of the profile. """ - return post_data(account_id, "biography", request) + try: + profile = Profile.objects.get(accountID=account_id) + data = json.loads(request.body) + + for field, value in data.items(): + if field in self.valid_fields: + setattr(profile, field, value) + profile.save() + return JsonResponse({"message": "Profile updated successfully."}, status=200) + except Profile.DoesNotExist: + return JsonResponse({"error": "Profile not found."}, status=404) + except json.JSONDecodeError: + return JsonResponse({"error": "Invalid JSON format."}, status=400) + except KeyError: + return JsonResponse({"error": "Invalid fields in the request."}, status=400) + class ProfilePicturesView(View): """ Handles HTTP requests related to Profile Pictures, supporting @@ -65,39 +81,20 @@ class ProfilePicturesView(View): DELETE to delete a profile picture. """ - def post(self, request, *args, **kwargs): + def post(self, request, account_id, *args, **kwargs): """ Handles POST requests. - Grabs an existing Profile and uploads a profile picture to imgur, + Grabs an existing Profile and uploads a profile picture to Backblaze, and saves the link with the Profile. - The required fields are "accountID" and "profilePicture". + The required field is "profilePicture". - Note: A user should not have a profile picture by this point, as in their - profilePicture URL should be empty. If they already have a picture, - a PATCH request should be made instead. If a user wishes to delete their picture, - a DELETE request should be made instead. + If a profile picture already exists, it will be replaced with the new one. """ try: - data = request.POST - - account_id = data["accountID"] - profile = Profile.objects.get(accountID=account_id) - - # Check if user already has a profilePicture - if profile.profilePicture is not None: - return JsonResponse( - { - "error": "Profile already has a profilePicture. " - + "Use PATCH to update the profilePicture or " - + "DELETE to delete the profilePicture." - }, - status=409, - ) - - backblaze_api, backblaze_bucket = initialize_backblaze_client() + _, backblaze_bucket = initialize_backblaze_client() # Grab photo from request files image_file = request.FILES["profilePicture"] @@ -109,16 +106,28 @@ def post(self, request, *args, **kwargs): image_file = image_file.read() - original_file_name = request.FILES["profilePicture"].name + if profile.profilePicture is not None: + # parse url, get file name + profile_picture_url = urlparse(profile.profilePicture) + file_name = os.path.basename(profile_picture_url.path) + + try: + # find file info to grab file id + file_version_info = backblaze_bucket.get_file_info_by_name(file_name) + file_id = file_version_info.as_dict()["fileId"] + + # delete old profile picture + backblaze_bucket.delete_file_version(file_id, file_name) + except: + # Handle the case where the file does not exist + pass - # rename photo to random string + accountID + original_file_name = request.FILES["profilePicture"].name new_file_name = generate_unique_filename(account_id, original_file_name) # upload image to backblaze upload_source = UploadSourceBytes(image_file) - upload_result = backblaze_bucket.upload( - upload_source, file_name=new_file_name - ) + upload_result = backblaze_bucket.upload(upload_source, file_name=new_file_name) image_link = generate_image_url(new_file_name) @@ -138,108 +147,21 @@ def post(self, request, *args, **kwargs): return JsonResponse({"error": "Uploaded file is not an image."}, status=400) except ValueError: - return JsonResponse( - {"error": "accountID or profilePicture field is empty."}, status=400 - ) - - def patch(self, request, *args, **kwargs): - try: - data = MultiPartParser( - request.META, request, request.upload_handlers - ).parse() - - account_id = data[0]["accountID"] - - profile = Profile.objects.get(accountID=account_id) - - if profile.profilePicture is None: - return JsonResponse( - { - "error": "Profile does not have an existing profilePicture." - + "Use a POST request to upload a profilePicture." - }, - status=409, - ) - - # need to delete old profile picture first, - # then upload new profile picture - - # Grab photo from request files - image_file = data[1]["profilePicture"] - - if check_if_file_is_not_image(image_file): - return JsonResponse( - {"error": "Uploaded file is not an image."}, status=400 - ) - - image_file = image_file.read() - - # parse url, get file name - profile_picture_url = urlparse(profile.profilePicture) - file_name = os.path.basename(profile_picture_url.path) - - # find file info to grab file id - backblaze_api, backblaze_bucket = initialize_backblaze_client() - file_version_info = backblaze_bucket.get_file_info_by_name(file_name) - file_id = file_version_info.as_dict()["fileId"] - - # delete old profile picture - backblaze_bucket.delete_file_version(file_id, file_name) - - # rename photo to random string + accountID - original_file_name = data[1]["profilePicture"].name - new_file_name = generate_unique_filename(account_id, original_file_name) - - # upload image to backblaze - upload_source = UploadSourceBytes(image_file) - upload_result = backblaze_bucket.upload( - upload_source, file_name=new_file_name - ) - - image_link = generate_image_url(new_file_name) - - # Save photo URL with user - profile.profilePicture = image_link - profile.save() - - return JsonResponse({"profileID": profile.accountID.accountID, "profilePicture": image_link}, status=200, safe=False) - - except MultiValueDictKeyError: - return JsonResponse({"error": "Missing required JSON fields."}, status=400) - - except Profile.DoesNotExist: - return JsonResponse({"error": "Profile not found."}, status=404) - - except HTTPError: - return JsonResponse({"error": "Uploaded file is not an image."}, status=400) - - except ValueError: - return JsonResponse( - {"error": "accountID or profilePicture field is empty."}, status=400 - ) - - def delete(self, request, *args, **kwargs): + return JsonResponse({"error": "accountID or profilePicture field is empty."}, status=400) + + def delete(self, request, account_id, *args, **kwargs): """ Handles DELETE requests. Grabs an existing Profile and deletes its profilePicture. - - The required field is "accountID". """ - - REQUIRED_FIELDS = ["accountID"] - try: - data = validate_json_and_required_fields(request.body, REQUIRED_FIELDS) - - account_id = data["accountID"] - profile = Profile.objects.get(accountID=account_id) # Ensure their profilePicture does not exist already. if profile.profilePicture is None: return JsonResponse( - {"error": "Profile's profilePicture not found, nothing to delete."}, + {"error": "Profile picture not found, nothing to delete."}, status=404, ) @@ -248,107 +170,30 @@ def delete(self, request, *args, **kwargs): file_name = os.path.basename(profile_picture_url.path) # find file info to grab file id - backblaze_api, backblaze_bucket = initialize_backblaze_client() - file_version_info = backblaze_bucket.get_file_info_by_name(file_name) - file_id = file_version_info.as_dict()["fileId"] + _, backblaze_bucket = initialize_backblaze_client() - # delete file - backblaze_bucket.delete_file_version(file_id, file_name) + try: + file_version_info = backblaze_bucket.get_file_info_by_name(file_name) + file_id = file_version_info.as_dict()["fileId"] + # delete file + backblaze_bucket.delete_file_version(file_id, file_name) + except: + pass # update profile profile.profilePicture = None profile.save() - return HttpResponse(status=204) - - except InvalidJsonFormat: - return JsonResponse({"error": "Invalid JSON format."}, status=400) - - except MissingRequiredFields: - return JsonResponse({"error": "Missing required JSON fields."}, status=400) + return JsonResponse({"message": "Profile picture deleted"}, status=204) except Profile.DoesNotExist: return JsonResponse({"error": "Profile not found."}, status=404) except ValueError: return JsonResponse( - {"error": "accountID or profilePicture field is empty."}, status=400 + {"error": "accountID field is empty."}, status=400 ) - - -class ProfilePictureDetailsView(View): - def get(self, request, account_id, *args, **kwargs): - return get_data(account_id, "profilePicture", lambda profile: profile.profilePicture) - -class UserNamesView(View): - def get(self, request, account_id, *args, **kwargs): - return get_data(account_id, "userName", lambda profile: profile.userName) - - def post(self, request, account_id, *args, **kwargs): - return post_data(account_id, "userName", request) - -class PreferredNamesView(View): - def get(self, request, account_id, *args, **kwargs): - return get_data( - account_id, "preferredName", lambda profile: profile.preferredName - ) - - def post(self, request, account_id, *args, **kwargs): - return post_data(account_id, "preferredName", request) - -class PreferredPronounsView(View): - def get(self, request, account_id, *args, **kwargs): - return get_data( - account_id, "preferredPronouns", lambda profile: profile.preferredPronouns - ) - - def post(self, request, account_id, *args, **kwargs): - return post_data(account_id, "preferredPronouns", request) - -def get_data(account_id, name, mapper): - """ - Handles GET requests when the client fetches for names/nicknames/pronouns - """ - try: - profile = Profile.objects.get(accountID=int(account_id)) - return JsonResponse( - {"status": "success", "data": {"get": {name: mapper(profile)}}}, status=200 - ) - except ObjectDoesNotExist: - return JsonResponse( - { - "status": "error", - "message": "An account does not exist with this account ID.", - }, - status=404, - ) - -def post_data(account_id, name, request): - """ - Handles POST requests when the client sends names/nicknames/prounouns to the back end - """ - try: - profile = Profile.objects.get(accountID = account_id) - data = json.loads(request.body)[name] - - if (name == "userName"): - profile.userName = data - elif (name == "preferredName"): - profile.preferredName = data - elif (name == "preferredPronouns"): - profile.preferredPronouns = data - elif (name == "biography"): - profile.biography = data - - profile.save() - return JsonResponse({f'{name}': data, 'message': f'{name} saved successfully'}, status = 200) - - except Profile.DoesNotExist: - return JsonResponse({'error': 'Account does not exist'}, status = 404) - - except KeyError: - return JsonResponse({'error': f'Missing {name} field.'}, status = 400) - + def initialize_backblaze_client(): BACKBLAZE_APPLICATION_KEY_ID = os.environ.get("BACKBLAZE_MASTER_KEY") BACKBLAZE_APPLICATION_KEY = os.environ.get("BACKBLAZE_APPLICATION_KEY") @@ -363,7 +208,6 @@ def initialize_backblaze_client(): return backblaze_api, bucket - # create unique, random file name containing accountID + random string def generate_unique_filename(account_id, original_filename): extension = original_filename.split(".")[-1] @@ -371,14 +215,12 @@ def generate_unique_filename(account_id, original_filename): new_filename = f"{account_id}_{unique_id}.{extension}" return new_filename - # for generating image urls for each file, to be saved with the profile def generate_image_url(file_name): BACKBLAZE_URL = "https://f005.backblazeb2.com/file/LetsMesh/" file_url = BACKBLAZE_URL + file_name return file_url - # ensure uploaded file is an image def check_if_file_is_not_image(file): ACCEPTED_FILE_TYPES = ["jpeg", "jpg", "png"] @@ -388,3 +230,4 @@ def check_if_file_is_not_image(file): return True return False + \ No newline at end of file diff --git a/mesh/tags/views.py b/mesh/tags/views.py index a38bf93c..0025d2e6 100644 --- a/mesh/tags/views.py +++ b/mesh/tags/views.py @@ -1,10 +1,16 @@ +# Django from django.http import JsonResponse from django.views import View +# Models +from ..accounts.models import Account +from ..tags.models import TagBridge, Tag + +# Exceptions from ..exceptions.InvalidJsonFormat import InvalidJsonFormat from ..exceptions.MissingRequiredFields import MissingRequiredFields -from ..tags.models import TagBridge, Tag -from ..accounts.models import Account + +# Utils from ..utils.validate_data import validate_json_and_required_fields diff --git a/mesh/tests/accounts_tests.py b/mesh/tests/accounts_tests.py index ad0f659a..2c45cd19 100644 --- a/mesh/tests/accounts_tests.py +++ b/mesh/tests/accounts_tests.py @@ -2,6 +2,8 @@ You can run this test with python manage.py test mesh.tests.accounts_tests + +pipenv run python manage.py test mesh.tests.accounts_tests ''' import json @@ -18,6 +20,7 @@ def setUp(self): """ self.client = Client() + self.test_account_password='some_encrypted_pass' salt, hash = encrypt(self.test_account_password) self.test_account = Account( @@ -38,8 +41,11 @@ def test_get_all_accounts(self): A GET request is sent to the '/accounts/' endpoint. The test passes if the response status code is 200. """ - response = self.client.get('/accounts/') + response = self.client.get('/accounts', follow=True) self.assertEqual(response.status_code, 200) + accounts = Account.objects.all() + json_response = json.loads(response.content.decode("utf-8")) + self.assertEqual(len(json_response), len(accounts)) def test_get_specific_account(self): """ @@ -48,7 +54,9 @@ def test_get_specific_account(self): A GET request is sent to the '/accounts/{account_id}/' endpoint. The test passes if the response status code is 200. """ - response = self.client.get(f'/accounts/{self.test_account.accountID}/') + # this needs to be set follow=True because django will permanently redirect + # the request from '/accounts/{self.test_account.accountID}' to '/accounts/{self.test_account.accountID}/' + response = self.client.get(f'/accounts/{self.test_account.accountID}', follow=True) self.assertEqual(response.status_code, 200) json_response = json.loads(response.content.decode("utf-8")) @@ -57,6 +65,10 @@ def test_get_specific_account(self): self.assertEqual(json_response["isMentor"], self.test_account.isMentor) self.assertEqual(json_response["isMentee"], self.test_account.isMentee) + # with trailing slash (now followup needed) + response = self.client.get(f'/accounts/{self.test_account.accountID}/') + self.assertEqual(response.status_code, 200) + def test_post_create_account(self): """ Test case for creating a new account. @@ -135,7 +147,11 @@ def test_check_password(self): 'email': "new_account_email@email.com", 'password': 'correct_password' } - response = self.client.post('/accounts/check-password/', json.dumps(correct_credentials), content_type='application/json') + response = self.client.post( + '/accounts/check-password/', + json.dumps(correct_credentials), + content_type='application/json' + ) self.assertEqual(response.status_code, 200) # Incorrect credentials @@ -143,7 +159,11 @@ def test_check_password(self): 'email': "new_account_email@email.com", 'password': 'wrong_password' } - response = self.client.post('/accounts/check-password/', json.dumps(incorrect_credentials), content_type='application/json') + response = self.client.post( + '/accounts/check-password/', + json.dumps(incorrect_credentials), + content_type='application/json' + ) self.assertEqual(response.status_code, 401) diff --git a/mesh/tests/profiles_tests.py b/mesh/tests/profiles_tests.py index b2e97a76..68272ee7 100644 --- a/mesh/tests/profiles_tests.py +++ b/mesh/tests/profiles_tests.py @@ -2,6 +2,9 @@ You can run this test with python manage.py test mesh.tests.profiles_tests + +Inside a docker container: +pipenv run python manage.py test mesh.tests.profiles_tests ''' import json import os @@ -12,7 +15,6 @@ from mesh.accounts.models import Account from mesh.profiles.models import Profile - class ProfilesTest(TestCase): def setUp(self): self.client = Client() @@ -31,10 +33,7 @@ def setUp(self): preferredName="Profile Test", preferredPronouns="Patrick", biography="Biography Test", - profilePicture=SimpleUploadedFile( - name="profile_test_image.png", - content=open("media/image/test_image.png", "rb").read() - ) + profilePicture=None ) def tearDown(self): @@ -42,112 +41,108 @@ def tearDown(self): if os.path.exists(image_path): os.remove(image_path) - """ - Profile Picture Testing - """ - def test_profile_picture(self): + def test_get_profile_details_no_query(self): test_user = Account.objects.get(email="profilestest@gmail.com") - response = self.client.get(f"/profiles/profile-picture/{test_user.accountID}") - json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("data"), {"get": {"profilePicture": "profile_test_image.png"}}) - - def test_no_account_profile_picture(self): - response = self.client.get("/profiles/profile-picture/9999") + response = self.client.get(f"/profiles/{test_user.accountID}", follow=True) + self.assertEqual(response.status_code, 200) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("status"), "error") - self.assertEquals(json_response.get("message"), "An account does not exist with this account ID.") - - """ - Profile username, preferred name, and pronoun Testing - """ - def test_user_name(self): + self.assertEqual(json_response.get("profile"), {'biography': 'Biography Test', 'userName': 'profileTest', 'preferredName': 'Profile Test', 'preferredPronouns': 'Patrick', 'profilePicture': None}) + + def test_get_profile_details_with_query(self): test_user = Account.objects.get(email="profilestest@gmail.com") - response = self.client.get(f"/profiles/user-name/{test_user.accountID}") + response = self.client.get(f"/profiles/{test_user.accountID}?fields=biography,userName", follow=True) + self.assertEqual(response.status_code, 200) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("data"), {"get": {"userName": "profileTest"}}) + self.assertEqual(json_response.get("profile"), {'biography': 'Biography Test', 'userName': 'profileTest'}) - def test_no_account_user_name(self): - response = self.client.get("/profiles/user-name/9999") + def test_get_profile_details_no_account(self): + response = self.client.get("/profiles/9999?fields=biography,userName",content_type="application/json", follow=True) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("status"), "error") - self.assertEquals(json_response.get("message"), "An account does not exist with this account ID.") + self.assertEqual(response.status_code, 404) + self.assertEqual(json_response.get("error"), "Profile not found.") - def test_preferred_name(self): + def test_patch_profile_details(self): test_user = Account.objects.get(email="profilestest@gmail.com") - response = self.client.get(f"/profiles/preferred-name/{test_user.accountID}") + patch_data = { + "biography": "Updated Biography", + "preferredName": "Updated Name" + } + response = self.client.patch( + f"/profiles/{test_user.accountID}/", + data=json.dumps(patch_data), + content_type="application/json" + ) + self.assertEqual(response.status_code, 200) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("data"), {"get": {"preferredName": "Profile Test"}}) - - def test_no_account_preferred_name(self): - response = self.client.get("/profiles/preferred-name/9999") + self.assertEqual(json_response.get("message"), "Profile updated successfully.") + + # Verify the changes + profile = Profile.objects.get(accountID=test_user) + self.assertEqual(profile.biography, "Updated Biography") + self.assertEqual(profile.preferredName, "Updated Name") + + def test_patch_profile_details_no_account(self): + patch_data = { + "biography": "Updated Biography" + } + response = self.client.patch( + "/profiles/9999/", + data=json.dumps(patch_data), + content_type="application/json", + ) + self.assertEqual(response.status_code, 404) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("status"), "error") - self.assertEquals(json_response.get("message"), "An account does not exist with this account ID.") + self.assertEqual(json_response.get("error"), "Profile not found.") - def test_preferred_pronouns(self): + def test_modify_user_profile_picture(self): test_user = Account.objects.get(email="profilestest@gmail.com") - response = self.client.get(f"/profiles/preferred-pronouns/{test_user.accountID}") - json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("data"), {"get": {"preferredPronouns": "Patrick"}}) - - def test_no_account_preferred_pronouns(self): - response = self.client.get("/profiles/preferred-pronouns/9999") + image_path = "media/image/test_image.png" + with open(image_path, "rb") as image: + response = self.client.post( + f"/profiles/{test_user.accountID}/profile-picture/", + data={ "profilePicture": image}, + format="multipart" + ) + self.assertEqual(response.status_code, 201) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("status"), "error") - self.assertEquals(json_response.get("message"), "An account does not exist with this account ID.") - - def test_post_user_name (self): - user_name_data = {"userName": "kwame brown"} - response = self.client.post( - f'/profiles/user-name/{self.test_profile.accountID}', - data=user_name_data, - content_type='application/json' - ) - self.assertEquals(response.status_code, 200) - - def test_post_preferred_name (self): - preferred_name_data = {"preferredName": "brown"} - response = self.client.post( - f'/profiles/preferred-name/{self.test_profile.accountID}', - data=preferred_name_data, - content_type='application/json' - ) - self.assertEquals(response.status_code, 200) - - def test_post_preferred_pronouns (self): - preferred_pronouns_data = {"preferredPronouns": "brown/black"} - response = self.client.post( - f'/profiles/preferred-pronouns/{self.test_profile.accountID}', - data=preferred_pronouns_data, - content_type='application/json' - ) - self.assertEquals(response.status_code, 200) - - """ - Biography Testing - """ - def test_biography(self): - """ - Test Case for seeing if biography can be retrieved from speicified account - - A GET request is sent to the '/profiles/biography/{account_id}' endpoint. - The test passes if the response status code is 200. - """ - test_user = Account.objects.get(email="profilestest@gmail.com") - response = self.client.get(f'/profiles/biography/{test_user.accountID}') - self.assertEqual(response.status_code, 200) - def test_biography_update(self): - """ - Test Case for seeing if biography can be updated from specified account + expected_profile_id = test_user.accountID + self.assertEqual(json_response.get("profileID"), expected_profile_id) - A POST request is sent to the '/profiles/biography/{account_id}' endpoint. - The test passes if the response status code is 200. - """ - test_user = Account.objects.get(email="profilestest@gmail.com") - response = self.client.post( - f"/profiles/biography/{test_user.accountID}", - data={'biography': "Testing..."}, - content_type='application/json' + profile_picture_url = json_response.get("profilePicture") + self.assertIsNotNone(profile_picture_url) + + import re + # Verify the profilePicture URL pattern + pattern = r'https://f005\.backblazeb2\.com/file/LetsMesh/[\w\d]+\.png' + self.assertTrue(re.match(pattern, profile_picture_url)) + + # delete user pfp + response = self.client.delete(f"/profiles/{test_user.accountID}/profile-picture/",) + + self.assertEqual(response.status_code, 204) + # Verify the changes + profile = Profile.objects.get(accountID=test_user) + self.assertIsNone(profile.profilePicture) + + def test_post_profile_picture_no_account(self): + image_path = "media/image/test_image.png" + with open(image_path, "rb") as image: + response = self.client.post( + f"/profiles/9999/profile-picture/", + data={ "profilePicture": image }, + format="multipart" + ) + self.assertEqual(response.status_code, 404) + json_response = json.loads(response.content.decode("utf-8")) + self.assertEqual(json_response.get("error"), "Profile not found.") + + def test_delete_profile_picture_no_account(self): + response = self.client.delete( + "/profiles/9999/profile-picture/", + + content_type="application/json" ) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 404) + json_response = json.loads(response.content.decode("utf-8")) + self.assertEqual(json_response.get("error"), "Profile not found.") diff --git a/mesh/tests/settings_tests.py b/mesh/tests/settings_tests.py index 29b3f90c..e8ef28a7 100644 --- a/mesh/tests/settings_tests.py +++ b/mesh/tests/settings_tests.py @@ -58,42 +58,64 @@ def test_get_specific_account_settings(self): A GET request is sent to the '/account-settings/{account_id}/' endpoint. The test passes if the response status code is 200 and the settings data matches the test account. """ - response = self.client.get(f"/account-settings/{self.test_settings.accountID}/") + + + response = self.client.get(f"/account-settings/{self.test_account.accountID}/") self.assertEqual(response.status_code, 200) + json_response = json.loads(response.content.decode("utf-8")) + account_setting = Settings.objects.get(accountID= self.test_settings.accountID) + + self.assertEqual(json_response.get('accountID'), account_setting.accountID.accountID) + self.assertEqual(json_response.get('displayTheme'), account_setting.displayTheme) + self.assertEqual(json_response.get('is2FAEnabled'), account_setting.is2FAEnabled) + self.assertEqual(json_response.get('isVerified'), account_setting.isVerified) + self.assertEqual(json_response.get('hasContentFilterEnabled'), account_setting.hasContentFilterEnabled) def test_display_theme(self): """ Test case for getting the display theme setting of a specific account. - A GET request is sent to the '/account-settings/displayTheme/{account_id}' endpoint. + A GET request is sent to the '/account-settings/{account_id}?fields=displayTheme' endpoint. The test passes if the response status code is 200 and the 'displayTheme' value is correct. """ test_user = Account.objects.get(email="settingstest@gmail.com") - response = self.client.get(f"/account-settings/displayTheme/{test_user.accountID}") + response = self.client.get(f"/account-settings/{test_user.accountID}?fields=displayTheme", follow=True) self.assertEqual(response.status_code, 200) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response, {"displayTheme": "0"}) + self.assertEquals(json_response.get('displayTheme'), "0") def test_missing_account_display_theme(self): """ Test case for attempting to get the display theme setting of a non-existent account. - A GET request is sent to the '/account-settings/displayTheme/9999' endpoint with an invalid account ID. + A GET request is sent to the '/account-settings?fields=displayTheme/9999' endpoint with an invalid account ID. The test passes if the response status code is 404, indicating that the account was not found. """ - response = self.client.get("/account-settings/displayTheme/9999") + response = self.client.get("/account-settings/9999?fields=displayTheme", follow=True) self.assertEqual(response.status_code, 404) def test_no_account_display_theme(self): """ Test case for verifying the error message when attempting to get the display theme of a non-existent account. - A GET request is sent to the '/account-settings/displayTheme/9999' endpoint with an invalid account ID. + A GET request is sent to the '/account-settings?fields=displayTheme/9999' endpoint with an invalid account ID. The test passes if the response includes an error message indicating that the account does not exist. """ - response = self.client.get("/account-settings/displayTheme/9999") + response = self.client.get("/account-settings/9999?fields=displayTheme", follow=True) + self.assertEqual(response.status_code, 404) + json_response = json.loads(response.content.decode("utf-8")) + self.assertEquals(json_response.get("error"), "Setting for account not found") + + def test_get_account_settings_invalid_fields(self): + """ + Test case for verifying the error message when attempting to get invalid fields from an account's settings + + A GET request is sent to '/account-settings?fields=invalidfields' endpoint which `invalidfields` is an invalid field. + """ + response = self.client.get("/account-settings/9999?fields=invalidfields", follow=True) + self.assertEqual(response.status_code, 400) json_response = json.loads(response.content.decode("utf-8")) - self.assertEquals(json_response.get("error"), "Account does not exist") + self.assertEquals(json_response.get("error"), "Invalid fields") def test_create_settings(self): """ diff --git a/mesh/urls.py b/mesh/urls.py index 9c6cab35..f72350a6 100644 --- a/mesh/urls.py +++ b/mesh/urls.py @@ -55,5 +55,5 @@ path('educations/', include(education_urls)), path('auth/', include(auth_urls)), path('conversations/', include(conversation_urls)), - re_path(r'.*', TemplateView.as_view(template_name='index.html')), + # re_path(r'.*', TemplateView.as_view(template_name='index.html')), ]