Merge pull request #47 from WDI-Ideas/staging

Staging
This commit is contained in:
BOBBY VISHWAKARMA
2024-05-16 19:19:37 +05:30
committed by GitHub
295 changed files with 49386 additions and 2135 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
.vscode
# C extensions

42
apple.py Normal file
View 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)

View File

@@ -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
View 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

Binary file not shown.

View File

@@ -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

View File

@@ -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()),
]

View File

@@ -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
View 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

View File

@@ -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)'),
),
]

View 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),
),
]

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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),
),
]

View 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',
},
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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'),
),
]

View File

@@ -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)

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -1,8 +1,10 @@
from django.contrib.auth import authenticate
from django.contrib.auth.hashers import make_password
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from module_iam.models import 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)

View File

@@ -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'),
]

View File

@@ -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)

View File

@@ -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 (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,13 +111,34 @@ 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)
return ApiResponse.error(
message=constants.EMAIL_REQUIRED, errors=constants.EMAIL_REQUIRED
)
print(f"email auth username: {settings.EMAIL_HOST_USER}")
email = request.data.get("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.EMAIL_HOST_USER,
)
# 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,153 @@ 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 = []
permission_classes = []
model = AppVersion
def get(self, request, *args, **kwargs):
app_version = request.GET.get('appVersion')
device_type = request.GET.get('deviceType')
if not app_version or not device_type:
return ApiResponse.error(message=constants.FAILURE, errors="App version and device type is required")
# Query the database to retrieve the upgrade flags based on the app version
try:
version = self.model.objects.get(version=app_version)
except self.model.DoesNotExist:
version = None
if version:
upgrade_flags = {
'forceUpgrade': version.force_upgrade,
'recommendUpgrade': version.recommend_upgrade,
}
else:
upgrade_flags = {
'forceUpgrade': False,
'recommendUpgrade': False,
}
return ApiResponse.success(message=constants.SUCCESS, data=upgrade_flags)

View File

@@ -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

View File

@@ -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")
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -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

View File

@@ -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")

View File

@@ -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(),
),
]

View File

@@ -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"

View File

@@ -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'),

View File

@@ -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

View 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,
}
}

View 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"
}
}
]

View 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"
}
}
]

View 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"
}
}
]

View 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
View 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")

View File

@@ -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

View 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

View 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)}")
)

View 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.')),
],
),
]

View 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',
),
]

View 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+$')]),
),
]

View File

@@ -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'),
),
]

View File

@@ -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+$')]),
),
]

View File

@@ -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'),
),
]

View File

@@ -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 = [
('apple', 'apple'),
('android', 'android'),
]
device_type = models.CharField(max_length=10, choices=DEVICE_CHOICES, verbose_name='Device Type (apple / 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

86
module_iam/permission.py Normal file
View File

@@ -0,0 +1,86 @@
from functools import wraps
from django.core.exceptions import PermissionDenied
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}")
raise PermissionDenied("You do not have permission to access this resource.")
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):
raise PermissionDenied("You do not have permission to access this resource.")
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator

View 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)

View File

@@ -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")
]

View File

@@ -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)

View 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']

View 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"),
]

View 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)

View File

@@ -0,0 +1,9 @@
from django import forms
from .models import PushNotification
class PushNotificationForm(forms.ModelForm):
class Meta:
model = PushNotification
fields = ('title', 'message')

View File

@@ -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()

View 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',
},
),
]

View 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'],
},
),
]

View File

@@ -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',
),
]

View File

@@ -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

View 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 ")

View 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"),
]

View File

@@ -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")

View File

@@ -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."

View File

@@ -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'):

View File

@@ -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)

View File

@@ -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))

View File

@@ -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", True)
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",
@@ -212,12 +217,17 @@ EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
EMAIL_PORT = env.str("EMAIL_PORT")
EMAIL_USE_TLS = True
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
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/4.2/topics/logging/#logging
# 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 +261,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 +284,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'))),
]

View File

@@ -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")]

View File

@@ -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'

View File

@@ -14,27 +14,30 @@ 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.urls import include, path
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")),
]
if settings.DEBUG:

