Compare commits
88 Commits
69dbc56374
...
bobby_dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7dc64b4cc | ||
|
|
3497d2f4a9 | ||
|
|
dab7734185 | ||
|
|
20796ce757 | ||
|
|
d4eb35406c | ||
|
|
15e591169a | ||
|
|
a78cf241dd | ||
|
|
e842b12957 | ||
|
|
345fa62865 | ||
|
|
be1ae7262a | ||
|
|
244cd7c383 | ||
|
|
585c863f66 | ||
|
|
59519ad43f | ||
|
|
971f3f6c92 | ||
|
|
9de5bcc122 | ||
|
|
fa5c897d99 | ||
|
|
14c415389e | ||
|
|
bbc01addda | ||
|
|
24d18e6a30 | ||
|
|
a6e97f25b0 | ||
|
|
5450ac31d2 | ||
|
|
e4ee75c7ee | ||
|
|
a3dd0cc941 | ||
|
|
5cbd822c33 | ||
|
|
f7370a90aa | ||
|
|
eff9eccf1e | ||
|
|
4e83b961c0 | ||
|
|
b4bacbe35a | ||
|
|
35af98676f | ||
|
|
bc2c961d5b | ||
|
|
f630151356 | ||
|
|
70de300837 | ||
|
|
9aa3fa9c2a | ||
|
|
08355290b4 | ||
|
|
281336b4c2 | ||
|
|
d1286fa6bf | ||
|
|
2fe49465b5 | ||
|
|
716590e718 | ||
|
|
36674c67f7 | ||
|
|
b93c207ee6 | ||
|
|
6361444cb8 | ||
|
|
816dd0367a | ||
|
|
61313ad7d2 | ||
|
|
6f8257d1d8 | ||
|
|
3461a7281e | ||
|
|
ddced5b98f | ||
|
|
6d55254013 | ||
|
|
694e74eb0c | ||
|
|
524186b801 | ||
|
|
0a7b910c46 | ||
|
|
6aa5c602d2 | ||
|
|
8878dc89ac | ||
|
|
514f9a96e2 | ||
|
|
f13b7d8d4e | ||
|
|
9e66760fae | ||
|
|
d41af6a628 | ||
|
|
ec1fc86c25 | ||
|
|
6892bd6675 | ||
|
|
1513c8609b | ||
|
|
82e85c488e | ||
|
|
db2fab79fa | ||
|
|
575c35691e | ||
|
|
2613e926c7 | ||
|
|
869a7ace06 | ||
|
|
de964c4620 | ||
|
|
9e6c995e59 | ||
|
|
5867445dcd | ||
|
|
5e85aea844 | ||
|
|
edc1f394db | ||
|
|
2a4f507a6f | ||
|
|
6b64a44242 | ||
|
|
6e3df95b1e | ||
|
|
64c172ab59 | ||
|
|
9ba43e8fd1 | ||
|
|
7cecbf90b7 | ||
|
|
090e806b16 | ||
|
|
a412eb8537 | ||
|
|
dce1bd46a7 | ||
|
|
167c379c6b | ||
|
|
27168352ab | ||
|
|
d8b121b6a5 | ||
|
|
d52a35c9e3 | ||
|
|
ac71e17e9f | ||
|
|
83224bc92a | ||
|
|
77cc6a0940 | ||
|
|
2a0bb98bd3 | ||
|
|
d12004612a | ||
|
|
fd4aef5a40 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.vscode
|
||||
|
||||
|
||||
# C extensions
|
||||
|
||||
77
README.md
Normal file
77
README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Installation Guide for Digest Backend
|
||||
|
||||
Follow these steps to set up the Digest Backend project on your local machine.
|
||||
|
||||
## Cloning the Repository
|
||||
|
||||
1. Clone the repository:
|
||||
```sh
|
||||
$ git clone https://github.com/WDI-Ideas/digest_backend.git
|
||||
$ cd digest_backend
|
||||
```
|
||||
|
||||
## Setting Up the Environment
|
||||
|
||||
2. Install `virtualenv` if you haven't already:
|
||||
```sh
|
||||
$ pip install virtualenv
|
||||
```
|
||||
|
||||
3. Create a virtual environment:
|
||||
```sh
|
||||
$ virtualenv venv
|
||||
```
|
||||
|
||||
4. Activate the virtual environment:
|
||||
|
||||
**Windows:**
|
||||
```sh
|
||||
venv\Scripts\activate
|
||||
```
|
||||
|
||||
**Mac/Linux:**
|
||||
```sh
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
5. Install the required dependencies using pip:
|
||||
```sh
|
||||
$ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuring the Environment Variables
|
||||
|
||||
6. Copy and rename the env.example file to .env and set all the required variables. Ensure there are no spaces around the =:
|
||||
```sh
|
||||
$ cp env.example .env
|
||||
```
|
||||
|
||||
## Migrating the Database
|
||||
|
||||
7. Run the following command to create the database tables:
|
||||
```sh
|
||||
$ python manage.py migrate
|
||||
```
|
||||
|
||||
## Pre-Populating the Database
|
||||
|
||||
8. Run the following command to pre-populate the database with custom command:
|
||||
```sh
|
||||
$ python manage.py load_iam_fixture
|
||||
```
|
||||
|
||||
## Running the Server
|
||||
|
||||
9. Run the following command to start the server:
|
||||
```sh
|
||||
$ python manage.py runserver
|
||||
```
|
||||
|
||||
Now, your server should be up and running. Access it in your browser at http://127.0.0.1:8000/.
|
||||
|
||||
## Note :
|
||||
- Make sure you have Python and pip installed on your system.
|
||||
- If you encounter any issues, ensure that all dependencies are properly installed and the environment variables are correctly configured.
|
||||
- Always activate the virtual environment before running any Python or Django commands.
|
||||
42
apple.py
Normal file
42
apple.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import jwt
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from django.contrib.auth import get_user_model
|
||||
from .utils import generate_token_and_user_data
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@api_view(['POST'])
|
||||
def signin_apple(request):
|
||||
try:
|
||||
id_token = request.data['id_token']
|
||||
email = request.data['email']
|
||||
full_name = request.data['full_name']
|
||||
|
||||
# Verify the JWT token
|
||||
header = {'alg': 'ES256', 'kid': 'YOUR_APPLE_KEY_ID'}
|
||||
key = open('path/to/your/Apple-developer-cert.p8', 'rb').read()
|
||||
decoded_token = jwt.decode(id_token, key, audience='YOUR_APP_BUNDLE_ID', algorithms=['ES256'], options={'verify_aud': False})
|
||||
|
||||
# Create a new user
|
||||
user, created = User.objects.get_or_create(
|
||||
email=email,
|
||||
defaults={
|
||||
'first_name': full_name.split()[0],
|
||||
'last_name': full_name.split()[1],
|
||||
'is_active': True,
|
||||
},
|
||||
)
|
||||
|
||||
if created:
|
||||
user.save()
|
||||
|
||||
# Generate a JWT token for the new user
|
||||
token_data = generate_token_and_user_data(user)
|
||||
|
||||
return Response(token_data, status=status.HTTP_200_OK)
|
||||
|
||||
except (KeyError, ExpiredSignatureError, InvalidTokenError) as e:
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
27
env.example
27
env.example
@@ -1,5 +1,5 @@
|
||||
# ENV_NAME options are Production, Staging and Development
|
||||
ENV_NAME=
|
||||
ENV_NAME=Development
|
||||
|
||||
SECRET_KEY=
|
||||
|
||||
@@ -7,32 +7,19 @@ DJANGO_DEBUG=True
|
||||
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
DB_DATABASE=niftyll
|
||||
DB_DATABASE=digest_db
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USERNAME=
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
DB_PORT=3306
|
||||
|
||||
CELERY_BROKER_URL=redis://localhost:6379
|
||||
|
||||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=587
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
EMAIL_USE_TLS=True
|
||||
|
||||
TWILIO_ACCOUNT_SID=
|
||||
TWILIO_AUTH_TOKEN=
|
||||
MY_TWILIO_NUMBER=
|
||||
|
||||
STRIPE_SECRET_KEY=
|
||||
|
||||
ALPHA_VANTAGE=
|
||||
|
||||
PAYTM_MER_ID=
|
||||
PAYTM_MER_KEY=
|
||||
PAYTM_WEBSITE=
|
||||
PAYTM_IND_TYPE=
|
||||
PAYTM_CHANL_ID_WEB=
|
||||
PAYTM_CHANL_ID_APP=
|
||||
ONESIGNAL_APP_ID=
|
||||
ONESIGNAL_REST_API_KEY=
|
||||
ONESIGNAL_USER_AUTH_KEY=
|
||||
|
||||
315
food_data.txt
Normal file
315
food_data.txt
Normal file
@@ -0,0 +1,315 @@
|
||||
Banana Bread: Flour, sugar, bananas, baking powder, salt, eggs, vegetable oil, vanilla
|
||||
Beef Burrito: Tortilla, ground beef, refried beans, cheddar cheese, lettuce, tomato, onion, salsa
|
||||
Broccoli Cheese Soup: Chicken broth, milk, butter, flour, broccoli, carrots, cheddar cheese, salt, pepper
|
||||
Caesar Salad: Romaine lettuce, croutons, parmesan cheese, Caesar dressing, garlic, anchovies
|
||||
Chicken Alfredo: Penne pasta, chicken, butter, heavy cream, garlic, parmesan cheese, salt, pepper
|
||||
Chocolate Chip Cookies: Flour, sugar, brown sugar, butter, eggs, vanilla, baking soda, salt, chocolate chips
|
||||
Cornbread: Cornmeal, flour, sugar, baking powder, salt, milk, eggs, butter
|
||||
Egg Rolls: Egg roll wrappers, cabbage, carrots, green onions, ground pork, garlic, soy sauce, sesame oil
|
||||
Fajitas: Flour tortillas, sliced chicken, bell peppers, onions, garlic, cumin, chili powder, lime juice
|
||||
French Fries: Potatoes, vegetable oil, salt, pepper
|
||||
Garlic Bread: Baguette, garlic, butter, parmesan cheese
|
||||
Grilled Cheese: Bread, butter, cheddar cheese
|
||||
Hamburger: Buns, beef, lettuce, tomato, onion, pickles, ketchup, mustard
|
||||
Hot Dog: Hot dog bun, hot dog, mustard, ketchup, relish
|
||||
Ice Cream: Cream, milk, sugar, vanilla
|
||||
Jambalaya: Rice, andouille sausage, chicken, onion, green pepper, celery, garlic, tomato, bay leaves, thyme, paprika, cayenne, salt
|
||||
Kung Pao Chicken: Chicken, peanuts, bell pepper, zucchini, carrot, green onion, soy sauce, hoisin sauce, rice vinegar, garlic, ginger, chili peppers
|
||||
Lasagna: Lasagna noodles, ground beef, ricotta cheese, mozzarella cheese, tomato sauce, garlic, onion, basil
|
||||
Mac and Cheese: Pasta, milk, butter, flour, cheddar cheese, salt, pepper
|
||||
Meatballs: Ground beef, breadcrumbs, parmesan cheese, garlic, egg, parsley
|
||||
Meatloaf: Ground beef, breadcrumbs, onion, garlic, egg, milk, ketchup, salt, pepper
|
||||
Muffins: Flour, sugar, baking powder, salt, milk, eggs, vegetable oil
|
||||
Nachos: Tortilla chips, beef, beans, cheddar cheese, lettuce, tomato, onion, jalapenos
|
||||
Omelette: Eggs, milk, salt, pepper, cheese, onion, bell pepper
|
||||
Pancakes: Flour, sugar, baking powder, salt, milk, eggs, butter
|
||||
Pasta: Penne, tomato sauce, garlic, onion, basil, olive oil, parmesan cheese
|
||||
Pizza: Dough, tomato sauce, mozzare,cheese, pepperoni, sausage, mushroom, bell pepper, onion, olives
|
||||
Pork Chops: Pork chops, salt, pepper, rosemary
|
||||
Quesadilla: Tortilla, cheese, chicken, beef, beans
|
||||
Queso: American cheese, Velveeta cheese, milk, Rotel
|
||||
Rice: White rice, water, butter, salt
|
||||
Salad: Lettuce, tomato, cucumber, onion
|
||||
Sandwich: Bread, meat, cheese, lettuce, tomato, onion, mayonnaise, mustard, ketchup
|
||||
Shrimp: Shrimp, garlic, butter, lemon
|
||||
Shrimp Scampi: Shrimp, garlic, butter, lemon, white wine, pasta
|
||||
Sloppy Joes: Ground beef, onion, bell pepper, tomato sauce, ketchup, brown sugar, buns
|
||||
Spaghetti: Spaghetti, tomato sauce, garlic, onion, beef
|
||||
Steak: Steak, salt, pepper, garlic
|
||||
Stew: Beef, potatoes, carrots, onion, garlic, beef, chicken, vegetable
|
||||
Stir Fry: Rice, vegetables, chicken, beef, shrimp, tofu
|
||||
Sushi: Rice, seaweed, fish, avocado, cucumber
|
||||
Tacos: Tortillas, meat, cheese, lettuce, tomato, onion, salsa
|
||||
Tater Tots: Tater tots, vegetable oil, ketchup
|
||||
Tuna Salad: Tuna, mayonnaise, onion, celery
|
||||
Vegetable Stir Fry: Rice, vegetables, soy sauce
|
||||
Waffles: Flour, sugar, baking powder, salt, milk, eggs, butter
|
||||
Wings: Chicken wings, sauce
|
||||
Yogurt Parfait: Yogurt, granola, strawberries, blueberries, peaches
|
||||
Ziti: Ziti noodles, tomato sauce, meat, cheese
|
||||
Zucchini Boats: Zucchini, ground beef, tomato sauce, onion, garlic, cheese
|
||||
Baked Potato: Potato, butter, sour cream, cheese, bacon bits
|
||||
Baked Ziti: Ziti noodles, tomato sauce, cheese, ground beef ,meatballs
|
||||
Banana Bread: Flour, sugar, bananas, eggs, butter, baking powder
|
||||
Beef and Broccoli: Beef, broccoli, soy sauce, garlic, brown sugar
|
||||
Beef Stroganoff: Beef, mushrooms, onion, beef broth, sour cream, egg noodles
|
||||
Berry Cobbler: Fruit, sugar, biscuit mix, butter
|
||||
Breakfast Burrito: Tortilla, eggs, cheese, sausage, potatoes
|
||||
Bratwurst: Bratwurst, buns, sauerkraut
|
||||
Bruschetta: Bread, tomatoes, basil, garlic, oil
|
||||
Buffalo Chicken Dip: Chicken, cream cheese, hot sauce, blue cheese, ranch dressing
|
||||
Burrito Bowl: Rice, beans, chicken, cheese, sour cream, salsa
|
||||
Butternut Squash Soup: Butternut squash, chicken broth, onion, garlic
|
||||
Caesar Salad: Romaine lettuce, croutons, grilled chicken, Caesar dressing
|
||||
Calzones: Pizza dough, tomato sauce, cheese, meat ,vegetables
|
||||
Carbonara: Spaghetti, eggs, bacon, parmesan cheese
|
||||
Chicken and Dumplings: Chicken, dumplings, vegetables, chicken broth
|
||||
Chicken Fried Rice: Rice, chicken, vegetables, eggs, soy sauce
|
||||
Chicken Parmesan: Chicken, tomato sauce, cheese, pasta
|
||||
Chicken Penne: Penne pasta, chicken, tomato sauce, cheese
|
||||
Chicken Tacos: Tortillas, chicken, cheese, lettuce, tomato, salsa
|
||||
Chicken Tikka Masala: Chicken, tomato sauce, coconut milk, spices
|
||||
Chili: Ground beef, beans, tomato sauce, chili powder
|
||||
Chili Cheese Dog: Hot dog, chili, cheese, bun
|
||||
Chili Rellenos: Poblano pepper, cheese, tomato sauce, eggs
|
||||
Chocolate Chip Cookies: Flour, sugar, butter, eggs, vanilla, chocolate chips, baking powder
|
||||
Chow Mein: Noodles, vegetables, meat
|
||||
Churros: Flour, sugar, water, cinnamon, oil
|
||||
Clam Chowder: Clams, potatoes, onion, milk, bacon
|
||||
Cobb Salad: Romaine lettuce, chicken, bacon, avocado, tomato, blue cheese, dressing
|
||||
Coleslaw: Cabbage, mayonnaise, sugar
|
||||
Cornbread: Cornmeal, flour, sugar, baking powder, eggs, milk
|
||||
Corn on the Cob: Corn, butter, salt
|
||||
Crab Cakes: Crab, breadcrumbs, mayonnaise, spices
|
||||
Cream of Mushroom Soup: Mushrooms, chicken broth, cream, flour
|
||||
Crepes: Flour, eggs, milk, sugar
|
||||
Croissants: Flour, butter, yeast, sugar
|
||||
Fried Rice: Rice, eggs, onions, peas, carrots, soy sauce, vegetable oil
|
||||
Beef and Broccoli: Beef, broccoli, soy sauce, garlic, ginger, sugar, cornstarch
|
||||
Chicken Fried Steak: Tenderized steak, flour, eggs, milk, breadcrumbs, salt, pepper, vegetable oil
|
||||
Chicken Curry: Chicken, onions, garlic, ginger, curry powder, coconut milk, chicken broth, tomato paste, salt, pepper, sugar
|
||||
Chicken Fajitas: Chicken, bell peppers, onions, garlic, cumin, chili powder, salt, pepper, flour tortillas
|
||||
Chicken Parmesan: Chicken, breadcrumbs, tomato sauce, mozzarella cheese, parmesan cheese, spaghetti
|
||||
Chicken Shawarma: Chicken, garlic, lemon juice, olive oil, cumin, coriander, paprika, pita bread
|
||||
Chili Relleno: Poblano peppers, cheese, eggs, flour, tomato sauce, vegetable oil
|
||||
Clam Chowder: Clams, potatoes, onions, celery, milk, butter, flour, thyme
|
||||
Cobb Salad: Greens, chicken, bacon, eggs, avocado, tomatoes, blue cheese, vinaigrette
|
||||
Corned Beef and Cabbage: Corned beef, cabbage, carrots, onions, garlic, beef broth
|
||||
Creamy Polenta: Polenta, milk, butter, parmesan cheese, salt
|
||||
Crab Cakes: Crab meat, breadcrumbs, eggs, mayonnaise, mustard, salt, pepper, vegetable oil
|
||||
Crispy Fish Tacos: White fish, flour tortillas, cabbage, cilantro, avocado, lime, chipotle mayo
|
||||
Cuban Sandwich: Ham, roasted pork, Swiss cheese, pickles, mustard, Cuban bread
|
||||
Deviled Eggs: Eggs, mayonnaise, mustard, vinegar, salt, pepper, paprika
|
||||
Egg Rolls: Cabbage, carrots, onions, garlic, ground pork, soy sauce, egg roll wrappers
|
||||
Enchiladas: Tortillas, cheese, chicken ,beef, onions, garlic, tomato sauce, chili powder
|
||||
Fettuccine Alfredo: Fettuccine, butter, heavy cream, parmesan cheese, salt, pepper
|
||||
Fish and Chips: Cod ,haddock, flour, beer, baking powder, salt, potatoes, vegetable oil
|
||||
Fried Chicken: Chicken, buttermilk, flour, salt, pepper, vegetable oil
|
||||
Goulash: Ground beef, onions, garlic, paprika, tomato sauce, beef broth, elbow macaroni, sour cream
|
||||
Gumbo: Shrimp, chicken, sausage, okra, onions, garlic, bell peppers, celery, tomato sauce, rice
|
||||
Hamburger Helper: Ground beef, egg noodles, milk, cheese, seasoning packet
|
||||
Hot and Sour Soup: Chicken, tofu, mushrooms, bamboo shoots, eggs, soy sauce, vinegar, cornstarch
|
||||
Jambalaya: Rice, chicken, sausage, shrimp, onions, garlic, bell peppers, tomato sauce, spices
|
||||
Kung Pao Chicken: Chicken, peanuts, bell peppers, dried red chilies, soy sauce, vinegar, sugar, cornstarch
|
||||
Lobster Bisque: Lobster, butter, onion, garlic, tomato paste, flour, chicken broth, sherry, cream
|
||||
Macaroni and Cheese: Macaroni, milk, butter, flour, cheddar cheese, salt
|
||||
Meatballs: Ground beef, breadcrumbs, egg, onion, garlic, parsley
|
||||
Meatloaf: Ground beef, breadcrumbs, egg, onion, garlic, milk, ketchup
|
||||
Moussaka: Eggplant, potatoes, ground beef, onion, garlic, tomato sauce, bechamel sauce
|
||||
Nachos: Tortilla chips, taco seasoning, beef, beans, cheese, salsa
|
||||
Omelette: Egg, milk, butter, salt, pepper, fillings such as cheese, onions, bell peppers
|
||||
Onion Soup: Onion, butter, flour, chicken broth, beef bouillon, sherry
|
||||
Paella: Rice, saffron, chicken, chorizo, onion, garlic, tomato, peas, shrimp
|
||||
Pancakes: Flour, milk, eggs, sugar, baking powder, salt
|
||||
Panna Cotta: Heavy cream, sugar, milk, gelatin
|
||||
Pasta Carbonara: Pasta, bacon, eggs, parmesan cheese, black pepper
|
||||
Pesto: Basil, garlic, pinenuts, parmesan cheese, olive oil, salt
|
||||
Pho: Beef, beef broth, rice noodles, onion, ginger, star anise, cinnamon, cloves, fish sauce
|
||||
Poutine: Fries, cheese curds, gravy
|
||||
Pumpkin Pie: Pumpkin, evaporated milk, eggs, sugar, cinnamon, nutmeg, pie crust
|
||||
Quesadilla: Tortilla, cheese, fillings such as chicken, onions, bell peppers
|
||||
Ratatouille: Eggplant, bell peppers, zucchini, onion, garlic, tomato, herbs
|
||||
Red Velvet Cake: Flour, sugar, cocoa powder, buttermilk, vegetable oil, baking soda, vinegar, cream cheese frosting
|
||||
Roasted Chicken: Chicken, oil, salt, pepper
|
||||
Ropa Vieja: Flank steak, tomato sauce, onion, bell pepper, garlic, olives
|
||||
Rum Cake: Yellow cake, rum, butter, pecans
|
||||
Salmon Cakes: Salmon, breadcrumbs, egg, onion, old bay seasoning, oil
|
||||
Seafood Risotto: Arborio rice, seafood, garlic, white wine, parmesan cheese
|
||||
Shrimp and Grits: Shrimp, grits, bacon, onion, garlic, hot sauce
|
||||
Sloppy Joes: Ground beef, onion, bell pepper, garlic, tomato sauce, spices, served on hamburger buns
|
||||
Spaghetti: Spaghetti, tomato sauce, meatballs ,meat sauce
|
||||
Spaghetti alla Carbonara: Spaghetti, bacon, eggs, parmesan cheese, black pepper
|
||||
Spaghetti and Meatballs: Spaghetti, tomato sauce, meatballs
|
||||
Spinach Artichoke Dip: Spinach, artichoke, cream cheese, parmesan cheese, garlic served with chips ,veggies
|
||||
Stuffed Bell Peppers: Bell peppers, tomato sauce, ,rice, ground beef, onion, garlic, cheese
|
||||
Chicken Alfredo: Fettuccine noodles, chicken breast, heavy cream, garlic, butter, parmesan cheese, salt
|
||||
Beef Tacos: tortillas, ground beef, taco seasoning, lettuce, tomatoes, onions, shredded cheese, sour cream
|
||||
Shrimp Scampi: Linguine noodles, shrimp, garlic, butter, white wine, lemon juice, red pepper flakes, parsley
|
||||
Baked Ziti: Ziti noodles, ground beef, marinara sauce, ricotta cheese, mozzarella cheese, parmesan cheese
|
||||
Grilled Cheese Sandwich: Bread, butter, cheese
|
||||
Chili: Ground beef, kidney beans, onions, garlic, chili powder, cumin, diced tomatoes, beef broth, salt
|
||||
Breakfast Burritos: Flour tortillas, scrambled eggs, bacon ,sausage, potatoes, cheese, salsa
|
||||
Chicken Stir Fry: Chicken breast, broccoli, carrots, onions, garlic, soy sauce, brown sugar
|
||||
Fajitas: Flour tortillas, chicken ,steak, onions, bell peppers, garlic, lime juice, fajita seasoning
|
||||
Meatball Sub: Sub rolls, meatballs, marinara sauce, mozzarella cheese, parmesan cheese
|
||||
Grilled Cheese Sandwich: Bread, cheese, butter Served with tomato soup ,a salad
|
||||
Chicken Caesar Salad: Romaine lettuce, grilled chicken, croutons, Caesar dressing, Parmesan cheese
|
||||
Beef Tacos: shell tortillas, ground beef, lettuce, tomatoes, onions, shredded cheese, salsa
|
||||
Shrimp Scampi: Linguine noodles, shrimp, garlic, butter, white wine, lemon juice, red pepper flakes, parsley
|
||||
BBQ Ribs: Pork ribs, barbecue sauce, brown sugar, spices Served with coleslaw cornbread
|
||||
Vegetable Stir Fry: Bell peppers, broccoli, carrots, onions, garlic, soy sauce, sesame oil Served over rice ,noodles
|
||||
Chicken Fajitas: Flour tortillas, chicken breast, onions, bell peppers, garlic, lime juice, fajita seasoning Served with sour cream, guacamole, salsa
|
||||
Beef and Broccoli: Flank steak, broccoli florets, soy sauce, brown sugar, garlic, ginger, vegetable oil Served over rice
|
||||
Baked Ziti: Ziti pasta, ground beef, marinara sauce, mozzarella cheese, Parmesan cheese
|
||||
Grilled Chicken Breast: Chicken breast, olive oil, lemon juice, garlic, salt, pepper Served with roasted vegetables quinoa
|
||||
Chicken Tikka Masala: Chicken, tomato sauce, onion, garlic, ginger, cumin, coriander, turmeric, garam masala, yogurt, heavy cream, cilantro
|
||||
Samosa: Potatoes, peas, onion, garlic, ginger, green chilies, coriander, cumin, garam masala, flour, vegetable oil, salt
|
||||
Butter Chicken: Chicken, butter, tomato sauce, onion, garlic, ginger, coriander, cumin, garam masala, turmeric, paprika, heavy cream, cilantro
|
||||
Chana Masala: Chickpeas, onion, tomato sauce, garlic, ginger, cumin, coriander, turmeric, garam masala, amchur powder, cilantro
|
||||
Biryani: Basmati rice, chicken, onion, garlic, ginger, yogurt, saffron, cardamom, cloves, cinnamon, bay leaves, coriander, mint
|
||||
Pakora: Chickpea flour, onion, spinach, potatoes, chili powder, coriander, turmeric, salt, water
|
||||
Tandoori Chicken: Chicken, yogurt, garlic, ginger, cumin, coriander, paprika, turmeric, cayenne, lemon juice, garam masala
|
||||
Aloo Gobi: Potatoes, cauliflower, onion, garlic, ginger, turmeric, cumin, coriander, garam masala, cilantro
|
||||
Raita: Yogurt, cucumber, cilantro, mint, cumin, red chili powder
|
||||
Mango Lassi: Mango, yogurt, milk, sugar, ice
|
||||
Rogan Josh: Lamb, onion, garlic, ginger, tomato sauce, paprika, cumin, coriander, cinnamon, cardamom, cloves, yogurt
|
||||
Gulab Jamun: Milk solids, flour, baking soda, ghee, yogurt, cardamom, sugar, rose water
|
||||
Palak Paneer: Paneer, spinach, onion, garlic, ginger, cumin, coriander, turmeric, garam masala, cream
|
||||
Naan: flour, yeast, sugar, yogurt, milk, salt, butter
|
||||
Masala Chai: Black tea, milk, sugar, cinnamon, cardamom, ginger
|
||||
Tandoori Chicken: Chicken breasts, yogurt, cumin, coriander, cayenne pepper, cinnamon, cardamom, cumin
|
||||
Bhaji: Onion, gram flour, chili powder, turmeric, coriander
|
||||
Butter Chicken: Chicken, butter, tomato sauce, onion, garlic, ginger, coriander, cumin, garam masala, turmeric
|
||||
Baingan Bharta: Eggplant, onion, tomato, garlic, ginger, cumin, coriander, turmeric, garam masala, cilantro
|
||||
Chole Bhature: Chickpeas, onion, tomato, garlic, ginger, cumin, coriander, turmeric, garam masala, chili, flour, baking powder
|
||||
Dosa: Lentils, rice, fenugreek seeds, salt, oil
|
||||
Idli: Lentils, rice, fenugreek seeds, salt, oil
|
||||
Kadhi: Gram flour, yogurt, onion, garlic, ginger, turmeric, cumin, coriander, mustard seeds, curry leaves, asafetida
|
||||
Korma: Chicken, cream, almonds, onion, garlic, ginger, coriander, cumin, cardamom, cinnamon, cloves
|
||||
Malai Kofta: Paneer, potatoes, onion, garlic, ginger, cashews, cream, tomato, coriander, cumin, cardamom, cinnamon
|
||||
Matar Paneer: Paneer, peas, onion, garlic, ginger, tomato, coriander, cumin, turmeric, garam masala
|
||||
Pongal: Rice, lentils, ghee, cashews, pepper, cumin, curry leaves
|
||||
Raita: Yogurt, cucumber, cilantro, mint, cumin, red chili powder
|
||||
Rasmalai: Milk, sugar, lemon juice, paneer, cardamom, pistachios, saffron
|
||||
Saag Paneer: Spinach, paneer, onion, garlic, ginger, coriander, cumin, turmeric, garam masala
|
||||
Samosas: Potatoes, peas, onion, garlic, ginger, green chilies, coriander, cumin, garam masala, flour, vegetable oil
|
||||
Shrimp Curry: Shrimp, onion, garlic, ginger, tomato, coriander, cumin, turmeric,
|
||||
Butter Paneer: Paneer, butter, tomato sauce, onion, garlic, ginger, coriander, cumin, turmeric, garam masala, cream
|
||||
Chicken Tikka: Chicken, yogurt, garlic, ginger, cumin, coriander, paprika, turmeric, cayenne, lemon juice, garam masala
|
||||
Lamb Biryani: Basmati rice, lamb, onion, garlic, ginger, yogurt, saffron, cardamom, cloves, cinnamon, bay leaves, coriander, mint
|
||||
Palak Paneer: Paneer, spinach, onion, garlic, ginger, cumin, coriander, turmeric, garam masala, cream
|
||||
Rogan Josh: Lamb, onion, garlic, ginger, tomato sauce, paprika, cumin, coriander, cinnamon, cardamom, cloves, yogurt
|
||||
Samosa Chaat: Samosa, chickpeas, yogurt, tamarind chutney, mint chutney, sev , chopped onion, cilantro
|
||||
Vegetable Biryani: Basmati rice, mixed vegetables, onion, garlic, ginger, yogurt, saffron, cardamom, cloves, cinnamon, coriander, mint
|
||||
Vindaloo: Chicken, lamb, or pork, vinegar, garlic, ginger, onion, tomato, chili powder, cumin, coriander, turmeric, cinnamon, cloves
|
||||
Aloo Tikki: Potatoes, onion, ginger, green chili, coriander, cumin, chaat masala, bread crumbs, oil
|
||||
Bhel Puri: Puffed rice, sev, boiled potatoes, onion, tomato, cilantro, tamarind chutney, mint chutney
|
||||
Chaat Papdi: Papdi (fried dough wafers), boiled potatoes, chickpeas, yogurt, tamarind chutney, mint chutney
|
||||
Baingan Bharta: Eggplant, onion, tomato, garlic, ginger, cumin, coriander, turmeric, garam masala, cilantro
|
||||
Chana Masala: Chickpeas, onion, tomato, garlic, ginger, cumin, coriander, turmeric, garam masala, amchur powder, cilantro
|
||||
Kadhi: Gram flour, yogurt, onion, garlic, ginger, turmeric, cumin, coriander, mustard seeds, curry leaves, asafetida
|
||||
Korma: Chicken, cream, almonds, onion, garlic, ginger, coriander, cumin, cardamom, cinnamon, cloves
|
||||
Malai Kofta: Paneer, potatoes, onion, garlic, ginger, cashews, cream, tomato, coriander, cumin, cardamom, cinnamon
|
||||
Matar Paneer: Paneer, peas, onion, garlic, ginger, tomato, coriander, cumin, turmeric, garam masala
|
||||
Pongal: Rice, lentils, ghee, cashews, pepper, cumin, curry leaves
|
||||
Raita: Yogurt, cucumber, cilantro, mint, cumin, red chili powder
|
||||
Rasmalai: Milk, sugar, lemon juice, paneer, cardamom, pistachios, saffron
|
||||
Saag Paneer: Spinach, paneer, onion, garlic, ginger, coriander, cumin, turmeric, garam masala, cream
|
||||
Samosas: Potatoes, peas, onion, garlic, ginger, green chilies, coriander, cumin, garam masala, flour, vegetable oil
|
||||
Shrimp Curry: Shrimp, onion, garlic, ginger, tomato, coriander, cumin, turmeric, chili powder, coconut milk
|
||||
Aloo Gobi: Potatoes, cauliflower, onion, garlic, ginger, turmeric, cumin, coriander, garam masala, cilantro
|
||||
Chole Bhature: Chickpeas, onion, tomato, garlic, ginger, cumin, coriander, turmeric, garam masala, chili, flour, baking powder
|
||||
Dosa: Lentils, rice, fenugreek seeds, salt, oil
|
||||
Gulab Jamun: Milk solids, flour, baking soda, ghee, yogurt, cardamom, sugar, rose water
|
||||
Idli: Lentils, rice, fenugreek seeds, salt, oil
|
||||
Jalebi: Gram flour, yogurt, water, sugar, saffron, cardamom, oil
|
||||
Kebab: chicken, beef, onion, garlic, ginger, coriander, cumin, chili powder, yogurt, oil
|
||||
Lassi: Yogurt, water, sugar, rose water, cardamom
|
||||
Masala Chai: Black tea, milk, sugar, cinnamon, cardamom, ginger
|
||||
Naan: Flour, yeast, sugar, yogurt, milk, salt, butter
|
||||
Paneer Tikka: Paneer, yogurt, garlic, ginger, cumin, coriander, turmeric, chili powder, oil
|
||||
Rajma: Kidney beans, onion, tomato, garlic, ginger, cumin, coriander, turmeric, garam masala, chili, oil
|
||||
Rogan Josh: Lamb, onion, garlic, ginger, tomato sauce, paprika, cumin, coriander, cinnamon, cardamom, cloves, yogurt
|
||||
Tandoori Chicken: Chicken, yogurt, garlic, ginger, cumin, coriander, paprika, turmeric, cayenne, lemon juice, garam masala
|
||||
Vada Pav: Spiced potato fritters, buns, green chutney, tamarind
|
||||
Biryani: rice, meat, yogurt, onion, garlic, ginger, saffron, cardamom, cloves, cinnamon, coriander, mint, fried onions
|
||||
Chaat: chickpeas, potatoes, yogurt, tamarind chutney, mint chutney, sev , chopped onion, cilantro
|
||||
Chicken 65: Chicken, yogurt, garlic, ginger, chili powder, coriander, cumin, turmeric, vinegar, oil
|
||||
Dhokla: Gram flour, semolina, yogurt, lemon juice, turmeric, baking soda, mustard seeds, sesame seeds, green chili, cilantro
|
||||
Gobi Manchurian: Cauliflower, gram flour, cornstarch, ginger, garlic, green chili, soy sauce, vinegar, ketchup, oil
|
||||
Hakka Noodles: Noodles, carrots, beans, cabbage, soy sauce, vinegar, chili sauce, garlic, oil
|
||||
Hyderabadi Biryani: rice, meat, yogurt, onion, garlic, ginger, saffron, cardamom, cloves, cinnamon, coriander, mint, fried onions, rose water, kewra water
|
||||
Kadai Paneer: Paneer, onion, tomato, garlic, ginger, coriander, cumin, turmeric, garam masala, chili, oil
|
||||
Kati Roll: Paratha, meat, egg, onion, chili sauce, lime
|
||||
Luchi: Flour, ghee, oil, salt
|
||||
Mirchi Bhajji: Green chili, gram flour, chili powder, coriander
|
||||
Beef Stroganoff: Beef, mushrooms, onion, beef broth, sour cream, egg noodles
|
||||
Bruschetta: Bread, tomatoes, basil, garlic, oil
|
||||
Butternut Squash Soup: Butternut squash, chicken broth, onion, garlic
|
||||
Carbonara: Spaghetti, eggs, bacon, parmesan cheese
|
||||
Chicken and Dumplings: Chicken, dumplings, vegetables, chicken broth
|
||||
Chicken Fried Rice: Rice, chicken, vegetables, eggs, soy sauce
|
||||
Chicken Parmesan: Chicken, tomato sauce, cheese, pasta
|
||||
Chicken Penne: Penne pasta, chicken, tomato sauce, cheese
|
||||
Chicken Tacos: Tortillas, chicken, cheese, lettuce, tomato, salsa
|
||||
Chicken Tikka Masala: Chicken, tomato sauce, coconut milk, spices
|
||||
Clam Chowder: Clams, potatoes, onion, milk, bacon
|
||||
Cobb Salad: Romaine lettuce, chicken, bacon, avocado, tomato, blue cheese, dressing
|
||||
Coleslaw: Cabbage, mayonnaise, sugar
|
||||
Corn on the Cob: Corn, butter, salt
|
||||
Crab Cakes: Crab, breadcrumbs, mayonnaise, spices
|
||||
Cream of Mushroom Soup: Mushrooms, chicken broth, cream, flour
|
||||
Crepes: Flour, eggs, milk, sugar
|
||||
Croissants: Flour, butter, yeast, sugar
|
||||
Cuban Sandwich: Ham, roasted pork, Swiss cheese, pickles, mustard, Cuban bread
|
||||
Deviled Eggs: Eggs, mayonnaise, mustard, vinegar, salt, pepper, paprika
|
||||
Egg Rolls: Cabbage, carrots, onions, garlic, ground pork, soy sauce, egg roll wrappers
|
||||
Enchiladas: Tortillas, cheese, chicken ,beef, onions, garlic, tomato sauce, chili powder
|
||||
Fried Rice: Rice, eggs, onions, peas, carrots
|
||||
Beef Bourguignon: Beef, bacon, onion, carrots, mushrooms, red wine, beef broth, tomato paste, garlic, thyme, bay leaves
|
||||
Beef Wellington: Beef tenderloin, puff pastry, mushroom duxelles, prosciutto, Dijon mustard, egg, salt, pepper
|
||||
Bouillabaisse: Fish, shellfish, onion, fennel, garlic, tomato, saffron, white wine, Pernod, fish stock, rouille
|
||||
Carne Asada: Flank steak, lime juice, lemon juice, olive oil, garlic, cumin, chili powder, salt, pepper
|
||||
Chicken Cordon Bleu: Chicken breast, ham, Swiss cheese, egg, breadcrumbs, flour, butter, oil, mustard, salt, pepper
|
||||
Chicken Kiev: Chicken breast, butter, garlic, parsley, egg, breadcrumbs, flour, oil, salt, pepper
|
||||
Cioppino: Fish, shellfish, onion, fennel, garlic, tomato, white wine, fish stock, basil, oregano, thyme, red pepper flakes
|
||||
Coq au Vin: Chicken, bacon, onion, carrots, mushrooms, red wine, chicken broth, tomato paste, garlic, thyme, bay leaves
|
||||
Crab Bisque: Crab, onion, celery, carrots, garlic, tomato, brandy, sherry, heavy cream, fish stock, cayenne pepper
|
||||
Eggplant Parmesan: Eggplant, tomato sauce, mozzarella cheese, Parmesan cheese, breadcrumbs, egg, salt, pepper
|
||||
Etouffee: Shrimp, onion, bell pepper, celery, garlic, tomato,
|
||||
Beef Stroganoff: Beef, mushrooms, onion, beef broth, sour cream, egg noodles
|
||||
Bruschetta: Bread, tomatoes, basil, garlic, oil
|
||||
Butternut Squash Soup: Butternut squash, chicken broth, onion, garlic
|
||||
Carbonara: Spaghetti, eggs, bacon, parmesan cheese
|
||||
Chicken and Dumplings: Chicken, dumplings, vegetables, chicken broth
|
||||
Chicken Fried Rice: Rice, chicken, vegetables, eggs, soy sauce
|
||||
Chicken Parmesan: Chicken, tomato sauce, cheese, pasta
|
||||
Chicken Penne: Penne pasta, chicken, tomato sauce, cheese
|
||||
Chicken Tacos: Tortillas, chicken, cheese, lettuce, tomato, salsa
|
||||
Chicken Tikka Masala: Chicken, tomato sauce, coconut milk, spices
|
||||
Clam Chowder: Clams, potatoes, onion, milk, bacon
|
||||
Cobb Salad: Romaine lettuce, chicken, bacon, avocado, tomato, blue cheese, dressing
|
||||
Coleslaw: Cabbage, mayonnaise, sugar
|
||||
Corn on the Cob: Corn, butter, salt
|
||||
Crab Cakes: Crab, breadcrumbs, mayonnaise, spices
|
||||
Cream of Mushroom Soup: Mushrooms, chicken broth, cream, flour
|
||||
Crepes: Flour, eggs, milk, sugar
|
||||
Croissants: Flour, butter, yeast, sugar
|
||||
Cuban Sandwich: Ham, roasted pork, Swiss cheese, pickles, mustard, Cuban bread
|
||||
Deviled Eggs: Eggs, mayonnaise, mustard, vinegar, salt, pepper, paprika
|
||||
Egg Rolls: Cabbage, carrots, onions, garlic, ground pork, soy sauce, egg roll wrappers
|
||||
Enchiladas: Tortillas, cheese, chicken ,beef, onions, garlic, tomato sauce, chili powder
|
||||
Fried Rice: Rice, eggs, onions, peas, carrots
|
||||
Bruschetta: Bread, tomatoes, basil, garlic, oil
|
||||
Carbonara: Spaghetti, eggs, bacon, parmesan cheese
|
||||
Eggplant Parmesan: Eggplant, tomato sauce, mozzarella cheese, Parmesan cheese, breadcrumbs, egg, salt, pepper
|
||||
Fettuccine Alfredo: Fettuccine, butter, heavy cream, parmesan cheese, salt, pepper
|
||||
Lasagna: Lasagna noodles, ground beef, ricotta cheese, mozzarella cheese, tomato sauce, garlic, onion, basil
|
||||
Manicotti: Manicotti shells, ricotta cheese, mozzarella cheese, tomato sauce, garlic, onion, basil
|
||||
Minestrone Soup: Vegetables, beans, pasta, tomato, garlic, onion, herbs
|
||||
Panzanella: Bread, tomatoes, cucumber, onion, basil, vinaigrette
|
||||
Pasta Carbonara: Pasta, bacon, eggs, parmesan cheese, black pepper
|
||||
Pizza: Dough, tomato sauce, mozzarella cheese, toppings of your choice
|
||||
Risotto: Arborio rice, vegetables, broth, butter, parmesan cheese
|
||||
Tiramisu: Ladyfingers, mascarpone cheese, espresso, cocoa powder
|
||||
Tortellini: Tortellini, cheese, tomato sauce, garlic, onion, basil
|
||||
BIN
formatted_data.xlsx
Normal file
BIN
formatted_data.xlsx
Normal file
Binary file not shown.
@@ -1,57 +1,85 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils import timezone
|
||||
import math
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
||||
from module_iam.models import IAmPrincipal
|
||||
|
||||
from ..models import (
|
||||
PrincipalHealthData,
|
||||
Intolerance,
|
||||
Symptoms,
|
||||
PastTreatment,
|
||||
ChronicCondition,
|
||||
Medicine,
|
||||
Medication,
|
||||
BeverageRecord,
|
||||
Bowel,
|
||||
ChronicCondition,
|
||||
FoodIngredientRecord,
|
||||
FoodIngredintDataset,
|
||||
FoodRecord,
|
||||
Intolerance,
|
||||
MealRecord,
|
||||
MealSymptomRecord,
|
||||
Medication,
|
||||
Medicine,
|
||||
PastTreatment,
|
||||
PrincipalHealthData,
|
||||
Symptoms,
|
||||
SymptomTypeAfterMeal,
|
||||
SymptomTypeBeforeMeal,
|
||||
MealSymptomRecord,
|
||||
FoodIngredientRecord,
|
||||
FoodRecord,
|
||||
BeverageRecord,
|
||||
MealRecord,
|
||||
)
|
||||
|
||||
class FoodDatasetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FoodIngredintDataset
|
||||
fields = ["id", "food_name"]
|
||||
|
||||
class FoodIngredientDatasetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FoodIngredintDataset
|
||||
fields = ['id', 'food_name', 'ingredients']
|
||||
|
||||
|
||||
class IAmPrincipalSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = IAmPrincipal
|
||||
fields = [
|
||||
# "profile_photo",
|
||||
"profile_photo",
|
||||
"first_name",
|
||||
"date_of_birth",
|
||||
"gender",
|
||||
"phone_no",
|
||||
]
|
||||
|
||||
|
||||
class PrincipalHealthDataSerializer(serializers.ModelSerializer):
|
||||
weight = serializers.DecimalField(max_digits=5, decimal_places=2, read_only=True)
|
||||
height = serializers.DecimalField(max_digits=6, decimal_places=2, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PrincipalHealthData
|
||||
fields = [
|
||||
"ethenicity",
|
||||
"weight",
|
||||
"weight_unit",
|
||||
"height",
|
||||
"height_unit",
|
||||
"gastrointestinal_health",
|
||||
"exercise_frequency",
|
||||
"sleep_duration",
|
||||
"eat_frequency",
|
||||
]
|
||||
|
||||
|
||||
class PrincipalAndHealthSerializer(serializers.ModelSerializer):
|
||||
ethenicity = serializers.CharField(read_only=True)
|
||||
weight = serializers.DecimalField(max_digits=5, decimal_places=2, read_only=True)
|
||||
weight_unit = serializers.CharField(max_length=10, read_only=True)
|
||||
height = serializers.DecimalField(max_digits=6, decimal_places=2, read_only=True)
|
||||
height_unit = serializers.CharField(max_length=10, read_only=True)
|
||||
gastrointestinal_health = serializers.CharField(read_only=True)
|
||||
exercise_frequency = serializers.CharField(read_only=True)
|
||||
sleep_duration = serializers.CharField(read_only=True)
|
||||
eat_frequency = serializers.CharField(read_only=True)
|
||||
profile_complete = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IAmPrincipal
|
||||
@@ -62,8 +90,31 @@ class PrincipalAndHealthSerializer(serializers.ModelSerializer):
|
||||
"date_of_birth",
|
||||
"gender",
|
||||
"phone_no",
|
||||
"phone_verified",
|
||||
"email_verified",
|
||||
# "phone_verified",
|
||||
# "email_verified",
|
||||
"ethenicity",
|
||||
"weight",
|
||||
"weight_unit",
|
||||
"height",
|
||||
"height_unit",
|
||||
"gastrointestinal_health",
|
||||
"exercise_frequency",
|
||||
"sleep_duration",
|
||||
"eat_frequency",
|
||||
"profile_complete",
|
||||
]
|
||||
|
||||
def calculate_profile_completion(self, user):
|
||||
"""
|
||||
Calculates the profile completion percentage for a user based on the required fields.
|
||||
"""
|
||||
fields = [
|
||||
"profile_photo",
|
||||
"first_name",
|
||||
"email",
|
||||
"date_of_birth",
|
||||
"gender",
|
||||
"phone_no",
|
||||
"ethenicity",
|
||||
"weight",
|
||||
"height",
|
||||
@@ -72,26 +123,92 @@ class PrincipalAndHealthSerializer(serializers.ModelSerializer):
|
||||
"sleep_duration",
|
||||
"eat_frequency",
|
||||
]
|
||||
try:
|
||||
# Retrieve the user profile from the database
|
||||
profile = IAmPrincipal.objects.get(id=user)
|
||||
try:
|
||||
# Retrieve the user's health data from the database
|
||||
health_data = PrincipalHealthData.objects.get(principal=profile)
|
||||
except PrincipalHealthData.DoesNotExist:
|
||||
# If health data doesn't exist, set health_data to None
|
||||
health_data = None
|
||||
|
||||
# Initialize a counter for completed fields
|
||||
completed_fields = sum(
|
||||
1
|
||||
for field in fields
|
||||
if (
|
||||
# If the field is in the user profile and the field value is not None, not an empty string, and not an instance of datetime.date
|
||||
(
|
||||
field in vars(profile)
|
||||
and vars(profile).get(field, "")
|
||||
and vars(profile).get(field) != datetime.date
|
||||
)
|
||||
or
|
||||
# If health data exists, the field is in the user's health data, and the field value is not None, not an empty string, and not an instance of datetime.date
|
||||
(
|
||||
health_data
|
||||
and field in vars(health_data)
|
||||
and vars(health_data).get(field, "")
|
||||
and vars(health_data).get(field) != datetime.date
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
except IAmPrincipal.DoesNotExist:
|
||||
# If the user profile doesn't exist, return 0
|
||||
return 0
|
||||
|
||||
# Calculate the total number of fields
|
||||
total_fields = len(fields)
|
||||
|
||||
# Calculate the profile completion percentage
|
||||
completion_percentage = math.floor((completed_fields / total_fields) * 100)
|
||||
|
||||
# Return the profile completion percentage
|
||||
return completion_percentage
|
||||
|
||||
def get_image_url(self, obj, field_name, request):
|
||||
image_field = getattr(obj, field_name)
|
||||
if image_field:
|
||||
return request.build_absolute_uri(image_field.url)
|
||||
return ""
|
||||
else:
|
||||
# Return the URL of the default image from the static path
|
||||
default_image_path = os.path.join(
|
||||
settings.STATIC_URL, "img/default_profile.jpg"
|
||||
)
|
||||
return request.build_absolute_uri(default_image_path)
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
request = self.context.get("request")
|
||||
data["profile_photo"] = self.get_image_url(instance, "profile_photo", request)
|
||||
health_data = instance.health_data_principal
|
||||
if health_data:
|
||||
data['ethenicity'] = health_data.ethenicity
|
||||
data['weight'] = health_data.weight
|
||||
data['height'] = health_data.height
|
||||
data['gastrointestinal_health'] = health_data.gastrointestinal_health
|
||||
data['exercise_frequency'] = health_data.exercise_frequency
|
||||
data['sleep_duration'] = health_data.sleep_duration
|
||||
data['eat_frequency'] = health_data.eat_frequency
|
||||
data["profile_complete"] = self.calculate_profile_completion(request.user.id)
|
||||
if (
|
||||
hasattr(instance, "health_data_principal")
|
||||
and instance.health_data_principal
|
||||
):
|
||||
health_data = instance.health_data_principal
|
||||
data["ethenicity"] = health_data.ethenicity
|
||||
data["weight"] = health_data.weight
|
||||
data["weight_unit"] = health_data.weight_unit
|
||||
data["height"] = health_data.height
|
||||
data["height_unit"] = health_data.height_unit
|
||||
data["gastrointestinal_health"] = health_data.gastrointestinal_health
|
||||
data["exercise_frequency"] = health_data.exercise_frequency
|
||||
data["sleep_duration"] = health_data.sleep_duration
|
||||
data["eat_frequency"] = health_data.eat_frequency
|
||||
else:
|
||||
# If health_data_principal doesn't exist or is empty, set empty strings for all attributes
|
||||
data["ethenicity"] = ""
|
||||
data["weight"] = 0.00
|
||||
data["weight_unit"] = "kg"
|
||||
data["height"] = 0.00
|
||||
data["height_unit"] = "cm"
|
||||
data["gastrointestinal_health"] = ""
|
||||
data["exercise_frequency"] = ""
|
||||
data["sleep_duration"] = ""
|
||||
data["eat_frequency"] = ""
|
||||
return data
|
||||
|
||||
|
||||
@@ -100,6 +217,7 @@ class IntoleranceSerializer(serializers.ModelSerializer):
|
||||
model = Intolerance
|
||||
fields = ["id", "name", "duration"]
|
||||
|
||||
|
||||
class SymptomsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Symptoms
|
||||
@@ -109,7 +227,7 @@ class SymptomsSerializer(serializers.ModelSerializer):
|
||||
class PastTreatmentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = PastTreatment
|
||||
fields = ["id", "name", "duration"]
|
||||
fields = ["id", "name", "duration", "is_recurring", "treatment_frequency"]
|
||||
|
||||
|
||||
class ChronicConditionSerializer(serializers.ModelSerializer):
|
||||
@@ -121,7 +239,7 @@ class ChronicConditionSerializer(serializers.ModelSerializer):
|
||||
class FoodIngredientRecordSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FoodIngredientRecord
|
||||
fields = ["name"]
|
||||
fields = ["name", "from_dataset"]
|
||||
|
||||
|
||||
class FoodRecordSerializer(serializers.ModelSerializer):
|
||||
@@ -135,12 +253,11 @@ class BeverageRecordSerializer(serializers.ModelSerializer):
|
||||
model = BeverageRecord
|
||||
fields = [
|
||||
"beverage_type",
|
||||
"glass_type",
|
||||
"glass_count",
|
||||
"quantity",
|
||||
"quantity_measure",
|
||||
]
|
||||
|
||||
|
||||
class MealRecordSerializer(serializers.ModelSerializer):
|
||||
food_records = FoodRecordSerializer(many=True)
|
||||
beverage_records = BeverageRecordSerializer(many=True)
|
||||
@@ -148,7 +265,15 @@ class MealRecordSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MealRecord
|
||||
fields = ['id', 'date', 'time', 'meal_type', 'food_records', 'food_ingredient_records', 'beverage_records']
|
||||
fields = [
|
||||
"id",
|
||||
"date",
|
||||
"time",
|
||||
"meal_type",
|
||||
"food_records",
|
||||
"food_ingredient_records",
|
||||
"beverage_records",
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
food_record_data = validated_data.pop("food_records", [])
|
||||
@@ -156,7 +281,7 @@ class MealRecordSerializer(serializers.ModelSerializer):
|
||||
food_ingredient_record_data = validated_data.pop("food_ingredient_records", [])
|
||||
|
||||
meal_record = self.Meta.model.objects.create(**validated_data)
|
||||
|
||||
print(f"meal recordd id in serializer {meal_record.id}")
|
||||
food_record_list = []
|
||||
for data in food_record_data:
|
||||
obj, _ = FoodRecord.objects.get_or_create(**data)
|
||||
@@ -184,6 +309,8 @@ class MealRecordSerializer(serializers.ModelSerializer):
|
||||
instance.meal_type = validated_data.get("meal_type", instance.meal_type)
|
||||
instance.save()
|
||||
|
||||
print(f"meal record id in update 1 method {instance.id}")
|
||||
|
||||
food_record_data = validated_data.pop("food_records", [])
|
||||
beverage_record_data = validated_data.pop("beverage_records", [])
|
||||
food_ingredient_record_data = validated_data.pop("food_ingredient_records", [])
|
||||
@@ -208,8 +335,10 @@ class MealRecordSerializer(serializers.ModelSerializer):
|
||||
instance.beverage_records.set(beverage_record_list)
|
||||
|
||||
instance.save()
|
||||
print(f"meal record id in update 2 method {instance.id}")
|
||||
return instance
|
||||
|
||||
|
||||
class MedicineSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Medicine
|
||||
@@ -253,6 +382,7 @@ class BowelSerializer(serializers.ModelSerializer):
|
||||
"date",
|
||||
"time",
|
||||
"stool_type",
|
||||
"stool_name",
|
||||
"duration",
|
||||
"completeness_of_evacuation",
|
||||
"urgency",
|
||||
@@ -264,6 +394,18 @@ class BowelSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class MealRecordSymptomsSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = MealRecord
|
||||
fields = [
|
||||
"id",
|
||||
"date",
|
||||
"time",
|
||||
"meal_type",
|
||||
]
|
||||
|
||||
|
||||
class SymptomTypeBeforeMealSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SymptomTypeBeforeMeal
|
||||
@@ -279,6 +421,10 @@ class SymptomTypeAfterMealSerializer(serializers.ModelSerializer):
|
||||
class MealSymptomRecordSerializer(serializers.ModelSerializer):
|
||||
symptoms_before_meal = SymptomTypeBeforeMealSerializer(many=True)
|
||||
symptoms_after_meal = SymptomTypeAfterMealSerializer(many=True)
|
||||
related_meal_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=MealRecord.objects.all(), write_only=True
|
||||
) # Added field to accept meal ID
|
||||
related_meal = MealRecordSymptomsSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MealSymptomRecord
|
||||
@@ -290,13 +436,20 @@ class MealSymptomRecordSerializer(serializers.ModelSerializer):
|
||||
"interval",
|
||||
"symptoms_before_meal",
|
||||
"symptoms_after_meal",
|
||||
"related_meal_id",
|
||||
"related_meal"
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
before_meal_data = validated_data.pop("symptoms_before_meal")
|
||||
after_meal_data = validated_data.pop("symptoms_after_meal")
|
||||
related_meal_id = validated_data.pop("related_meal_id")
|
||||
|
||||
meal_symptom_record = MealSymptomRecord.objects.create(**validated_data)
|
||||
meal_symptom_record = MealSymptomRecord.objects.create(
|
||||
related_meal=related_meal_id, **validated_data
|
||||
)
|
||||
|
||||
print(f"symptoms meal record in create {meal_symptom_record.id, meal_symptom_record.related_meal.id}")
|
||||
|
||||
before_meal_list = []
|
||||
for data in before_meal_data:
|
||||
@@ -314,17 +467,17 @@ class MealSymptomRecordSerializer(serializers.ModelSerializer):
|
||||
return meal_symptom_record
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.date = validated_data.get(
|
||||
"date", instance.date
|
||||
)
|
||||
instance.time = validated_data.get(
|
||||
"time", instance.time
|
||||
)
|
||||
instance.date = validated_data.get("date", instance.date)
|
||||
instance.time = validated_data.get("time", instance.time)
|
||||
instance.symptoms_description = validated_data.get(
|
||||
"symptoms_description", instance.symptoms_description
|
||||
)
|
||||
instance.interval = validated_data.get("interval", instance.interval)
|
||||
|
||||
related_meal_id = validated_data.pop("related_meal_id", None) # Extract meal ID
|
||||
if related_meal_id is not None:
|
||||
instance.related_meal = related_meal_id
|
||||
|
||||
before_meal_data = validated_data.pop("symptoms_before_meal", [])
|
||||
after_meal_data = validated_data.pop("symptoms_after_meal", [])
|
||||
|
||||
@@ -342,5 +495,6 @@ class MealSymptomRecordSerializer(serializers.ModelSerializer):
|
||||
instance.symptoms_after_meal.set(after_meal_instances)
|
||||
|
||||
instance.save()
|
||||
print(f"symptoms meal record in update {instance.id, instance.related_meal.id}")
|
||||
|
||||
return instance
|
||||
return instance
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("profile/", views.ProfileAPIView.as_view()),
|
||||
path("profile/complete/", views.ProfileCompleteAPIView.as_view()),
|
||||
|
||||
path("daily-records/", views.DailyRecordAPIView.as_view()),
|
||||
|
||||
@@ -24,6 +25,13 @@ urlpatterns = [
|
||||
path("meal-symptoms/<int:pk>/", views.MealSymptomAPIView.as_view()),
|
||||
|
||||
path("meal/", views.MealAPIView.as_view()),
|
||||
path("meal/duplicate/", views.MealDuplicateAPIView.as_view()),
|
||||
path("meal/date/", views.MealDateAPIView.as_view()),
|
||||
path("meal/<int:pk>/", views.MealAPIView.as_view()),
|
||||
|
||||
path("food/data/", views.FoodDataAPIView.as_view()),
|
||||
path("food/ingredient/data/", views.FoodIngredientSearchAPIView.as_view()),
|
||||
|
||||
path("report/", views.ReportAPIView.as_view()),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
from datetime import datetime
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Count, Max, Min, Prefetch
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from django.db.models import Prefetch
|
||||
from module_project import constants
|
||||
from module_project.utils import ApiResponse
|
||||
|
||||
from module_iam.models import IAmPrincipal
|
||||
from ..models import (
|
||||
PrincipalHealthData,
|
||||
Intolerance,
|
||||
Symptoms,
|
||||
PastTreatment,
|
||||
ChronicCondition,
|
||||
Medication,
|
||||
Bowel,
|
||||
MealSymptomRecord,
|
||||
MealRecord,
|
||||
)
|
||||
from .serializers import (
|
||||
IntoleranceSerializer,
|
||||
SymptomsSerializer,
|
||||
PastTreatmentSerializer,
|
||||
ChronicConditionSerializer,
|
||||
MedicationSerializer,
|
||||
BowelSerializer,
|
||||
MealSymptomRecordSerializer,
|
||||
MealRecordSerializer,
|
||||
IAmPrincipalSerializer,
|
||||
PrincipalHealthDataSerializer,
|
||||
PrincipalAndHealthSerializer,
|
||||
)
|
||||
from module_project import constants, date_utils
|
||||
from module_project.service import OneSignalNotificationService
|
||||
from module_project.utils import ApiResponse
|
||||
|
||||
from ..models import (BeverageRecord, Bowel, ChronicCondition,
|
||||
FoodIngredientRecord, FoodIngredintDataset, FoodRecord,
|
||||
Intolerance, MealRecord, MealSymptomRecord, Medication,
|
||||
PastTreatment, PrincipalHealthData, Symptoms)
|
||||
from .serializers import (BowelSerializer, ChronicConditionSerializer,
|
||||
FoodDatasetSerializer,
|
||||
FoodIngredientDatasetSerializer,
|
||||
IAmPrincipalSerializer, IntoleranceSerializer,
|
||||
MealRecordSerializer, MealSymptomRecordSerializer,
|
||||
MedicationSerializer, PastTreatmentSerializer,
|
||||
PrincipalAndHealthSerializer,
|
||||
PrincipalHealthDataSerializer, SymptomsSerializer)
|
||||
|
||||
|
||||
class ProfileAPIView(APIView):
|
||||
@@ -51,7 +43,6 @@ class ProfileAPIView(APIView):
|
||||
return ApiResponse.error(
|
||||
status=status.HTTP_404_NOT_FOUND, message=constants.RECORD_NOT_FOUND
|
||||
)
|
||||
print(f"object data is {obj}")
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
@@ -83,6 +74,8 @@ class ProfileAPIView(APIView):
|
||||
try:
|
||||
# with transaction.atomic(): # Ensure atomicity of database operations
|
||||
principal_instance = principal_serializer.save()
|
||||
principal_instance.register_complete = True
|
||||
principal_instance.save()
|
||||
|
||||
# Check if health data already exists for the principal
|
||||
health_data_instance, created = PrincipalHealthData.objects.get_or_create(
|
||||
@@ -96,14 +89,24 @@ class ProfileAPIView(APIView):
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
|
||||
|
||||
class ProfileCompleteAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = IAmPrincipal
|
||||
|
||||
def get(self, request):
|
||||
user = IAmPrincipal.objects.filter(id=request.user.id).update(register_complete=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
|
||||
class DailyRecordAPIView(APIView):
|
||||
|
||||
def serialize_record(self, record):
|
||||
time_obj = datetime.strptime(str(record.time), '%H:%M:%S')
|
||||
time_obj = datetime.strptime(str(record.time), "%H:%M:%S")
|
||||
return {
|
||||
"id": record.id,
|
||||
"date": record.date,
|
||||
"time": time_obj.strftime('%I:%M %p'),
|
||||
"time": time_obj.strftime("%I:%M %p"),
|
||||
"sort_time": time_obj,
|
||||
# Add other fields as needed
|
||||
}
|
||||
|
||||
@@ -112,36 +115,40 @@ class DailyRecordAPIView(APIView):
|
||||
# date = datetime.now().date()
|
||||
|
||||
if not date:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors="Date parameter is missing")
|
||||
return ApiResponse.error(
|
||||
message=constants.FAILURE, errors="Date parameter is missing"
|
||||
)
|
||||
|
||||
try:
|
||||
# Convert the date string to a datetime object
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors="Invalid date format")
|
||||
return ApiResponse.error(
|
||||
message=constants.FAILURE, errors="Invalid date format"
|
||||
)
|
||||
|
||||
# Define prefetch related queries for filtering the record of paticular date of all related models
|
||||
meal_records_prefetch = Prefetch(
|
||||
"meal_principal",
|
||||
queryset=MealRecord.objects.filter(date=date),
|
||||
queryset=MealRecord.objects.filter(date=date, deleted=False),
|
||||
to_attr="filtered_meal_record",
|
||||
)
|
||||
|
||||
medication_prefetch = Prefetch(
|
||||
"medication_principal",
|
||||
queryset=Medication.objects.filter(date=date),
|
||||
queryset=Medication.objects.filter(date=date, deleted=False),
|
||||
to_attr="filtered_medication",
|
||||
)
|
||||
|
||||
bowel_prefetch = Prefetch(
|
||||
"bowel_principal",
|
||||
queryset=Bowel.objects.filter(date=date),
|
||||
queryset=Bowel.objects.filter(date=date, deleted=False),
|
||||
to_attr="filtered_bowel",
|
||||
)
|
||||
|
||||
meal_symptom_prefetch = Prefetch(
|
||||
"meal_symptom_principal",
|
||||
queryset=MealSymptomRecord.objects.filter(date=date),
|
||||
queryset=MealSymptomRecord.objects.filter(date=date, deleted=False),
|
||||
to_attr="filtered_meal_symptom",
|
||||
)
|
||||
|
||||
@@ -154,7 +161,7 @@ class DailyRecordAPIView(APIView):
|
||||
).get(id=request.user.id)
|
||||
|
||||
serialized_meal_records = [
|
||||
{"type": "Meal", **self.serialize_record(record)}
|
||||
{"type": f"Meal - {record.meal_type}", **self.serialize_record(record)}
|
||||
for record in principal.filtered_meal_record
|
||||
]
|
||||
|
||||
@@ -173,19 +180,17 @@ class DailyRecordAPIView(APIView):
|
||||
for record in principal.filtered_meal_symptom
|
||||
]
|
||||
|
||||
all_records = (serialized_symptom + serialized_meal_records + serialized_medication + serialized_bowel)
|
||||
all_records = (
|
||||
serialized_symptom
|
||||
+ serialized_meal_records
|
||||
+ serialized_medication
|
||||
+ serialized_bowel
|
||||
)
|
||||
|
||||
# all_records_sorted = sorted(all_records, key=lambda x: x["time"], reverse=True)
|
||||
all_records_sorted = sorted(
|
||||
all_records,
|
||||
key=lambda x: x["time"],
|
||||
reverse=True
|
||||
)
|
||||
all_records_sorted = sorted(all_records, key=lambda x: x["sort_time"], reverse=True)
|
||||
|
||||
|
||||
return ApiResponse.success(
|
||||
message=constants.SUCCESS, data=all_records_sorted
|
||||
)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=all_records_sorted)
|
||||
|
||||
|
||||
class IntoleranceListCreateAPIView(APIView):
|
||||
@@ -195,7 +200,7 @@ class IntoleranceListCreateAPIView(APIView):
|
||||
model = Intolerance
|
||||
|
||||
def get(self, request):
|
||||
obj = self.model.objects.filter(principal=request.user)
|
||||
obj = self.model.objects.filter(principal=request.user, active=True)
|
||||
serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
@@ -226,7 +231,7 @@ class SymptomsListCreateAPIView(APIView):
|
||||
model = Symptoms
|
||||
|
||||
def get(self, request):
|
||||
obj = self.model.objects.filter(principal=request.user)
|
||||
obj = self.model.objects.filter(principal=request.user, active=True)
|
||||
serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
@@ -257,7 +262,7 @@ class PastTreatmentListCreateAPIView(APIView):
|
||||
model = PastTreatment
|
||||
|
||||
def get(self, request):
|
||||
obj = self.model.objects.filter(principal=request.user)
|
||||
obj = self.model.objects.filter(principal=request.user, active=True)
|
||||
serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
@@ -288,7 +293,7 @@ class ChronicConditionListCreateAPIView(APIView):
|
||||
model = ChronicCondition
|
||||
|
||||
def get(self, request):
|
||||
obj = self.model.objects.filter(principal=request.user)
|
||||
obj = self.model.objects.filter(principal=request.user, active=True)
|
||||
serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
@@ -495,7 +500,8 @@ class MealSymptomAPIView(APIView):
|
||||
message=constants.FAILURE, errors=serializer.errors
|
||||
)
|
||||
try:
|
||||
serializer.save(principal=request.user)
|
||||
instance = serializer.save(principal=request.user)
|
||||
print(f"symptoms meal in put view {instance.id}")
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=str(e))
|
||||
|
||||
@@ -516,7 +522,8 @@ class MealSymptomAPIView(APIView):
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=serializer.data)
|
||||
|
||||
try:
|
||||
serializer.save()
|
||||
instance = serializer.save()
|
||||
print(f"symptoms meal in put view {instance.id}")
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=str(e))
|
||||
|
||||
@@ -571,7 +578,8 @@ class MealAPIView(APIView):
|
||||
message=constants.FAILURE, errors=serializer.errors
|
||||
)
|
||||
try:
|
||||
serializer.save(principal=request.user)
|
||||
meal_id = serializer.save(principal=request.user)
|
||||
print(f"meal id in views {meal_id.id}")
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=str(e))
|
||||
|
||||
@@ -592,7 +600,8 @@ class MealAPIView(APIView):
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=serializer.data)
|
||||
|
||||
try:
|
||||
serializer.save()
|
||||
instance = serializer.save()
|
||||
print(f"meal record id in put emthod {instance.id}")
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=str(e))
|
||||
|
||||
@@ -615,3 +624,282 @@ class MealAPIView(APIView):
|
||||
return ApiResponse.success(
|
||||
message=constants.RECORD_DELETED, status=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
|
||||
|
||||
class MealDuplicateAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = MealRecordSerializer
|
||||
model = MealRecord
|
||||
|
||||
def post(self, request):
|
||||
meal_id = request.data.get("id")
|
||||
meal_time = request.data.get("time")
|
||||
print(f"meal_id is {meal_id}, and time is {meal_time}, and type of {type(meal_time)}")
|
||||
|
||||
try:
|
||||
original_meal = self.model.objects.get(pk=meal_id, principal=request.user)
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message="Meal not found", errors=str(e))
|
||||
|
||||
instance = self.model(
|
||||
principal=original_meal.principal,
|
||||
date=original_meal.date,
|
||||
time=meal_time,
|
||||
meal_type=original_meal.meal_type
|
||||
)
|
||||
print(f"record id is {original_meal}")
|
||||
|
||||
instance.save()
|
||||
|
||||
for food_obj in original_meal.food_records.all():
|
||||
instance.food_records.add(food_obj)
|
||||
|
||||
for beverage_obj in original_meal.beverage_records.all():
|
||||
instance.beverage_records.add(beverage_obj)
|
||||
|
||||
for ingredient_obj in original_meal.food_ingredient_records.all():
|
||||
instance.food_ingredient_records.add(ingredient_obj)
|
||||
|
||||
instance.save()
|
||||
|
||||
serializer_obj = self.serializer_class(instance).data
|
||||
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer_obj)
|
||||
|
||||
class FoodDataAPIView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
model = FoodIngredintDataset
|
||||
serializer_class = FoodDatasetSerializer
|
||||
|
||||
def get(self, request):
|
||||
obj = self.model.objects.all().order_by("food_name")
|
||||
serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
class FoodIngredientSearchAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = FoodIngredintDataset
|
||||
serializer_class = FoodIngredientDatasetSerializer
|
||||
|
||||
def get(self, request):
|
||||
query = request.query_params.getlist('query')
|
||||
print(f"Query : {query}")
|
||||
obj = self.model.objects.filter(food_name__in=query).values_list('ingredients', flat=True)
|
||||
|
||||
unique_ingredients = set()
|
||||
for ingredients_list in obj:
|
||||
unique_ingredients.update(ingredients_list)
|
||||
# serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data={'ingredients_list': list(unique_ingredients)})
|
||||
|
||||
class MealDateAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = MealRecordSerializer
|
||||
model = MealRecord
|
||||
|
||||
def get(self, request):
|
||||
date = request.GET.get("date")
|
||||
|
||||
if not date:
|
||||
return ApiResponse.error(
|
||||
message=constants.FAILURE, errors="Date parameter is missing"
|
||||
)
|
||||
|
||||
try:
|
||||
# Convert the date string to a datetime object
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return ApiResponse.error(
|
||||
message=constants.FAILURE, errors="Invalid date format"
|
||||
)
|
||||
obj = self.model.objects.filter(principal=request.user.id, date=date_obj, deleted=False)
|
||||
serializer = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class ReportAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = MealRecord
|
||||
|
||||
def get_user(self):
|
||||
return self.request.user
|
||||
|
||||
def enough_records_exist(self, start_date, end_date):
|
||||
"""
|
||||
Check if at least 7 days of records exist within the date range.
|
||||
|
||||
This method calculates the minimum date by subtracting the required number of days
|
||||
(min_days_required - 1) from the end_date. It then filters the queryset based on the
|
||||
principal and date range. If any objects exist within this range, the method returns True,
|
||||
indicating that there are at least 7 days of records.
|
||||
|
||||
:param start_date: The initial date in the date range
|
||||
:type start_date: datetime.date
|
||||
:param end_date: The final date in the date range
|
||||
:type end_date: datetime.date
|
||||
:return: True if at least 7 days of records exist, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
min_days_required = 7
|
||||
current_date = start_date
|
||||
count = 0
|
||||
|
||||
while current_date <= end_date:
|
||||
if self.model.objects.filter(principal=self.get_user(), date=current_date).exists():
|
||||
count += 1
|
||||
if count >= min_days_required:
|
||||
return True
|
||||
else:
|
||||
count = 0 # Reset count if record is missing for any day
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return False
|
||||
|
||||
def get_top_food_avoid(self, start_date, end_date):
|
||||
"""Get the top food to avoid."""
|
||||
food_counts = defaultdict(int)
|
||||
ingredient_counts = defaultdict(int)
|
||||
beverage_counts = defaultdict(int)
|
||||
|
||||
# symptom_records = MealSymptomRecord.objects.select_related("related_meal").filter(
|
||||
# principal=self.get_user(), date__range=(start_date, end_date)
|
||||
# )
|
||||
|
||||
# for symptom_record in symptom_records:
|
||||
# closest_meal = (
|
||||
# MealRecord.objects.filter(
|
||||
# principal=symptom_record.principal, date__lte=symptom_record.date
|
||||
# )
|
||||
# .order_by("-date", "-time")
|
||||
# .first()
|
||||
# )
|
||||
# if closest_meal:
|
||||
# for food_record in closest_meal.food_records.all():
|
||||
# food_counts[food_record.name] += 1
|
||||
# for ingredient_record in closest_meal.food_ingredient_records.all():
|
||||
# ingredient_counts[ingredient_record.name] += 1
|
||||
# for beverage_record in closest_meal.beverage_records.all():
|
||||
# beverage_counts[beverage_record.beverage_type] += 1
|
||||
|
||||
# Fetch symptom records with related meal records and related food/ingredient/beverage records
|
||||
symptom_records = MealSymptomRecord.objects.prefetch_related(
|
||||
Prefetch('related_meal__food_records', queryset=FoodRecord.objects.all()),
|
||||
Prefetch('related_meal__food_ingredient_records', queryset=FoodIngredientRecord.objects.all()),
|
||||
Prefetch('related_meal__beverage_records', queryset=BeverageRecord.objects.all())
|
||||
).filter(
|
||||
principal=self.get_user(), date__range=(start_date, end_date)
|
||||
)
|
||||
|
||||
# Loop through symptom records and count food, ingredient, and beverage occurrences for each related meal record
|
||||
for symptom_record in symptom_records:
|
||||
closest_meal = symptom_record.related_meal
|
||||
if closest_meal:
|
||||
for food_record in closest_meal.food_records.all():
|
||||
food_counts[food_record.name] += 1
|
||||
for ingredient_record in closest_meal.food_ingredient_records.all():
|
||||
ingredient_counts[ingredient_record.name] += 1
|
||||
for beverage_record in closest_meal.beverage_records.all():
|
||||
beverage_counts[beverage_record.beverage_type] += 1
|
||||
|
||||
# Sort the dictionaries by their values in descending order and getting only top 3 record
|
||||
food_counts = dict(
|
||||
sorted(food_counts.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
ingredient_counts = dict(
|
||||
sorted(ingredient_counts.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
beverage_counts = dict(
|
||||
sorted(beverage_counts.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
|
||||
food_avoid = next(iter(food_counts), None)
|
||||
return food_avoid, food_counts, ingredient_counts, beverage_counts
|
||||
|
||||
def get_symptoms_frequency(self, start_date, end_date):
|
||||
"""Get the frequency of symptoms."""
|
||||
symptom_records = MealSymptomRecord.objects.filter(
|
||||
principal=self.get_user(), date__range=(start_date, end_date)
|
||||
).annotate(
|
||||
before_meal_count=Count("symptoms_before_meal"),
|
||||
after_meal_count=Count("symptoms_after_meal"),
|
||||
)
|
||||
symptoms_frequency = defaultdict(int)
|
||||
|
||||
for record in symptom_records:
|
||||
for symptom in record.symptoms_before_meal.all():
|
||||
symptoms_frequency[symptom.name] += record.before_meal_count
|
||||
for symptom in record.symptoms_after_meal.all():
|
||||
symptoms_frequency[symptom.name] += record.after_meal_count
|
||||
|
||||
sorted_symptoms_frequency = dict(
|
||||
sorted(symptoms_frequency.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
|
||||
return sorted_symptoms_frequency
|
||||
|
||||
def get_stool_type_counts(self, start_date, end_date):
|
||||
"""Get the count of stool types."""
|
||||
stool_type_counts = (
|
||||
Bowel.objects.filter(
|
||||
principal=self.get_user(), date__range=(start_date, end_date)
|
||||
)
|
||||
.values("stool_type")
|
||||
.annotate(stool_type_count=Count("stool_type"))
|
||||
)
|
||||
stool_type_counts_dict = {
|
||||
item["stool_type"]: item["stool_type_count"] for item in stool_type_counts
|
||||
}
|
||||
|
||||
stool_type_counts_sort = dict(
|
||||
sorted(stool_type_counts_dict.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
|
||||
highest_stool = next(iter(stool_type_counts_sort), None)
|
||||
return stool_type_counts_sort, highest_stool
|
||||
|
||||
def get(self, request):
|
||||
date_range = request.GET.get("date_range")
|
||||
start_date, end_date = date_utils.get_date_range(date_range)
|
||||
print(f"start date is {start_date}, end_date is {end_date}")
|
||||
|
||||
print(f"is dats exist {self.enough_records_exist(start_date, end_date)}")
|
||||
|
||||
if not self.enough_records_exist(start_date, end_date):
|
||||
return ApiResponse.error(
|
||||
message="No report is generated. Minimum Previous 7 days of records required."
|
||||
)
|
||||
|
||||
# Get top food to avoid
|
||||
food_avoid, food_counts, ingredient_counts, beverage_counts = (
|
||||
self.get_top_food_avoid(start_date, end_date)
|
||||
)
|
||||
|
||||
# Get symptoms frequency
|
||||
sorted_symptoms_frequency = self.get_symptoms_frequency(start_date, end_date)
|
||||
|
||||
# Get stool type counts
|
||||
stool_type_counts_sort, highest_stool = self.get_stool_type_counts(
|
||||
start_date, end_date
|
||||
)
|
||||
|
||||
nested_json = {
|
||||
"food_avoid": food_avoid,
|
||||
"same_food_avoid": {
|
||||
"food": food_counts,
|
||||
"ingredient": ingredient_counts,
|
||||
"beverage": beverage_counts,
|
||||
},
|
||||
"symptoms_frequency": sorted_symptoms_frequency,
|
||||
"highest_stool": highest_stool,
|
||||
"stool_type": stool_type_counts_sort,
|
||||
}
|
||||
print(f"nested_json data is {nested_json}")
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=nested_json)
|
||||
|
||||
98
module_activity/forms.py
Normal file
98
module_activity/forms.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from django import forms
|
||||
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_project import constants
|
||||
|
||||
from .models import FoodIngredintDataset, ChronicCondition, Intolerance, PastTreatment, Symptoms
|
||||
|
||||
|
||||
class FoodIngredintDatasetForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FoodIngredintDataset
|
||||
fields = ['food_name', 'ingredients']
|
||||
|
||||
class UploadFileForm(forms.Form):
|
||||
file = forms.FileField()
|
||||
|
||||
class IntoleranceForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Intolerance
|
||||
fields = ['name', 'duration']
|
||||
label = {
|
||||
"name": "intolerance",
|
||||
"duration": "For how long have you been experiencing this intolerance"
|
||||
}
|
||||
|
||||
def save(self, principal_id, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.principal = IAmPrincipal.objects.get(pk=principal_id)
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class SymptomsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Symptoms
|
||||
fields = ['name', 'duration']
|
||||
label = {
|
||||
"name": "Symptoms",
|
||||
"duration": "For how long have you been experiencing this intolerance"
|
||||
}
|
||||
|
||||
def save(self, principal_id, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.principal = IAmPrincipal.objects.get(pk=principal_id)
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
class SymptomsForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Symptoms
|
||||
fields = ['name', 'duration']
|
||||
label = {
|
||||
"name": "Symptoms",
|
||||
"duration": "For how long have you been experiencing this intolerance"
|
||||
}
|
||||
|
||||
def save(self, principal_id, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.principal = IAmPrincipal.objects.get(pk=principal_id)
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class PastTreatmentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PastTreatment
|
||||
fields = ['name', 'duration']
|
||||
label = {
|
||||
"name": "PastTreatment",
|
||||
"duration": "Treatment Date"
|
||||
}
|
||||
|
||||
def save(self, principal_id, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.principal = IAmPrincipal.objects.get(pk=principal_id)
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class ChronicConditionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ChronicCondition
|
||||
fields = ['name', 'duration']
|
||||
label = {
|
||||
"name": "Chronic Condition",
|
||||
"duration": "For how long have you been experiencing this disease"
|
||||
}
|
||||
|
||||
def save(self, principal_id, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.principal = IAmPrincipal.objects.get(pk=principal_id)
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.2 on 2024-02-29 12:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0006_mealrecord_meal_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='principalhealthdata',
|
||||
name='height',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='Enter your height in centimeters.', max_digits=6, null=True, verbose_name='Height (cm)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='principalhealthdata',
|
||||
name='weight',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='Enter your weight in kilograms.', max_digits=5, null=True, verbose_name='Weight (kg)'),
|
||||
),
|
||||
]
|
||||
18
module_activity/migrations/0008_bowel_stool_name.py
Normal file
18
module_activity/migrations/0008_bowel_stool_name.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-01 07:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0007_alter_principalhealthdata_height_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='stool_name',
|
||||
field=models.CharField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,141 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-03 10:16
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0008_bowel_stool_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='created_on',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bowel',
|
||||
name='modified_on',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealrecord',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealrecord',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealrecord',
|
||||
name='created_on',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealrecord',
|
||||
name='deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealrecord',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealrecord',
|
||||
name='modified_on',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='created_on',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='modified_on',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medication',
|
||||
name='active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medication',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medication',
|
||||
name='created_on',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medication',
|
||||
name='deleted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medication',
|
||||
name='modified_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='medication',
|
||||
name='modified_on',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-26 10:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0009_bowel_active_bowel_created_by_bowel_created_on_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='principalhealthdata',
|
||||
name='converted_height_cm',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='The equivalent height converted into centimeters.', max_digits=5, null=True, verbose_name='Converted Height'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='principalhealthdata',
|
||||
name='converted_weight_kg',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='The equivalent weight converted into kilograms.', max_digits=5, null=True, verbose_name='Converted Weight'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='principalhealthdata',
|
||||
name='height_unit',
|
||||
field=models.CharField(blank=True, choices=[('cm', 'cm'), ('ft+inch', 'ft+inch')], help_text='Select your height unit.', max_length=10, null=True, verbose_name='Height Unit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='principalhealthdata',
|
||||
name='weight_unit',
|
||||
field=models.CharField(blank=True, choices=[('kg', 'kg'), ('lbs', 'lbs')], help_text='Select your weight unit.', max_length=10, null=True, verbose_name='Weight Unit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='principalhealthdata',
|
||||
name='height',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='Enter your height in centimeters.', max_digits=6, null=True, verbose_name='Height'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='principalhealthdata',
|
||||
name='weight',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='Enter your weight in kilograms.', max_digits=5, null=True, verbose_name='Weight'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-27 07:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0010_principalhealthdata_converted_height_cm_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='mealsymptomrecord',
|
||||
name='related_meal',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='symptom_related_meal', to='module_activity.mealrecord'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-27 13:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0011_mealsymptomrecord_related_meal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pasttreatment',
|
||||
name='is_recurring',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pasttreatment',
|
||||
name='treatment_frequency',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
24
module_activity/migrations/0013_foodingredintdataset.py
Normal file
24
module_activity/migrations/0013_foodingredintdataset.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-28 07:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0012_pasttreatment_is_recurring_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='FoodIngredintDataset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('food_name', models.CharField(max_length=100)),
|
||||
('ingredients', models.JSONField(default=list)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'food_ingredient_dataset',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-28 13:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0013_foodingredintdataset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='foodingredientrecord',
|
||||
name='from_dataset',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-28 14:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0014_foodingredientrecord_from_dataset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='beveragerecord',
|
||||
name='glass_count',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='beveragerecord',
|
||||
name='glass_type',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.2 on 2024-04-01 06:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_activity', '0015_remove_beveragerecord_glass_count_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='principalhealthdata',
|
||||
name='height_unit',
|
||||
field=models.CharField(blank=True, choices=[('cm', 'cm'), ('ft+inch', 'ft+inch')], default='cm', help_text='Select your height unit.', max_length=10, null=True, verbose_name='Height Unit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='principalhealthdata',
|
||||
name='weight_unit',
|
||||
field=models.CharField(blank=True, choices=[('kg', 'kg'), ('lbs', 'lbs')], default='lbs', help_text='Select your weight unit.', max_length=10, null=True, verbose_name='Weight Unit'),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,23 @@
|
||||
import json
|
||||
from django.db import models
|
||||
|
||||
from module_iam.models import BaseModel, IAmPrincipal
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class FoodIngredintDataset(models.Model):
|
||||
food_name = models.CharField(max_length=100)
|
||||
ingredients = models.JSONField(default=list)
|
||||
|
||||
class Meta:
|
||||
db_table = "food_ingredient_dataset"
|
||||
|
||||
def set_ingredients(self, x):
|
||||
self.ingredients = json.dumps(x)
|
||||
|
||||
def get_ingredients(self):
|
||||
return json.loads(self.ingredients)
|
||||
|
||||
|
||||
class PrincipalHealthData(BaseModel):
|
||||
principal = models.OneToOneField(
|
||||
IAmPrincipal,
|
||||
@@ -49,21 +64,67 @@ class PrincipalHealthData(BaseModel):
|
||||
weight = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
default=0.0,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Weight (kg)",
|
||||
verbose_name="Weight",
|
||||
help_text="Enter your weight in kilograms.",
|
||||
)
|
||||
|
||||
weight_unit = models.CharField(
|
||||
max_length=10,
|
||||
choices=[
|
||||
('kg', 'kg'),
|
||||
('lbs', 'lbs'),
|
||||
],
|
||||
default="lbs",
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Weight Unit",
|
||||
help_text="Select your weight unit.",
|
||||
)
|
||||
|
||||
converted_weight_kg = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Converted Weight",
|
||||
help_text="The equivalent weight converted into kilograms.",
|
||||
)
|
||||
|
||||
height = models.DecimalField(
|
||||
max_digits=6,
|
||||
decimal_places=2,
|
||||
default=0.0,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Height (cm)",
|
||||
verbose_name="Height",
|
||||
help_text="Enter your height in centimeters.",
|
||||
)
|
||||
|
||||
height_unit = models.CharField(
|
||||
max_length=10,
|
||||
choices=[
|
||||
('cm', 'cm'),
|
||||
('ft+inch', 'ft+inch'),
|
||||
],
|
||||
default='cm',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Height Unit",
|
||||
help_text="Select your height unit.",
|
||||
)
|
||||
|
||||
converted_height_cm = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Converted Height",
|
||||
help_text="The equivalent height converted into centimeters.",
|
||||
)
|
||||
|
||||
# Eat frequency (choices if applicable)
|
||||
eat_frequency = models.CharField(
|
||||
max_length=255,
|
||||
@@ -78,6 +139,26 @@ class PrincipalHealthData(BaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"Health Data for {self.principal}"
|
||||
|
||||
@property
|
||||
def converted_height_in_cm(self):
|
||||
if self.height_unit == 'ft+inch':
|
||||
feet, inches = divmod(float(self.height) * 10, 12)
|
||||
return round(feet * 30.48 + inches * 2.54, 2) # Convert feet to cm and inches to cm
|
||||
return None
|
||||
|
||||
@property
|
||||
def converted_weight_in_kg(self):
|
||||
if self.weight_unit == 'lbs':
|
||||
return round(float(self.weight) * 0.453592, 2) # Convert pounds to kg
|
||||
return None
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.height_unit == 'ft+inch':
|
||||
self.converted_height_cm = self.converted_height_in_cm # Update converted height
|
||||
if self.weight_unit == 'lbs':
|
||||
self.converted_weight_kg = self.converted_weight_in_kg # Update converted weight
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Intolerance(BaseModel):
|
||||
@@ -123,6 +204,8 @@ class PastTreatment(BaseModel):
|
||||
)
|
||||
name = models.CharField(max_length=255, blank=True, null=True)
|
||||
duration = models.DateField()
|
||||
is_recurring = models.BooleanField(default=False)
|
||||
treatment_frequency = models.TextField(blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "past_treatment"
|
||||
@@ -150,6 +233,7 @@ class ChronicCondition(BaseModel):
|
||||
|
||||
class FoodIngredientRecord(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
from_dataset = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
db_table = "food_ingredient_record"
|
||||
@@ -163,15 +247,13 @@ class FoodRecord(models.Model):
|
||||
|
||||
class BeverageRecord(models.Model):
|
||||
beverage_type = models.CharField(max_length=100)
|
||||
glass_type = models.CharField(max_length=100)
|
||||
glass_count = models.IntegerField()
|
||||
quantity = models.IntegerField()
|
||||
quantity_measure = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
db_table = "beverage_record"
|
||||
|
||||
class MealRecord(models.Model):
|
||||
class MealRecord(BaseModel):
|
||||
principal = models.ForeignKey(
|
||||
IAmPrincipal, related_name="meal_principal", on_delete=models.CASCADE
|
||||
)
|
||||
@@ -217,7 +299,7 @@ class Medicine(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.name} Medicine"
|
||||
|
||||
class Medication(models.Model):
|
||||
class Medication(BaseModel):
|
||||
principal = models.ForeignKey(
|
||||
IAmPrincipal, related_name="medication_principal", on_delete=models.CASCADE
|
||||
)
|
||||
@@ -240,13 +322,14 @@ class MedicationMedicine(models.Model):
|
||||
db_table = "medication_medicine"
|
||||
|
||||
|
||||
class Bowel(models.Model):
|
||||
class Bowel(BaseModel):
|
||||
principal = models.ForeignKey(
|
||||
IAmPrincipal, related_name="bowel_principal", on_delete=models.CASCADE
|
||||
)
|
||||
date = models.DateField()
|
||||
time = models.TimeField()
|
||||
stool_type = models.CharField(max_length=100, blank=True, null=True)
|
||||
stool_name = models.CharField(max_length=100, blank=True, null=True)
|
||||
duration = models.DurationField(blank=True, null=True)
|
||||
completeness_of_evacuation = models.CharField(max_length=100, blank=True, null=True)
|
||||
urgency = models.CharField(max_length=100, blank=True, null=True)
|
||||
@@ -272,8 +355,9 @@ class SymptomTypeAfterMeal(models.Model):
|
||||
class Meta:
|
||||
db_table = "symptom_type_after_meal"
|
||||
|
||||
class MealSymptomRecord(models.Model):
|
||||
class MealSymptomRecord(BaseModel):
|
||||
principal = models.ForeignKey(IAmPrincipal, related_name="meal_symptom_principal", on_delete=models.CASCADE)
|
||||
related_meal = models.ForeignKey(MealRecord, related_name="symptom_related_meal", on_delete=models.SET_NULL, null=True)
|
||||
date = models.DateField()
|
||||
time = models.TimeField()
|
||||
symptoms_description = models.TextField(blank=True, null=True)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "module_activity"
|
||||
@@ -6,21 +8,49 @@ app_name = "module_activity"
|
||||
urlpatterns = [
|
||||
|
||||
|
||||
path('populate/food/dataset/', views.PopulateFoodIngredientView.as_view(), name='populate_food'),
|
||||
|
||||
path('intolerance/<int:principal_id>/', views.IntoleranceView.as_view(), name='intolerance'),
|
||||
path('intolerance/<int:principal_id>/add/', views.CreateOrUpdateIntoleranceView.as_view(), name='intolerance_add'),
|
||||
path('intolerance/<int:principal_id>/edit/<int:pk>', views.CreateOrUpdateIntoleranceView.as_view(), name='intolerance_edit'),
|
||||
path('intolerance/list/<int:principal_id>/', views.IntoleranceListJson.as_view(), name='intolerance_list'),
|
||||
path('intolerance/action/', views.IntoleranceActionView.as_view(), name='intolerance_action'),
|
||||
path('intolerance/archive/list/<int:principal_id>/', views.IntoleranceArchiveView.as_view(), name='intolerance_archive'),
|
||||
|
||||
|
||||
path('symptoms/<int:principal_id>/', views.SymptomsView.as_view(), name='symptoms'),
|
||||
path('symptoms/<int:principal_id>/add/', views.CreateOrUpdateSymptomsView.as_view(), name='symptoms_add'),
|
||||
path('symptoms/<int:principal_id>/edit/<int:pk>', views.CreateOrUpdateSymptomsView.as_view(), name='symptoms_edit'),
|
||||
path('symptoms/list/<int:principal_id>/', views.SymptomsListJson.as_view(), name='symptoms_list'),
|
||||
path('symptoms/action/', views.SymptomsActionView.as_view(), name='symptoms_action'),
|
||||
path('symptoms/archive/list/<int:principal_id>/', views.SymptomsArchiveView.as_view(), name='symptoms_archive'),
|
||||
|
||||
path('past_treatment/<int:principal_id>/', views.PastTreatmentView.as_view(), name='past_treatment'),
|
||||
path('past_treatment/<int:principal_id>/add/', views.CreateOrUpdatePastTreatmentView.as_view(), name='past_treatment_add'),
|
||||
path('past_treatment/<int:principal_id>/edit/<int:pk>', views.CreateOrUpdatePastTreatmentView.as_view(), name='past_treatment_edit'),
|
||||
path('past_treatment/list/<int:principal_id>/', views.PastTreatmentListJson.as_view(), name='past_treatment_list'),
|
||||
path('past_treatment/action/', views.PastTreatmentActionView.as_view(), name='past_treatment_action'),
|
||||
path('past_treatment/archive/list/<int:principal_id>/', views.PastTreatmentArchiveView.as_view(), name='past_treatment_archive'),
|
||||
|
||||
path('chronic_condition/<int:principal_id>/', views.ChronicConditionView.as_view(), name='chronic_condition'),
|
||||
path('chronic_condition/<int:principal_id>/add/', views.CreateOrUpdateChronicConditionView.as_view(), name='chronic_condition_add'),
|
||||
path('chronic_condition/<int:principal_id>/edit/<int:pk>', views.CreateOrUpdateChronicConditionView.as_view(), name='chronic_condition_edit'),
|
||||
path('chronic_condition/list/<int:principal_id>/', views.ChronicConditionListJson.as_view(), name='chronic_condition_list'),
|
||||
path('chronic_condition/action/', views.ChronicConditionActionView.as_view(), name='chronic_condition_action'),
|
||||
path('chronic_condition/archive/list/<int:principal_id>/', views.ChronicConditionArchiveView.as_view(), name='chronic_condition_archive'),
|
||||
|
||||
path('user/meal/<int:principal_id>/', views.MealView.as_view(), name='meal_view'),
|
||||
path('user/meal/list/<int:principal_id>/', views.MealListJsonView.as_view(), name='meal_list'),
|
||||
|
||||
path('user/medication/<int:principal_id>/', views.MedicationView.as_view(), name='medication_view'),
|
||||
path('user/medication/list/<int:principal_id>/', views.MedicationListJsonView.as_view(), name='medication_list'),
|
||||
|
||||
path('user/bowel/<int:principal_id>/', views.BowelView.as_view(), name='bowel_view'),
|
||||
path('user/bowel/list/<int:principal_id>/', views.BowelListJsonView.as_view(), name='bowel_list'),
|
||||
|
||||
path('user/meal-symptoms/<int:principal_id>/', views.MealSymptomsView.as_view(), name='meal_symptoms_view'),
|
||||
path('user/meal-symptoms/list/<int:principal_id>/', views.MealSymptomsListJsonView.as_view(), name='meal_symptoms_list'),
|
||||
|
||||
|
||||
path('user_activity/<int:principal_id>/', views.UserActivityRecordView.as_view(), name='activity_list'),
|
||||
path('meal_detail/<int:pk>/', views.MealDetialView.as_view(), name='meal_detail'),
|
||||
@@ -28,4 +58,10 @@ urlpatterns = [
|
||||
path('bowel_detail/<int:pk>/', views.BowelDetailView.as_view(), name='bowel_detail'),
|
||||
path('meal_symptom_detail/<int:pk>/', views.MealSymptomDetailView.as_view(), name='meal_symptom_detail'),
|
||||
|
||||
|
||||
path('daily_report/chart/count/', views.ReportChartView.as_view(), name='chart_data'),
|
||||
|
||||
path('report/<int:principal_id>/', views.ReportDataView.as_view(), name='report_data'),
|
||||
|
||||
|
||||
]
|
||||
|
||||
@@ -1,154 +1,490 @@
|
||||
import csv
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from datetime import datetime
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
import pandas as pd
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Count, Prefetch, Q
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from django.db.models import Q, Prefetch
|
||||
from .models import Intolerance, Symptoms, ChronicCondition, PastTreatment, MealRecord, Bowel, MealSymptomRecord, Medication
|
||||
from django_datatables_view.base_datatable_view import BaseDatatableView
|
||||
|
||||
from module_iam import iam_constant, permission
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_project import constants
|
||||
from module_project import constants, date_utils
|
||||
from module_project.utils import JsonResponseUtil
|
||||
from django.http import JsonResponse
|
||||
|
||||
from .forms import (ChronicConditionForm, IntoleranceForm, PastTreatmentForm,
|
||||
SymptomsForm, UploadFileForm)
|
||||
from .models import (BeverageRecord, Bowel, ChronicCondition, FoodIngredientRecord, FoodIngredintDataset, FoodRecord,
|
||||
Intolerance, MealRecord, MealSymptomRecord, Medication,
|
||||
PastTreatment, Symptoms)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseView(generic.TemplateView):
|
||||
page_name = None
|
||||
resource = None
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
action = None
|
||||
template_name = None
|
||||
model = Intolerance
|
||||
model = None
|
||||
context_objext_name = "obj"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
context["principal_id"] = self.kwargs.get('principal_id')
|
||||
context["principal_id"] = get_object_or_404(IAmPrincipal, id=self.kwargs.get("principal_id"))
|
||||
return context
|
||||
|
||||
|
||||
class BaseCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
page_title = None
|
||||
model = None
|
||||
template_name = "module_activity/base_add.html"
|
||||
form_class = None
|
||||
success_url = None
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"page_title": self.page_title,
|
||||
"principal_id": self.kwargs.get("principal_id"),
|
||||
"operation": "Add" if not self.object else "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
principal_id = kwargs.get("principal_id")
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
form.save(principal_id=principal_id)
|
||||
messages.success(self.request, self.get_success_message())
|
||||
success_url = reverse_lazy(
|
||||
self.success_url, kwargs={"principal_id": principal_id}
|
||||
)
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
class BaseListJson(BaseDatatableView):
|
||||
model = Intolerance
|
||||
columns = ["id", "name", "duration", "active", "deleted"]
|
||||
order_columns = ["id", "name", "duration", "active", "deleted"]
|
||||
FILTER_ICONTAINS = "icontains"
|
||||
|
||||
def get_filter_method(self):
|
||||
"""Returns preferred filter method"""
|
||||
return self.FILTER_ICONTAINS
|
||||
|
||||
def get_initial_queryset(self):
|
||||
principal_id = self.kwargs.get('principal_id')
|
||||
deleted_flag = self.request.GET.get('deleted_flag', None)
|
||||
principal_id = self.kwargs.get("principal_id")
|
||||
deleted_flag = self.request.GET.get("deleted_flag", None)
|
||||
|
||||
if deleted_flag == 'true':
|
||||
# Show only deleted records
|
||||
return self.model.objects.filter(principal=principal_id, deleted=True)
|
||||
else:
|
||||
# Show all records except deleted ones
|
||||
return self.model.objects.filter(principal=principal_id, deleted=False)
|
||||
return self.model.objects.filter(principal=principal_id, deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
print(f"request is {self.request.GET}")
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
print(f"order column is {order_column}")
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=search_value) |
|
||||
Q(duration__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class BaseActionView(generic.View):
|
||||
model = Intolerance
|
||||
model = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
|
||||
ids = request.POST.getlist('ids[]') # List of user IDs to perform action on
|
||||
active = request.POST.get('active')
|
||||
|
||||
if self.model is None:
|
||||
raise NotImplementedError(
|
||||
"Subclasses of BaseActionView must define a 'model' attribute."
|
||||
)
|
||||
|
||||
action = request.POST.get("action") # 'archive', 'active', or 'unarchive'
|
||||
ids = request.POST.getlist("ids[]") # List of user IDs to perform action on
|
||||
active = request.POST.get("active")
|
||||
print(f"arhive action {action} and id is {ids} and active data is {active}")
|
||||
if action == 'archive':
|
||||
if action == "archive":
|
||||
# Update 'deleted' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=True, active=False)
|
||||
message = 'Record archived successfully.'
|
||||
elif action == 'active':
|
||||
message = "Record archived successfully."
|
||||
elif action == "active":
|
||||
# Update 'active' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(active=active.capitalize())
|
||||
message = 'Record activated successfully.'
|
||||
elif action == 'unarchive':
|
||||
message = "Record activated successfully."
|
||||
elif action == "unarchive":
|
||||
# Update 'deleted' field to False for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=False)
|
||||
message = 'Record unarchived successfully.'
|
||||
message = "Record unarchived successfully."
|
||||
else:
|
||||
return JsonResponseUtil.error(message="Invalid Action")
|
||||
|
||||
return JsonResponseUtil.success(message=message)
|
||||
|
||||
|
||||
class BaseArchiveView(generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
template_name = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["principal_id"] = kwargs.get("principal_id")
|
||||
data["page_name"] = self.page_name
|
||||
return data
|
||||
|
||||
|
||||
class PopulateFoodIngredientView(permission.ResourcePermissionRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
resource = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
template_name = "module_activity/food_ingredient_form.html"
|
||||
model = FoodIngredintDataset
|
||||
form_class = UploadFileForm
|
||||
success_url = reverse_lazy("module_iam:dashboard")
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
# Add page_name and operation to the context
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Add",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
form = self.form_class()
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
print("Request data: ", request.POST)
|
||||
|
||||
form = self.form_class(request.POST, request.FILES)
|
||||
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
uploaded_file = request.FILES['file']
|
||||
# decoded_file = uploaded_file.read().decode('utf-8', errors='ignore').splitlines()
|
||||
# csv_reader = csv.reader(decoded_file)
|
||||
|
||||
df = pd.read_excel(uploaded_file)
|
||||
|
||||
# this loop is for insert the food dataset record in table
|
||||
|
||||
for index, row in df.iterrows():
|
||||
food_name = row['Food'].strip().capitalize()
|
||||
ingredients_str = row['Ingredients'].strip()
|
||||
ingredients = [ingredient.strip().capitalize() for ingredient in ingredients_str.split(',')]
|
||||
|
||||
if self.model.objects.filter(food_name=food_name).exists():
|
||||
continue
|
||||
|
||||
print(f"{food_name} : {ingredients}")
|
||||
food = self.model(food_name=food_name, ingredients=ingredients)
|
||||
food.save()
|
||||
|
||||
# this loop is for updating the existing record in the database
|
||||
|
||||
# for index, row in df.iterrows():
|
||||
# food_name = row['Food'].strip().capitalize()
|
||||
# ingredients_str = row['Ingredients'].strip()
|
||||
# ingredients = [ingredient.strip().capitalize() for ingredient in ingredients_str.split(',')]
|
||||
|
||||
# exist_food = self.model.objects.filter(food_name=food_name)
|
||||
|
||||
# if exist_food.exists():
|
||||
# exist_food.update(ingredients=ingredients)
|
||||
|
||||
# print(f"{food_name} : {ingredients}")
|
||||
|
||||
messages.success(self.request, constants.RECORD_CREATED)
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class IntoleranceView(BaseView):
|
||||
model = Intolerance
|
||||
template_name = "module_activity/intolerance_list.html"
|
||||
|
||||
|
||||
class CreateOrUpdateIntoleranceView(BaseCreateOrUpdateView):
|
||||
model = Intolerance
|
||||
page_title = "Intolerance"
|
||||
form_class = IntoleranceForm
|
||||
success_url = "module_activity:intolerance"
|
||||
|
||||
|
||||
class IntoleranceListJson(BaseListJson):
|
||||
model = Intolerance
|
||||
|
||||
|
||||
class IntoleranceActionView(BaseActionView):
|
||||
model = Intolerance
|
||||
|
||||
|
||||
class IntoleranceArchiveView(generic.TemplateView):
|
||||
template_name = "module_activity/intolerance_archive_list.html"
|
||||
|
||||
|
||||
class SymptomsView(BaseView):
|
||||
model = Symptoms
|
||||
template_name = "module_activity/symptoms_list.html"
|
||||
|
||||
|
||||
class CreateOrUpdateSymptomsView(BaseCreateOrUpdateView):
|
||||
model = Symptoms
|
||||
page_title = "Symptoms"
|
||||
form_class = SymptomsForm
|
||||
success_url = "module_activity:symptoms"
|
||||
|
||||
|
||||
class SymptomsListJson(BaseListJson):
|
||||
model = Symptoms
|
||||
|
||||
|
||||
class SymptomsActionView(BaseActionView):
|
||||
model = Symptoms
|
||||
|
||||
|
||||
class SymptomsArchiveView(generic.TemplateView):
|
||||
template_name = "module_activity/symptoms_archive_list.html"
|
||||
|
||||
|
||||
class PastTreatmentView(BaseView):
|
||||
model = PastTreatment
|
||||
template_name = "module_activity/past_treatment_list.html"
|
||||
|
||||
|
||||
class CreateOrUpdatePastTreatmentView(BaseCreateOrUpdateView):
|
||||
model = PastTreatment
|
||||
page_title = "Past Treatment"
|
||||
form_class = PastTreatmentForm
|
||||
success_url = "module_activity:past_treatment"
|
||||
|
||||
|
||||
class PastTreatmentListJson(BaseListJson):
|
||||
model = PastTreatment
|
||||
|
||||
|
||||
class PastTreatmentActionView(BaseActionView):
|
||||
model = PastTreatment
|
||||
|
||||
|
||||
class PastTreatmentArchiveView(generic.TemplateView):
|
||||
template_name = "module_activity/past_treatment_archive_list.html"
|
||||
|
||||
|
||||
class ChronicConditionView(BaseView):
|
||||
model = ChronicCondition
|
||||
template_name = "module_activity/chronic_conditon_list.html"
|
||||
|
||||
|
||||
class CreateOrUpdateChronicConditionView(BaseCreateOrUpdateView):
|
||||
model = ChronicCondition
|
||||
page_title = "Chronic Conditon/Disease"
|
||||
form_class = ChronicConditionForm
|
||||
success_url = "module_activity:chronic_condition"
|
||||
|
||||
|
||||
class ChronicConditionListJson(BaseListJson):
|
||||
model = ChronicCondition
|
||||
|
||||
|
||||
class ChronicConditionActionView(BaseActionView):
|
||||
model = ChronicCondition
|
||||
|
||||
|
||||
class ChronicConditionArchiveView(generic.TemplateView):
|
||||
template_name = "module_activity/chronic_condition_archive_list.html"
|
||||
|
||||
|
||||
class MealView(BaseView):
|
||||
template_name = "module_activity/meal_list.html"
|
||||
|
||||
|
||||
class MealListJsonView(BaseDatatableView):
|
||||
model = MealRecord
|
||||
columns = ["id", "date", "time", "meal_type"]
|
||||
order_columns = ["id", "date", "time", "meal_type"]
|
||||
FILTER_ICONTAINS = "icontains"
|
||||
|
||||
def get_filter_method(self):
|
||||
"""Returns preferred filter method"""
|
||||
return self.FILTER_ICONTAINS
|
||||
|
||||
def get_initial_queryset(self):
|
||||
principal_id = self.kwargs.get("principal_id")
|
||||
deleted_flag = self.request.GET.get("deleted_flag", None)
|
||||
return self.model.objects.filter(principal=principal_id, deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
qs = super().ordering(qs)
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class MedicationView(BaseView):
|
||||
template_name = "module_activity/medication_list.html"
|
||||
|
||||
|
||||
class MedicationListJsonView(BaseDatatableView):
|
||||
model = Medication
|
||||
columns = ["id", "date", "time"]
|
||||
order_columns = ["id", "date", "time"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
principal_id = self.kwargs.get("principal_id")
|
||||
deleted_flag = self.request.GET.get("deleted_flag", None)
|
||||
return self.model.objects.filter(principal=principal_id, deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class BowelView(BaseView):
|
||||
template_name = "module_activity/bowel_list.html"
|
||||
|
||||
|
||||
class BowelListJsonView(BaseDatatableView):
|
||||
model = Bowel
|
||||
columns = ["id", "date", "time", "stool_type"]
|
||||
order_columns = ["id", "date", "time", "stool_type"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
principal_id = self.kwargs.get("principal_id")
|
||||
deleted_flag = self.request.GET.get("deleted_flag", None)
|
||||
return self.model.objects.filter(principal=principal_id, deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class MealSymptomsView(BaseView):
|
||||
template_name = "module_activity/meal_symptoms_list.html"
|
||||
|
||||
|
||||
class MealSymptomsListJsonView(BaseDatatableView):
|
||||
model = MealSymptomRecord
|
||||
columns = ["id", "date", "time"]
|
||||
order_columns = ["id", "date", "time"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
principal_id = self.kwargs.get("principal_id")
|
||||
deleted_flag = self.request.GET.get("deleted_flag", None)
|
||||
return self.model.objects.filter(principal=principal_id, deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class UserActivityRecordView(generic.View):
|
||||
def serialize_record(self, record):
|
||||
time_obj = datetime.strptime(str(record.time), '%H:%M:%S')
|
||||
time_obj = datetime.strptime(str(record.time), "%H:%M:%S")
|
||||
return {
|
||||
"id": record.id,
|
||||
"date": record.date,
|
||||
"time": time_obj.strftime('%I:%M %p'),
|
||||
"time": time_obj.strftime("%I:%M %p"),
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
try:
|
||||
principal_id = self.kwargs.get('principal_id')
|
||||
date = request.GET.get("date")
|
||||
print(f"principal_id is {principal_id} data is {date} and type is {type(date)}")
|
||||
if not date:
|
||||
return JsonResponseUtil.error(message="Date parameter is missing")
|
||||
principal_id = self.kwargs.get("principal_id")
|
||||
date_range = request.GET.get("date_range")
|
||||
|
||||
try:
|
||||
date_obj = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
return JsonResponseUtil.error(message="Invalid date format")
|
||||
if not date_range:
|
||||
return JsonResponseUtil.error(message="Date range parameter is missing")
|
||||
|
||||
start_date, end_date = date_utils.get_date_range(date_range)
|
||||
|
||||
# Retrieve data from different models
|
||||
meal_records = MealRecord.objects.filter(principal=principal_id, date=date_obj)
|
||||
medication_records = Medication.objects.filter(principal=principal_id, date=date_obj)
|
||||
bowel_records = Bowel.objects.filter(principal=principal_id, date=date_obj)
|
||||
meal_symptom_records = MealSymptomRecord.objects.filter(principal=principal_id, date=date_obj)
|
||||
meal_records = MealRecord.objects.filter(
|
||||
principal=principal_id, date__range=(start_date, end_date)
|
||||
)
|
||||
medication_records = Medication.objects.filter(
|
||||
principal=principal_id, date__range=(start_date, end_date)
|
||||
)
|
||||
bowel_records = Bowel.objects.filter(principal=principal_id, date__range=(start_date, end_date))
|
||||
meal_symptom_records = MealSymptomRecord.objects.filter(
|
||||
principal=principal_id, date__range=(start_date, end_date)
|
||||
)
|
||||
print(f"==================meal record {meal_records}")
|
||||
# Prepare combined results
|
||||
data = []
|
||||
@@ -177,71 +513,281 @@ class UserActivityRecordView(generic.View):
|
||||
except Exception as e:
|
||||
return JsonResponseUtil.error(message="Something went wrong", errors=str(e))
|
||||
|
||||
class MealDetialView(generic.TemplateView):
|
||||
|
||||
class MealDetialView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
template_name = "module_activity/meal_detail.html"
|
||||
model = MealRecord
|
||||
|
||||
def get_record(self):
|
||||
id = self.kwargs.get('pk')
|
||||
id = self.kwargs.get("pk")
|
||||
meal_record = get_object_or_404(
|
||||
self.model.objects.prefetch_related(
|
||||
'food_records', 'beverage_records', 'food_ingredient_records'
|
||||
"food_records", "beverage_records", "food_ingredient_records"
|
||||
),
|
||||
id=id
|
||||
id=id,
|
||||
)
|
||||
return meal_record
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['obj'] = self.get_record()
|
||||
context["obj"] = self.get_record()
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class MedicationDetailView(generic.TemplateView):
|
||||
class MedicationDetailView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
template_name = "module_activity/medication_detail.html"
|
||||
model = Medication
|
||||
|
||||
def get_record(self):
|
||||
id = self.kwargs.get('pk')
|
||||
obj = get_object_or_404(
|
||||
self.model.objects.prefetch_related('medicines'),
|
||||
id=id
|
||||
)
|
||||
id = self.kwargs.get("pk")
|
||||
obj = get_object_or_404(self.model.objects.prefetch_related("medicines"), id=id)
|
||||
return obj
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['obj'] = self.get_record()
|
||||
context["obj"] = self.get_record()
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class BowelDetailView(generic.TemplateView):
|
||||
class BowelDetailView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
template_name = "module_activity/bowel_detail.html"
|
||||
model = Bowel
|
||||
|
||||
def get_record(self):
|
||||
id = self.kwargs.get('pk')
|
||||
id = self.kwargs.get("pk")
|
||||
obj = get_object_or_404(self.model, id=id)
|
||||
print(f"obj data of bowel is {obj}")
|
||||
return obj
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['obj'] = self.get_record()
|
||||
context["obj"] = self.get_record()
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class MealSymptomDetailView(generic.TemplateView):
|
||||
|
||||
class MealSymptomDetailView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
template_name = "module_activity/meal_symptom_details.html"
|
||||
model = MealSymptomRecord
|
||||
|
||||
def get_record(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
pk = self.kwargs.get("pk")
|
||||
obj = get_object_or_404(
|
||||
MealSymptomRecord.objects.prefetch_related('symptoms_before_meal', 'symptoms_after_meal'),
|
||||
id=pk
|
||||
MealSymptomRecord.objects.prefetch_related(
|
||||
"symptoms_before_meal", "symptoms_after_meal"
|
||||
),
|
||||
id=pk,
|
||||
)
|
||||
return obj
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['obj'] = self.get_record()
|
||||
return context
|
||||
context["obj"] = self.get_record()
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class ReportChartView(generic.View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
current_year = int(self.request.GET.get("year"))
|
||||
|
||||
monthly_counts = {
|
||||
'Meal': [0] * 12,
|
||||
'Medication': [0] * 12,
|
||||
'Symptoms': [0] * 12,
|
||||
'Bowel': [0] * 12,
|
||||
}
|
||||
|
||||
for month in range(1, 13):
|
||||
start_date = datetime(current_year, month, 1)
|
||||
end_date = datetime(current_year, month + 1, 1) if month < 12 else datetime(current_year + 1, 1, 1)
|
||||
|
||||
monthly_counts['Meal'][month - 1] = MealRecord.objects.filter(date__range=(start_date, end_date)).count()
|
||||
monthly_counts['Medication'][month - 1] = Medication.objects.filter(date__range=(start_date, end_date)).count()
|
||||
monthly_counts['Symptoms'][month - 1] = MealSymptomRecord.objects.filter(date__range=(start_date, end_date)).count()
|
||||
monthly_counts['Bowel'][month - 1] = Bowel.objects.filter(date__range=(start_date, end_date)).count()
|
||||
|
||||
print(f"===========================================================data is {monthly_counts}")
|
||||
|
||||
return JsonResponseUtil.success(message=constants.SUCCESS, data=monthly_counts)
|
||||
|
||||
|
||||
class ReportDataView(generic.View):
|
||||
model = MealRecord
|
||||
|
||||
def get_user(self, *args, **kwargs):
|
||||
user = IAmPrincipal.objects.filter(id=self.kwargs.get("principal_id")).first()
|
||||
print(f"user is {user}")
|
||||
return user
|
||||
|
||||
def enough_records_exist(self, start_date, end_date):
|
||||
|
||||
min_days_required = 7
|
||||
current_date = start_date
|
||||
count = 0
|
||||
|
||||
obj = self.model.objects.filter(principal=self.get_user())
|
||||
|
||||
while current_date <= end_date:
|
||||
if obj.filter(date=current_date).exists():
|
||||
count += 1
|
||||
if count >= min_days_required:
|
||||
return True
|
||||
else:
|
||||
count = 0 # Reset count if record is missing for any day
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return False
|
||||
|
||||
def get_top_food_avoid(self, start_date, end_date):
|
||||
"""Get the top food to avoid."""
|
||||
food_counts = defaultdict(int)
|
||||
ingredient_counts = defaultdict(int)
|
||||
beverage_counts = defaultdict(int)
|
||||
|
||||
# symptom_records = MealSymptomRecord.objects.filter(
|
||||
# principal=self.get_user(), date__range=(start_date, end_date)
|
||||
# )
|
||||
|
||||
# for symptom_record in symptom_records:
|
||||
# closest_meal = (
|
||||
# MealRecord.objects.filter(
|
||||
# principal=symptom_record.principal, date__lte=symptom_record.date
|
||||
# )
|
||||
# .order_by("-date", "-time")
|
||||
# .first()
|
||||
# )
|
||||
# if closest_meal:
|
||||
# for food_record in closest_meal.food_records.all():
|
||||
# food_counts[food_record.name] += 1
|
||||
# for ingredient_record in closest_meal.food_ingredient_records.all():
|
||||
# ingredient_counts[ingredient_record.name] += 1
|
||||
# for beverage_record in closest_meal.beverage_records.all():
|
||||
# beverage_counts[beverage_record.beverage_type] += 1
|
||||
|
||||
# Fetch symptom records with related meal records and related food/ingredient/beverage records
|
||||
symptom_records = MealSymptomRecord.objects.prefetch_related(
|
||||
Prefetch('related_meal__food_records', queryset=FoodRecord.objects.all()),
|
||||
Prefetch('related_meal__food_ingredient_records', queryset=FoodIngredientRecord.objects.all()),
|
||||
Prefetch('related_meal__beverage_records', queryset=BeverageRecord.objects.all())
|
||||
).filter(
|
||||
principal=self.get_user(), date__range=(start_date, end_date)
|
||||
)
|
||||
|
||||
# Loop through symptom records and count food, ingredient, and beverage occurrences for each related meal record
|
||||
for symptom_record in symptom_records:
|
||||
closest_meal = symptom_record.related_meal
|
||||
if closest_meal:
|
||||
for food_record in closest_meal.food_records.all():
|
||||
food_counts[food_record.name] += 1
|
||||
for ingredient_record in closest_meal.food_ingredient_records.all():
|
||||
ingredient_counts[ingredient_record.name] += 1
|
||||
for beverage_record in closest_meal.beverage_records.all():
|
||||
beverage_counts[beverage_record.beverage_type] += 1
|
||||
|
||||
# Sort the dictionaries by their values in descending order and getting only top 3 record
|
||||
food_counts = dict(
|
||||
sorted(food_counts.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
ingredient_counts = dict(
|
||||
sorted(ingredient_counts.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
beverage_counts = dict(
|
||||
sorted(beverage_counts.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
|
||||
food_avoid = next(iter(food_counts), None)
|
||||
return food_avoid, food_counts, ingredient_counts, beverage_counts
|
||||
|
||||
def get_symptoms_frequency(self, start_date, end_date):
|
||||
"""Get the frequency of symptoms."""
|
||||
symptom_records = MealSymptomRecord.objects.filter(
|
||||
principal=self.get_user(), date__range=(start_date, end_date)
|
||||
).annotate(
|
||||
before_meal_count=Count("symptoms_before_meal"),
|
||||
after_meal_count=Count("symptoms_after_meal"),
|
||||
)
|
||||
symptoms_frequency = defaultdict(int)
|
||||
|
||||
for record in symptom_records:
|
||||
for symptom in record.symptoms_before_meal.all():
|
||||
symptoms_frequency[symptom.name] += record.before_meal_count
|
||||
for symptom in record.symptoms_after_meal.all():
|
||||
symptoms_frequency[symptom.name] += record.after_meal_count
|
||||
|
||||
sorted_symptoms_frequency = dict(
|
||||
sorted(symptoms_frequency.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
|
||||
return sorted_symptoms_frequency
|
||||
|
||||
def get_stool_type_counts(self, start_date, end_date):
|
||||
"""Get the count of stool types."""
|
||||
stool_type_counts = (
|
||||
Bowel.objects.filter(
|
||||
principal=self.get_user(), date__range=(start_date, end_date)
|
||||
)
|
||||
.values("stool_type")
|
||||
.annotate(stool_type_count=Count("stool_type"))
|
||||
)
|
||||
stool_type_counts_dict = {
|
||||
item["stool_type"]: item["stool_type_count"] for item in stool_type_counts
|
||||
}
|
||||
|
||||
stool_type_counts_sort = dict(
|
||||
sorted(stool_type_counts_dict.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||
)
|
||||
|
||||
highest_stool = next(iter(stool_type_counts_sort), None)
|
||||
return stool_type_counts_sort, highest_stool
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
date_range = request.GET.get("date_range")
|
||||
start_date, end_date = date_utils.get_date_range(date_range)
|
||||
print(f"start date is {start_date}, end_date is {end_date}")
|
||||
|
||||
print(f"is dats exist {self.enough_records_exist(start_date, end_date)}")
|
||||
|
||||
if not self.enough_records_exist(start_date, end_date):
|
||||
print("report does not exist")
|
||||
return JsonResponseUtil.success(
|
||||
message="No report is generated. Minimum Previous 7 days of records required.", status=202
|
||||
)
|
||||
|
||||
# Get top food to avoid
|
||||
food_avoid, food_counts, ingredient_counts, beverage_counts = (
|
||||
self.get_top_food_avoid(start_date, end_date)
|
||||
)
|
||||
|
||||
# Get symptoms frequency
|
||||
sorted_symptoms_frequency = self.get_symptoms_frequency(start_date, end_date)
|
||||
|
||||
# Get stool type counts
|
||||
stool_type_counts_sort, highest_stool = self.get_stool_type_counts(
|
||||
start_date, end_date
|
||||
)
|
||||
|
||||
nested_json = {
|
||||
"food_avoid": food_avoid,
|
||||
"same_food_avoid": {
|
||||
"food": food_counts,
|
||||
"ingredient": ingredient_counts,
|
||||
"beverage": beverage_counts,
|
||||
},
|
||||
"symptoms_frequency": sorted_symptoms_frequency,
|
||||
"highest_stool": highest_stool,
|
||||
"stool_type": stool_type_counts_sort,
|
||||
}
|
||||
print(f"nested_json data is {nested_json}")
|
||||
return JsonResponseUtil.success(message=constants.SUCCESS, data=nested_json)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from rest_framework import serializers
|
||||
from module_iam.models import IAmPrincipal
|
||||
from rest_framework.validators import UniqueValidator
|
||||
|
||||
from module_iam.models import AppVersion, IAmPrincipal
|
||||
from module_project import constants
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
# class BasePasswordSerializer(serializers.Serializer):
|
||||
# confirm_password = serializers.CharField(write_only=True, required=True)
|
||||
@@ -22,6 +24,10 @@ from django.contrib.auth import authenticate
|
||||
# return instance
|
||||
|
||||
class RegistrationSerializer(serializers.ModelSerializer):
|
||||
email = serializers.EmailField(
|
||||
required=True,
|
||||
validators=[UniqueValidator(queryset=IAmPrincipal.objects.all(), message="This email address is already in use.")]
|
||||
)
|
||||
password = serializers.CharField(write_only=True, required=True)
|
||||
confirm_password = serializers.CharField(write_only=True, required=True)
|
||||
|
||||
@@ -101,3 +107,13 @@ class PasswordResetSerializer(serializers.ModelSerializer):
|
||||
instance.password = make_password(new_password)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
class AppVersionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = AppVersion
|
||||
fields = [
|
||||
"device_type",
|
||||
"version",
|
||||
"force_upgrade",
|
||||
"recommend_upgrade",
|
||||
]
|
||||
@@ -1,16 +1,23 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from rest_framework_simplejwt.views import TokenRefreshView
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||
path("signup/", views.RegistrationView.as_view()),
|
||||
path("login/", views.LoginView.as_view()),
|
||||
path("logout/", views.LogoutView.as_view()),
|
||||
|
||||
path("request-otp/", views.OtpRequestView.as_view()),
|
||||
path("verify-otp/", views.OTPVerificationView.as_view()),
|
||||
path("forget-password/", views.ForgetPasswordView.as_view()),
|
||||
|
||||
# path("profile/", views.Profile)
|
||||
path("account/deactivate/", views.AccountDeactivateView.as_view()),
|
||||
|
||||
path('google-signin/', views.GoogleSignin.as_view(), name='google_signin'),
|
||||
path('apple-signin/', views.AppleSignin.as_view(), name='apple_signin'),
|
||||
|
||||
path('version-check/', views.VersionCheck.as_view(), name='version_check'),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework_simplejwt.tokens import RefreshToken, TokenError
|
||||
from rest_framework_simplejwt.exceptions import TokenError
|
||||
|
||||
from module_iam.models import IAmPrincipal, IAmPrincipalOtp
|
||||
from module_project import constants
|
||||
from module_project.utils import ApiResponse
|
||||
from module_iam.models import IAmPrincipal, IAmPrincipalOtp
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.core.exceptions import ValidationError
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,12 +27,29 @@ def generate_token_and_user_data(principal):
|
||||
data = {
|
||||
"access": str(refresh.access_token),
|
||||
"refresh": str(refresh),
|
||||
"first_name": principal.first_name,
|
||||
"phone_no": str(principal.phone_no),
|
||||
"complete": principal.register_complete,
|
||||
}
|
||||
return data
|
||||
|
||||
def blacklist_token(token):
|
||||
try:
|
||||
RefreshToken(token).blacklist()
|
||||
print("token is blacklisted")
|
||||
except TokenError:
|
||||
print("error occurs")
|
||||
pass
|
||||
|
||||
class GoogleAuthService():
|
||||
@staticmethod
|
||||
def get_user_info(access_token):
|
||||
headers = {'Authorization': f'Bearer {access_token}'}
|
||||
response = requests.get(
|
||||
'https://www.googleapis.com/oauth2/v3/userinfo',
|
||||
headers=headers,
|
||||
)
|
||||
user_info = response.json()
|
||||
return user_info
|
||||
|
||||
class AuthService:
|
||||
"""
|
||||
Provides authentication services for IAmPrincipal users.
|
||||
@@ -145,7 +166,7 @@ def authticate_with_otp_and_passsword(principal: IAmPrincipal, otp=None, passwor
|
||||
|
||||
if otp:
|
||||
otp_instance = IAmPrincipalOtp.objects.filter(
|
||||
principal=principal, otp_code=otp
|
||||
principal=principal, otp_code=otp, is_used=False
|
||||
).last()
|
||||
|
||||
if not otp_instance:
|
||||
@@ -165,7 +186,7 @@ def authticate_with_otp_and_passsword(principal: IAmPrincipal, otp=None, passwor
|
||||
print(password)
|
||||
if not principal.check_password(password):
|
||||
return ApiResponse.error(
|
||||
message=constants.INVALID_PASSWORD, errors=constants.INVALID_PASSWORD
|
||||
message=constants.INCORRECT_CREDENTIALS, errors=constants.INCORRECT_CREDENTIALS
|
||||
)
|
||||
print("after passsowrd", password)
|
||||
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import datetime
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from module_project import constants
|
||||
from module_project.service import SMSService, EmailService
|
||||
from module_project.utils import ApiResponse
|
||||
from .utils import AuthService
|
||||
from module_iam.models import IAmPrincipal, IAmPrincipalOtp
|
||||
from .serializers import RegistrationSerializer, LoginSerializer, OtpVerificationSerializer, PasswordResetSerializer
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from datetime import datetime
|
||||
|
||||
from .utils import (
|
||||
generate_token_and_user_data, get_principal_by_email, authticate_with_otp_and_passsword
|
||||
)
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from module_iam.models import (AppVersion, IAmPrincipal, IAmPrincipalOtp,
|
||||
IAmPrincipalSource, IAmPrincipalType)
|
||||
from module_project import constants
|
||||
from module_project.service import EmailService, SMSService
|
||||
from module_project.utils import ApiResponse
|
||||
|
||||
from .serializers import (AppVersionSerializer, LoginSerializer, OtpVerificationSerializer,
|
||||
PasswordResetSerializer, RegistrationSerializer)
|
||||
from .utils import (AuthService, GoogleAuthService,
|
||||
authticate_with_otp_and_passsword, blacklist_token,
|
||||
generate_token_and_user_data, get_principal_by_email)
|
||||
|
||||
|
||||
class RegistrationView(APIView):
|
||||
@@ -36,14 +41,19 @@ class RegistrationView(APIView):
|
||||
|
||||
try:
|
||||
instance = serializer.save()
|
||||
principal = instance
|
||||
token_data = generate_token_and_user_data(principal)
|
||||
instance.last_login = datetime.now()
|
||||
instance.principal_type = IAmPrincipalType.get_principal_user()
|
||||
instance.principal_source = IAmPrincipalSource.get_principal_app()
|
||||
instance.save()
|
||||
token_data = generate_token_and_user_data(instance)
|
||||
except Exception as e:
|
||||
return ApiResponse.error(
|
||||
status=status.HTTP_403_FORBIDDEN, message=str(e), errors=str(e)
|
||||
)
|
||||
|
||||
return ApiResponse.success(message=constants.REGISTRATION_SUCCESS, data=token_data)
|
||||
return ApiResponse.success(
|
||||
message=constants.REGISTRATION_SUCCESS, data=token_data
|
||||
)
|
||||
|
||||
|
||||
class LoginView(APIView):
|
||||
@@ -67,10 +77,14 @@ class LoginView(APIView):
|
||||
password = request.data.get("password")
|
||||
player_id = request.data.get("player_id")
|
||||
|
||||
principal = get_principal_by_email(email=email)
|
||||
|
||||
if isinstance(principal, Response):
|
||||
return principal
|
||||
try:
|
||||
principal = IAmPrincipal.objects.get(email=email)
|
||||
except IAmPrincipal.DoesNotExist:
|
||||
error_response = {
|
||||
"message": constants.INCORRECT_CREDENTIALS,
|
||||
"errors": constants.INCORRECT_CREDENTIALS,
|
||||
}
|
||||
return ApiResponse.error(**error_response)
|
||||
|
||||
validation_result = authticate_with_otp_and_passsword(
|
||||
principal, otp=otp, password=password
|
||||
@@ -81,32 +95,9 @@ class LoginView(APIView):
|
||||
print("Errror reponse")
|
||||
return validation_result # Return the error response if validation fails
|
||||
|
||||
|
||||
# auth_service = AuthService(principal_model=IAmPrincipal)
|
||||
|
||||
# try:
|
||||
# principal = self.model.objects.get(email=email)
|
||||
# except Exception as e:
|
||||
# error_response = {
|
||||
# "status": status.HTTP_403_FORBIDDEN,
|
||||
# "message": constants.INVALID_EMAIL_PASSWORD,
|
||||
# "errors": constants.INVALID_EMAIL_PASSWORD,
|
||||
# }
|
||||
# return ApiResponse.error(**error_response)
|
||||
|
||||
# try:
|
||||
# auth_service.authenticate(principal_id=principal.id, password=password)
|
||||
# except Exception as e:
|
||||
# error_response = {
|
||||
# "status": status.HTTP_403_FORBIDDEN,
|
||||
# "message": e,
|
||||
# "errors": e,
|
||||
# }
|
||||
# return ApiResponse.error(**error_response)
|
||||
|
||||
try:
|
||||
principal.player_id = player_id
|
||||
principal.last_login = datetime.datetime.now()
|
||||
principal.last_login = datetime.now()
|
||||
principal.save()
|
||||
except Exception as e:
|
||||
error_response = {
|
||||
@@ -120,14 +111,35 @@ class LoginView(APIView):
|
||||
return ApiResponse.success(message=constants.LOGIN_SUCCESS, data=token_data)
|
||||
|
||||
|
||||
class LogoutView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = IAmPrincipal
|
||||
|
||||
def post(self, request):
|
||||
token = request.data.get("refresh")
|
||||
if not token:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors='Provide refresh token')
|
||||
|
||||
user = request.user
|
||||
user.player_id = None
|
||||
user.save()
|
||||
|
||||
blacklist_token(token)
|
||||
|
||||
return ApiResponse.success(message=constants.LOGOUT_SUCCESS)
|
||||
|
||||
|
||||
class OtpRequestView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
if "email" not in request.data:
|
||||
return ApiResponse.error(message=constants.EMAIL_REQUIRED, errors=constants.EMAIL_REQUIRED)
|
||||
print(f"email auth username: {settings.EMAIL_HOST_USER}")
|
||||
return ApiResponse.error(
|
||||
message=constants.EMAIL_REQUIRED, errors=constants.EMAIL_REQUIRED
|
||||
)
|
||||
print(f"email auth username: {settings.DEFAULT_FROM_EMAIL}")
|
||||
email = request.data.get("email")
|
||||
|
||||
principal = get_principal_by_email(email=email)
|
||||
@@ -139,26 +151,33 @@ class OtpRequestView(APIView):
|
||||
# auth_service = AuthService(IAmPrincipal)
|
||||
# principal = auth_service.get_principal_by_email(request.data.get("email"))
|
||||
|
||||
otp_code = SMSService().create_otp(principal=principal, otp_purpose="Forget password")
|
||||
otp_code = SMSService().create_otp(
|
||||
principal=principal, otp_purpose="Password Reset Request"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=str(e), errors=str(e))
|
||||
|
||||
email_service = EmailService(
|
||||
subject="Forget Password",
|
||||
subject="Password Reset Request",
|
||||
to=principal.email,
|
||||
from_email=settings.EMAIL_HOST_USER
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
||||
# Send the email
|
||||
try:
|
||||
email_service.load_template("module_auth/email_template.html", context={"code": otp_code} )
|
||||
email_service.load_template(
|
||||
"module_auth/otp_email_template.html", context={"code": otp_code, "name": principal.first_name}
|
||||
)
|
||||
email_service.send()
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=f"Error sending email: {str(e)}", errors=str(e))
|
||||
return ApiResponse.error(
|
||||
message=f"Error sending email: {str(e)}", errors=str(e)
|
||||
)
|
||||
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
|
||||
|
||||
class OTPVerificationView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
@@ -173,7 +192,7 @@ class OTPVerificationView(APIView):
|
||||
"errors": serializer.errors,
|
||||
}
|
||||
return ApiResponse.error(**error_response)
|
||||
|
||||
|
||||
email = serializer.validated_data.get("email")
|
||||
otp = serializer.validated_data.get("otp")
|
||||
|
||||
@@ -181,26 +200,39 @@ class OTPVerificationView(APIView):
|
||||
|
||||
if isinstance(principal, Response):
|
||||
return principal
|
||||
|
||||
validation_result = authticate_with_otp_and_passsword(
|
||||
principal, otp=otp
|
||||
)
|
||||
|
||||
validation_result = authticate_with_otp_and_passsword(principal, otp=otp)
|
||||
print("pasword instance ", validation_result)
|
||||
|
||||
if isinstance(validation_result, Response):
|
||||
print("Errror reponse")
|
||||
return validation_result # Return the error response if validation fails
|
||||
|
||||
token_data = generate_token_and_user_data(principal)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=token_data)
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
|
||||
|
||||
class ForgetPasswordView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = PasswordResetSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(request.user, data=request.data)
|
||||
email = request.data.get("email")
|
||||
print("email for password reset", email)
|
||||
principal = get_principal_by_email(email=email)
|
||||
|
||||
if isinstance(principal, Response):
|
||||
return principal
|
||||
|
||||
otp_instance = IAmPrincipalOtp.objects.filter(principal=principal, is_used=True).last()
|
||||
|
||||
if not otp_instance:
|
||||
return ApiResponse.error(message=constants.PASSWORD_RESET_SESSION_EXPIRE)
|
||||
|
||||
if otp_instance.is_expired():
|
||||
return ApiResponse.error(message=constants.PASSWORD_RESET_SESSION_EXPIRE)
|
||||
|
||||
serializer = self.serializer_class(principal, data=request.data)
|
||||
if not serializer.is_valid():
|
||||
error_response = {
|
||||
"status": status.HTTP_403_FORBIDDEN,
|
||||
@@ -214,4 +246,141 @@ class ForgetPasswordView(APIView):
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=str(e), errors=str(e))
|
||||
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
|
||||
|
||||
class AccountDeactivateView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def delete(self, request):
|
||||
try:
|
||||
user = IAmPrincipal.objects.get(id=request.user.id)
|
||||
user.is_active = False
|
||||
user.deleted = True
|
||||
user.save()
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.INTERNAL_SERVER_ERROR, errors=str(e))
|
||||
|
||||
return ApiResponse.success(message=constants.ACCOUNT_DEACTIVATED)
|
||||
|
||||
|
||||
class GoogleSignin(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
access_token = request.data["access_token"]
|
||||
player_id = request.data["player_id"]
|
||||
user_info = GoogleAuthService.get_user_info(access_token)
|
||||
|
||||
print(f"User Info : {user_info} and player id is {player_id}")
|
||||
|
||||
# Authenticate user with the email provided by Google
|
||||
user = IAmPrincipal.objects.filter(email=user_info['email']).first(
|
||||
) or authenticate(email=user_info['email'], password=None)
|
||||
|
||||
if user:
|
||||
# Update the player_id for the existing user
|
||||
IAmPrincipal.objects.filter(email=user_info['email']).update(player_id=player_id)
|
||||
else:
|
||||
# Create a new user if not found
|
||||
user = IAmPrincipal.objects.create_user(
|
||||
username=user_info['email'],
|
||||
email=user_info['email'],
|
||||
first_name=f"{user_info['given_name']} {user_info['family_name']}",
|
||||
last_login=datetime.now(),
|
||||
player_id=player_id,
|
||||
principal_type=IAmPrincipalType.get_principal_user(),
|
||||
principal_source=IAmPrincipalSource.get_principal_google()
|
||||
)
|
||||
user.save()
|
||||
|
||||
token_data = generate_token_and_user_data(user)
|
||||
|
||||
# return Response({"token": token.key}, status=status.HTTP_200_OK)
|
||||
return ApiResponse.success(
|
||||
message=constants.SUCCESS, data=token_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=str(e))
|
||||
|
||||
|
||||
import jwt
|
||||
|
||||
|
||||
class AppleSignin(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
authorization_code = request.data['authorization_code']
|
||||
headers = {
|
||||
'Authorization': f"Bearer {settings.SOCIAL_AUTH_APPLE_CLIENT_SECRET}"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://appleid.apple.com/auth/token',
|
||||
data={
|
||||
'client_id': settings.SOCIAL_AUTH_APPLE_CLIENT_ID,
|
||||
'code': authorization_code,
|
||||
'grant_type': 'authorization_code',
|
||||
'redirect_uri': False,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
response_data = response.json()
|
||||
id_token = response_data.get('id_token')
|
||||
|
||||
decoded = jwt.decode(
|
||||
id_token,
|
||||
'',
|
||||
algorithms=['ES256'],
|
||||
options={
|
||||
'verify_aud': False,
|
||||
'verify_exp': False,
|
||||
'verify_iat': False,
|
||||
},
|
||||
)
|
||||
email = decoded.get('email')
|
||||
full_name = f"{decoded.get('given_name')} {decoded.get('family_name')}"
|
||||
if IAmPrincipal.objects.filter(email=email).exists():
|
||||
user = IAmPrincipal.objects.get(email=email)
|
||||
else:
|
||||
user = IAmPrincipal.objects.create_user(
|
||||
username=email,
|
||||
email=email,
|
||||
first_name=full_name,
|
||||
)
|
||||
user.save()
|
||||
|
||||
token_data = generate_token_and_user_data(user)
|
||||
|
||||
return ApiResponse.success(
|
||||
message=constants.SUCCESS, data=token_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors=str(e))
|
||||
|
||||
|
||||
class VersionCheck(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = AppVersion
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
print("app version is called")
|
||||
device_type = request.GET.get('deviceType')
|
||||
|
||||
if not device_type:
|
||||
return ApiResponse.error(message=constants.FAILURE, errors="device type is required")
|
||||
# Query the database to retrieve the upgrade flags based on the app version
|
||||
version = self.model.objects.filter(device_type=device_type).last()
|
||||
version_data = AppVersionSerializer(version)
|
||||
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=version_data.data)
|
||||
@@ -1,7 +1,10 @@
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_project import constants
|
||||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
email = forms.EmailField(
|
||||
max_length=254,
|
||||
@@ -12,4 +15,71 @@ class LoginForm(forms.Form):
|
||||
label="Password",
|
||||
strip=False,
|
||||
widget=forms.PasswordInput()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "off"}),
|
||||
validators=[
|
||||
validators.MinLengthValidator(
|
||||
limit_value=6, message="Password must be at least 6 characters long. "
|
||||
)
|
||||
],
|
||||
)
|
||||
confirm_password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "off"})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = IAmPrincipal
|
||||
fields = [
|
||||
"first_name",
|
||||
"email",
|
||||
"password",
|
||||
"confirm_password",
|
||||
]
|
||||
labels = {
|
||||
"first_name": "Name",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
instance = kwargs.get("instance")
|
||||
if instance and instance.pk:
|
||||
# Remove password and confirm_password fields if instance exists (update action)
|
||||
self.fields.pop("password")
|
||||
self.fields.pop("confirm_password")
|
||||
# Make email field readonly
|
||||
self.fields["email"].widget.attrs["readonly"] = True
|
||||
|
||||
def clean_email(self):
|
||||
# Prevent email from being updated
|
||||
instance = self.instance
|
||||
if instance and instance.pk:
|
||||
return instance.email
|
||||
else:
|
||||
email = self.cleaned_data.get("email")
|
||||
if IAmPrincipal.objects.filter(email=email).exclude(pk=instance.pk).exists():
|
||||
raise forms.ValidationError("This email address is already in use.")
|
||||
return email
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password = cleaned_data.get("password")
|
||||
confirm_password = cleaned_data.get("confirm_password")
|
||||
|
||||
if password and confirm_password and password != confirm_password:
|
||||
self.add_error("confirm_password", "Passwords do not match.")
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
# Check if it's a new object (create action) or an existing one (update action)
|
||||
if not instance.pk: # pk is None for new objects
|
||||
instance.username = self.cleaned_data["email"]
|
||||
instance.set_password(self.cleaned_data["password"])
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
from django.urls import path
|
||||
from django.views.generic import RedirectView, TemplateView
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "module_auth"
|
||||
|
||||
urlpatterns = [
|
||||
# redirect to different url
|
||||
path('', RedirectView.as_view(url='login'), name='index'),
|
||||
path('login/', views.AdminLoginView.as_view(), name="login"),
|
||||
path('logout/', views.AdminLogoutView.as_view(), name="logout"),
|
||||
path('password-reset/', views.CustomPasswordResetView.as_view(), name='password_reset'),
|
||||
@@ -11,7 +15,12 @@ urlpatterns = [
|
||||
path('password-reset-confirm/<uidb64>/<token>/', views.CustomPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
path('password-reset-complete/', views.CustomPasswordResetCompleteView.as_view(), name='password_reset_complete'),
|
||||
path('users/', views.UserDashView.as_view(), name='users'),
|
||||
path('users/add/', views.UserCreateOrUpdateView.as_view(), name='user_add'),
|
||||
path('users/edit/<int:pk>/', views.UserCreateOrUpdateView.as_view(), name='user_edit'),
|
||||
path('users/list/', views.UserListJson.as_view(), name='users_list'),
|
||||
path('users/action/', views.UserActionView.as_view(), name='users_action'),
|
||||
path('user/view/<int:id>/', views.UserRecordView.as_view(), name='user_view'),
|
||||
path('user/archive/list/', views.UserArchiveList.as_view(), name='user_archive'),
|
||||
path('user/count/', views.UsersCountView.as_view(), name="user_count")
|
||||
|
||||
]
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from django.db.models import Q, Prefetch
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import (
|
||||
LoginView,
|
||||
PasswordResetCompleteView,
|
||||
PasswordResetConfirmView,
|
||||
PasswordResetDoneView,
|
||||
PasswordResetView,
|
||||
)
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.views import (LoginView, LogoutView,
|
||||
PasswordResetCompleteView,
|
||||
PasswordResetConfirmView,
|
||||
PasswordResetDoneView,
|
||||
PasswordResetView)
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from .forms import LoginForm
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_activity.models import PrincipalHealthData, Intolerance, Symptoms, PastTreatment, ChronicCondition
|
||||
from django_datatables_view.base_datatable_view import BaseDatatableView
|
||||
|
||||
from module_activity.models import (Bowel, ChronicCondition, Intolerance, MealRecord, MealSymptomRecord, Medication,
|
||||
PastTreatment, PrincipalHealthData,
|
||||
Symptoms)
|
||||
from module_iam import iam_constant, permission
|
||||
from module_iam.models import IAmPrincipal, IAmPrincipalType
|
||||
from module_project import constants
|
||||
from module_project.mixins import ActionMixin
|
||||
from module_project.utils import JsonResponseUtil
|
||||
|
||||
from .forms import LoginForm, UserForm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,6 +58,7 @@ class AdminLoginView(generic.View):
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
login(request, user)
|
||||
messages.success(request, constants.LOGIN_SUCCESS)
|
||||
logging.info(f"User {user.email} logged in.")
|
||||
|
||||
return redirect(self.success_url)
|
||||
@@ -73,9 +78,9 @@ class CustomPasswordResetDoneView(PasswordResetDoneView):
|
||||
template_name = "module_auth/password_reset_done.html"
|
||||
|
||||
|
||||
class UserDashView(LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = None
|
||||
resource = None
|
||||
class UserDashView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
action = None
|
||||
template_name = "module_auth/users_list.html"
|
||||
model = IAmPrincipal
|
||||
@@ -86,43 +91,166 @@ class UserDashView(LoginRequiredMixin, generic.TemplateView):
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class UserCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
model = IAmPrincipal
|
||||
form_class = UserForm
|
||||
template_name = "module_auth/user_add.html"
|
||||
success_url = reverse_lazy("module_auth:users")
|
||||
success_message = "Saved Successfully"
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Edit" if self.object else "Add",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
# @transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
print(request.POST)
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
try:
|
||||
if form.is_valid():
|
||||
principal = form.save(commit=False)
|
||||
|
||||
# Check if it's a new object (create action) or an existing one (update action)
|
||||
if not principal.pk: # pk is None for new objects
|
||||
principal.created_by = request.user
|
||||
principal.principal_type = IAmPrincipalType.objects.filter(name=iam_constant.PRINCIPAL_TYPE_USER).first()
|
||||
principal.modified_by = request.user
|
||||
principal.modified_on = datetime.now()
|
||||
|
||||
# Save the object
|
||||
principal.save()
|
||||
|
||||
messages.success(request, "Form submitted successfully")
|
||||
return redirect(self.success_url)
|
||||
except Exception as e:
|
||||
self.error_message = constants.ERROR_OCCURR.format(str(e))
|
||||
print(self.error_message)
|
||||
messages.error(request, self.error_message)
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, template_name=self.template_name, context=context)
|
||||
|
||||
|
||||
class UserListJson(BaseDatatableView):
|
||||
model = IAmPrincipal
|
||||
columns = ["id", "first_name", "email", "phone_no", "date_of_birth", "is_active"]
|
||||
order_columns = ["id", "first_name", "email", "phone_no", "date_of_birth", "is_active"]
|
||||
FILTER_ICONTAINS = "icontains"
|
||||
|
||||
def get_filter_method(self):
|
||||
"""Returns preferred filter method"""
|
||||
return self.FILTER_ICONTAINS
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get('deleted_flag', False)
|
||||
return self.model.objects.filter(principal_type=IAmPrincipalType.get_principal_user(), deleted=deleted_flag)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
print(f"request is {self.request.GET}")
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
# print(f"isdiget {search_value.isdigit()}")
|
||||
# if search_value.isdigit():
|
||||
# qs = qs.filter(Q(id=search_value))
|
||||
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(first_name__icontains=search_value)
|
||||
| Q(email__icontains=search_value)
|
||||
| Q(date_of_birth__icontains=search_value)
|
||||
| Q(phone_no__icontains=search_value)
|
||||
)
|
||||
qs = super().filter_queryset(qs) # Call the built-in filtering first
|
||||
|
||||
for column in self.columns:
|
||||
search_value = self.request.GET.get(f'columns[{self.columns.index(column)}][search][value]', None)
|
||||
print(f" columen index pattern {self.request.GET.get(f'columns[{self.columns.index(column)+2}][search][value]', None)}")
|
||||
search_value = self.request.GET.get(f'columns[{self.columns.index(column)+2}][search][value]', None)
|
||||
if search_value:
|
||||
qs = qs.filter(**{f"{column}__icontains": search_value})
|
||||
column_data = self.request.GET.get(f'columns[{self.columns.index(column)+2}][data]')
|
||||
if column_data == "is_active":
|
||||
qs = qs.filter(**{f"{column}": search_value})
|
||||
else:
|
||||
qs = qs.filter(**{f"{column}__icontains": search_value})
|
||||
|
||||
return qs
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 2
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
class UserRecordView(LoginRequiredMixin, generic.View):
|
||||
page_name = None
|
||||
resource = None
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
class UserActionView(ActionMixin):
|
||||
model = IAmPrincipal
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
|
||||
ids = request.POST.getlist('ids[]') # List of IDs to perform action on
|
||||
active = request.POST.get('active')
|
||||
print(f"arhive action {action} and id is {ids} and active data is {active}")
|
||||
if action == 'archive':
|
||||
# Update 'deleted' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=True, is_active=False)
|
||||
message = 'Record archived successfully.'
|
||||
elif action == 'active':
|
||||
# Update 'active' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(is_active=active.capitalize())
|
||||
message = 'Record updated successfully.'
|
||||
elif action == 'unarchive':
|
||||
# Update 'deleted' field to False for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=False)
|
||||
message = 'Record unarchived successfully.'
|
||||
else:
|
||||
return JsonResponseUtil.error(message="Invalid Action")
|
||||
|
||||
return JsonResponseUtil.success(message=message)
|
||||
|
||||
class UserRecordView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
action = None
|
||||
model = IAmPrincipal
|
||||
template_name = "module_auth/user_view.html"
|
||||
|
||||
def get_recent_meal(self, id):
|
||||
recent_meal_records = MealRecord.objects.filter(
|
||||
principal=id
|
||||
).order_by('-id')[:5]
|
||||
return recent_meal_records
|
||||
|
||||
def get_recent_medication(self, id):
|
||||
recent_medication_records = Medication.objects.filter(
|
||||
principal=id
|
||||
).order_by('-id')[:5]
|
||||
return recent_medication_records
|
||||
|
||||
def get_recent_bowel(self, id):
|
||||
recent_bowel_records = Bowel.objects.filter(
|
||||
principal=id
|
||||
).order_by('-id')[:5]
|
||||
return recent_bowel_records
|
||||
|
||||
def get_recent_meal_symptom(self, id):
|
||||
recent_meal_symptom_records = MealSymptomRecord.objects.filter(
|
||||
principal=id
|
||||
).order_by('-id')[:5]
|
||||
return recent_meal_symptom_records
|
||||
|
||||
def get(self, request, id):
|
||||
# Retrieve the IAmPrincipal instance
|
||||
principal_instance = get_object_or_404(IAmPrincipal, id=id)
|
||||
@@ -160,39 +288,33 @@ class UserRecordView(LoginRequiredMixin, generic.View):
|
||||
chronic_prefetch
|
||||
).get(id=id)
|
||||
|
||||
print(f"prefetch datatas")
|
||||
for data in obj.chronic_data:
|
||||
print(f"data is {data.name, data.duration}")
|
||||
|
||||
context = {
|
||||
'page_name': self.page_name,
|
||||
'obj': obj,
|
||||
"recent_meal": self.get_recent_meal(id),
|
||||
"recent_medication": self.get_recent_medication(id),
|
||||
"recent_bowel": self.get_recent_bowel(id),
|
||||
"recent_meal_symptom": self.get_recent_meal_symptom(id),
|
||||
}
|
||||
|
||||
# Render the template with the principal instance and related data
|
||||
return render(request, self.template_name, {'obj': obj})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
|
||||
class UserArchiveList(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_USER
|
||||
resource = iam_constant.RESOURCE_MANAGE_USER
|
||||
action = None
|
||||
template_name = "module_auth/users_archive_list.html"
|
||||
model = IAmPrincipal
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class CustomPasswordResetConfirmView(PasswordResetConfirmView):
|
||||
template_name = "module_auth/password_reset_confirm.html"
|
||||
@@ -201,3 +323,23 @@ class CustomPasswordResetConfirmView(PasswordResetConfirmView):
|
||||
|
||||
class CustomPasswordResetCompleteView(PasswordResetCompleteView):
|
||||
template_name = "module_auth/password_reset_complete.html"
|
||||
|
||||
|
||||
class UsersCountView(generic.View):
|
||||
|
||||
def get(self, request):
|
||||
current_year = int(self.request.GET.get("year"))
|
||||
user_counts = []
|
||||
|
||||
# Iterate over each month from January to December
|
||||
for month in range(1, 13):
|
||||
# Calculate the start and end dates for the current month
|
||||
start_date = datetime(current_year, month, 1)
|
||||
end_date = datetime(current_year, month + 1, 1) if month < 12 else datetime(current_year + 1, 1, 1)
|
||||
# Query the User model to count users created within the current month
|
||||
user_count = IAmPrincipal.objects.filter(date_joined__range=(start_date, end_date)).count()
|
||||
|
||||
# Append the count to the list
|
||||
user_counts.append(user_count)
|
||||
|
||||
return JsonResponseUtil.success(message=constants.SUCCESS, data=user_counts)
|
||||
@@ -1,17 +1,14 @@
|
||||
from rest_framework import serializers
|
||||
from taggit.models import Tag
|
||||
|
||||
from module_cms.models import Faqs, Organization
|
||||
|
||||
|
||||
class FaqSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Faqs
|
||||
fields = ["id", "question", "answer"]
|
||||
|
||||
class FaqListSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Faqs
|
||||
fields = "__all__"
|
||||
|
||||
class OrganizationSerializer(serializers.ModelSerializer):
|
||||
about_us = serializers.CharField(source='about_us.html', read_only=True)
|
||||
terms_condition = serializers.CharField(source='terms_condition.html', read_only=True)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from module_project import constants
|
||||
from module_project.utils import ApiResponse
|
||||
from .serializers import FaqSerializer, OrganizationSerializer
|
||||
|
||||
from ..models import Faqs, Organization
|
||||
from .serializers import FaqSerializer, OrganizationSerializer
|
||||
|
||||
|
||||
class FaqListAPIView(APIView):
|
||||
@@ -19,8 +21,8 @@ class FaqListAPIView(APIView):
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
|
||||
|
||||
class OrganizationAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = OrganizationSerializer
|
||||
model = Organization
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core import validators
|
||||
from .models import (
|
||||
Organization,
|
||||
FaqCategory,
|
||||
Faqs,
|
||||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from module_project import constants
|
||||
|
||||
from .models import FaqCategory, Faqs, Organization
|
||||
|
||||
|
||||
class OrganizationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -68,16 +65,4 @@ class FaqsForm(forms.ModelForm):
|
||||
# "faq_category",
|
||||
"question",
|
||||
"answer",
|
||||
"active",
|
||||
]
|
||||
# labels = {"faq_category": "Category"}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get("instance")
|
||||
super().__init__(*args, **kwargs)
|
||||
# Fetch the choices for the faq_category field from the database
|
||||
# self.fields["faq_category"].queryset = FaqCategory.objects.all()
|
||||
|
||||
if instance is None:
|
||||
# This is an add operation, exclude the 'active' field
|
||||
self.fields.pop("active")
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-26 22:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_cms', '0003_alter_faqs_faq_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='faqs',
|
||||
name='answer',
|
||||
field=models.TextField(default=1),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='faqs',
|
||||
name='question',
|
||||
field=models.TextField(),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,9 @@
|
||||
from django.db import models
|
||||
from module_iam.models import BaseModel, IAmPrincipal
|
||||
from taggit.managers import TaggableManager
|
||||
from django_quill.fields import QuillField
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from module_iam.models import BaseModel, IAmPrincipal
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class Organization(BaseModel):
|
||||
@@ -41,8 +43,8 @@ class Faqs(BaseModel):
|
||||
faq_category = models.ForeignKey(
|
||||
FaqCategory, related_name="faqs_category", blank=True, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
question = models.TextField(max_length=255)
|
||||
answer = models.TextField(blank=True, null=True)
|
||||
question = models.TextField()
|
||||
answer = models.TextField()
|
||||
|
||||
class Meta:
|
||||
db_table = "faq"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "module_cms"
|
||||
@@ -7,6 +8,9 @@ urlpatterns = [
|
||||
path('faq/', views.FaqView.as_view(), name="faq"),
|
||||
path('faq/list/', views.FaqListJson.as_view(), name="faq_list"),
|
||||
path('faq/add/', views.FaqCreateOrUpdateView.as_view(), name='faq_add'),
|
||||
path('faq/edit/<int:pk>/', views.FaqCreateOrUpdateView.as_view(), name='faq_edit'),
|
||||
path('faq/action/', views.FaqActionView.as_view(), name='faq_action'),
|
||||
path('faq/archive/', views.FaqArchiveView.as_view(), name='faq_archive'),
|
||||
|
||||
path('about-us/', views.AboutUsView.as_view(), name='about_us'),
|
||||
path('about-us/edit/', views.AboutUsCreateOrUpdateView.as_view(), name='about_us_add'),
|
||||
|
||||
@@ -3,24 +3,26 @@ import logging
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from module_iam.models import IAmPrincipal
|
||||
from .forms import AboutUsForm, TermsAndConditionForm, FaqCategoryFrom, PrivacyPolicyForm
|
||||
from .models import Faqs, Organization
|
||||
from .api.serializers import FaqListSerializer
|
||||
from module_project.mixins import DatatablesMixin
|
||||
from django_datatables_view.base_datatable_view import BaseDatatableView
|
||||
|
||||
from module_iam import iam_constant, permission
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_project import constants
|
||||
from module_project.mixins import ActionMixin, DatatablesMixin
|
||||
|
||||
from .forms import (AboutUsForm, FaqsForm, PrivacyPolicyForm,
|
||||
TermsAndConditionForm)
|
||||
from .models import Faqs, Organization
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FaqView(LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = None
|
||||
resource = None
|
||||
class FaqView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_FAQS
|
||||
resource = iam_constant.RESOURCE_MANAGE_FAQS
|
||||
action = None
|
||||
template_name = "module_cms/faq.html"
|
||||
model = Faqs
|
||||
@@ -32,70 +34,118 @@ class FaqView(LoginRequiredMixin, generic.TemplateView):
|
||||
return context
|
||||
|
||||
|
||||
# class FaqDatatableView(DatatablesMixin, LoginRequiredMixin, generic.View):
|
||||
# model = Faqs
|
||||
|
||||
# def get_queryset(self):
|
||||
# return self.model.objects.filter(deleted=False)
|
||||
|
||||
# def get(self, request):
|
||||
# (
|
||||
# draw,
|
||||
# start,
|
||||
# length,
|
||||
# order_columns,
|
||||
# order_directions,
|
||||
# search_value,
|
||||
# ) = self.get_datatables_params(request)
|
||||
# queryset = self.get_queryset()
|
||||
|
||||
# page_obj, total_count, filtered_count = self.get_pagination(
|
||||
# queryset, start, length
|
||||
# )
|
||||
|
||||
# serializer = FaqListSerializer(
|
||||
# page_obj.object_list, many=True
|
||||
# )
|
||||
|
||||
# response = self.prepare_datatables_response(
|
||||
# draw, total_count, filtered_count, serializer.data
|
||||
# )
|
||||
|
||||
# return response
|
||||
|
||||
|
||||
class FaqListJson(BaseDatatableView):
|
||||
model = Faqs
|
||||
columns = ["id", "question", "answer", "active", "deleted"]
|
||||
order_columns = ["id", "question", "answer", "active", "deleted"]
|
||||
columns = ["id", "question", "answer", "active"]
|
||||
order_columns = ["id", "question", "answer", "active"]
|
||||
FILTER_ICONTAINS = "icontains"
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
# Implement your custom filtering logic here
|
||||
print(f"request is {self.request.GET}")
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(question__icontains=search_value)
|
||||
| Q(answer__icontains=search_value)
|
||||
)
|
||||
def get_filter_method(self):
|
||||
"""Returns preferred filter method"""
|
||||
return self.FILTER_ICONTAINS
|
||||
|
||||
for column in self.columns:
|
||||
search_value = self.request.GET.get(f'columns[{self.columns.index(column)}][search][value]', None)
|
||||
if search_value:
|
||||
qs = qs.filter(**{f"{column}__icontains": search_value})
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get('deleted_flag', None)
|
||||
|
||||
return self.model.objects.filter(deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class FaqCreateOrUpdateView(generic.View):
|
||||
pass
|
||||
class FaqCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = iam_constant.RESOURCE_MANAGE_FAQS
|
||||
resource = iam_constant.RESOURCE_MANAGE_FAQS
|
||||
|
||||
# Initialize the action as ACTION_CREATE (can change based on logic)
|
||||
action = iam_constant.ACTION_CREATE # Default action
|
||||
|
||||
template_name = "module_cms/faq_add.html"
|
||||
model = Faqs
|
||||
form_class = FaqsForm
|
||||
success_url = reverse_lazy("module_cms:faq")
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
# Determine the success message dynamically based on whether it's an update or create
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
# Get the object (if exists) based on URL parameter 'pk
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
# Add page_name and operation to the context
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Add" if not self.object else "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# If an object is found, change action to ACTION_UPDATE
|
||||
if self.object is not None:
|
||||
self.action = iam_constant.ACTION_UPDATE
|
||||
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
print("Request data: ", request.POST)
|
||||
self.object = self.get_object()
|
||||
|
||||
# If an object is found, change action to ACTION_UPDATE
|
||||
if self.object is not None:
|
||||
self.action = iam_constant.ACTION_UPDATE
|
||||
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
form.save()
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.success_url)
|
||||
|
||||
class FaqActionView(ActionMixin):
|
||||
model = Faqs
|
||||
|
||||
|
||||
class FaqArchiveView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_FAQS
|
||||
resource = iam_constant.RESOURCE_MANAGE_FAQS
|
||||
action = None
|
||||
template_name = "module_cms/faq_archive.html"
|
||||
model = Faqs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class AboutUsView(LoginRequiredMixin, generic.DetailView):
|
||||
page_name = None
|
||||
class AboutUsView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.DetailView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_CMS
|
||||
resource = iam_constant.RESOURCE_MANAGE_CMS
|
||||
template_name = "module_cms/about_us_view.html"
|
||||
model = Organization
|
||||
context_object_name = "organization"
|
||||
@@ -109,11 +159,10 @@ class AboutUsView(LoginRequiredMixin, generic.DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class AboutUsCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
class AboutUsCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = None
|
||||
resource = None
|
||||
|
||||
page_name = iam_constant.RESOURCE_MANAGE_CMS
|
||||
resource = iam_constant.RESOURCE_MANAGE_CMS
|
||||
# Initialize the action as ACTION_CREATE (can change based on logic)
|
||||
action = None # Default action
|
||||
|
||||
@@ -172,9 +221,9 @@ class AboutUsCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class TermsConditionView(LoginRequiredMixin, generic.DetailView):
|
||||
page_name = None
|
||||
resource = None
|
||||
class TermsConditionView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.DetailView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_T_C
|
||||
resource = iam_constant.RESOURCE_MANAGE_T_C
|
||||
action = None
|
||||
template_name = "module_cms/terms_and_condition_view.html"
|
||||
model = Organization
|
||||
@@ -189,10 +238,10 @@ class TermsConditionView(LoginRequiredMixin, generic.DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class TermsConditionCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
class TermsConditionCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = None
|
||||
resource = None
|
||||
page_name = iam_constant.RESOURCE_MANAGE_T_C
|
||||
resource = iam_constant.RESOURCE_MANAGE_T_C
|
||||
|
||||
# Initialize the action as ACTION_CREATE (can change based on logic)
|
||||
action = None # Default action
|
||||
@@ -252,9 +301,9 @@ class TermsConditionCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class PrivacyPolicyView(LoginRequiredMixin, generic.DetailView):
|
||||
page_name = None
|
||||
resource = None
|
||||
class PrivacyPolicyView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.DetailView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_PRIVACYPOLICY
|
||||
resource = iam_constant.RESOURCE_MANAGE_PRIVACYPOLICY
|
||||
action = None
|
||||
template_name = "module_cms/privacy_policy_view.html"
|
||||
model = Organization
|
||||
@@ -269,10 +318,10 @@ class PrivacyPolicyView(LoginRequiredMixin, generic.DetailView):
|
||||
return context
|
||||
|
||||
|
||||
class PrivacyPolicyCreateOrUpdateView(LoginRequiredMixin, generic.View):
|
||||
class PrivacyPolicyCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = None
|
||||
resource = None
|
||||
page_name = iam_constant.RESOURCE_MANAGE_PRIVACYPOLICY
|
||||
resource = iam_constant.RESOURCE_MANAGE_PRIVACYPOLICY
|
||||
|
||||
# Initialize the action as ACTION_CREATE (can change based on logic)
|
||||
action = None # Default action
|
||||
|
||||
62
module_iam/context_processors.py
Normal file
62
module_iam/context_processors.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from .iam_constant import (
|
||||
PRINCIPAL_TYPE_USER,
|
||||
PRINCIPAL_TYPE_ADMIN,
|
||||
PRINCIPAL_TYPE_SUBADMIN,
|
||||
PRINCIPAL_SOURCE_APP,
|
||||
PRINCIPAL_SOURCE_WEB,
|
||||
PRINCIPAL_SOURCE_GOOGLE,
|
||||
PRINCIPAL_SOURCE_APPLE,
|
||||
ACTION_CREATE,
|
||||
ACTION_READ,
|
||||
ACTION_UPDATE,
|
||||
ACTION_DELETE,
|
||||
RESOURCE_MANAGE_DASHBOARD,
|
||||
RESOURCE_MANAGE_IAM,
|
||||
RESOURCE_MANAGE_USER,
|
||||
RESOURCE_MANAGE_SUPPORT,
|
||||
RESOURCE_MANAGE_CONTACT_US,
|
||||
RESOURCE_MANAGE_FEEDBACK,
|
||||
RESOURCE_MANAGE_NOTIFICATION,
|
||||
RESOURCE_MANAGE_CMS,
|
||||
RESOURCE_MANAGE_FAQS,
|
||||
RESOURCE_MANAGE_T_C,
|
||||
RESOURCE_MANAGE_PRIVACYPOLICY,
|
||||
RESOURCE_IAM_PRINCIPAL,
|
||||
RESOURCE_IAM_PRINCIPAL_GROUP,
|
||||
RESOURCE_IAM_GROUP,
|
||||
RESOURCE_IAM_ROLE,
|
||||
)
|
||||
|
||||
from .models import IAmPrincipal
|
||||
|
||||
def iam_constants_context(request):
|
||||
return {
|
||||
'iam_constants_context': {
|
||||
'PRINCIPAL_TYPE_USER': PRINCIPAL_TYPE_USER,
|
||||
'PRINCIPAL_TYPE_ADMIN': PRINCIPAL_TYPE_ADMIN,
|
||||
'PRINCIPAL_TYPE_SUBADMIN': PRINCIPAL_TYPE_SUBADMIN,
|
||||
'PRINCIPAL_SOURCE_APP': PRINCIPAL_SOURCE_APP,
|
||||
'PRINCIPAL_SOURCE_WEB': PRINCIPAL_SOURCE_WEB,
|
||||
'PRINCIPAL_SOURCE_GOOGLE': PRINCIPAL_SOURCE_GOOGLE,
|
||||
'PRINCIPAL_SOURCE_APPLE': PRINCIPAL_SOURCE_APPLE,
|
||||
'ACTION_CREATE': ACTION_CREATE,
|
||||
'ACTION_READ': ACTION_READ,
|
||||
'ACTION_UPDATE': ACTION_UPDATE,
|
||||
'ACTION_DELETE': ACTION_DELETE,
|
||||
'RESOURCE_MANAGE_DASHBOARD': RESOURCE_MANAGE_DASHBOARD,
|
||||
'RESOURCE_MANAGE_IAM': RESOURCE_MANAGE_IAM,
|
||||
'RESOURCE_MANAGE_USER': RESOURCE_MANAGE_USER,
|
||||
'RESOURCE_MANAGE_SUPPORT': RESOURCE_MANAGE_SUPPORT,
|
||||
'RESOURCE_MANAGE_CONTACT_US': RESOURCE_MANAGE_CONTACT_US,
|
||||
'RESOURCE_MANAGE_FEEDBACK': RESOURCE_MANAGE_FEEDBACK,
|
||||
'RESOURCE_MANAGE_NOTIFICATION': RESOURCE_MANAGE_NOTIFICATION,
|
||||
'RESOURCE_MANAGE_CMS': RESOURCE_MANAGE_CMS,
|
||||
'RESOURCE_MANAGE_FAQS': RESOURCE_MANAGE_FAQS,
|
||||
'RESOURCE_MANAGE_T_C': RESOURCE_MANAGE_T_C,
|
||||
'RESOURCE_MANAGE_PRIVACYPOLICY': RESOURCE_MANAGE_PRIVACYPOLICY,
|
||||
'RESOURCE_IAM_PRINCIPAL': RESOURCE_IAM_PRINCIPAL,
|
||||
'RESOURCE_IAM_PRINCIPAL_GROUP': RESOURCE_IAM_PRINCIPAL_GROUP,
|
||||
'RESOURCE_IAM_GROUP': RESOURCE_IAM_GROUP,
|
||||
'RESOURCE_IAM_ROLE': RESOURCE_IAM_ROLE,
|
||||
}
|
||||
}
|
||||
46
module_iam/fixtures/iam_actions_fixture.json
Normal file
46
module_iam/fixtures/iam_actions_fixture.json
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"model": "module_iam.iamappaction",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "create",
|
||||
"label": "create",
|
||||
"slug": "create",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappaction",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "read",
|
||||
"label": "read",
|
||||
"slug": "read",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappaction",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "update",
|
||||
"label": "update",
|
||||
"slug": "update",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappaction",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "delete",
|
||||
"label": "delete",
|
||||
"slug": "delete",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
}
|
||||
]
|
||||
46
module_iam/fixtures/iam_principal_source_fixture.json
Normal file
46
module_iam/fixtures/iam_principal_source_fixture.json
Normal file
@@ -0,0 +1,46 @@
|
||||
[
|
||||
{
|
||||
"model": "module_iam.iamprincipalsource",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "app",
|
||||
"label": "app",
|
||||
"slug": "app",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamprincipalsource",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "web",
|
||||
"label": "web",
|
||||
"slug": "web",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamprincipalsource",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "google",
|
||||
"label": "google",
|
||||
"slug": "google",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamprincipalsource",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "apple",
|
||||
"label": "apple",
|
||||
"slug": "apple",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813"
|
||||
}
|
||||
}
|
||||
]
|
||||
35
module_iam/fixtures/iam_principal_type_fixture.json
Normal file
35
module_iam/fixtures/iam_principal_type_fixture.json
Normal file
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"model": "module_iam.iamprincipaltype",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "admin",
|
||||
"label": "admin",
|
||||
"slug": "admin",
|
||||
"created_on": "2024-04-01T11:17:39.364808",
|
||||
"modified_on": "2024-04-01T11:17:39.364808"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamprincipaltype",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "subadmin",
|
||||
"label": "subadmin",
|
||||
"slug": "subadmin",
|
||||
"created_on": "2024-04-01T11:17:39.364808",
|
||||
"modified_on": "2024-04-01T11:17:39.364808"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamprincipaltype",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "user",
|
||||
"label": "user",
|
||||
"slug": "user",
|
||||
"created_on": "2024-04-01T11:17:39.364808",
|
||||
"modified_on": "2024-04-01T11:17:39.364808"
|
||||
}
|
||||
}
|
||||
]
|
||||
172
module_iam/fixtures/iam_resources_fixture.json
Normal file
172
module_iam/fixtures/iam_resources_fixture.json
Normal file
@@ -0,0 +1,172 @@
|
||||
[
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "manage_iam",
|
||||
"label": "manage_iam",
|
||||
"slug": "manage_iam",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "manage_user",
|
||||
"label": "manage_user",
|
||||
"slug": "manage_user",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "manage_support",
|
||||
"label": "manage_support",
|
||||
"slug": "manage_support",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "manage_contact_us",
|
||||
"label": "manage_contact_us",
|
||||
"slug": "manage_contact_us",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "manage_feedback",
|
||||
"label": "manage_feedback",
|
||||
"slug": "manage_feedback",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"name": "manage_cms",
|
||||
"label": "manage_cms",
|
||||
"slug": "manage_cms",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "manage_faqs",
|
||||
"label": "manage_faqs",
|
||||
"slug": "manage_faqs",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"name": "manage_tc",
|
||||
"label": "manage_tc",
|
||||
"slug": "manage_tc",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"name": "manage_privacypolicy",
|
||||
"label": "manage_privacypolicy",
|
||||
"slug": "manage_privacypolicy",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"name": "manage_notification",
|
||||
"label": "manage_notification",
|
||||
"slug": "manage_notification",
|
||||
"created_on": "2024-04-01T11:17:39.378813",
|
||||
"modified_on": "2024-04-01T11:17:39.378813",
|
||||
"action": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
356
module_iam/forms.py
Normal file
356
module_iam/forms.py
Normal file
@@ -0,0 +1,356 @@
|
||||
from typing import Any
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core import validators
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from module_project import constants
|
||||
|
||||
from . import models
|
||||
# from .backend import EmailBackend
|
||||
# from phonenumber_field.formfields import PhoneNumberField
|
||||
from .iam_constant import PRINCIPAL_TYPE_ADMIN, PRINCIPAL_TYPE_SUBADMIN
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
|
||||
class CustomAuthenticationForm(forms.Form):
|
||||
email = forms.EmailField(
|
||||
max_length=254,
|
||||
widget=forms.TextInput(attrs={"autofocus": True}),
|
||||
label=_("Email"),
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_("Password"),
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get("email")
|
||||
password = self.cleaned_data.get("password")
|
||||
self.user = None
|
||||
if email and password:
|
||||
|
||||
user = authenticate(email=email, password=password)
|
||||
|
||||
if user is None:
|
||||
raise ValidationError({"__all__": [constants.INVALID_EMAIL_PASSWORD]})
|
||||
elif not user.is_active:
|
||||
raise ValidationError({"__all__": [constants.ACCOUNT_DEACTIVATED]})
|
||||
self.user = user
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class IAmPrincipalForm(forms.ModelForm):
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "off"}),
|
||||
validators=[
|
||||
validators.MinLengthValidator(
|
||||
limit_value=6, message="Password must be at least 6 characters long. "
|
||||
)
|
||||
],
|
||||
)
|
||||
confirm_password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "off"})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.IAmPrincipal
|
||||
fields = [
|
||||
"principal_type",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"password",
|
||||
"confirm_password",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get("instance")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["principal_type"].queryset = models.IAmPrincipalType.objects.filter(
|
||||
active=True, deleted=False, name__in=(PRINCIPAL_TYPE_ADMIN, PRINCIPAL_TYPE_SUBADMIN)
|
||||
)
|
||||
# If it's a create action, exclude 'is_active' field
|
||||
if instance is not None and instance.pk is not None:
|
||||
self.fields.pop("password", None)
|
||||
self.fields.pop("confirm_password", None)
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get("email")
|
||||
# Skip uniqueness validation if it's an update action (instance exists)
|
||||
if self.instance and self.instance.email == email:
|
||||
return email
|
||||
if models.IAmPrincipal.objects.filter(email=email).exists():
|
||||
raise forms.ValidationError(constants.EMAIL_EXISTS)
|
||||
|
||||
return email
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
# Check if it's a new object (create action) or an existing one (update action)
|
||||
if not instance.pk: # pk is None for new objects
|
||||
instance.username = self.cleaned_data["email"]
|
||||
instance.set_password(self.cleaned_data["password"])
|
||||
|
||||
principal_type = self.cleaned_data.get("principal_type")
|
||||
if principal_type is not None:
|
||||
# Set is_superuser and is_staff based on principal_type
|
||||
if principal_type == models.IAmPrincipalType.objects.get(name=PRINCIPAL_TYPE_ADMIN):
|
||||
instance.is_superuser = True
|
||||
elif principal_type == models.IAmPrincipalType.objects.get(name=PRINCIPAL_TYPE_SUBADMIN):
|
||||
instance.is_superuser = False
|
||||
instance.is_staff = True
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class IAmPrincipalProfileForm(forms.ModelForm):
|
||||
GENDER_CHOICES = (
|
||||
("male", "Male"),
|
||||
("female", "Female"),
|
||||
("other", "Other"),
|
||||
)
|
||||
first_name = forms.CharField(required=True)
|
||||
last_name = forms.CharField(required=True)
|
||||
email = forms.EmailField(required=True)
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "off"})
|
||||
)
|
||||
confirm_password = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "off"})
|
||||
)
|
||||
# date_of_birth = forms.CharField(widget=forms.DateInput(attrs={'type': 'date'}))
|
||||
phone_number = forms.CharField(
|
||||
widget=forms.TextInput(),
|
||||
)
|
||||
# is_staff = forms.BooleanField(
|
||||
# label="Staff Status",
|
||||
# label_suffix="",
|
||||
# initial=True,
|
||||
# required=False,
|
||||
# help_text="Check this box to designate that this user will be assigned permissions in the future.",
|
||||
# )
|
||||
# is_superuser = forms.BooleanField(
|
||||
# label="SuperAdmin Status",
|
||||
# label_suffix="",
|
||||
# required=False,
|
||||
# help_text="Check this box to designates that this user has all permissions without explicitly assigning them.",
|
||||
# )
|
||||
# gender = forms.ChoiceField(choices=GENDER_CHOICES)
|
||||
|
||||
class Meta:
|
||||
model = models.IAmPrincipal
|
||||
fields = [
|
||||
"principal_type",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"password",
|
||||
"confirm_password",
|
||||
# 'gender',
|
||||
# 'date_of_birth',
|
||||
"phone_number",
|
||||
# 'address_line1',
|
||||
# 'address_line2',
|
||||
# 'city',
|
||||
# 'state',
|
||||
# 'country',
|
||||
# 'post_code',
|
||||
# 'profile_photo',
|
||||
# "is_staff",
|
||||
# "is_superuser",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get("instance")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["principal_type"].queryset = models.IAmPrincipalType.objects.filter(
|
||||
active=True, deleted=False
|
||||
)
|
||||
# self.fields['principal_source'].queryset = models.IAmPrincipalSource.objects.filter(active=True, deleted=False)
|
||||
# Check if an instance is provided and customize the form fields accordingly
|
||||
if instance is not None:
|
||||
# Exclude the 'password' and 'confirm_password' fields
|
||||
self.fields.pop("password", None)
|
||||
self.fields.pop("confirm_password", None)
|
||||
|
||||
# Make the 'email' field read-only
|
||||
self.fields["email"].widget.attrs["readonly"] = True
|
||||
|
||||
# Modify the 'is_superuser' field to be not required
|
||||
# self.fields["is_superuser"].required = False
|
||||
# self.fields["is_staff"].required = False
|
||||
|
||||
# Add or modify the 'is_active' field
|
||||
self.fields["is_active"] = forms.BooleanField(
|
||||
label="Active",
|
||||
initial=instance.is_active,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
required=False,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
password = cleaned_data.get("password")
|
||||
confirm_password = cleaned_data.get("confirm_password")
|
||||
|
||||
if password and confirm_password and password != confirm_password:
|
||||
self.add_error("confirm_password", "Password does not match")
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
user.set_password(self.cleaned_data["password"])
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
class ProfileEditForm(forms.ModelForm):
|
||||
gender = forms.ChoiceField(choices=(('Male', 'Male'),('Female', 'Female'),('Other', 'Other')))
|
||||
profile_photo = forms.ImageField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = models.IAmPrincipal
|
||||
fields = [
|
||||
"profile_photo",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"date_of_birth",
|
||||
"gender",
|
||||
"phone_no"
|
||||
]
|
||||
labels = {
|
||||
'phone_no': 'Phone Number', # Map phone_no field to phone_number_label variable
|
||||
}
|
||||
|
||||
|
||||
class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
|
||||
|
||||
class Meta:
|
||||
model = models.IAmPrincipal
|
||||
fields = [
|
||||
"principal_type",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"password",
|
||||
"confirm_password",
|
||||
"principal_resource",
|
||||
]
|
||||
|
||||
principal_resource = forms.ModelMultipleChoiceField(
|
||||
label="Module Permission",
|
||||
queryset=models.IAmAppResource.objects.filter(active=True, deleted=False),
|
||||
required=False,
|
||||
widget=forms.widgets.SelectMultiple(
|
||||
attrs={"class": "form_select js-example-basic-multiple"}
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
# First, save the instance of the IAmPrincipal model as usual
|
||||
principal = super().save(commit=False)
|
||||
# If the principal_resource field has data
|
||||
if self.cleaned_data['principal_resource']:
|
||||
# Get the principal_resource data
|
||||
principal_resource_data = self.cleaned_data['principal_resource']
|
||||
# Update the many-to-many relationship
|
||||
principal.principal_resource.set(principal_resource_data)
|
||||
# Save the instance to the database
|
||||
if commit:
|
||||
principal.save()
|
||||
|
||||
|
||||
class IAmPrincipalGroupLinkForm(IAmPrincipalForm):
|
||||
|
||||
class Meta:
|
||||
model = models.IAmPrincipal
|
||||
fields = [
|
||||
"principal_type",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"password",
|
||||
"confirm_password",
|
||||
"principal_group",
|
||||
]
|
||||
|
||||
principal_group = forms.ModelMultipleChoiceField(
|
||||
label="Groups",
|
||||
queryset=models.IAmPrincipalGroup.objects.filter(active=True, deleted=False),
|
||||
required=False,
|
||||
widget=forms.widgets.SelectMultiple(
|
||||
attrs={"class": "form_select js-example-basic-multiple"}
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
# First, save the instance of the IAmPrincipal model as usual
|
||||
principal = super().save(commit=False)
|
||||
# If the principal_group field has data
|
||||
if self.cleaned_data['principal_group']:
|
||||
# Get the principal_group data
|
||||
principal_group_data = self.cleaned_data['principal_group']
|
||||
# Update the many-to-many relationship
|
||||
principal.principal_group.set(principal_group_data)
|
||||
# Save the instance to the database
|
||||
if commit:
|
||||
principal.save()
|
||||
|
||||
class IAmPrincipalTypeForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.IAmPrincipalType
|
||||
fields = ["name", "active"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get("instance")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if instance is None:
|
||||
self.fields.pop("active")
|
||||
|
||||
|
||||
class IAmPrincipalGroupRoleLinkForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.IAmPrincipalGroup
|
||||
fields = ["name", "role", "active"]
|
||||
|
||||
role = forms.ModelMultipleChoiceField(
|
||||
queryset=models.IAmRole.objects.filter(active=True, deleted=False),
|
||||
required=False,
|
||||
widget=forms.widgets.SelectMultiple(
|
||||
attrs={"class": "form-select js-example-basic-multiple"}
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get("instance")
|
||||
# data = kwargs.get('data')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if instance is None:
|
||||
# This is an add operation, exclude the 'active' field
|
||||
self.fields.pop("active")
|
||||
|
||||
|
||||
class IAmPrincipalRoleAppResourceActionLinkForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = models.IAmRole
|
||||
fields = ["name", "active", "app_resource_action"]
|
||||
required = {"app_resource_action": False}
|
||||
|
||||
app_resource_action = forms.ModelMultipleChoiceField(
|
||||
queryset=models.IAmAppResourceActionLink.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get("instance")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if instance is None:
|
||||
self.fields.pop("active")
|
||||
@@ -1,25 +1,34 @@
|
||||
|
||||
# principal type constant
|
||||
PRINCIPAL_TYPE_USER = "user"
|
||||
PRINCIPAL_TYPE_ADMIN = "admin"
|
||||
PRINCIPAL_TYPE_SUBADMIN = "subadmin"
|
||||
|
||||
# principal source constant
|
||||
PRINCIPAL_SOURCE_APP = "app"
|
||||
PRINCIPAL_SOURCE_WEB = "web"
|
||||
PRINCIPAL_SOURCE_GOOGLE = "google"
|
||||
PRINCIPAL_SOURCE_APPLE = "apple"
|
||||
|
||||
# app action constant
|
||||
ACTION_CREATE = "create"
|
||||
ACTION_READ = "read"
|
||||
ACTION_UPDATE = "update"
|
||||
ACTION_DELETE = "delete"
|
||||
|
||||
|
||||
RESOURCE_MANAGE_DASHBOARD = "manage_dashboard"
|
||||
RESOURCE_MANAGE_IAM = "manage_iam"
|
||||
RESOURCE_MANAGE_CUSTOMER = "manage_customer"
|
||||
RESOURCE_MANAGE_WALLET = "manage_wallet"
|
||||
RESOURCE_MANAGE_PAYMENT = "manage_payment"
|
||||
RESOURCE_MANAGE_GAMES = "manage_games"
|
||||
RESOURCE_MANAGE_USER = "manage_user"
|
||||
|
||||
RESOURCE_MANAGE_SUPPORT = "manage_support"
|
||||
RESOURCE_MANAGE_CONTACT_US = "manage_contact_us"
|
||||
RESOURCE_MANAGE_TICKET = "manage_ticket"
|
||||
RESOURCE_MANAGE_CMS = "manage_cms"
|
||||
RESOURCE_MANAGE_REPORTS = "manage_reports"
|
||||
RESOURCE_MANAGE_COUPON = "manage_coupon"
|
||||
RESOURCE_MANAGE_FEEDBACK = "manage_feedback"
|
||||
RESOURCE_MANAGE_STOCK = "manage_stock"
|
||||
RESOURCE_MANAGE_NOTIFICATION = "manage_notification"
|
||||
|
||||
RESOURCE_MANAGE_CMS = "manage_cms"
|
||||
RESOURCE_MANAGE_FAQS = "manage_faqs"
|
||||
RESOURCE_MANAGE_T_C = "manage_tc"
|
||||
RESOURCE_MANAGE_PRIVACYPOLICY = "manage_privacypolicy"
|
||||
|
||||
|
||||
# These constants are used solely for managing the active and inactive state of pages
|
||||
170
module_iam/iam_fixture_script.py
Normal file
170
module_iam/iam_fixture_script.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from datetime import datetime
|
||||
|
||||
from .iam_constant import (
|
||||
PRINCIPAL_TYPE_USER,
|
||||
PRINCIPAL_TYPE_ADMIN,
|
||||
PRINCIPAL_TYPE_SUBADMIN,
|
||||
PRINCIPAL_SOURCE_APP,
|
||||
PRINCIPAL_SOURCE_WEB,
|
||||
PRINCIPAL_SOURCE_GOOGLE,
|
||||
PRINCIPAL_SOURCE_APPLE,
|
||||
ACTION_CREATE,
|
||||
ACTION_READ,
|
||||
ACTION_UPDATE,
|
||||
ACTION_DELETE,
|
||||
RESOURCE_MANAGE_DASHBOARD,
|
||||
RESOURCE_MANAGE_IAM,
|
||||
RESOURCE_MANAGE_USER,
|
||||
RESOURCE_MANAGE_CONTACT_US,
|
||||
RESOURCE_MANAGE_FEEDBACK,
|
||||
RESOURCE_MANAGE_FAQS,
|
||||
RESOURCE_MANAGE_T_C,
|
||||
RESOURCE_MANAGE_CMS,
|
||||
RESOURCE_MANAGE_PRIVACYPOLICY,
|
||||
RESOURCE_MANAGE_SUPPORT,
|
||||
RESOURCE_MANAGE_NOTIFICATION
|
||||
)
|
||||
|
||||
class IAMPrincipalType:
|
||||
ADMIN = PRINCIPAL_TYPE_ADMIN
|
||||
SUBADMIN = PRINCIPAL_TYPE_SUBADMIN
|
||||
USER = PRINCIPAL_TYPE_USER
|
||||
|
||||
categories = [
|
||||
ADMIN,
|
||||
SUBADMIN,
|
||||
USER,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def create_iam_principal_type_fixture_data():
|
||||
iam_category_fixture_data = []
|
||||
created_on = datetime.now().isoformat()
|
||||
modified_on = datetime.now().isoformat()
|
||||
for idx, category in enumerate(IAMPrincipalType.categories, start=1):
|
||||
iam_category_fixture_data.append(
|
||||
{
|
||||
"model": "module_iam.iamprincipaltype",
|
||||
"pk": idx,
|
||||
"fields": {
|
||||
"name": category,
|
||||
"label": category,
|
||||
"slug": category,
|
||||
"created_on": created_on,
|
||||
"modified_on": modified_on,
|
||||
},
|
||||
}
|
||||
)
|
||||
return iam_category_fixture_data
|
||||
|
||||
class IAMPrincipalSource:
|
||||
source = [
|
||||
PRINCIPAL_SOURCE_APP,
|
||||
PRINCIPAL_SOURCE_WEB,
|
||||
PRINCIPAL_SOURCE_GOOGLE,
|
||||
PRINCIPAL_SOURCE_APPLE
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def create_iam_principal_source_fixture_data():
|
||||
iam_principal_source_fixture_data = []
|
||||
created_on = datetime.now().isoformat()
|
||||
modified_on = datetime.now().isoformat()
|
||||
|
||||
for idx, principal_source in enumerate(IAMPrincipalSource.source, start=1,):
|
||||
iam_principal_source_fixture_data.append(
|
||||
{
|
||||
"model": "module_iam.iamprincipalsource",
|
||||
"pk": idx,
|
||||
"fields": {
|
||||
"name": principal_source,
|
||||
"label": principal_source,
|
||||
"slug": principal_source,
|
||||
"created_on": created_on,
|
||||
"modified_on": modified_on,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return iam_principal_source_fixture_data
|
||||
|
||||
class IAMActions:
|
||||
CREATE = ACTION_CREATE
|
||||
READ = ACTION_READ
|
||||
UPDATE = ACTION_UPDATE
|
||||
DELETE = ACTION_DELETE
|
||||
|
||||
actions = [
|
||||
CREATE,
|
||||
READ,
|
||||
UPDATE,
|
||||
DELETE,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def create_iam_action_fixture_data():
|
||||
iam_action_fixture_data = []
|
||||
created_on = datetime.now().isoformat()
|
||||
modified_on = datetime.now().isoformat()
|
||||
for idx, action in enumerate(IAMActions.actions, start=1):
|
||||
iam_action_fixture_data.append(
|
||||
{
|
||||
"model": "module_iam.iamappaction",
|
||||
"pk": idx,
|
||||
"fields": {
|
||||
"name": action,
|
||||
"label": action,
|
||||
"slug": action,
|
||||
"created_on": created_on,
|
||||
"modified_on": modified_on,
|
||||
},
|
||||
}
|
||||
)
|
||||
return iam_action_fixture_data
|
||||
|
||||
class IAMResources:
|
||||
IAM = RESOURCE_MANAGE_IAM
|
||||
USER = RESOURCE_MANAGE_USER
|
||||
SUPPORT = RESOURCE_MANAGE_SUPPORT
|
||||
CONTACT_US = RESOURCE_MANAGE_CONTACT_US
|
||||
FEEDBACK = RESOURCE_MANAGE_FEEDBACK
|
||||
CMS = RESOURCE_MANAGE_CMS
|
||||
FAQS = RESOURCE_MANAGE_FAQS
|
||||
T_C = RESOURCE_MANAGE_T_C
|
||||
PRIVACYPOLICY = RESOURCE_MANAGE_PRIVACYPOLICY
|
||||
NOTIFICATION = RESOURCE_MANAGE_NOTIFICATION
|
||||
|
||||
resources = [
|
||||
IAM,
|
||||
USER,
|
||||
SUPPORT,
|
||||
CONTACT_US,
|
||||
FEEDBACK,
|
||||
CMS,
|
||||
FAQS,
|
||||
T_C,
|
||||
PRIVACYPOLICY,
|
||||
NOTIFICATION,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def create_iam_resource_fixture_data():
|
||||
iam_resource_fixture_data = []
|
||||
created_on = datetime.now().isoformat()
|
||||
modified_on = datetime.now().isoformat()
|
||||
for idx, resource in enumerate(IAMResources.resources, start=1):
|
||||
iam_resource_fixture_data.append(
|
||||
{
|
||||
"model": "module_iam.iamappresource",
|
||||
"pk": idx,
|
||||
"fields": {
|
||||
"name": resource,
|
||||
"label": resource,
|
||||
"slug": resource,
|
||||
"created_on": created_on,
|
||||
"modified_on": modified_on,
|
||||
"action": [1, 2, 3, 4],
|
||||
},
|
||||
}
|
||||
)
|
||||
return iam_resource_fixture_data
|
||||
122
module_iam/management/commands/load_iam_fixture.py
Normal file
122
module_iam/management/commands/load_iam_fixture.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
from django.core.management.base import BaseCommand
|
||||
from module_iam.iam_fixture_script import IAMPrincipalType, IAMActions, IAMResources, IAMPrincipalSource
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Load IAM fixtures data"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
app_name = "module_iam"
|
||||
try:
|
||||
self.stdout.write(self.style.SUCCESS("IAM fixtures data loading started..."))
|
||||
|
||||
# Ensure the fixture directory exists
|
||||
fixture_directory = os.path.join(app_name, "fixtures")
|
||||
if not os.path.exists(fixture_directory):
|
||||
os.makedirs(fixture_directory)
|
||||
|
||||
# Generate IAM category fixture data
|
||||
principal_type_fixture_data = IAMPrincipalType.create_iam_principal_type_fixture_data()
|
||||
|
||||
# Specify the app name and fixture filename for category fixtures
|
||||
categories_fixture_filename = os.path.join(fixture_directory, "iam_principal_type_fixture.json")
|
||||
|
||||
principal_type_fixture_data_list = []
|
||||
with tqdm(total=len(principal_type_fixture_data), desc="Loading IAM principal type fixture") as pbar:
|
||||
for item in principal_type_fixture_data:
|
||||
principal_type_fixture_data_list.append(item)
|
||||
pbar.update(1)
|
||||
|
||||
# Dump category fixture data as JSON
|
||||
with open(categories_fixture_filename, "w") as fixture_file:
|
||||
json.dump(principal_type_fixture_data, fixture_file, indent=4)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"IAM category fixture data has been loaded successfully. Fixture file location: {categories_fixture_filename}")
|
||||
)
|
||||
|
||||
principal_source_fixture_data = IAMPrincipalSource.create_iam_principal_source_fixture_data()
|
||||
|
||||
# Specify the app name and fixture filename for source fixtures
|
||||
source_fixture_filename = os.path.join(fixture_directory, "iam_principal_source_fixture.json")
|
||||
|
||||
principal_source_fixture_data_list = []
|
||||
with tqdm(total=len(principal_source_fixture_data), desc="Loading IAM principal source fixture") as pbar:
|
||||
for item in principal_source_fixture_data:
|
||||
principal_source_fixture_data_list.append(item)
|
||||
pbar.update(1)
|
||||
|
||||
# Dump category fixture data as JSON
|
||||
with open(source_fixture_filename, "w") as fixture_file:
|
||||
json.dump(principal_source_fixture_data, fixture_file, indent=4)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"IAM category fixture data has been loaded successfully. Fixture file location: {categories_fixture_filename}")
|
||||
)
|
||||
|
||||
# Generate IAM action fixture data
|
||||
action_fixture_data = IAMActions.create_iam_action_fixture_data()
|
||||
|
||||
# Specify the fixture filename for action fixtures
|
||||
action_fixture_filename = os.path.join(fixture_directory, "iam_actions_fixture.json")
|
||||
|
||||
action_fixture_data_list = []
|
||||
with tqdm(total=len(action_fixture_data), desc="Loading IAM action fixture") as pbar:
|
||||
for item in action_fixture_data:
|
||||
action_fixture_data_list.append(item)
|
||||
pbar.update(1)
|
||||
|
||||
# Dump action fixture data as JSON
|
||||
with open(action_fixture_filename, "w") as fixture_file:
|
||||
json.dump(action_fixture_data, fixture_file, indent=4)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"IAM action fixture data has been loaded successfully. Fixture file location: {action_fixture_filename}")
|
||||
)
|
||||
|
||||
# Generate IAM resource fixture data
|
||||
resource_fixture_data = IAMResources.create_iam_resource_fixture_data()
|
||||
|
||||
# Specify the fixture filename for resource fixtures
|
||||
resource_fixture_filename = os.path.join(fixture_directory, "iam_resources_fixture.json")
|
||||
|
||||
resource_fixture_data_list = []
|
||||
with tqdm(total=len(resource_fixture_data), desc="Loading IAM resource fixture") as pbar:
|
||||
for item in resource_fixture_data:
|
||||
resource_fixture_data_list.append(item)
|
||||
pbar.update(1)
|
||||
|
||||
# Dump resource fixture data as JSON
|
||||
with open(resource_fixture_filename, "w") as fixture_file:
|
||||
json.dump(resource_fixture_data, fixture_file, indent=4)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f"IAM resource fixture data has been loaded successfully. Fixture file location: {resource_fixture_filename}")
|
||||
)
|
||||
|
||||
# Run the loaddata command to load the created fixtures
|
||||
loaddata_command_categories = f"python manage.py loaddata {categories_fixture_filename}"
|
||||
subprocess.run(loaddata_command_categories, shell=True)
|
||||
|
||||
loaddata_command_categories = f"python manage.py loaddata {source_fixture_filename}"
|
||||
subprocess.run(loaddata_command_categories, shell=True)
|
||||
|
||||
loaddata_command_actions = f"python manage.py loaddata {action_fixture_filename}"
|
||||
subprocess.run(loaddata_command_actions, shell=True)
|
||||
|
||||
loaddata_command_resources = f"python manage.py loaddata {resource_fixture_filename}"
|
||||
subprocess.run(loaddata_command_resources, shell=True)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("IAM fixtures data loading completed successfully.")
|
||||
)
|
||||
except Exception as e:
|
||||
# Handle exceptions here
|
||||
self.stderr.write(
|
||||
self.style.ERROR(f"IAM fixtures data loading failed: {str(e)}")
|
||||
)
|
||||
22
module_iam/migrations/0004_appversion.py
Normal file
22
module_iam/migrations/0004_appversion.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-11 07:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0003_alter_iamprincipal_gender'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AppVersion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('version', models.CharField(max_length=10)),
|
||||
('force_upgrade', models.BooleanField(default=False, help_text='Indicates whether a force upgrade is needed for this app version.')),
|
||||
('recommend_upgrade', models.BooleanField(default=False, help_text='Indicates whether a recommend upgrade is needed for this app version.')),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
module_iam/migrations/0005_alter_appversion_table.py
Normal file
17
module_iam/migrations/0005_alter_appversion_table.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-11 07:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0004_appversion'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelTable(
|
||||
name='appversion',
|
||||
table='app_version',
|
||||
),
|
||||
]
|
||||
19
module_iam/migrations/0006_alter_appversion_version.py
Normal file
19
module_iam/migrations/0006_alter_appversion_version.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-11 08:18
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0005_alter_appversion_table'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='version',
|
||||
field=models.CharField(max_length=10, validators=[django.core.validators.RegexValidator('^\\d+\\.\\d+\\.\\d+$')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-31 09:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0006_alter_appversion_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IAmPrincipalResourceLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('principal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='principal_resource_link_principal', to=settings.AUTH_USER_MODEL)),
|
||||
('principal_resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='principal_resource_link_resource', to='module_iam.iamappresource')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'iam_principal_resource_group_link',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='iamprincipal',
|
||||
name='principal_resource',
|
||||
field=models.ManyToManyField(related_name='principal_resources', through='module_iam.IAmPrincipalResourceLink', to='module_iam.iamappresource'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.0.2 on 2024-04-04 10:10
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0007_iamprincipalresourcelink_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='appversion',
|
||||
name='device_type',
|
||||
field=models.CharField(choices=[('apple', 'apple'), ('android', 'android')], default=1, max_length=10),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='version',
|
||||
field=models.CharField(max_length=10, validators=[django.core.validators.RegexValidator('^\\d+\\.\\d+$')]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.2 on 2024-04-05 07:06
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0008_appversion_device_type_alter_appversion_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='device_type',
|
||||
field=models.CharField(choices=[('apple', 'apple'), ('android', 'android')], max_length=10, verbose_name='Device Type (apple / android)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='force_upgrade',
|
||||
field=models.BooleanField(default=False, help_text='Indicates whether a force upgrade is needed for this app version.', verbose_name='Force Upgrade'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='recommend_upgrade',
|
||||
field=models.BooleanField(default=False, help_text='Indicates whether a recommend upgrade is needed for this app version.', verbose_name='Recommend Upgrade'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='version',
|
||||
field=models.CharField(max_length=5, validators=[django.core.validators.RegexValidator('^\\d+\\.\\d+\\.\\d+$')], verbose_name='Version'),
|
||||
),
|
||||
]
|
||||
18
module_iam/migrations/0010_alter_appversion_device_type.py
Normal file
18
module_iam/migrations/0010_alter_appversion_device_type.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.2 on 2024-05-21 14:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_iam', '0009_alter_appversion_device_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='appversion',
|
||||
name='device_type',
|
||||
field=models.CharField(choices=[('ios', 'ios'), ('android', 'android')], max_length=10, verbose_name='Device Type (ios / android)'),
|
||||
),
|
||||
]
|
||||
@@ -1,21 +1,28 @@
|
||||
from collections.abc import Iterable
|
||||
import datetime
|
||||
import random
|
||||
import string
|
||||
from collections.abc import Iterable
|
||||
|
||||
# from manage_wallets.models import Wallet, Transaction, TransactionStatus, TransactionType
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AbstractUser, BaseUserManager
|
||||
from django.core.cache import cache
|
||||
# from .utils import UserContext
|
||||
from django.core.validators import (MaxValueValidator, MinValueValidator,
|
||||
RegexValidator)
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
# from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from module_project.utils import RandomGenerator
|
||||
from .resource_action import PRINCIPAL_TYPE_USER, PRINCIPAL_TYPE_ADMIN
|
||||
|
||||
# from .utils import UserContext
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
|
||||
from .iam_constant import (PRINCIPAL_SOURCE_APP, PRINCIPAL_SOURCE_APPLE,
|
||||
PRINCIPAL_SOURCE_GOOGLE, PRINCIPAL_SOURCE_WEB,
|
||||
PRINCIPAL_TYPE_ADMIN, PRINCIPAL_TYPE_SUBADMIN,
|
||||
PRINCIPAL_TYPE_USER)
|
||||
|
||||
# from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
|
||||
class BaseModel(models.Model):
|
||||
@@ -102,14 +109,60 @@ class IAmPrincipalType(MasterModel):
|
||||
db_table = "iam_principal_type"
|
||||
|
||||
@classmethod
|
||||
def get_principal_type(cls, type):
|
||||
return cls.objects.filter(name=type).first()
|
||||
def get_principal_type(cls, name):
|
||||
cache_key = f"principal_{name}"
|
||||
principal = cache.get(cache_key)
|
||||
|
||||
if not principal:
|
||||
principal = cls.objects.filter(name=name).first()
|
||||
cache.set(cache_key, principal, timeout=60 * 15) # Cache for 15 minutes
|
||||
|
||||
return principal
|
||||
|
||||
@classmethod
|
||||
def get_principal_user(cls):
|
||||
return cls.get_principal_type(PRINCIPAL_TYPE_USER)
|
||||
|
||||
@classmethod
|
||||
def get_principal_admin(cls):
|
||||
return cls.get_principal_type(PRINCIPAL_TYPE_ADMIN)
|
||||
|
||||
@classmethod
|
||||
def get_principal_subadmin(cls):
|
||||
return cls.get_principal_type(PRINCIPAL_TYPE_SUBADMIN)
|
||||
|
||||
|
||||
class IAmPrincipalSource(MasterModel):
|
||||
class Meta:
|
||||
db_table = "iam_principal_source"
|
||||
|
||||
@classmethod
|
||||
def get_principal_source(cls, name):
|
||||
cache_key = f"principal_{name}"
|
||||
principal = cache.get(cache_key)
|
||||
|
||||
if not principal:
|
||||
principal = cls.objects.filter(name=name).first()
|
||||
cache.set(cache_key, principal, timeout=60 * 15) # Cache for 15 minutes
|
||||
|
||||
return principal
|
||||
|
||||
@classmethod
|
||||
def get_principal_web(cls):
|
||||
return cls.get_principal_source(PRINCIPAL_SOURCE_WEB)
|
||||
|
||||
@classmethod
|
||||
def get_principal_app(cls):
|
||||
return cls.get_principal_source(PRINCIPAL_SOURCE_APP)
|
||||
|
||||
@classmethod
|
||||
def get_principal_google(cls):
|
||||
return cls.get_principal_source(PRINCIPAL_SOURCE_GOOGLE)
|
||||
|
||||
@classmethod
|
||||
def get_principal_apple(cls):
|
||||
return cls.get_principal_source(PRINCIPAL_SOURCE_APPLE)
|
||||
|
||||
|
||||
class IAmAppAction(MasterModel):
|
||||
class Meta:
|
||||
@@ -239,7 +292,7 @@ class IAmPrincipalManager(BaseUserManager):
|
||||
extra_fields.setdefault("is_staff", True)
|
||||
extra_fields.setdefault("is_superuser", True)
|
||||
extra_fields.setdefault("phone_no", "+919978895465")
|
||||
extra_fields.setdefault("gender", "M")
|
||||
extra_fields.setdefault("gender", "Male")
|
||||
extra_fields.setdefault("date_of_birth", timezone.now())
|
||||
extra_fields.setdefault("created_by", None)
|
||||
extra_fields.setdefault("created_on", timezone.now())
|
||||
@@ -298,7 +351,17 @@ class IAmPrincipal(AbstractUser):
|
||||
related_name="principal_groups",
|
||||
)
|
||||
register_complete = models.BooleanField(default=False)
|
||||
player_id = models.CharField(max_length=255, null=True, blank=True, help_text="OneSignal player id for push notification")
|
||||
player_id = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="OneSignal player id for push notification",
|
||||
)
|
||||
principal_resource = models.ManyToManyField(
|
||||
IAmAppResource,
|
||||
through="IAmPrincipalResourceLink",
|
||||
related_name="principal_resources",
|
||||
)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = []
|
||||
@@ -312,6 +375,21 @@ class IAmPrincipal(AbstractUser):
|
||||
return f"{self.email}"
|
||||
|
||||
|
||||
class IAmPrincipalResourceLink(models.Model):
|
||||
principal = models.ForeignKey(
|
||||
IAmPrincipal,
|
||||
related_name="principal_resource_link_principal",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
principal_resource = models.ForeignKey(
|
||||
IAmAppResource,
|
||||
related_name="principal_resource_link_resource",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "iam_principal_resource_group_link"
|
||||
|
||||
class IAmPrincipalGroupLink(models.Model):
|
||||
principal = models.ForeignKey(
|
||||
IAmPrincipal,
|
||||
@@ -367,3 +445,20 @@ class IAmPrincipalBiometric(BaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.principal.first_name}:{self.biometric_type}"
|
||||
|
||||
|
||||
class AppVersion(models.Model):
|
||||
DEVICE_CHOICES = [
|
||||
('ios', 'ios'),
|
||||
('android', 'android'),
|
||||
]
|
||||
device_type = models.CharField(max_length=10, choices=DEVICE_CHOICES, verbose_name='Device Type (ios / android)')
|
||||
version = models.CharField(max_length=5, validators=[RegexValidator(r'^\d+\.\d+\.\d+$')], verbose_name='Version')
|
||||
force_upgrade = models.BooleanField(default=False, verbose_name='Force Upgrade', help_text='Indicates whether a force upgrade is needed for this app version.')
|
||||
recommend_upgrade = models.BooleanField(default=False, verbose_name='Recommend Upgrade', help_text='Indicates whether a recommend upgrade is needed for this app version.')
|
||||
|
||||
class Meta:
|
||||
db_table = "app_version"
|
||||
|
||||
def __str__(self):
|
||||
return self.version
|
||||
87
module_iam/permission.py
Normal file
87
module_iam/permission.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from functools import wraps
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import render
|
||||
from . import models
|
||||
from django.db.models import Q
|
||||
# import logging
|
||||
|
||||
# logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# class CustomPermissionRequiredMixin:
|
||||
# resource = None
|
||||
# action = None
|
||||
|
||||
# def has_custom_permission(self, user, resource, action):
|
||||
# if not self.resource or not self.action:
|
||||
# raise AttributeError("Resource and action attributes must be defined in the view")
|
||||
|
||||
# # if not request.user.is_authenticated:
|
||||
# # return self.handle_no_permission()
|
||||
|
||||
# if user.is_superuser: # will chagne to principal type for admin
|
||||
# return True
|
||||
|
||||
# permission_query = Q(
|
||||
# principal_group__role__app_resource_action__app_resource__name=resource,
|
||||
# principal_group__role__app_resource_action__app_action__name=action
|
||||
# )
|
||||
# return models.IAmPrincipal.objects.filter(permission_query, id=user.id).exists()
|
||||
|
||||
# def dispatch(self, request, *args, **kwargs):
|
||||
# if not self.has_custom_permission(request.user, self.resource, self.action):
|
||||
# # logger.warning(f"Permission denied for user {request.user} accessing {self.resource}:{self.action}")
|
||||
# raise PermissionDenied("You do not have permission to access this resource.")
|
||||
# return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
# @classmethod
|
||||
# def as_decorator(cls, resource, action):
|
||||
# def decorator(view_func):
|
||||
# @wraps(view_func)
|
||||
# def _wrapped_view(request, *args, **kwargs):
|
||||
# instance = cls()
|
||||
# instance.resource = resource
|
||||
# instance.action = action
|
||||
# if not instance.has_custom_permission(request.user, instance.resource, instance.action):
|
||||
# raise PermissionDenied("You do not have permission to access this resource.")
|
||||
# return view_func(request, *args, **kwargs)
|
||||
# return _wrapped_view
|
||||
# return decorator
|
||||
|
||||
|
||||
class ResourcePermissionRequiredMixin:
|
||||
resource = None
|
||||
|
||||
def has_resource_permission(self, user, resource):
|
||||
# if not self.resource or resource:
|
||||
# raise AttributeError("Resource attributes must be defined in the view")
|
||||
|
||||
# if not request.user.is_authenticated:
|
||||
# return self.handle_no_permission()
|
||||
|
||||
if user.is_superuser: # will chagne to principal type for admin
|
||||
return True
|
||||
|
||||
permission_query = Q(
|
||||
principal_resource__name=resource,
|
||||
)
|
||||
return models.IAmPrincipal.objects.filter(permission_query, id=user.id).exists()
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.has_resource_permission(request.user, self.resource):
|
||||
# logger.warning(f"Permission denied for user {request.user} accessing {self.resource}:{self.action}")
|
||||
return render(request, "module_iam/permission_denied.html")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def as_decorator(cls, resource):
|
||||
def decorator(view_func):
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
instance = cls()
|
||||
instance.resource = resource
|
||||
if not instance.has_resource_permission(request.user, instance.resource):
|
||||
return render(request, "module_iam/permission_denied.html")
|
||||
return view_func(request, *args, **kwargs)
|
||||
return _wrapped_view
|
||||
return decorator
|
||||
27
module_iam/templatetags/resource_permission.py
Normal file
27
module_iam/templatetags/resource_permission.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from django import template
|
||||
from module_iam.permission import ResourcePermissionRequiredMixin
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='has_resource_permission')
|
||||
def has_resource_permission(user, resource):
|
||||
"""
|
||||
Check if a user has a specific resource and action permission.
|
||||
|
||||
Args:
|
||||
user (User): The user to check for permission.
|
||||
resource_action (str): The resource and action string (e.g., "resource_name.action_name").
|
||||
|
||||
Returns:
|
||||
bool: True if the user has the specified permission, False otherwise.
|
||||
|
||||
Example usage in a template:
|
||||
{% if user|has_resource_permission:"article" %}
|
||||
<!-- Render content for users with permission -->
|
||||
{% else %}
|
||||
<!-- Render content for users without permission -->
|
||||
{% endif %}
|
||||
"""
|
||||
# resource, action = resource_action.split(".")
|
||||
return ResourcePermissionRequiredMixin().has_resource_permission(user, resource)
|
||||
@@ -1,8 +1,41 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "module_iam"
|
||||
|
||||
urlpatterns = [
|
||||
path('dashboard/', views.DashboardView.as_view(), name="dashboard")
|
||||
path('dashboard/', views.DashboardView.as_view(), name="dashboard"),
|
||||
|
||||
# path('principal/', views.PrincipalListView.as_view(), name="principal_list"),
|
||||
path('principal/add/', views.PrincipalCreateOrUpdateView.as_view(), name="principal_add"),
|
||||
path('principal/edit/<int:pk>', views.PrincipalCreateOrUpdateView.as_view(), name="principal_edit"),
|
||||
# path('principal/delete/<int:pk>', views.PrincipalDeleteView.as_view(), name="principal_delete"),
|
||||
path('principal/archive/', views.PrincipalArchiveView.as_view(), name="principal_archive"),
|
||||
path('principal/archive/list/', views.PrincipalArchiveListJsonView.as_view(), name="principal_archive_list"),
|
||||
path('principal/resource/permission/edit/<int:pk>/', views.PrincipalResourcePermissionEditView.as_view(), name="principal_resource_permission_edit"),
|
||||
|
||||
path('principal/group/link/', views.PrincipalGroupLinkView.as_view(), name="principal_group_link"),
|
||||
path('principal/group/link/list/admin/', views.PrincipalGroupLinkAdminListJsonView.as_view(), name="principal_group_link_list"),
|
||||
path('principal/group/link/list/subadmin/', views.PrincipalGroupLinkSubAdminListJsonView.as_view(), name="principal_group_link_list_sub"),
|
||||
path('principal/group/link/edit/<int:pk>/', views.PrincipalGroupLinkEditView.as_view(), name="principal_group_link_edit"),
|
||||
path('principal/group/link/action/', views.PrincipalGroupLinkActionView.as_view(), name="principal_group_link_action"),
|
||||
|
||||
path('principal/group/', views.PrincipalGroupView.as_view(), name="principal_group"),
|
||||
path('principal/group/list/', views.PrincipalGroupListJsonView.as_view(), name="principal_group_list"),
|
||||
path('principal/group/add/', views.PrincipalGroupCreateOrUpdateView.as_view(), name="principal_group_add"),
|
||||
path('principal/group/edit/<int:pk>/', views.PrincipalGroupCreateOrUpdateView.as_view(), name="principal_group_edit"),
|
||||
path('principal/group/action/', views.PrincipalGroupActionView.as_view(), name="principal_group_action"),
|
||||
path('principal/group/archive/list/', views.PrincipalGroupArchiveView.as_view(), name="principal_group_archive"),
|
||||
|
||||
path('principal/role/', views.AppRoleView.as_view(), name="role"),
|
||||
path('principal/role/list/', views.AppRoleListJsonView.as_view(), name="role_list"),
|
||||
path('principal/role/add/', views.AppRoleCreateOrUpdateView.as_view(), name="role_add"),
|
||||
path('principal/role/edit/<int:pk>/', views.AppRoleCreateOrUpdateView.as_view(), name="role_edit"),
|
||||
path('principal/role/action/', views.AppRoleActionView.as_view(), name="role_action"),
|
||||
path('principal/role/archive/list/', views.AppRoleArchiveView.as_view(), name="role_archive"),
|
||||
|
||||
path("profile/", views.PrincipalProfileView.as_view(), name="profile_details"),
|
||||
path("profile/edit/", views.PrincipalProfileEditView.as_view(), name="profile_details_edit")
|
||||
|
||||
]
|
||||
|
||||
@@ -1,7 +1,601 @@
|
||||
from django.shortcuts import render
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.db.models.base import Model as Model
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from django_datatables_view.base_datatable_view import BaseDatatableView
|
||||
|
||||
from module_iam import iam_constant, permission
|
||||
from module_project import constants
|
||||
from module_project.mixins import ActionMixin
|
||||
from module_project.utils import JsonResponseUtil
|
||||
|
||||
from .forms import (CustomAuthenticationForm, IAmPrincipalForm, IAmPrincipalResourceLinkForm,
|
||||
IAmPrincipalGroupLinkForm, IAmPrincipalGroupRoleLinkForm,
|
||||
IAmPrincipalRoleAppResourceActionLinkForm, ProfileEditForm)
|
||||
from .models import (IAmAppResourceActionLink, IAmPrincipal, IAmPrincipalGroup,
|
||||
IAmPrincipalType, IAmRole)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create your views here.
|
||||
|
||||
class DashboardView(generic.TemplateView):
|
||||
template_name = "base_structure/layout/dashboard.html"
|
||||
|
||||
class DashboardView(LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
resource = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
template_name = "base_structure/layout/dashboard.html"
|
||||
|
||||
def get_user_count(self):
|
||||
obj = IAmPrincipal.objects.all()
|
||||
# Count active users
|
||||
active_user_count = obj.filter(is_active=True).count()
|
||||
# Count total users
|
||||
total_user_count = obj.count()
|
||||
return active_user_count, total_user_count
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
active_user_count, total_user_count = self.get_user_count()
|
||||
context["active_user_count"] = active_user_count
|
||||
context["total_user_count"] = total_user_count
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class PrincipalCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL
|
||||
resource = iam_constant.RESOURCE_IAM_PRINCIPAL
|
||||
model = IAmPrincipal
|
||||
form_class = IAmPrincipalForm
|
||||
template_name = "module_iam/iam_principal_add.html"
|
||||
success_url = reverse_lazy("module_iam:principal_group_link")
|
||||
success_message = "Saved Successfully"
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Edit" if self.object else "Add",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
print(request.POST)
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
try:
|
||||
if form.is_valid():
|
||||
principal = form.save(commit=False)
|
||||
|
||||
# Check if it's a new object (create action) or an existing one (update action)
|
||||
if not principal.pk: # pk is None for new objects
|
||||
principal.created_by = request.user
|
||||
principal.modified_by = request.user
|
||||
principal.modified_on = datetime.now()
|
||||
|
||||
# Save the object
|
||||
principal.save()
|
||||
|
||||
messages.success(request, "Form submitted successfully")
|
||||
return redirect(self.success_url)
|
||||
except Exception as e:
|
||||
self.error_message = constants.ERROR_OCCURR.format(str(e))
|
||||
print(self.error_message)
|
||||
messages.error(request, self.error_message)
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, template_name=self.template_name, context=context)
|
||||
|
||||
|
||||
class PrincipalArchiveListJsonView(BaseDatatableView):
|
||||
model = IAmPrincipal
|
||||
columns = ["id", "first_name", "email", "is_active"]
|
||||
order_columns = ["id", "first_name", "email", "is_active"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get("deleted_flag", False)
|
||||
return self.model.objects.filter(
|
||||
deleted=deleted_flag,
|
||||
principal_type__name__in=(
|
||||
iam_constant.PRINCIPAL_TYPE_ADMIN,
|
||||
iam_constant.PRINCIPAL_TYPE_SUBADMIN,
|
||||
),
|
||||
)
|
||||
|
||||
def render_column(self, row, column):
|
||||
if column == "principal_type_name":
|
||||
return row.principal_type.name if row.principal_type else None
|
||||
return super().render_column(row, column)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(first_name__icontains=search_value)
|
||||
| Q(email__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
class PrincipalResourcePermissionEditView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL
|
||||
resource = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
|
||||
model = IAmPrincipal
|
||||
template_name = "module_iam/iam_principal_resource_permission_edit.html"
|
||||
form_class = IAmPrincipalResourceLinkForm
|
||||
success_url = reverse_lazy("module_iam:principal_group_link")
|
||||
success_message = "Record Updated Successfully"
|
||||
error_message = "An error occurred while saving the data"
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
form.save()
|
||||
messages.success(request, self.success_message)
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class PrincipalGroupLinkView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
|
||||
resource = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
|
||||
model = IAmPrincipal
|
||||
template_name = "module_iam/iam_principal_group_link.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class PrincipalArchiveView(PrincipalGroupLinkView):
|
||||
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL
|
||||
resource = iam_constant.RESOURCE_IAM_PRINCIPAL
|
||||
action = None
|
||||
template_name = "module_iam/iam_principal_archive.html"
|
||||
|
||||
|
||||
class PrincipalGroupLinkAdminListJsonView(BaseDatatableView):
|
||||
model = IAmPrincipal
|
||||
columns = ["id", "first_name", "email", "is_active"]
|
||||
order_columns = ["id", "first_name", "email", "is_active"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get("deleted_flag", False)
|
||||
return self.model.objects.filter(
|
||||
deleted=deleted_flag, principal_type__name=iam_constant.PRINCIPAL_TYPE_ADMIN
|
||||
)
|
||||
|
||||
def render_column(self, row, column):
|
||||
if column == "principal_type_name":
|
||||
return row.principal_type.name if row.principal_type else None
|
||||
return super().render_column(row, column)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(first_name__icontains=search_value)
|
||||
| Q(email__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class PrincipalGroupLinkSubAdminListJsonView(permission.ResourcePermissionRequiredMixin, BaseDatatableView):
|
||||
model = IAmPrincipal
|
||||
columns = ["id", "first_name", "email", "is_active"]
|
||||
order_columns = ["id", "first_name", "email", "is_active"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get("deleted_flag", False)
|
||||
return self.model.objects.filter(
|
||||
deleted=deleted_flag,
|
||||
principal_type__name=iam_constant.PRINCIPAL_TYPE_SUBADMIN,
|
||||
)
|
||||
|
||||
def render_column(self, row, column):
|
||||
if column == "principal_type_name":
|
||||
return row.principal_type.name if row.principal_type else None
|
||||
if column == "permission":
|
||||
return [{"name": resource.name} for resource in row.principal_resource.all()]
|
||||
return super().render_column(row, column)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(first_name__icontains=search_value)
|
||||
| Q(email__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class PrincipalGroupLinkEditView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
|
||||
resource = iam_constant.RESOURCE_IAM_PRINCIPAL_GROUP
|
||||
model = IAmPrincipal
|
||||
template_name = "module_iam/iam_principal_group_link_edit.html"
|
||||
form_class = IAmPrincipalGroupLinkForm
|
||||
success_url = reverse_lazy("module_iam:principal_group_link")
|
||||
success_message = "Record Updated Successfully"
|
||||
error_message = "An error occurred while saving the data"
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
form.save()
|
||||
messages.success(request, self.success_message)
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class PrincipalGroupLinkActionView(generic.View):
|
||||
model = IAmPrincipal
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if self.model is None:
|
||||
raise NotImplementedError(
|
||||
"Subclasses of BaseActionView must define a 'model' attribute."
|
||||
)
|
||||
|
||||
action = request.POST.get("action") # 'archive', 'active', or 'unarchive'
|
||||
ids = request.POST.getlist("ids[]") # List of IDs to perform action on
|
||||
active = request.POST.get("active")
|
||||
print(f"arhive action {action} and id is {ids} and active data is {active}")
|
||||
if action == "archive":
|
||||
# Update 'deleted' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=True, is_active=False)
|
||||
message = "Record archived successfully."
|
||||
elif action == "active":
|
||||
# Update 'active' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(is_active=active.capitalize())
|
||||
message = "Record updated successfully."
|
||||
elif action == "unarchive":
|
||||
# Update 'deleted' field to False for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=False)
|
||||
message = "Record unarchived successfully."
|
||||
else:
|
||||
return JsonResponseUtil.error(message="Invalid Action")
|
||||
|
||||
return JsonResponseUtil.success(message=message)
|
||||
|
||||
|
||||
class PrincipalGroupView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_IAM_GROUP
|
||||
resource = iam_constant.RESOURCE_IAM_GROUP
|
||||
model = IAmPrincipalGroup
|
||||
template_name = "module_iam/iam_group.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class PrincipalGroupListJsonView(BaseDatatableView):
|
||||
model = IAmPrincipalGroup
|
||||
columns = ["id", "name", "active"]
|
||||
order_columns = ["id", "name", "active"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get("deleted_flag", False)
|
||||
return self.model.objects.filter(deleted=deleted_flag)
|
||||
|
||||
def render_column(self, row, column):
|
||||
if column == "roles":
|
||||
return [{"name": role.name} for role in row.role.all()]
|
||||
return super().render_column(row, column)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value) | Q(name__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class PrincipalGroupCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_IAM_GROUP
|
||||
resource = iam_constant.RESOURCE_IAM_GROUP
|
||||
page_title = "Principal Group"
|
||||
model = IAmPrincipalGroup
|
||||
template_name = "module_iam/iam_group_add.html"
|
||||
form_class = IAmPrincipalGroupRoleLinkForm
|
||||
success_url = reverse_lazy("module_iam:principal_group")
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Add" if not self.object else "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
form.save()
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class PrincipalGroupActionView(ActionMixin):
|
||||
model = IAmPrincipalGroup
|
||||
|
||||
|
||||
class PrincipalGroupArchiveView(PrincipalGroupView):
|
||||
template_name = "module_iam/iam_group_archive_list.html"
|
||||
|
||||
class AppRoleView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_IAM_ROLE
|
||||
resource = iam_constant.RESOURCE_IAM_ROLE
|
||||
model = IAmRole
|
||||
template_name = "module_iam/iam_role.html"
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class AppRoleListJsonView(BaseDatatableView):
|
||||
model = IAmRole
|
||||
columns = ["id", "name", "active", "resources"]
|
||||
order_columns = ["id", "name"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get("deleted_flag", False)
|
||||
return (
|
||||
super(AppRoleListJsonView, self)
|
||||
.get_initial_queryset()
|
||||
.prefetch_related(
|
||||
"app_resource_action",
|
||||
"app_resource_action__app_resource",
|
||||
"app_resource_action__app_action",
|
||||
)
|
||||
.filter(deleted=deleted_flag)
|
||||
)
|
||||
|
||||
def render_column(self, row, column):
|
||||
if column == "resources":
|
||||
resources = {}
|
||||
# Loop through all the app_resource_action links for the current ro
|
||||
for link in row.app_resource_action.all():
|
||||
resource = link.app_resource.name
|
||||
action = link.app_action.name
|
||||
# If the resource is already in the dictionary, append the action to the list of actions
|
||||
if resource in resources:
|
||||
resources[resource].append(action)
|
||||
# Otherwise, add the resource to the dictionary with a list containing the action
|
||||
else:
|
||||
resources[resource] = [action]
|
||||
return resources
|
||||
return super().render_column(row, column)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(name__icontains=search_value)
|
||||
| Q(app_resource_action__app_resource__name__icontains=search_value)
|
||||
| Q(app_resource_action__app_action__name__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class AppRoleCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_IAM_ROLE
|
||||
resource = iam_constant.RESOURCE_IAM_ROLE
|
||||
model = IAmRole
|
||||
template_name = "module_iam/iam_role_add.html"
|
||||
form_class = IAmPrincipalRoleAppResourceActionLinkForm
|
||||
success_url = reverse_lazy("module_iam:role")
|
||||
success_message = "Saved Successfully"
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
f"Record {'Created' if not self.object else 'Updated'} Successfully"
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Add" if not self.object else "Edit",
|
||||
"app_resource_action": IAmAppResourceActionLink.objects.generate_app_resource_action_data(),
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
except Exception as e:
|
||||
messages.error(request, str(e))
|
||||
return redirect(self.success_url)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
form.save()
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.success_url)
|
||||
except Exception as e:
|
||||
messages.error(self.request, str(e))
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class AppRoleActionView(LoginRequiredMixin, ActionMixin):
|
||||
model = IAmRole
|
||||
|
||||
|
||||
class AppRoleArchiveView(AppRoleView):
|
||||
template_name = "module_iam/iam_role_archive.html"
|
||||
|
||||
|
||||
class PrincipalProfileView( LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
resource = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
model = IAmPrincipal
|
||||
template_name = "module_iam/profile_details.html"
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
user = self.request.user.id
|
||||
return get_object_or_404(
|
||||
self.model.objects.select_related("principal_type", "principal_source"),
|
||||
pk=user,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
context["data_obj"] = self.get_object()
|
||||
return context
|
||||
|
||||
|
||||
class PrincipalProfileEditView(LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
resource = iam_constant.RESOURCE_MANAGE_DASHBOARD
|
||||
model = IAmPrincipal
|
||||
template_name = "module_iam/profile_details_edit.html"
|
||||
form_class = ProfileEditForm
|
||||
success_url = reverse_lazy("module_iam:profile_details")
|
||||
success_message = "Saved Successfully"
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
f"Record {'Created' if not self.object else 'Updated'} Successfully"
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
# "page_name": self.page_name,
|
||||
"operation": "Edit",
|
||||
"page_name": self.page_name,
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# try:
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.form_class(request.POST, request.FILES, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
form.save()
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.success_url)
|
||||
|
||||
9
module_notification/api/serializers.py
Normal file
9
module_notification/api/serializers.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
from ..models import InAppNotification
|
||||
|
||||
class InAppNotificationSerializer(serializers.ModelSerializer):
|
||||
created_on = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
|
||||
|
||||
class Meta:
|
||||
model = InAppNotification
|
||||
fields = ['id', 'message', 'is_read', 'created_on']
|
||||
11
module_notification/api/urls.py
Normal file
11
module_notification/api/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("count/", views.InAppNotificationCountAPIView.as_view(), name="inapp_notification_count"),
|
||||
path("list/", views.InAppNotificationListAPIView.as_view(), name="inapp_notification_list"),
|
||||
path("read/", views.InAppNotificationReadAPIView.as_view(), name="inapp_notification_read"),
|
||||
|
||||
]
|
||||
49
module_notification/api/views.py
Normal file
49
module_notification/api/views.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from module_project.utils import ApiResponse
|
||||
from module_project import constants
|
||||
|
||||
from .serializers import InAppNotificationSerializer
|
||||
|
||||
from ..models import InAppNotification
|
||||
|
||||
|
||||
class InAppNotificationCountAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = InAppNotification
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
count = InAppNotification.pending_read_count(user=request.user)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data={"count": count})
|
||||
|
||||
|
||||
class InAppNotificationListAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = InAppNotification
|
||||
serializer_class = InAppNotificationSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
obj = InAppNotification.latest_15(user=request.user)
|
||||
serializer_obj = self.serializer_class(obj, many=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS, data=serializer_obj.data)
|
||||
|
||||
|
||||
class InAppNotificationReadAPIView(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
model = InAppNotification
|
||||
serializer_class = InAppNotificationSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
obj = InAppNotification.objects.filter(user=request.user).update(is_read=True)
|
||||
return ApiResponse.success(message=constants.SUCCESS)
|
||||
9
module_notification/forms.py
Normal file
9
module_notification/forms.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django import forms
|
||||
|
||||
from .models import PushNotification
|
||||
|
||||
|
||||
class PushNotificationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = PushNotification
|
||||
fields = ('title', 'message')
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from ...tasks import notification_for_meal_and_medication
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Sends notifications to users'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
notification_for_meal_and_medication()
|
||||
36
module_notification/migrations/0001_initial.py
Normal file
36
module_notification/migrations/0001_initial.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-05 18:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PushNotification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('deleted', models.BooleanField(default=False)),
|
||||
('created_on', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_on', models.DateTimeField(auto_now=True)),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('banner_image', models.ImageField(blank=True, null=True, upload_to='push_notification_images/')),
|
||||
('message', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'push_notification',
|
||||
},
|
||||
),
|
||||
]
|
||||
34
module_notification/migrations/0002_inappnotification.py
Normal file
34
module_notification/migrations/0002_inappnotification.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-30 18:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_notification', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InAppNotification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('deleted', models.BooleanField(default=False)),
|
||||
('created_on', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_on', models.DateTimeField(auto_now=True)),
|
||||
('message', models.CharField(max_length=255)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_on'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-30 18:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('module_notification', '0002_inappnotification'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelTable(
|
||||
name='inappnotification',
|
||||
table='inapp_notification',
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,37 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
from module_iam.models import BaseModel, IAmPrincipal
|
||||
|
||||
|
||||
class InAppNotification(BaseModel):
|
||||
user = models.ForeignKey(IAmPrincipal, on_delete=models.CASCADE, related_name='notifications')
|
||||
message = models.CharField(max_length=255)
|
||||
is_read = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
db_table = "inapp_notification"
|
||||
ordering = ['-created_on']
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
@classmethod
|
||||
def latest_15(cls, user):
|
||||
return cls.objects.filter(user=user).order_by('-created_on')[:15]
|
||||
|
||||
@classmethod
|
||||
def pending_read_count(cls, user):
|
||||
return cls.objects.filter(user=user, is_read=False).count()
|
||||
|
||||
|
||||
class PushNotification(BaseModel):
|
||||
title = models.CharField(max_length=255)
|
||||
banner_image = models.ImageField(upload_to='push_notification_images/', blank=True, null=True)
|
||||
message = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "push_notification"
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
168
module_notification/tasks.py
Normal file
168
module_notification/tasks.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from datetime import datetime, timedelta
|
||||
import itertools
|
||||
|
||||
from module_project.service import OneSignalService
|
||||
from .models import InAppNotification
|
||||
from module_iam.models import IAmPrincipal, IAmPrincipalType
|
||||
from module_activity.models import MealRecord, Bowel, MealSymptomRecord, Medication
|
||||
|
||||
|
||||
def notification_for_meal():
|
||||
notification_message = "Have you eaten yet? It's been a while since you logged a meal."
|
||||
current_date = datetime.now()
|
||||
fifteen_days_ago = current_date - timedelta(days=15)
|
||||
|
||||
users = IAmPrincipal.objects.filter(
|
||||
last_login__gte=fifteen_days_ago,
|
||||
principal_type=IAmPrincipalType.get_principal_user(),
|
||||
).values_list("id", flat=True)
|
||||
|
||||
meal_obj = MealRecord.objects.filter(date=current_date).values_list(
|
||||
"principal", flat=True
|
||||
)
|
||||
|
||||
# Remove IDs of users who have recorded meals for the current day
|
||||
users_without_meals = set(users) - set(meal_obj)
|
||||
print(f"user id {set(users)}")
|
||||
print(
|
||||
f"userwithoutmeal {users_without_meals}"
|
||||
)
|
||||
|
||||
player_ids = list(
|
||||
IAmPrincipal.objects.filter(
|
||||
id__in=users_without_meals
|
||||
).values_list("player_id", flat=True)
|
||||
)
|
||||
# removing none from list
|
||||
player_ids = list(itertools.filterfalse(lambda x: x is None, player_ids))
|
||||
|
||||
notifications_to_create = []
|
||||
if users_without_meals:
|
||||
for user_id in users_without_meals:
|
||||
message = notification_message
|
||||
notifications_to_create.append(
|
||||
InAppNotification(user_id=user_id, message=message)
|
||||
)
|
||||
|
||||
# Bulk create notifications
|
||||
if notifications_to_create:
|
||||
InAppNotification.objects.bulk_create(notifications_to_create)
|
||||
|
||||
try:
|
||||
notification = OneSignalService()
|
||||
response = notification.send_notification(
|
||||
headings="Meal",
|
||||
contents=notification_message,
|
||||
include_player_ids=player_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Notification error in meal {str(e)}")
|
||||
pass
|
||||
|
||||
def notification_for_medication():
|
||||
notification_message = "Have you taken your medication today? Remember to log your medication to stay on track with your treatment!"
|
||||
current_date = datetime.now()
|
||||
fifteen_days_ago = current_date - timedelta(days=15)
|
||||
|
||||
users = IAmPrincipal.objects.filter(
|
||||
last_login__gte=fifteen_days_ago,
|
||||
principal_type=IAmPrincipalType.get_principal_user(),
|
||||
).values_list("id", flat=True)
|
||||
|
||||
medication_obj = Medication.objects.filter(date=current_date).values_list(
|
||||
"principal", flat=True
|
||||
)
|
||||
|
||||
# Remove IDs of users who have recorded medication for the current day
|
||||
users_without_medications = set(users) - set(medication_obj)
|
||||
print(f"user id {set(users)}")
|
||||
print(
|
||||
f"users_without_medication {users_without_medications}"
|
||||
)
|
||||
|
||||
player_ids = list(
|
||||
IAmPrincipal.objects.filter(
|
||||
id__in=users_without_medications
|
||||
).values_list("player_id", flat=True)
|
||||
)
|
||||
# removing none from list
|
||||
player_ids = list(itertools.filterfalse(lambda x: x is None, player_ids))
|
||||
|
||||
notifications_to_create = []
|
||||
if users_without_medications:
|
||||
for user_id in users_without_medications:
|
||||
message = notification_message
|
||||
notifications_to_create.append(
|
||||
InAppNotification(user_id=user_id, message=message)
|
||||
)
|
||||
|
||||
# Bulk create notifications
|
||||
if notifications_to_create:
|
||||
InAppNotification.objects.bulk_create(notifications_to_create)
|
||||
|
||||
try:
|
||||
notification = OneSignalService()
|
||||
response = notification.send_notification(
|
||||
headings="Medication",
|
||||
contents=notification_message,
|
||||
include_player_ids=player_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Notification error in medication {str(e)}")
|
||||
pass
|
||||
|
||||
def notification_for_symptom():
|
||||
notification_message = "How are you feeling? Don't forget to input your symptoms."
|
||||
current_date = datetime.now()
|
||||
fifteen_days_ago = current_date - timedelta(days=15)
|
||||
|
||||
users = IAmPrincipal.objects.filter(
|
||||
last_login__gte=fifteen_days_ago,
|
||||
principal_type=IAmPrincipalType.get_principal_user(),
|
||||
).values_list("id", flat=True)
|
||||
|
||||
symptom_obj = MealSymptomRecord.objects.filter(date=current_date).values_list(
|
||||
"principal", flat=True
|
||||
)
|
||||
|
||||
# Remove IDs of users who have recorded symptoms for the current day
|
||||
users_without_medications = set(users) - set(symptom_obj)
|
||||
print(f"user id {set(users)}")
|
||||
print(
|
||||
f"users_without_medication {users_without_medications}"
|
||||
)
|
||||
|
||||
player_ids = list(
|
||||
IAmPrincipal.objects.filter(
|
||||
id__in=users_without_medications
|
||||
).values_list("player_id", flat=True)
|
||||
)
|
||||
# removing none from list
|
||||
player_ids = list(itertools.filterfalse(lambda x: x is None, player_ids))
|
||||
|
||||
notifications_to_create = []
|
||||
if users_without_medications:
|
||||
for user_id in users_without_medications:
|
||||
message = notification_message
|
||||
notifications_to_create.append(
|
||||
InAppNotification(user_id=user_id, message=message)
|
||||
)
|
||||
|
||||
# Bulk create notifications
|
||||
if notifications_to_create:
|
||||
InAppNotification.objects.bulk_create(notifications_to_create)
|
||||
|
||||
try:
|
||||
notification = OneSignalService()
|
||||
response = notification.send_notification(
|
||||
headings="Symptom",
|
||||
contents=notification_message,
|
||||
include_player_ids=player_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Notification error in medication {str(e)}")
|
||||
pass
|
||||
|
||||
|
||||
def testing_cron():
|
||||
print("cron is working ")
|
||||
18
module_notification/urls.py
Normal file
18
module_notification/urls.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "module_notification"
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path("notification/", views.NotificationView.as_view(), name="notification"),
|
||||
path("notification/add/", views.NotificationCreateOrUpdateView.as_view(), name="notification_add"),
|
||||
path("notification/edit/<int:pk>", views.NotificationCreateOrUpdateView.as_view(), name="notification_edit"),
|
||||
path("notification/list/", views.NotificationListJsonView.as_view(), name="notification_list"),
|
||||
path("notification/action/", views.NotificationActionView.as_view(), name="notification_action"),
|
||||
path("notification/archive/list/", views.NotificationArchiveView.as_view(), name="notification_archive"),
|
||||
path("notification/send/", views.NotificationSendView.as_view(), name="notification_send"),
|
||||
|
||||
]
|
||||
@@ -1,3 +1,203 @@
|
||||
from django.shortcuts import render
|
||||
import itertools
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest
|
||||
from django.http.response import HttpResponse as HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from django_datatables_view.base_datatable_view import BaseDatatableView
|
||||
|
||||
from module_iam import iam_constant, permission
|
||||
from module_iam.iam_constant import PRINCIPAL_TYPE_USER
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_project import constants, date_utils
|
||||
from module_project.mixins import ActionMixin
|
||||
from module_project.service import OneSignalService
|
||||
from module_project.utils import JsonResponseUtil
|
||||
|
||||
from .forms import PushNotificationForm
|
||||
from .models import PushNotification
|
||||
|
||||
|
||||
# Create your views here.
|
||||
class NotificationView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_NOTIFICATION
|
||||
resource = iam_constant.RESOURCE_MANAGE_NOTIFICATION
|
||||
template_name = "module_notification/notification.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class NotificationListJsonView(BaseDatatableView):
|
||||
model = PushNotification
|
||||
columns = ["id", "title", "message", "active", "timestamp"]
|
||||
order_columns = ["id", "title", "message", "active", "timestamp"]
|
||||
|
||||
FILTER_ICONTAINS = "icontains"
|
||||
|
||||
def get_filter_method(self):
|
||||
"""Returns preferred filter method"""
|
||||
return self.FILTER_ICONTAINS
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get("deleted_flag", None)
|
||||
return self.model.objects.filter(deleted=deleted_flag)
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class NotificationCreateOrUpdateView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.View):
|
||||
# Set the page_name and resource
|
||||
page_name = iam_constant.RESOURCE_MANAGE_NOTIFICATION
|
||||
resource = iam_constant.RESOURCE_MANAGE_NOTIFICATION
|
||||
|
||||
# Initialize the action as ACTION_CREATE (can change based on logic)
|
||||
action = iam_constant.ACTION_CREATE # Default action
|
||||
|
||||
template_name = "module_notification/add_notification.html"
|
||||
model = PushNotification
|
||||
form_class = PushNotificationForm
|
||||
success_url = reverse_lazy("module_notification:notification")
|
||||
error_message = "An error occurred while saving the data."
|
||||
|
||||
# Determine the success message dynamically based on whether it's an update or create
|
||||
def get_success_message(self):
|
||||
self.success_message = (
|
||||
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
|
||||
)
|
||||
return self.success_message
|
||||
|
||||
# Get the object (if exists) based on URL parameter 'pk
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
return get_object_or_404(self.model, pk=pk) if pk else None
|
||||
|
||||
# Add page_name and operation to the context
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
"page_name": self.page_name,
|
||||
"operation": "Add" if not self.object else "Edit",
|
||||
}
|
||||
context.update(kwargs) # Include any additional context data passed to the view
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
# If an object is found, change action to ACTION_UPDATE
|
||||
if self.object is not None:
|
||||
self.action = iam_constant.ACTION_UPDATE
|
||||
|
||||
form = self.form_class(instance=self.object)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
print("Request data: ", request.POST)
|
||||
self.object = self.get_object()
|
||||
|
||||
# If an object is found, change action to ACTION_UPDATE
|
||||
if self.object is not None:
|
||||
self.action = iam_constant.ACTION_UPDATE
|
||||
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
if not form.is_valid():
|
||||
print(form.errors)
|
||||
context = self.get_context_data(form=form)
|
||||
return render(request, self.template_name, context=context)
|
||||
|
||||
form.save()
|
||||
messages.success(self.request, self.get_success_message())
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
class NotificationActionView(ActionMixin):
|
||||
model = PushNotification
|
||||
|
||||
class NotificationArchiveView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_NOTIFICATION
|
||||
resource = iam_constant.RESOURCE_MANAGE_NOTIFICATION
|
||||
action = None
|
||||
template_name = "module_notification/notification_archive.html"
|
||||
model = PushNotification
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class NotificationSendView(generic.View):
|
||||
model = PushNotification
|
||||
|
||||
def get_image_url(self, obj, field_name, request):
|
||||
image_field = getattr(obj, field_name)
|
||||
if image_field:
|
||||
return request.build_absolute_uri(image_field.url)
|
||||
return ""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
id = request.POST.get("id")
|
||||
obj = self.model.objects.filter(pk=int(id)).first()
|
||||
|
||||
if not obj:
|
||||
return JsonResponseUtil.error(
|
||||
message="No notification with such ID exists."
|
||||
)
|
||||
|
||||
if not obj.active:
|
||||
return JsonResponseUtil.error(
|
||||
message="The notification cannot be sent because it is inactive. Please activate the notification before attempting to send it."
|
||||
)
|
||||
|
||||
# Get the current date and subtract 15 days
|
||||
fifteen_days_ago = datetime.now() - timedelta(days=3)
|
||||
|
||||
# Filter the IAmPrincipal objects based on the last_login field being greater than or equal to fifteen_days_ago
|
||||
player_ids = list(
|
||||
IAmPrincipal.objects.filter(
|
||||
principal_type__name=PRINCIPAL_TYPE_USER,
|
||||
last_login__gte=fifteen_days_ago,
|
||||
).values_list("player_id", flat=True)
|
||||
)
|
||||
# removing none from list
|
||||
player_ids = list(itertools.filterfalse(lambda x: x is None, player_ids))
|
||||
|
||||
print(f"player id is {player_ids}")
|
||||
|
||||
try:
|
||||
notification = OneSignalService()
|
||||
response = notification.send_notification(
|
||||
headings=obj.title,
|
||||
contents=obj.message,
|
||||
include_player_ids=player_ids,
|
||||
)
|
||||
except Exception as e:
|
||||
error_response = {
|
||||
"status": 400,
|
||||
"message": constants.INTERNAL_SERVER_ERROR,
|
||||
"errors": str(e),
|
||||
}
|
||||
return JsonResponseUtil.error(**error_response)
|
||||
|
||||
return JsonResponseUtil.success(message="success")
|
||||
|
||||
|
||||
|
||||
@@ -31,9 +31,11 @@ LOGIN_REQUIRED = "Login required to perform this action."
|
||||
LOGIN_SUCCESS = "Login successful."
|
||||
LOGOUT_SUCCESS = "Logout successful."
|
||||
SESSION_EXPIRED = "Your session has expired. Please log in again."
|
||||
PASSWORD_RESET_SESSION_EXPIRE = "Password reset session has expired"
|
||||
ACCOUNT_DEACTIVATED = "Your account is inactive. Please contact support."
|
||||
EMAIL_EXISTS = "This email address is already in use. Please use a different email."
|
||||
INVALID_EMAIL_PASSWORD = "Invalid email or password."
|
||||
INCORRECT_CREDENTIALS = "Invalid Credentials"
|
||||
INVALID_PASSWORD = "Invalid password."
|
||||
PASSWORD_NOT_MATCH = "Password do not match"
|
||||
INVALID_OPERATION = "Invalid operation requested."
|
||||
|
||||
@@ -16,6 +16,11 @@ def get_current_date():
|
||||
def get_current_time():
|
||||
return datetime.now().time()
|
||||
|
||||
def get_date_range(days):
|
||||
end_date = datetime.now()
|
||||
start_date = end_date - timedelta(days=int(days))
|
||||
return start_date, end_date
|
||||
|
||||
# Get current date in a specific timezone
|
||||
from pytz import timezone
|
||||
def get_current_date_in_timezone(timezone_str='UTC'):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.db.models import Q
|
||||
from django.http.response import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from .utils import JsonResponseUtil
|
||||
from django.views import generic
|
||||
|
||||
class DatatablesMixin:
|
||||
"""
|
||||
@@ -83,4 +85,34 @@ class DatatablesMixin:
|
||||
"recordsTotal": total_count,
|
||||
"recordsFiltered": filtered_count,
|
||||
"data": data
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
class ActionMixin(generic.View):
|
||||
model = None
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if self.model is None:
|
||||
raise NotImplementedError("Subclasses of BaseActionView must define a 'model' attribute.")
|
||||
|
||||
action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
|
||||
ids = request.POST.getlist('ids[]') # List of IDs to perform action on
|
||||
active = request.POST.get('active')
|
||||
print(f"arhive action {action} and id is {ids} and active data is {active}")
|
||||
if action == 'archive':
|
||||
# Update 'deleted' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=True, active=False)
|
||||
message = 'Record archived successfully.'
|
||||
elif action == 'active':
|
||||
# Update 'active' field to True for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(active=active.capitalize())
|
||||
message = 'Record updated successfully.'
|
||||
elif action == 'unarchive':
|
||||
# Update 'deleted' field to False for the selected users
|
||||
self.model.objects.filter(id__in=ids).update(deleted=False)
|
||||
message = 'Record unarchived successfully.'
|
||||
else:
|
||||
return JsonResponseUtil.error(message="Invalid Action")
|
||||
|
||||
return JsonResponseUtil.success(message=message)
|
||||
@@ -15,68 +15,107 @@ from django.db.models import F
|
||||
from django.db import transaction
|
||||
from datetime import timedelta, time, datetime
|
||||
from django.utils import timezone
|
||||
# from onesignal_sdk.client import Client as OneSignalClient
|
||||
import requests
|
||||
from onesignal_sdk.client import Client as OneSignalClient
|
||||
import logging
|
||||
|
||||
import onesignal
|
||||
from onesignal.models import Notification
|
||||
from onesignal.api import default_api
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailService:
|
||||
email = None
|
||||
body = None
|
||||
subject = None
|
||||
to = None
|
||||
from_email = None
|
||||
"""
|
||||
A reusable class for sending emails
|
||||
|
||||
content_subtype = "html"
|
||||
Example:
|
||||
```python
|
||||
email_service = EmailService(
|
||||
subject="Hello",
|
||||
to="recipient@example.com",
|
||||
from_email="sender@example.com",
|
||||
)
|
||||
email_service.set_html_body("<p>Hello, this is a test email!</p>")
|
||||
email_service.send()
|
||||
```
|
||||
|
||||
This class provides methods to set the subject, recipient(s), sender, and body of the email.
|
||||
It also supports loading email content from templates and attaching files.
|
||||
"""
|
||||
def __init__(self, subject=None, to=None, from_email=None):
|
||||
self.subject = subject
|
||||
self.to = (to,)
|
||||
self.to = to if isinstance(to, (list, tuple)) else [to]
|
||||
self.from_email = from_email
|
||||
|
||||
def set_to(self, to):
|
||||
self.to = to
|
||||
self.body = None
|
||||
self.content_subtype = "html"
|
||||
|
||||
def set_subject(self, subject):
|
||||
self.subject = subject
|
||||
|
||||
def set_to(self, to):
|
||||
"""
|
||||
Set the recipient email address or addresses.
|
||||
|
||||
Parameters:
|
||||
- to (str or list): The recipient email address or a list of recipient email addresses.
|
||||
"""
|
||||
self.to = to if isinstance(to, (list, tuple)) else [to]
|
||||
|
||||
def set_from_email(self, from_email):
|
||||
self.from_email = from_email
|
||||
|
||||
def set_text_body(self, body):
|
||||
"""Set the plain text body of the email."""
|
||||
self.body = strip_tags(body)
|
||||
|
||||
def set_html_body(self, html_body):
|
||||
"""Set the HTML body of the email."""
|
||||
self.body = html_body
|
||||
|
||||
def load_template(self, path=None, context={}):
|
||||
"""
|
||||
Load an email template from a file and render it with the provided context.
|
||||
|
||||
Parameters:
|
||||
- path (str): The path to the email template file.
|
||||
- context (dict): The context data to render the template.
|
||||
"""
|
||||
if path is None:
|
||||
raise Exception("Email temaplate path is not provided.")
|
||||
raise Exception("Email template path is not provided.")
|
||||
|
||||
self.content_subtype = "html"
|
||||
html_body = render_to_string(path, context=context)
|
||||
self.body = html_body
|
||||
self.body = render_to_string(path, context=context)
|
||||
|
||||
def attach(self, file_path):
|
||||
self.email.attach_file(file_path)
|
||||
"""
|
||||
Attach a file to the email.
|
||||
|
||||
Parameters:
|
||||
- file_path (str): The path to the file to be attached.
|
||||
"""
|
||||
if not hasattr(self, 'attachments'):
|
||||
self.attachments = []
|
||||
self.attachments.append(file_path)
|
||||
|
||||
def send(self):
|
||||
try:
|
||||
self.email = EmailMessage(
|
||||
email = EmailMessage(
|
||||
subject=self.subject,
|
||||
body=self.body,
|
||||
to=self.to,
|
||||
from_email=self.from_email,
|
||||
)
|
||||
email.content_subtype = self.content_subtype
|
||||
|
||||
self.email.content_subtype = self.content_subtype
|
||||
|
||||
self.email.send()
|
||||
except SMTPException as e:
|
||||
logger.error(str(e))
|
||||
|
||||
if hasattr(self, 'attachments'):
|
||||
for attachment in self.attachments:
|
||||
email.attach_file(attachment)
|
||||
|
||||
email.send()
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending email: {str(e)}")
|
||||
class SMSError(Exception):
|
||||
def __init__(self, message, payload=None):
|
||||
self.message = message
|
||||
@@ -123,6 +162,7 @@ class SMSService:
|
||||
# raise SMSError(message=str(e))
|
||||
|
||||
def create_otp(self, principal: IAmPrincipal, otp_purpose: str):
|
||||
old_otp_change = IAmPrincipalOtp.objects.filter(principal=principal).update(is_used=True)
|
||||
otp = IAmPrincipalOtp.objects.create(
|
||||
principal=principal, otp_purpose=otp_purpose
|
||||
)
|
||||
@@ -175,81 +215,115 @@ class SMSService:
|
||||
# self.send(phone_numbers, body)
|
||||
return otp_code
|
||||
|
||||
# by using office onesignal package onesignal-python-api
|
||||
class OneSignalService:
|
||||
def __init__(self):
|
||||
|
||||
# class OneSignalNotificationService:
|
||||
# Get the OneSignal app key and user key from the environment variables
|
||||
self.configuration = onesignal.Configuration(
|
||||
|
||||
# """
|
||||
# Class for sending notifications using the OneSignal API.
|
||||
app_key=settings.ONESIGNAL_APP_ID,
|
||||
api_key=settings.ONESIGNAL_REST_API_KEY
|
||||
)
|
||||
|
||||
# Provides a convenient way to create and send notifications to OneSignal users,
|
||||
# with features like targeting specific devices or segments, customizing notification content,
|
||||
# and handling errors gracefully.
|
||||
# Create an instance of the OneSignal API
|
||||
self.api_client = onesignal.ApiClient(self.configuration)
|
||||
self.api_instance = default_api.DefaultApi(self.api_client)
|
||||
|
||||
# **Parameters:**
|
||||
def send_notification(self, headings, contents, include_player_ids=None):
|
||||
# Create a notification object using a dictionary
|
||||
notification = Notification(
|
||||
app_id=self.configuration.app_key,
|
||||
include_player_ids=include_player_ids,
|
||||
headings={"en": headings},
|
||||
contents={"en": contents}
|
||||
)
|
||||
try:
|
||||
# Send the notification
|
||||
response = self.api_instance.create_notification(
|
||||
notification=notification,
|
||||
async_req=True
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception("Generic OneSignal error: {}".format(e))
|
||||
print("complete service is succeesss")
|
||||
return response
|
||||
|
||||
# - **app_id** (str): Your OneSignal App ID.
|
||||
# - **rest_api_key** (str): Your OneSignal REST API Key.
|
||||
# - **user_auth_key** (str): Your OneSignal User Auth Key.
|
||||
# by using community packgae onesignal-sdk
|
||||
class OneSignalNotificationService:
|
||||
|
||||
# **Keyword Arguments:**
|
||||
"""
|
||||
Class for sending notifications using the OneSignal API.
|
||||
|
||||
# This method accepts additional keyword arguments (`**kwargs`) to customize the notification
|
||||
# further, including:
|
||||
Provides a convenient way to create and send notifications to OneSignal users,
|
||||
with features like targeting specific devices or segments, customizing notification content,
|
||||
and handling errors gracefully.
|
||||
|
||||
# - `url` (str): URL to open when the notification is clicked.
|
||||
# - `data` (dict): Custom data to be sent with the notification.
|
||||
# - `buttons` (list): List of action buttons to display within the notification.
|
||||
# - `send_after` (str): Timestamp for scheduling the notification.
|
||||
# - `delayed_option` (dict): Option for delayed delivery (Android-specific).
|
||||
# - `android_channel_id` (str): Channel ID for Android notifications.
|
||||
# - `ios_sound` (str): Sound to play for iOS notifications.
|
||||
# - `ios_badgeType` (str): Badge type for iOS notifications.
|
||||
# - `ios_badgeCount` (int): Badge count for iOS notifications.
|
||||
# - `ios_thread_id` (str): Thread ID to group notifications in iOS.
|
||||
# - `android_background_layout` (str): Layout for background notifications on Android.
|
||||
# - `android_group` (str): Group notification on Android.
|
||||
# - `android_group_message` (str): Summary for grouped notifications on Android.
|
||||
# - `android_group_summary` (str): Summary for grouped notifications on Android.
|
||||
# - `android_led_color` (str): LED color for Android notifications.
|
||||
# - `android_accent_color` (str): Accent color for Android notifications.
|
||||
# - `android_visibility` (str): Visibility settings for Android notifications.
|
||||
**Parameters:**
|
||||
|
||||
# **Example usage:**
|
||||
- **app_id** (str): Your OneSignal App ID.
|
||||
- **rest_api_key** (str): Your OneSignal REST API Key.
|
||||
- **user_auth_key** (str): Your OneSignal User Auth Key.
|
||||
|
||||
# notification = OneSignalNotificationService()
|
||||
# response = notification.send_notification(
|
||||
# headings="Welcome",
|
||||
# message="Thanks for signing up!",
|
||||
# player_tokens=["PLAYER_TOKEN1", "PLAYER_TOKEN2"],
|
||||
# url="https://yourwebsite.com/welcome",
|
||||
# data={"user_id": 123},
|
||||
# )
|
||||
# """
|
||||
**Keyword Arguments:**
|
||||
|
||||
# def __init__(self):
|
||||
# self.config = OneSignalClient(
|
||||
# app_id=settings.ONESIGNAL_APP_ID,
|
||||
# rest_api_key=settings.ONESIGNAL_REST_API_KEY,
|
||||
# user_auth_key=settings.ONESIGNAL_USER_AUTH_KEY
|
||||
# )
|
||||
This method accepts additional keyword arguments (`**kwargs`) to customize the notification
|
||||
further, including:
|
||||
|
||||
# # Set up logging
|
||||
# self.logger = logging.getLogger(__name__)
|
||||
- `url` (str): URL to open when the notification is clicked.
|
||||
- `data` (dict): Custom data to be sent with the notification.
|
||||
- `buttons` (list): List of action buttons to display within the notification.
|
||||
- `send_after` (str): Timestamp for scheduling the notification.
|
||||
- `delayed_option` (dict): Option for delayed delivery (Android-specific).
|
||||
- `android_channel_id` (str): Channel ID for Android notifications.
|
||||
- `ios_sound` (str): Sound to play for iOS notifications.
|
||||
- `ios_badgeType` (str): Badge type for iOS notifications.
|
||||
- `ios_badgeCount` (int): Badge count for iOS notifications.
|
||||
- `ios_thread_id` (str): Thread ID to group notifications in iOS.
|
||||
- `android_background_layout` (str): Layout for background notifications on Android.
|
||||
- `android_group` (str): Group notification on Android.
|
||||
- `android_group_message` (str): Summary for grouped notifications on Android.
|
||||
- `android_group_summary` (str): Summary for grouped notifications on Android.
|
||||
- `android_led_color` (str): LED color for Android notifications.
|
||||
- `android_accent_color` (str): Accent color for Android notifications.
|
||||
- `android_visibility` (str): Visibility settings for Android notifications.
|
||||
|
||||
# def send_notification(self, headings, message, player_tokens=None, **kwargs):
|
||||
# notification_obj = {
|
||||
# "headings": {"en": headings},
|
||||
# "contents": {"en": message},
|
||||
# **kwargs
|
||||
# }
|
||||
**Example usage:**
|
||||
|
||||
# if player_tokens:
|
||||
# notification_obj["include_player_ids"] = player_tokens
|
||||
notification = OneSignalNotificationService()
|
||||
response = notification.send_notification(
|
||||
headings="Welcome",
|
||||
message="Thanks for signing up!",
|
||||
player_tokens=["PLAYER_TOKEN1", "PLAYER_TOKEN2"],
|
||||
url="https://yourwebsite.com/welcome",
|
||||
data={"user_id": 123},
|
||||
)
|
||||
"""
|
||||
|
||||
# try:
|
||||
# response = self.config.send_notification(notification_obj)
|
||||
# self.logger.info(f"Notification send successfully : {response}")
|
||||
# return response
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"OneSignal error {e}")
|
||||
# raise Exception("Generic OneSignal error: {}".format(e))
|
||||
def __init__(self):
|
||||
self.config = OneSignalClient(
|
||||
app_id=settings.ONESIGNAL_APP_ID,
|
||||
rest_api_key=settings.ONESIGNAL_REST_API_KEY,
|
||||
user_auth_key=settings.ONESIGNAL_USER_AUTH_KEY
|
||||
)
|
||||
|
||||
# Set up logging
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def send_notification(self, headings, message, player_tokens=None, **kwargs):
|
||||
notification_obj = {
|
||||
"headings": {"en": headings},
|
||||
"contents": {"en": message},
|
||||
**kwargs
|
||||
}
|
||||
|
||||
if player_tokens:
|
||||
notification_obj["include_player_ids"] = player_tokens
|
||||
|
||||
try:
|
||||
response = self.config.send_notification(notification_obj)
|
||||
self.logger.info(f"Notification send successfully : {response}")
|
||||
return response
|
||||
except Exception as e:
|
||||
self.logger.error(f"OneSignal error {e}")
|
||||
raise Exception("Generic OneSignal error: {}".format(e))
|
||||
|
||||
@@ -28,10 +28,11 @@ if READ_DOT_ENV_FILE:
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-#7rdu=fr58ba9_!n3$l5pm!xs8l%6%8xt@vb8$&o@hqhd@rtd%'
|
||||
|
||||
SECRET_KEY = env.str("SECRET_KEY")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
DEBUG = env.bool("DJANGO_DEBUG", False)
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
@@ -52,14 +53,17 @@ LOCAL_APPS = [
|
||||
"module_activity",
|
||||
"module_cms",
|
||||
"module_support",
|
||||
"module_notification",
|
||||
]
|
||||
|
||||
THIRD_PARTY_APPS = [
|
||||
"corsheaders",
|
||||
"widget_tweaks",
|
||||
"rest_framework_simplejwt",
|
||||
'rest_framework_simplejwt.token_blacklist',
|
||||
"taggit",
|
||||
"django_quill",
|
||||
"django_crontab",
|
||||
]
|
||||
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||
@@ -86,6 +90,7 @@ TEMPLATES = [
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'module_iam.context_processors.iam_constants_context',
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
@@ -105,7 +110,7 @@ WSGI_APPLICATION = 'module_project.wsgi.application'
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"NAME": "digest_db",
|
||||
"NAME": env.str("DB_DATABASE"),
|
||||
"HOST": env.str("DB_HOST"),
|
||||
"USER": env.str("DB_USERNAME"),
|
||||
"PASSWORD": env.str("DB_PASSWORD"),
|
||||
@@ -151,12 +156,12 @@ SHORT_DATE_FORMAT = "d-m-Y"
|
||||
TIME_FORMAT = "H:i p"
|
||||
|
||||
# otp expire time limit
|
||||
OTP_EXPIRE_TIME = 10 # mins
|
||||
OTP_EXPIRE_TIME = 2 # mins
|
||||
|
||||
APPEND_SLASH = True
|
||||
LOGIN_REDIRECT_URL = "/iam/dashboard/"
|
||||
LOGIN_URL = "/auth/login/"
|
||||
LOGOUT_REDIRECT_URL = "/auth/login/"
|
||||
LOGIN_URL = "/login/"
|
||||
LOGOUT_REDIRECT_URL = "/login/"
|
||||
|
||||
|
||||
# https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model
|
||||
@@ -192,7 +197,7 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
messages.DEBUG: "alert-info",
|
||||
messages.DEBUG: "alert-primary",
|
||||
messages.INFO: "alert-info",
|
||||
messages.SUCCESS: "alert-success",
|
||||
messages.WARNING: "alert-warning",
|
||||
@@ -211,6 +216,11 @@ EMAIL_HOST_USER = env.str("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_PORT = env.str("EMAIL_PORT")
|
||||
EMAIL_USE_TLS = True
|
||||
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL")
|
||||
|
||||
ONESIGNAL_APP_ID = env.str("ONESIGNAL_APP_ID")
|
||||
ONESIGNAL_REST_API_KEY = env.str("ONESIGNAL_REST_API_KEY")
|
||||
ONESIGNAL_USER_AUTH_KEY = env.str("ONESIGNAL_USER_AUTH_KEY")
|
||||
|
||||
# LOGGING
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -218,6 +228,7 @@ EMAIL_USE_TLS = True
|
||||
# https://docs.djangoproject.com/en/dev/ref/settings/#logging
|
||||
# See https://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
@@ -251,10 +262,10 @@ LOGGING = {
|
||||
# jwt configuration
|
||||
# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html#settings
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=10),
|
||||
"ACCESS_TOKEN_LIFETIME": datetime.timedelta(days=15),
|
||||
"REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=15),
|
||||
"ROTATE_REFRESH_TOKENS": False,
|
||||
"BLACKLIST_AFTER_ROTATION": False,
|
||||
"BLACKLIST_AFTER_ROTATION": True,
|
||||
"UPDATE_LAST_LOGIN": False,
|
||||
"ALGORITHM": "HS256",
|
||||
"SIGNING_KEY": SECRET_KEY,
|
||||
@@ -274,3 +285,11 @@ SIMPLE_JWT = {
|
||||
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
|
||||
"JTI_CLAIM": "jti",
|
||||
}
|
||||
|
||||
CRONJOBS = [
|
||||
('0 18 * * *', 'module_notification.tasks.notification_for_meal', '>> {}'.format(str(BASE_DIR / 'logs/cron.log'))),
|
||||
('0 20 * * *', 'module_notification.tasks.notification_for_medication', '>> {}'.format(str(BASE_DIR / 'logs/cron.log'))),
|
||||
('0 22 * * *', 'module_notification.tasks.notification_for_symptom', '>> {}'.format(str(BASE_DIR / 'logs/cron.log'))),
|
||||
# ('* * * * *', 'module_notification.tasks.testing_cron', '>> {}'.format(str(BASE_DIR / 'logs/cron.log'))),
|
||||
|
||||
]
|
||||
|
||||
@@ -5,18 +5,17 @@ import colorlog
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = ["admin.eatwithdigest.com"]
|
||||
|
||||
# CORS_ALLOWED_ORIGINS = [
|
||||
# # "http://127.0.0.1:3000",
|
||||
# "http://127.0.0.1:3000",
|
||||
# ]
|
||||
|
||||
|
||||
# CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
|
||||
# CORS_ORIGIN_WHITELIST = ("",)
|
||||
# CORS_ORIGIN_WHITELIST = ("http://localhost:3000",)
|
||||
|
||||
|
||||
LOGGING_DIR = os.path.join(
|
||||
@@ -29,7 +28,7 @@ if not os.path.exists(LOGGING_DIR):
|
||||
|
||||
LOGGING_LEVEL = env.str(
|
||||
"LOG_LEVEL", "INFO"
|
||||
) # Set your desired log level (e.g., DEBUG, INFO, WARNING, ERROR)
|
||||
) # Set your desired log level (e.g., DEBUG, INFO, WARNING, ERROR) in the env file
|
||||
|
||||
|
||||
LOGGING = {
|
||||
@@ -70,8 +69,7 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
BASE_DOMAIN = ""
|
||||
BASE_DOMAIN = "https://admin.eatwithdigest.com"
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
@@ -80,8 +78,8 @@ BASE_DOMAIN = ""
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
# STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
|
||||
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
# STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
@@ -5,14 +5,14 @@ import colorlog
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = ["staging.eatwithdigest.com"]
|
||||
|
||||
# CORS_ALLOWED_ORIGINS = [
|
||||
# "http://127.0.0.1:3000",
|
||||
# ]
|
||||
|
||||
|
||||
# CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
|
||||
# CORS_ORIGIN_WHITELIST = ("http://localhost:3000",)
|
||||
@@ -69,7 +69,7 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
|
||||
BASE_DOMAIN = ""
|
||||
BASE_DOMAIN = "https://staging.eatwithdigest.com"
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
@@ -78,8 +78,8 @@ BASE_DOMAIN = ""
|
||||
MEDIA_URL = "/media/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
|
||||
# STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
|
||||
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
# STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
"""
|
||||
Django settings for module_project project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-#7rdu=fr58ba9_!n3$l5pm!xs8l%6%8xt@vb8$&o@hqhd@rtd%'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'module_project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'module_project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
@@ -14,31 +14,49 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.shortcuts import render
|
||||
from django.urls import include, path
|
||||
|
||||
def handler404(request, exception):
|
||||
if request.user.is_authenticated:
|
||||
print("True")
|
||||
return render(request, '404.html')
|
||||
else:
|
||||
print("False")
|
||||
return render(request, '1404.html')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
path('iam/', include('module_iam.urls')),
|
||||
|
||||
path('auth/', include('module_auth.urls')),
|
||||
|
||||
path('', include('module_auth.urls')),
|
||||
path('api/auth/', include('module_auth.api.urls')),
|
||||
|
||||
path('cms/', include('module_cms.urls')),
|
||||
path('api/cms/', include('module_cms.api.urls')),
|
||||
|
||||
# path('support/', include('module_support.urls')),
|
||||
path('support/', include('module_support.urls')),
|
||||
path('api/support/', include('module_support.api.urls')),
|
||||
|
||||
path('activity/', include("module_activity.urls")),
|
||||
path('api/activity/', include("module_activity.api.urls")),
|
||||
|
||||
path('notification/', include("module_notification.urls")),
|
||||
path('api/notification/', include("module_notification.api.urls")),
|
||||
]
|
||||
|
||||
# handler404 = handler404
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += [path('__debug__/', include(debug_toolbar.urls))]
|
||||
|
||||
if not settings.DEBUG:
|
||||
handler404 = handler404
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import NotFound
|
||||
from django.http import JsonResponse
|
||||
from . import constants
|
||||
from rest_framework import status
|
||||
import string
|
||||
import random
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import string
|
||||
|
||||
from django.http import JsonResponse
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.response import Response
|
||||
|
||||
from . import constants
|
||||
|
||||
|
||||
class GroupWriteRotatingFileHandler(logging.handlers.RotatingFileHandler):
|
||||
|
||||
@@ -36,25 +39,25 @@ class ApiResponse:
|
||||
if errors is not None:
|
||||
response_data["errors"] = errors
|
||||
return Response(response_data, status=status)
|
||||
|
||||
|
||||
# @staticmethod
|
||||
# def validation_error(errors, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY):
|
||||
# return ApiResponse.error("Validation error", errors, status_code)
|
||||
# def validation_error(errors, status=status.HTTP_422_UNPROCESSABLE_ENTITY):
|
||||
# return ApiResponse.error("Validation error", errors, status)
|
||||
|
||||
class JsonResponseUtil:
|
||||
@staticmethod
|
||||
def success(message, data=None, status_code=200):
|
||||
response_data = {"success": True, "status": status_code, "message": message}
|
||||
def success(message, data=None, status=200):
|
||||
response_data = {"success": True, "status": status, "message": message}
|
||||
if data is not None:
|
||||
response_data["data"] = data
|
||||
return JsonResponse(response_data, status=status_code)
|
||||
return JsonResponse(response_data, status=status)
|
||||
|
||||
@staticmethod
|
||||
def error(message, errors=None, status_code=403):
|
||||
response_data = {"success": False, "status": status_code, "message": message}
|
||||
def error(message, errors=None, status=403):
|
||||
response_data = {"success": False, "status": status, "message": message}
|
||||
if errors is not None:
|
||||
response_data["errors"] = errors
|
||||
return JsonResponse(response_data, status=status_code)
|
||||
return JsonResponse(response_data, status=status)
|
||||
|
||||
|
||||
class RandomGenerator:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from module_support.models import ContactUs, Feedback
|
||||
|
||||
|
||||
class ContactUsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ContactUs
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from module_project import constants
|
||||
from module_project.utils import ApiResponse
|
||||
from .serializers import ContactUsSerializer, FeedbackSerializer
|
||||
|
||||
from ..models import ContactUs, Feedback
|
||||
from .serializers import ContactUsSerializer, FeedbackSerializer
|
||||
|
||||
|
||||
class ContactusAPIView(APIView):
|
||||
|
||||
0
module_support/forms.py
Normal file
0
module_support/forms.py
Normal file
@@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
|
||||
from module_iam.models import BaseModel, IAmPrincipal
|
||||
|
||||
# Create your models here.
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "manage_support"
|
||||
app_name = "module_support"
|
||||
|
||||
urlpatterns = [
|
||||
# path('contact_us/', views.ContactUsListView.as_view(), name='contact_us_list'),
|
||||
# path('contact_us/reply/', views.ContactUsReplyView.as_view(), name='contact_us_reply'),
|
||||
|
||||
path('contact_us/', views.ContactUsView.as_view(), name="contact_us"),
|
||||
path('contact_us/list/', views.ContactUsListJson.as_view(), name="contact_us_list"),
|
||||
path('contact_us/reply/<int:id>/', views.ContactUsReplyView.as_view(), name='contact_us_reply'),
|
||||
path('contact_us/action/', views.ContactUsActionView.as_view(), name='contact_us_action'),
|
||||
path('contact_us/archive/list/', views.ContactUsArchiveView.as_view(), name='contact_us_archive'),
|
||||
|
||||
path('contact_us/landing/', views.ContactUsLandingView, name="contact_us_landing"),
|
||||
|
||||
path('feedback/', views.FeedbackView.as_view(), name="feedback"),
|
||||
path('feedback/list/', views.FeedbackListJson.as_view(), name="feedback_list"),
|
||||
path('feedback/action/', views.FeedbackActionView.as_view(), name='feedback_action'),
|
||||
|
||||
|
||||
|
||||
# path('feedback/', views.FeedbackListView.as_view(), name='feedback_list'),
|
||||
# path('feedback/delete/<int:pk>', views.FeedbackDeleteView.as_view(), name='feedback_delete'),
|
||||
|
||||
]
|
||||
|
||||
@@ -1,3 +1,232 @@
|
||||
from django.shortcuts import render
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.views import generic
|
||||
from django_datatables_view.base_datatable_view import BaseDatatableView
|
||||
|
||||
from module_iam import iam_constant, permission
|
||||
from module_iam.models import IAmPrincipal
|
||||
from module_project import constants
|
||||
from module_project.mixins import ActionMixin, DatatablesMixin
|
||||
from module_project.service import EmailService
|
||||
from module_project.utils import JsonResponseUtil
|
||||
|
||||
from .models import ContactUs, Feedback
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class ContactUsView(LoginRequiredMixin, permission.ResourcePermissionRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_CONTACT_US
|
||||
resource = iam_constant.RESOURCE_MANAGE_CONTACT_US
|
||||
action = None
|
||||
template_name = "module_support/contact_us.html"
|
||||
model = ContactUs
|
||||
context_objext_name = "obj"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class ContactUsListJson(BaseDatatableView):
|
||||
model = ContactUs
|
||||
columns = ["id", "email_address", "subject", "message", "reply", "active"]
|
||||
order_columns = ["id", "email_address", "subject", "message", "reply", "active"]
|
||||
|
||||
FILTER_ICONTAINS = "icontains"
|
||||
|
||||
def get_filter_method(self):
|
||||
"""Returns preferred filter method"""
|
||||
return self.FILTER_ICONTAINS
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get('deleted_flag', None)
|
||||
|
||||
return self.model.objects.filter(deleted=deleted_flag)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
print(f"request is {self.request.GET}")
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = super().filter_queryset(qs) # Call the built-in filtering first
|
||||
|
||||
for column in self.columns:
|
||||
print(f" columen index pattern {self.request.GET.get(f'columns[{self.columns.index(column)+1}][search][value]', None)}")
|
||||
search_value = self.request.GET.get(f'columns[{self.columns.index(column)+1}][search][value]', None)
|
||||
if search_value:
|
||||
column_data = self.request.GET.get(f'columns[{self.columns.index(column)+1}][data]')
|
||||
if column_data == "active":
|
||||
qs = qs.filter(**{f"{column}": search_value})
|
||||
else:
|
||||
qs = qs.filter(**{f"{column}__icontains": search_value})
|
||||
|
||||
return qs
|
||||
|
||||
def ordering(self, qs):
|
||||
order = self.request.GET.get('order[0][dir]', None)
|
||||
if order:
|
||||
column_index = int(self.request.GET.get('order[0][column]', None)) - 1
|
||||
order_column = self.order_columns[column_index]
|
||||
|
||||
if order == "asc":
|
||||
qs = qs.order_by(order_column)
|
||||
elif order == "desc":
|
||||
qs = qs.order_by("-" + order_column)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class ContactUsActionView(ActionMixin):
|
||||
model = ContactUs
|
||||
|
||||
class ContactUsArchiveView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_CONTACT_US
|
||||
resource = iam_constant.RESOURCE_MANAGE_CONTACT_US
|
||||
action = None
|
||||
template_name = "module_support/contactus_archive_list.html"
|
||||
model = ContactUs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
class ContactUsReplyView(LoginRequiredMixin, generic.View):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_CONTACT_US
|
||||
model = ContactUs
|
||||
success_message = constants.DATA_SAVED
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
id = self.kwargs.get("id")
|
||||
message = request.POST.get("message")
|
||||
print(f"id and message is {id} {message}")
|
||||
if id and message:
|
||||
try:
|
||||
instance = self.model.objects.get(id=id)
|
||||
instance.reply = message
|
||||
instance.save()
|
||||
|
||||
email_service = EmailService(
|
||||
subject=f"Reply of your inquiry - {instance.subject}",
|
||||
to=instance.email_address,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
email_service.set_text_body(message)
|
||||
email_service.send()
|
||||
print(f"email service is {email_service}")
|
||||
return JsonResponseUtil.success(message=self.success_message)
|
||||
except self.model.DoesNotExist:
|
||||
return JsonResponseUtil.error(message=constants.FAILURE, errors="Invalid contact us ID.")
|
||||
except Exception as e:
|
||||
return JsonResponseUtil.error(message=constants.FAILURE, errors=str(e))
|
||||
else:
|
||||
return JsonResponseUtil.error(message=constants.FAILURE, errors="Missing 'id' or 'message' in the request")
|
||||
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
@csrf_exempt
|
||||
def ContactUsLandingView(request):
|
||||
name = request.POST.get('full_name')
|
||||
email = request.POST.get('email')
|
||||
subject = request.POST.get('subject')
|
||||
message = request.POST.get('message')
|
||||
|
||||
if not all([name, email, subject, message]):
|
||||
return JsonResponseUtil.error(message=constants.FAILURE, errors='Please fill in all the fields.')
|
||||
|
||||
try:
|
||||
contact_us = ContactUs(
|
||||
name=name,
|
||||
email_address=email,
|
||||
subject=subject,
|
||||
message=message
|
||||
)
|
||||
contact_us.save()
|
||||
|
||||
context = {
|
||||
"name": name,
|
||||
"email": email,
|
||||
"subject": subject,
|
||||
"message": message,
|
||||
"year": datetime.datetime.year
|
||||
}
|
||||
print(context)
|
||||
|
||||
# Send the email to support team
|
||||
support_team_service = EmailService(
|
||||
subject=f"New contact us - {subject}",
|
||||
to=email,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
||||
support_team_service.load_template(
|
||||
"module_support/contact_us_support.html", context=context
|
||||
)
|
||||
support_team_service.send()
|
||||
|
||||
# Send the email to support team
|
||||
thankyou_service = EmailService(
|
||||
subject=f"New contact us - {subject}",
|
||||
to=email,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
)
|
||||
|
||||
thankyou_service.load_template(
|
||||
"module_support/thank_you_support.html", context=context
|
||||
)
|
||||
thankyou_service.send()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error occur in ContactUsLandingView {str(e)}")
|
||||
return JsonResponseUtil.error(message=constants.FAILURE, errors=constants.SOMETHING_WRONG)
|
||||
|
||||
return JsonResponseUtil.success(message='Thank you for contacting us!')
|
||||
|
||||
|
||||
class FeedbackView(permission.ResourcePermissionRequiredMixin, LoginRequiredMixin, generic.TemplateView):
|
||||
page_name = iam_constant.RESOURCE_MANAGE_FEEDBACK
|
||||
resource = iam_constant.RESOURCE_MANAGE_FEEDBACK
|
||||
action = None
|
||||
template_name = "module_support/feedback.html"
|
||||
model = Feedback
|
||||
context_objext_name = "obj"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["page_name"] = self.page_name
|
||||
return context
|
||||
|
||||
|
||||
class FeedbackListJson(BaseDatatableView):
|
||||
model = Feedback
|
||||
columns = ["id", "principal.email", "feedback_reaction", "comment", "active"]
|
||||
order_columns = ["id", "principal.email", "feedback_reaction", "comment", "active"]
|
||||
|
||||
def get_initial_queryset(self):
|
||||
deleted_flag = self.request.GET.get('deleted_flag', None)
|
||||
|
||||
return self.model.objects.filter(deleted=deleted_flag)
|
||||
|
||||
def filter_queryset(self, qs):
|
||||
# Implement your custom filtering logic here
|
||||
print(f"request is {self.request.GET}")
|
||||
search_value = self.request.GET.get("search[value]", None)
|
||||
if search_value:
|
||||
qs = qs.filter(
|
||||
Q(id__icontains=search_value)
|
||||
| Q(feedback_reaction__icontains=search_value)
|
||||
| Q(principal__email__icontains=search_value)
|
||||
| Q(comment__icontains=search_value)
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class FeedbackActionView(ActionMixin):
|
||||
model = Feedback
|
||||
pass
|
||||
|
||||
22
openfoodfact.py
Normal file
22
openfoodfact.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import openfoodfacts
|
||||
import json
|
||||
|
||||
# User-Agent is mandatory
|
||||
api = openfoodfacts.API(user_agent="Digest/1.0")
|
||||
|
||||
# Search for pizza products
|
||||
data = api.product.text_search("Rice Noodles")
|
||||
|
||||
# Create filename (adjust as needed)
|
||||
filename = "pizza_products.json"
|
||||
|
||||
# Open the file in write mode (will create if non-existent)
|
||||
with open(filename, "w") as json_file:
|
||||
|
||||
# Convert data to JSON string (ensure proper indentation)
|
||||
json_string = json.dumps(data, indent=4)
|
||||
|
||||
# Write the JSON string to the file
|
||||
json_file.write(json_string)
|
||||
|
||||
print(f"Pizza product data saved to '{filename}'.")
|
||||
54
process_food_data.py
Normal file
54
process_food_data.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import openpyxl
|
||||
|
||||
# Read existing foods from Excel file
|
||||
existing_foods = set()
|
||||
try:
|
||||
existing_workbook = openpyxl.load_workbook('formatted_data.xlsx')
|
||||
existing_sheet = existing_workbook.active
|
||||
for row in existing_sheet.iter_rows(min_row=2, max_col=1, max_row=existing_sheet.max_row):
|
||||
existing_foods.add(row[0].value)
|
||||
except FileNotFoundError:
|
||||
# If the file does not exist, there are no existing foods
|
||||
pass
|
||||
|
||||
# Read data from text file
|
||||
with open('food_data.txt', 'r') as file:
|
||||
data = file.readlines()
|
||||
|
||||
# Process the data
|
||||
formatted_data = []
|
||||
for line in data:
|
||||
# Split each line by colon ':'
|
||||
parts = line.strip().split(':')
|
||||
# Check if there are two parts (food and ingredients)
|
||||
if len(parts) == 2:
|
||||
food = parts[0].strip()
|
||||
ingredients = parts[1].strip()
|
||||
# Check if the food is already in the existing foods set
|
||||
if food not in existing_foods:
|
||||
# If not, add the food to the set and to the formatted data list
|
||||
existing_foods.add(food)
|
||||
formatted_data.append((food, ingredients))
|
||||
else:
|
||||
# If only one part is present, add an empty string for ingredients
|
||||
food = parts[0].strip()
|
||||
if food not in existing_foods:
|
||||
existing_foods.add(food)
|
||||
formatted_data.append((food, ''))
|
||||
|
||||
# Write data to Excel file
|
||||
workbook = openpyxl.Workbook()
|
||||
sheet = workbook.active
|
||||
|
||||
# Add headers
|
||||
sheet.cell(row=1, column=1).value = 'Food'
|
||||
sheet.cell(row=1, column=2).value = 'Ingredients'
|
||||
|
||||
# Add data
|
||||
for idx, entry in enumerate(formatted_data, start=2):
|
||||
# Write food and ingredients to respective columns
|
||||
sheet.cell(row=idx, column=1).value = entry[0]
|
||||
sheet.cell(row=idx, column=2).value = entry[1]
|
||||
|
||||
# Save Excel file
|
||||
workbook.save('formatted_data.xlsx')
|
||||
@@ -1,8 +1,16 @@
|
||||
annotated-types==0.6.0
|
||||
anyio==4.3.0
|
||||
asgiref==3.7.2
|
||||
certifi==2024.2.2
|
||||
cffi==1.16.0
|
||||
charset-normalizer==3.3.2
|
||||
colorama==0.4.6
|
||||
colorlog==6.7.0
|
||||
cryptography==42.0.5
|
||||
defusedxml==0.7.1
|
||||
Django==5.0.2
|
||||
django-cors-headers==4.3.1
|
||||
django-crontab==0.7.1
|
||||
django-datatables-view==1.20.0
|
||||
django-debug-toolbar==4.3.0
|
||||
django-environ==0.11.2
|
||||
@@ -12,10 +20,38 @@ django-taggit==5.0.1
|
||||
django-widget-tweaks==1.5.0
|
||||
djangorestframework==3.14.0
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
et-xmlfile==1.1.0
|
||||
h11==0.14.0
|
||||
httpcore==1.0.4
|
||||
httpx==0.27.0
|
||||
idna==3.6
|
||||
iniconfig==2.0.0
|
||||
mysqlclient==2.2.4
|
||||
numpy==1.26.4
|
||||
oauthlib==3.2.2
|
||||
onesignal-python-api==2.0.2
|
||||
onesignal-sdk==2.0.0
|
||||
openfoodfacts==0.2.1
|
||||
openpyxl==3.1.2
|
||||
packaging==23.2
|
||||
pandas==2.2.1
|
||||
phonenumbers==8.13.30
|
||||
pillow==10.2.0
|
||||
pluggy==1.4.0
|
||||
pycparser==2.21
|
||||
pydantic==2.6.4
|
||||
pydantic_core==2.16.3
|
||||
PyJWT==2.8.0
|
||||
pytest==8.0.2
|
||||
python-dateutil==2.9.0.post0
|
||||
python3-openid==3.2.0
|
||||
pytz==2024.1
|
||||
requests==2.31.0
|
||||
requests-oauthlib==1.3.1
|
||||
six==1.16.0
|
||||
sniffio==1.3.1
|
||||
sqlparse==0.4.4
|
||||
tqdm==4.66.2
|
||||
typing_extensions==4.11.0
|
||||
tzdata==2023.4
|
||||
urllib3==2.2.1
|
||||
|
||||
275
static/admin/css/autocomplete.css
Normal file
275
static/admin/css/autocomplete.css
Normal file
@@ -0,0 +1,275 @@
|
||||
select.admin-autocomplete {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single,
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||
border-color: var(--body-quiet-color);
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||
color: var(--body-fg);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 10px 5px 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: var(--darkened-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--body-quiet-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||
border: solid var(--body-quiet-color) 1px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown {
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||
background: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
color: var(--body-fg);
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
color: var(--body-fg);
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||
background-color: var(--selected-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
||||
1156
static/admin/css/base.css
Normal file
1156
static/admin/css/base.css
Normal file
File diff suppressed because it is too large
Load Diff
338
static/admin/css/changelists.css
Normal file
338
static/admin/css/changelists.css
Normal file
@@ -0,0 +1,338 @@
|
||||
/* CHANGELISTS */
|
||||
|
||||
#changelist {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#changelist .changelist-form-container {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#changelist table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list .hiddenfields { display:none; }
|
||||
|
||||
.change-list .filtered table {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.change-list .filtered {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.change-list .filtered .results, .change-list .filtered .paginator,
|
||||
.filtered #toolbar, .filtered div.xfull {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.change-list .filtered table tbody th {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
#changelist-form .results {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist .toplinks {
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
#changelist .paginator {
|
||||
color: var(--body-quiet-color);
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
background: var(--body-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* CHANGELIST TABLES */
|
||||
|
||||
#changelist table thead th {
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#changelist table thead th.action-checkbox-column {
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tbody td.action-checkbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#changelist table tfoot {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
/* TOOLBAR */
|
||||
|
||||
#toolbar {
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 15px;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#toolbar form input {
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
padding: 5px;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#toolbar #searchbar {
|
||||
height: 1.1875rem;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 5px;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
font-size: 0.8125rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#toolbar #searchbar:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#toolbar form input[type="submit"] {
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.8125rem;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
background: var(--body-bg);
|
||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||
cursor: pointer;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#toolbar form input[type="submit"]:focus,
|
||||
#toolbar form input[type="submit"]:hover {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#changelist-search img {
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
#changelist-search .help {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* FILTER COLUMN */
|
||||
|
||||
#changelist-filter {
|
||||
flex: 0 0 240px;
|
||||
order: 1;
|
||||
background: var(--darkened-bg);
|
||||
border-left: none;
|
||||
margin: 0 0 0 30px;
|
||||
}
|
||||
|
||||
#changelist-filter h2 {
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 5px 15px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist-filter h3,
|
||||
#changelist-filter details summary {
|
||||
font-weight: 400;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#changelist-filter details summary > * {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#changelist-filter details > summary {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#changelist-filter details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#changelist-filter details > summary::before {
|
||||
content: '→';
|
||||
font-weight: bold;
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
#changelist-filter details[open] > summary::before {
|
||||
content: '↓';
|
||||
}
|
||||
|
||||
#changelist-filter ul {
|
||||
margin: 5px 0;
|
||||
padding: 0 15px 15px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
#changelist-filter ul:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist-filter li {
|
||||
list-style-type: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#changelist-filter a {
|
||||
display: block;
|
||||
color: var(--body-quiet-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected {
|
||||
border-left: 5px solid var(--hairline-color);
|
||||
padding-left: 10px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
#changelist-filter li.selected a {
|
||||
color: var(--link-selected-fg);
|
||||
}
|
||||
|
||||
#changelist-filter a:focus, #changelist-filter a:hover,
|
||||
#changelist-filter li.selected a:focus,
|
||||
#changelist-filter li.selected a:hover {
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
#changelist-filter #changelist-filter-extra-actions {
|
||||
font-size: 0.8125rem;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
/* DATE DRILLDOWN */
|
||||
|
||||
.change-list .toplinks {
|
||||
display: flex;
|
||||
padding-bottom: 5px;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px 17px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.change-list .toplinks a {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.change-list .toplinks .date-back {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.change-list .toplinks .date-back:focus,
|
||||
.change-list .toplinks .date-back:hover {
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
.filtered .actions {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#changelist table input {
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* Once the :has() pseudo-class is supported by all browsers, the tr.selected
|
||||
selector and the JS adding the class can be removed. */
|
||||
#changelist tbody tr.selected {
|
||||
background-color: var(--selected-row);
|
||||
}
|
||||
|
||||
#changelist tbody tr:has(.action-select:checked) {
|
||||
background-color: var(--selected-row);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
#changelist tbody tr.selected {
|
||||
background-color: SelectedItem;
|
||||
}
|
||||
#changelist tbody tr:has(.action-select:checked) {
|
||||
background-color: SelectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
padding: 10px;
|
||||
background: var(--body-bg);
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
line-height: 1.5rem;
|
||||
color: var(--body-quiet-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist .actions span.all,
|
||||
#changelist .actions span.action-counter,
|
||||
#changelist .actions span.clear,
|
||||
#changelist .actions span.question {
|
||||
font-size: 0.8125rem;
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
#changelist .actions:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
vertical-align: top;
|
||||
height: 1.5rem;
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
padding: 0 0 0 4px;
|
||||
margin: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#changelist .actions select:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
#changelist .actions .button {
|
||||
font-size: 0.8125rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--body-bg);
|
||||
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||
cursor: pointer;
|
||||
height: 1.5rem;
|
||||
line-height: 1;
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#changelist .actions .button:focus, #changelist .actions .button:hover {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
124
static/admin/css/dark_mode.css
Normal file
124
static/admin/css/dark_mode.css
Normal file
@@ -0,0 +1,124 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary: #264b5d;
|
||||
--primary-fg: #f7f7f7;
|
||||
|
||||
--body-fg: #eeeeee;
|
||||
--body-bg: #121212;
|
||||
--body-quiet-color: #e0e0e0;
|
||||
--body-loud-color: #ffffff;
|
||||
|
||||
--breadcrumbs-link-fg: #e0e0e0;
|
||||
--breadcrumbs-bg: var(--primary);
|
||||
|
||||
--link-fg: #81d4fa;
|
||||
--link-hover-color: #4ac1f7;
|
||||
--link-selected-fg: #6f94c6;
|
||||
|
||||
--hairline-color: #272727;
|
||||
--border-color: #353535;
|
||||
|
||||
--error-fg: #e35f5f;
|
||||
--message-success-bg: #006b1b;
|
||||
--message-warning-bg: #583305;
|
||||
--message-error-bg: #570808;
|
||||
|
||||
--darkened-bg: #212121;
|
||||
--selected-bg: #1b1b1b;
|
||||
--selected-row: #00363a;
|
||||
|
||||
--close-button-bg: #333333;
|
||||
--close-button-hover-bg: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--primary: #264b5d;
|
||||
--primary-fg: #f7f7f7;
|
||||
|
||||
--body-fg: #eeeeee;
|
||||
--body-bg: #121212;
|
||||
--body-quiet-color: #e0e0e0;
|
||||
--body-loud-color: #ffffff;
|
||||
|
||||
--breadcrumbs-link-fg: #e0e0e0;
|
||||
--breadcrumbs-bg: var(--primary);
|
||||
|
||||
--link-fg: #81d4fa;
|
||||
--link-hover-color: #4ac1f7;
|
||||
--link-selected-fg: #6f94c6;
|
||||
|
||||
--hairline-color: #272727;
|
||||
--border-color: #353535;
|
||||
|
||||
--error-fg: #e35f5f;
|
||||
--message-success-bg: #006b1b;
|
||||
--message-warning-bg: #583305;
|
||||
--message-error-bg: #570808;
|
||||
|
||||
--darkened-bg: #212121;
|
||||
--selected-bg: #1b1b1b;
|
||||
--selected-row: #00363a;
|
||||
|
||||
--close-button-bg: #333333;
|
||||
--close-button-hover-bg: #666666;
|
||||
}
|
||||
|
||||
/* THEME SWITCH */
|
||||
.theme-toggle {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
vertical-align: middle;
|
||||
margin-inline-start: 5px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
vertical-align: middle;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Fully hide screen reader text so we only show the one matching the current
|
||||
theme.
|
||||
*/
|
||||
.theme-toggle .visually-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle .theme-label-when-dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle .theme-label-when-light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ICONS */
|
||||
.theme-toggle svg.theme-icon-when-auto,
|
||||
.theme-toggle svg.theme-icon-when-dark,
|
||||
.theme-toggle svg.theme-icon-when-light {
|
||||
fill: var(--header-link-color);
|
||||
color: var(--header-bg);
|
||||
}
|
||||
|
||||
html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
|
||||
display: block;
|
||||
}
|
||||
29
static/admin/css/dashboard.css
Normal file
29
static/admin/css/dashboard.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/* DASHBOARD */
|
||||
.dashboard td, .dashboard th {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dashboard .module table th {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard .module table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard .module table td a {
|
||||
display: block;
|
||||
padding-right: .6em;
|
||||
}
|
||||
|
||||
/* RECENT ACTIONS MODULE */
|
||||
|
||||
.module ul.actionlist {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul.actionlist li {
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
534
static/admin/css/forms.css
Normal file
534
static/admin/css/forms.css
Normal file
@@ -0,0 +1,534 @@
|
||||
@import url('widgets.css');
|
||||
|
||||
/* FORM ROWS */
|
||||
|
||||
.form-row {
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
font-size: 0.8125rem;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.form-row img, .form-row input {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form-row label input[type="checkbox"] {
|
||||
margin-top: 0;
|
||||
vertical-align: 0;
|
||||
}
|
||||
|
||||
form .form-row p {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-multiline {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-multiline > div {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* FORM LABELS */
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.required label, label.required {
|
||||
font-weight: bold;
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
/* RADIO BUTTONS */
|
||||
|
||||
form div.radiolist div {
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
form div.radiolist.inline div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
form div.radiolist label {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
form div.radiolist input[type="radio"] {
|
||||
margin: -2px 4px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form ul.inline {
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form ul.inline li {
|
||||
float: left;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
/* ALIGNED FIELDSETS */
|
||||
|
||||
.aligned label {
|
||||
display: block;
|
||||
padding: 4px 10px 0 0;
|
||||
min-width: 160px;
|
||||
width: 160px;
|
||||
word-wrap: break-word;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aligned label:not(.vCheckboxLabel):after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 1.625rem;
|
||||
}
|
||||
|
||||
.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly {
|
||||
padding: 6px 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.aligned ul label {
|
||||
display: inline;
|
||||
float: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.aligned .form-row input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
form .aligned ul {
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form .aligned p.help,
|
||||
form .aligned div.help {
|
||||
margin-top: 0;
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned p.date div.help.timezonewarning,
|
||||
form .aligned p.datetime div.help.timezonewarning,
|
||||
form .aligned p.time div.help.timezonewarning {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
form .aligned p.help:last-child,
|
||||
form .aligned div.help:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
form .aligned input + p.help,
|
||||
form .aligned textarea + p.help,
|
||||
form .aligned select + p.help,
|
||||
form .aligned input + div.help,
|
||||
form .aligned textarea + div.help,
|
||||
form .aligned select + div.help {
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned ul li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
form .aligned table p {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel {
|
||||
float: none;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
padding: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel + p.help,
|
||||
.aligned .vCheckboxLabel + div.help {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField {
|
||||
width: 610px;
|
||||
}
|
||||
|
||||
fieldset .fieldBox {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* WIDE FIELDSETS */
|
||||
|
||||
.wide label {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
form .wide p,
|
||||
form .wide ul.errorlist,
|
||||
form .wide input + p.help,
|
||||
form .wide input + div.help {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
form .wide p.help,
|
||||
form .wide div.help {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
form div.help ul {
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField {
|
||||
width: 450px;
|
||||
}
|
||||
|
||||
/* COLLAPSED FIELDSETS */
|
||||
|
||||
fieldset.collapsed * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
fieldset.collapsed h2, fieldset.collapsed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
fieldset.collapsed {
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
fieldset.collapsed h2 {
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
fieldset .collapse-toggle {
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
fieldset.collapsed .collapse-toggle {
|
||||
background: transparent;
|
||||
display: inline;
|
||||
color: var(--link-fg);
|
||||
}
|
||||
|
||||
/* MONOSPACE TEXTAREAS */
|
||||
|
||||
fieldset.monospace textarea {
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
/* SUBMIT ROW */
|
||||
|
||||
.submit-row {
|
||||
padding: 12px 14px 12px;
|
||||
margin: 0 0 20px;
|
||||
background: var(--darkened-bg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
body.popup .submit-row {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.submit-row input {
|
||||
height: 2.1875rem;
|
||||
line-height: 0.9375rem;
|
||||
}
|
||||
|
||||
.submit-row input, .submit-row a {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submit-row input.default {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
display: block;
|
||||
background: var(--delete-button-bg);
|
||||
border-radius: 4px;
|
||||
padding: 0.625rem 0.9375rem;
|
||||
height: 0.9375rem;
|
||||
line-height: 0.9375rem;
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.submit-row a.closelink {
|
||||
display: inline-block;
|
||||
background: var(--close-button-bg);
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
height: 0.9375rem;
|
||||
line-height: 0.9375rem;
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.submit-row a.deletelink:focus,
|
||||
.submit-row a.deletelink:hover,
|
||||
.submit-row a.deletelink:active {
|
||||
background: var(--delete-button-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.submit-row a.closelink:focus,
|
||||
.submit-row a.closelink:hover,
|
||||
.submit-row a.closelink:active {
|
||||
background: var(--close-button-hover-bg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* CUSTOM FORM FIELDS */
|
||||
|
||||
.vSelectMultipleField {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.vCheckboxField {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.vDateField, .vTimeField {
|
||||
margin-right: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.vDateField {
|
||||
min-width: 6.85em;
|
||||
}
|
||||
|
||||
.vTimeField {
|
||||
min-width: 4.7em;
|
||||
}
|
||||
|
||||
.vURLField {
|
||||
width: 30em;
|
||||
}
|
||||
|
||||
.vLargeTextField, .vXMLLargeTextField {
|
||||
width: 48em;
|
||||
}
|
||||
|
||||
.flatpages-flatpage #id_content {
|
||||
height: 40.2em;
|
||||
}
|
||||
|
||||
.module table .vPositiveSmallIntegerField {
|
||||
width: 2.2em;
|
||||
}
|
||||
|
||||
.vIntegerField {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.vBigIntegerField {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.vForeignKeyRawIdAdminField {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.vTextField, .vUUIDField {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
/* INLINES */
|
||||
|
||||
.inline-group {
|
||||
padding: 0;
|
||||
margin: 0 0 30px;
|
||||
}
|
||||
|
||||
.inline-group thead th {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.inline-group .aligned label {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.inline-related {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inline-related h3 {
|
||||
margin: 0;
|
||||
color: var(--body-quiet-color);
|
||||
padding: 5px;
|
||||
font-size: 0.8125rem;
|
||||
background: var(--darkened-bg);
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.inline-related h3 span.delete label {
|
||||
margin-left: 2px;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.inline-related fieldset {
|
||||
margin: 0;
|
||||
background: var(--body-bg);
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-related fieldset.module h3 {
|
||||
margin: 0;
|
||||
padding: 2px 5px 3px 5px;
|
||||
font-size: 0.6875rem;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background: #bcd;
|
||||
color: var(--body-bg);
|
||||
}
|
||||
|
||||
.inline-group .tabular fieldset.module {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inline-related.tabular fieldset.module table {
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.last-related fieldset {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr.has_original td {
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.inline-group .tabular tr td.original {
|
||||
padding: 2px 0 0 0;
|
||||
width: 0;
|
||||
_position: relative;
|
||||
}
|
||||
|
||||
.inline-group .tabular th.original {
|
||||
width: 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-group .tabular td.original p {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.1em;
|
||||
padding: 2px 9px;
|
||||
overflow: hidden;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: bold;
|
||||
color: var(--body-quiet-color);
|
||||
_width: 700px;
|
||||
}
|
||||
|
||||
.inline-group ul.tools {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.inline-group ul.tools li {
|
||||
display: inline;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.inline-group div.add-row,
|
||||
.inline-group .tabular tr.add-row td {
|
||||
color: var(--body-quiet-color);
|
||||
background: var(--darkened-bg);
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.inline-group .tabular tr.add-row td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
.inline-group ul.tools a.add,
|
||||
.inline-group div.add-row a,
|
||||
.inline-group .tabular tr.add-row td a {
|
||||
background: url(../img/icon-addlink.svg) 0 1px no-repeat;
|
||||
padding-left: 16px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* RELATED FIELD ADD ONE / LOOKUP */
|
||||
|
||||
.related-lookup {
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 14px;
|
||||
}
|
||||
|
||||
.related-lookup {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background-image: url(../img/search.svg);
|
||||
}
|
||||
|
||||
form .related-widget-wrapper ul {
|
||||
display: inline-block;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.clearable-file-input input {
|
||||
margin-top: 0;
|
||||
}
|
||||
61
static/admin/css/login.css
Normal file
61
static/admin/css/login.css
Normal file
@@ -0,0 +1,61 @@
|
||||
/* LOGIN FORM */
|
||||
|
||||
.login {
|
||||
background: var(--darkened-bg);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login #header {
|
||||
height: auto;
|
||||
padding: 15px 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login #header h1 {
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login #header h1 a {
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
.login #content {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
background: var(--body-bg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
width: 28em;
|
||||
min-width: 300px;
|
||||
margin: 100px auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login .form-row {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.login .form-row #id_username, .login .form-row #id_password {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
padding: 1em 0 0 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login .password-reset-link {
|
||||
text-align: center;
|
||||
}
|
||||
150
static/admin/css/nav_sidebar.css
Normal file
150
static/admin/css/nav_sidebar.css
Normal file
@@ -0,0 +1,150 @@
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.toggle-nav-sidebar {
|
||||
z-index: 20;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 23px;
|
||||
width: 23px;
|
||||
border: 0;
|
||||
border-right: 1px solid var(--hairline-color);
|
||||
background-color: var(--body-bg);
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
color: var(--link-fg);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .toggle-nav-sidebar {
|
||||
border-left: 1px solid var(--hairline-color);
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.toggle-nav-sidebar:hover,
|
||||
.toggle-nav-sidebar:focus {
|
||||
background-color: var(--darkened-bg);
|
||||
}
|
||||
|
||||
#nav-sidebar {
|
||||
z-index: 15;
|
||||
flex: 0 0 275px;
|
||||
left: -276px;
|
||||
margin-left: -276px;
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid var(--hairline-color);
|
||||
background-color: var(--body-bg);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
[dir="rtl"] #nav-sidebar {
|
||||
border-left: 1px solid var(--hairline-color);
|
||||
border-right: 0;
|
||||
left: 0;
|
||||
margin-left: 0;
|
||||
right: -276px;
|
||||
margin-right: -276px;
|
||||
}
|
||||
|
||||
.toggle-nav-sidebar::before {
|
||||
content: '\00BB';
|
||||
}
|
||||
|
||||
.main.shifted .toggle-nav-sidebar::before {
|
||||
content: '\00AB';
|
||||
}
|
||||
|
||||
.main > #nav-sidebar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.main.shifted > #nav-sidebar {
|
||||
margin-left: 0;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
[dir="rtl"] .main.shifted > #nav-sidebar {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
#nav-sidebar .module th {
|
||||
width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#nav-sidebar .module th,
|
||||
#nav-sidebar .module caption {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
#nav-sidebar .module td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[dir="rtl"] #nav-sidebar .module th,
|
||||
[dir="rtl"] #nav-sidebar .module caption {
|
||||
padding-left: 8px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
#nav-sidebar .current-app .section:link,
|
||||
#nav-sidebar .current-app .section:visited {
|
||||
color: var(--header-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#nav-sidebar .current-model {
|
||||
background: var(--selected-row);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
#nav-sidebar .current-model {
|
||||
background-color: SelectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
.main > #nav-sidebar + .content {
|
||||
max-width: calc(100% - 23px);
|
||||
}
|
||||
|
||||
.main.shifted > #nav-sidebar + .content {
|
||||
max-width: calc(100% - 299px);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#nav-sidebar, #toggle-nav-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main > #nav-sidebar + .content,
|
||||
.main.shifted > #nav-sidebar + .content {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-filter {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5px;
|
||||
margin: 5px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--darkened-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
#nav-filter:focus {
|
||||
border-color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
#nav-filter.no-results {
|
||||
background: var(--message-error-bg);
|
||||
}
|
||||
|
||||
#nav-sidebar table {
|
||||
width: 100%;
|
||||
}
|
||||
970
static/admin/css/responsive.css
Normal file
970
static/admin/css/responsive.css
Normal file
@@ -0,0 +1,970 @@
|
||||
/* Tablets */
|
||||
|
||||
input[type="submit"], button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* Basic */
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 10px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
#container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#content {
|
||||
padding: 15px 20px 20px;
|
||||
}
|
||||
|
||||
div.breadcrumbs {
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
#header {
|
||||
flex-direction: column;
|
||||
padding: 15px 30px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#site-name {
|
||||
margin: 0 0 8px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
#user-tools {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
line-height: 1.85;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#user-tools a {
|
||||
display: inline-block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
|
||||
.dashboard #content {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#content-related {
|
||||
margin-right: -290px;
|
||||
}
|
||||
|
||||
.colSM #content-related {
|
||||
margin-left: -290px;
|
||||
}
|
||||
|
||||
.colMS {
|
||||
margin-right: 290px;
|
||||
}
|
||||
|
||||
.colSM {
|
||||
margin-left: 290px;
|
||||
}
|
||||
|
||||
.dashboard .module table td a {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
td .changelink, td .addlink {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Changelist */
|
||||
|
||||
#toolbar {
|
||||
border: none;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#changelist-search > div {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#changelist-search label {
|
||||
line-height: 1.375rem;
|
||||
}
|
||||
|
||||
#toolbar form #searchbar {
|
||||
flex: 1 0 auto;
|
||||
width: 0;
|
||||
height: 1.375rem;
|
||||
margin: 0 10px 0 6px;
|
||||
}
|
||||
|
||||
#toolbar form input[type=submit] {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
#changelist-search .quiet {
|
||||
width: 0;
|
||||
flex: 1 0 auto;
|
||||
margin: 5px 0 0 25px;
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
#changelist .actions .button {
|
||||
min-width: 48px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
#changelist .actions span.all,
|
||||
#changelist .actions span.clear,
|
||||
#changelist .actions span.question,
|
||||
#changelist .actions span.action-counter {
|
||||
font-size: 0.6875rem;
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
flex-basis: 200px;
|
||||
}
|
||||
|
||||
.change-list .filtered .results,
|
||||
.change-list .filtered .paginator,
|
||||
.filtered #toolbar,
|
||||
.filtered .actions,
|
||||
|
||||
#changelist .paginator {
|
||||
border-top-color: var(--hairline-color); /* XXX Is this used at all? */
|
||||
}
|
||||
|
||||
#changelist .results + .paginator {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-row input[type=text],
|
||||
.form-row input[type=password],
|
||||
.form-row input[type=email],
|
||||
.form-row input[type=url],
|
||||
.form-row input[type=tel],
|
||||
.form-row input[type=number],
|
||||
.form-row textarea,
|
||||
.form-row select,
|
||||
.form-row .vTextField {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 6px 8px;
|
||||
min-height: 2.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-row select {
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
.form-row select[multiple] {
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
fieldset .fieldBox + .fieldBox {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.aligned label {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.aligned .related-lookup,
|
||||
.aligned .datetimeshortcuts,
|
||||
.aligned .related-lookup + strong {
|
||||
align-self: center;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
padding: 10px 7px;
|
||||
}
|
||||
|
||||
.button, input[type=submit], input[type=button], .submit-row input, a.button {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
/* Selector */
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selector .selector-filter label {
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.selector .selector-filter input {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
width: auto;
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
width: 100%;
|
||||
flex: 1 0 auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
width: 26px;
|
||||
height: 52px;
|
||||
padding: 2px 0;
|
||||
border-radius: 20px;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.selector-add, .selector-remove {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: 20px auto;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background-position: 0 -120px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background-position: 0 -80px;
|
||||
}
|
||||
|
||||
a.selector-chooseall, a.selector-clearall {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.stacked {
|
||||
flex-direction: column;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.stacked > * {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.stacked select {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stacked .selector-available, .stacked .selector-chosen {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
padding: 0 2px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stacked .selector-chooser li {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.stacked .selector-add, .stacked .selector-remove {
|
||||
background-size: 20px auto;
|
||||
}
|
||||
|
||||
.stacked .selector-add {
|
||||
background-position: 0 -40px;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add {
|
||||
background-position: 0 -40px;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -140px;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
|
||||
background-position: 0 -60px;
|
||||
}
|
||||
|
||||
.stacked .selector-remove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -100px;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
|
||||
.help-tooltip, .selector .help-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.datetime input {
|
||||
width: 50%;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.datetime span {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.datetime .timezonewarning {
|
||||
display: block;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.datetimeshortcuts {
|
||||
color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */
|
||||
}
|
||||
|
||||
.form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.inline-group {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
ul.messagelist li {
|
||||
padding-left: 55px;
|
||||
background-position: 30px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.error {
|
||||
background-position: 30px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.warning {
|
||||
background-position: 30px 14px;
|
||||
}
|
||||
|
||||
/* Login */
|
||||
|
||||
.login #header {
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.login #site-name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* GIS */
|
||||
|
||||
div.olMap {
|
||||
max-width: calc(100vw - 30px);
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.olMap + .clear_features {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* Docs */
|
||||
|
||||
.module table.xfull {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre.literal-block {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Layout */
|
||||
|
||||
#header, #content, #footer {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#footer:empty {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.breadcrumbs {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
/* Dashboard */
|
||||
|
||||
.colMS, .colSM {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#content-related, .colSM #content-related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#content-related .module {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#content-related .module h2 {
|
||||
padding: 10px 15px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Changelist */
|
||||
|
||||
#changelist {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#changelist .actions label {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
#changelist .actions select {
|
||||
flex: 1 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#changelist .actions span {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
#changelist-filter {
|
||||
position: static;
|
||||
width: auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.object-tools {
|
||||
float: none;
|
||||
margin: 0 0 15px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.object-tools li {
|
||||
height: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.object-tools li + li {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
.form-row {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.aligned .form-row,
|
||||
.aligned .form-row > div {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.aligned .form-row > div {
|
||||
width: calc(100vw - 30px);
|
||||
}
|
||||
|
||||
.flex-container {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.flex-container.checkbox-row {
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.vURLField {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
fieldset .fieldBox + .fieldBox {
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
fieldset.collapsed .form-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.aligned label {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
padding: 0 0 10px;
|
||||
}
|
||||
|
||||
.aligned label:after {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.aligned .form-row input,
|
||||
.aligned .form-row select,
|
||||
.aligned .form-row textarea {
|
||||
flex: 1 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.aligned .checkbox-row input {
|
||||
flex: 0 1 auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.aligned .vCheckboxLabel {
|
||||
flex: 1 0;
|
||||
padding: 1px 0 0 5px;
|
||||
}
|
||||
|
||||
.aligned label + p,
|
||||
.aligned label + div.help,
|
||||
.aligned label + div.readonly {
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.aligned p.file-upload {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
span.clearable-file-input {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
span.clearable-file-input label {
|
||||
font-size: 0.8125rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.aligned .timezonewarning {
|
||||
flex: 1 0 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
form .aligned .form-row div.help {
|
||||
width: 100%;
|
||||
margin: 5px 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
form .aligned ul,
|
||||
form .aligned ul.errorlist {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist {
|
||||
margin-top: 5px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
|
||||
form .aligned div.radiolist:not(.inline) div + div {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Related widget */
|
||||
|
||||
.related-widget-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.related-widget-wrapper .selector {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.related-widget-wrapper > a {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.related-widget-wrapper .radiolist ~ a {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.related-widget-wrapper > select ~ a {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
/* Selector */
|
||||
|
||||
.selector {
|
||||
flex-direction: column;
|
||||
gap: 10px 0;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
max-height: 96px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
display: block;
|
||||
width: 52px;
|
||||
height: 26px;
|
||||
padding: 0 2px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -20px;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background-position: 0 -40px;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -60px;
|
||||
}
|
||||
|
||||
/* Inlines */
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related {
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related .module {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row {
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related h3 {
|
||||
padding: 10px;
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 2px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .inline-related h3 span.delete {
|
||||
float: none;
|
||||
flex: 1 1 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] .aligned label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-group[data-inline-type="stacked"] div.add-row {
|
||||
margin-top: 15px;
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.inline-group div.add-row,
|
||||
.inline-group .tabular tr.add-row td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inline-group div.add-row a,
|
||||
.inline-group .tabular tr.add-row td a {
|
||||
display: block;
|
||||
padding: 8px 10px 8px 26px;
|
||||
background-position: 8px 9px;
|
||||
}
|
||||
|
||||
/* Submit row */
|
||||
|
||||
.submit-row {
|
||||
padding: 10px;
|
||||
margin: 0 0 15px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit-row input, .submit-row input.default, .submit-row a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-row a.closelink {
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
ul.messagelist li {
|
||||
padding-left: 40px;
|
||||
background-position: 15px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.error {
|
||||
background-position: 15px 12px;
|
||||
}
|
||||
|
||||
ul.messagelist li.warning {
|
||||
background-position: 15px 14px;
|
||||
}
|
||||
|
||||
/* Paginator */
|
||||
|
||||
.paginator .this-page, .paginator a:link, .paginator a:visited {
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
/* Login */
|
||||
|
||||
body.login {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
width: auto;
|
||||
max-width: 480px;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.login #header,
|
||||
.login #content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.login #content-main {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.login .form-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.login .form-row + .form-row {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
margin: 0 0 5px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
padding: 15px 0 0;
|
||||
}
|
||||
|
||||
.login br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login .submit-row input {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.errornote {
|
||||
margin: 0 0 20px;
|
||||
padding: 8px 12px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Calendar and clock */
|
||||
|
||||
.calendarbox, .clockbox {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.calendarbox:before, .clockbox:before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.calendarbox > *, .clockbox > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.calendarbox > div:first-child {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.calendarbox .calendar, .clockbox h2 {
|
||||
border-radius: 4px 4px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendarbox .calendar-cancel, .clockbox .calendar-cancel {
|
||||
border-radius: 0 0 4px 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-shortcuts {
|
||||
padding: 10px 0;
|
||||
font-size: 0.75rem;
|
||||
line-height: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-shortcuts a {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.timelist a {
|
||||
background: var(--body-bg);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.calendar-cancel {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.clockbox h2 {
|
||||
padding: 8px 15px;
|
||||
}
|
||||
|
||||
.calendar caption {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
||||
z-index: 1;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
/* History */
|
||||
|
||||
table#change-history tbody th, table#change-history tbody td {
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
table#change-history tbody th {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Docs */
|
||||
|
||||
table.model tbody th, table.model tbody td {
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user