View File

@@ -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:

View File

@@ -8,6 +8,11 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
import sys
sys.path.append('/var/www/testing_django/testing')
sys.path.append('/var/www/testing_django/testing/testing')
sys.path.append('/var/www/testing_django/testing/venv/lib/python3.11/site-packages')
from django.core.wsgi import get_wsgi_application

View File

@@ -1,6 +1,8 @@
from rest_framework import serializers
from module_support.models import ContactUs, Feedback
class ContactUsSerializer(serializers.ModelSerializer):
class Meta:
model = ContactUs

View File

@@ -1,4 +1,5 @@
from django.urls import path
from . import views
urlpatterns = [

View File

@@ -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
View File

View File

@@ -1,4 +1,5 @@
from django.db import models
from module_iam.models import BaseModel, IAmPrincipal
# Create your models here.

View File

@@ -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'),
]

View File

@@ -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.EMAIL_HOST_USER,
)
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.EMAIL_HOST_USER,
)
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.EMAIL_HOST_USER,
)
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
View 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
View 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')

View File

@@ -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

View 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

File diff suppressed because it is too large Load Diff

View 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);
}

View 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;
}

View 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
View 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;
}

View 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;
}

View 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%;
}

View 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;
}
}

View File

@@ -0,0 +1,84 @@
/* TABLETS */
@media (max-width: 1024px) {
[dir="rtl"] .colMS {
margin-right: 0;
}
[dir="rtl"] #user-tools {
text-align: right;
}
[dir="rtl"] #changelist .actions label {
padding-left: 10px;
padding-right: 0;
}
[dir="rtl"] #changelist .actions select {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .change-list .filtered .results,
[dir="rtl"] .change-list .filtered .paginator,
[dir="rtl"] .filtered #toolbar,
[dir="rtl"] .filtered div.xfull,
[dir="rtl"] .filtered .actions,
[dir="rtl"] #changelist-filter {
margin-left: 0;
}
[dir="rtl"] .inline-group ul.tools a.add,
[dir="rtl"] .inline-group div.add-row a,
[dir="rtl"] .inline-group .tabular tr.add-row td a {
padding: 8px 26px 8px 10px;
background-position: calc(100% - 8px) 9px;
}
[dir="rtl"] .related-widget-wrapper-link + .selector {
margin-right: 0;
margin-left: 15px;
}
[dir="rtl"] .selector .selector-filter label {
margin-right: 0;
margin-left: 8px;
}
[dir="rtl"] .object-tools li {
float: right;
}
[dir="rtl"] .object-tools li + li {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .dashboard .module table td a {
padding-left: 0;
padding-right: 16px;
}
}
/* MOBILE */
@media (max-width: 767px) {
[dir="rtl"] .aligned .related-lookup,
[dir="rtl"] .aligned .datetimeshortcuts {
margin-left: 0;
margin-right: 15px;
}
[dir="rtl"] .aligned ul,
[dir="rtl"] form .aligned ul.errorlist {
margin-right: 0;
}
[dir="rtl"] #changelist-filter {
margin-left: 0;
margin-right: 0;
}
[dir="rtl"] .aligned .vCheckboxLabel {
padding: 1px 5px 0 0;
}
}

302
static/admin/css/rtl.css Normal file
View File

@@ -0,0 +1,302 @@
/* GLOBAL */
th {
text-align: right;
}
.module h2, .module caption {
text-align: right;
}
.module ul, .module ol {
margin-left: 0;
margin-right: 1.5em;
}
.viewlink, .addlink, .changelink, .hidelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.deletelink {
padding-left: 0;
padding-right: 16px;
background-position: 100% 1px;
}
.object-tools {
float: left;
}
thead th:first-child,
tfoot td:first-child {
border-left: none;
}
/* LAYOUT */
#user-tools {
right: auto;
left: 0;
text-align: left;
}
div.breadcrumbs {
text-align: right;
}
#content-main {
float: right;
}
#content-related {
float: left;
margin-left: -300px;
margin-right: auto;
}
.colMS {
margin-left: 300px;
margin-right: 0;
}
/* SORTABLE TABLES */
table thead th.sorted .sortoptions {
float: left;
}
thead th.sorted .text {
padding-right: 0;
padding-left: 42px;
}
/* dashboard styles */
.dashboard .module table td a {
padding-left: .6em;
padding-right: 16px;
}
/* changelists styles */
.change-list .filtered table {
border-left: none;
border-right: 0px none;
}
#changelist-filter {
border-left: none;
border-right: none;
margin-left: 0;
margin-right: 30px;
}
#changelist-filter li.selected {
border-left: none;
padding-left: 10px;
margin-left: 0;
border-right: 5px solid var(--hairline-color);
padding-right: 10px;
margin-right: -15px;
}
#changelist table tbody td:first-child, #changelist table tbody th:first-child {
border-right: none;
border-left: none;
}
.paginator .end {
margin-left: 6px;
margin-right: 0;
}
.paginator input {
margin-left: 0;
margin-right: auto;
}
/* FORMS */
.aligned label {
padding: 0 0 3px 1em;
}
.submit-row a.deletelink {
margin-left: 0;
margin-right: auto;
}
.vDateField, .vTimeField {
margin-left: 2px;
}
.aligned .form-row input {
margin-left: 5px;
}
form .aligned ul {
margin-right: 163px;
padding-right: 10px;
margin-left: 0;
padding-left: 0;
}
form ul.inline li {
float: right;
padding-right: 0;
padding-left: 7px;
}
form .aligned p.help,
form .aligned div.help {
margin-right: 160px;
padding-right: 10px;
}
form div.help ul,
form .aligned .checkbox-row + .help,
form .aligned p.date div.help.timezonewarning,
form .aligned p.datetime div.help.timezonewarning,
form .aligned p.time div.help.timezonewarning {
margin-right: 0;
padding-right: 0;
}
form .wide p.help, form .wide div.help {
padding-left: 0;
padding-right: 50px;
}
form .wide p,
form .wide ul.errorlist,
form .wide input + p.help,
form .wide input + div.help {
margin-right: 200px;
margin-left: 0px;
}
.submit-row {
text-align: right;
}
fieldset .fieldBox {
margin-left: 20px;
margin-right: 0;
}
.errorlist li {
background-position: 100% 12px;
padding: 0;
}
.errornote {
background-position: 100% 12px;
padding: 10px 12px;
}
/* WIDGETS */
.calendarnav-previous {
top: 0;
left: auto;
right: 10px;
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
}
.calendarbox .calendarnav-previous:focus,
.calendarbox .calendarnav-previous:hover {
background-position: 0 -45px;
}
.calendarnav-next {
top: 0;
right: auto;
left: 10px;
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
}
.calendarbox .calendarnav-next:focus,
.calendarbox .calendarnav-next:hover {
background-position: 0 -15px;
}
.calendar caption, .calendarbox h2 {
text-align: center;
}
.selector {
float: right;
}
.selector .selector-filter {
text-align: right;
}
.selector-add {
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
}
.active.selector-add:focus, .active.selector-add:hover {
background-position: 0 -80px;
}
.selector-remove {
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-remove:focus, .active.selector-remove:hover {
background-position: 0 -112px;
}
a.selector-chooseall {
background: url(../img/selector-icons.svg) right -128px no-repeat;
}
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
background-position: 100% -144px;
}
a.selector-clearall {
background: url(../img/selector-icons.svg) 0 -160px no-repeat;
}
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
background-position: 0 -176px;
}
.inline-deletelink {
float: left;
}
form .form-row p.datetime {
overflow: hidden;
}
.related-widget-wrapper {
float: right;
}
/* MISC */
.inline-related h2, .inline-group h2 {
text-align: right
}
.inline-related h3 span.delete {
padding-right: 20px;
padding-left: inherit;
left: 10px;
right: inherit;
float:left;
}
.inline-related h3 span.delete label {
margin-left: inherit;
margin-right: 2px;
}
.selector .selector-chooser {
margin: 0;
}

Some files were not shown because too many files have changed in this diff Show More