create: new folder
13
backend/easycookapi-main/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
env/
|
||||
sent_emails/
|
||||
media/profile_picts
|
||||
media/profile_pictures
|
||||
EasyCookAPI/settings.py
|
||||
*.pyc
|
||||
__pycache__/
|
||||
accounts/__pycache__/
|
||||
EasyCookAPI/__pycache__/
|
||||
pantry/__pycache__/
|
||||
personalize/__pycache__/
|
||||
profiles/__pycache__/
|
||||
recipes/__pycache__/
|
||||
0
backend/easycookapi-main/EasyCookAPI/__init__.py
Normal file
16
backend/easycookapi-main/EasyCookAPI/asgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for EasyCookAPI project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'EasyCookAPI.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
37
backend/easycookapi-main/EasyCookAPI/urls.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""EasyCookAPI URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
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 rest_framework.authtoken.views import obtain_auth_token
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('accounts/', include('accounts.urls')),
|
||||
path('profiles/', include('profiles.urls')),
|
||||
path('recipes/', include('recipes.urls')),
|
||||
path('pantry/', include('pantry.urls')),
|
||||
path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
|
||||
]
|
||||
|
||||
# Serving the media files in development mode
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL,
|
||||
document_root=settings.MEDIA_ROOT)
|
||||
else:
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
16
backend/easycookapi-main/EasyCookAPI/wsgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for EasyCookAPI project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'EasyCookAPI.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
0
backend/easycookapi-main/accounts/__init__.py
Normal file
1
backend/easycookapi-main/accounts/admin.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from django.contrib import admin
|
||||
5
backend/easycookapi-main/accounts/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'accounts'
|
||||
4
backend/easycookapi-main/accounts/models.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from django.contrib.auth.models import User
|
||||
|
||||
User._meta.get_field('email')._unique = True
|
||||
User._meta.get_field('username')._unique = True
|
||||
105
backend/easycookapi-main/accounts/serializers.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
from rest_framework import serializers, status
|
||||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User
|
||||
from profiles.models import Account
|
||||
from django.db import IntegrityError
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.ModelSerializer):
|
||||
username = serializers.CharField(
|
||||
error_messages={
|
||||
"required": "Username harus diisi",
|
||||
"invalid": "Username yang diisi tidak sesuai dengan format",
|
||||
"unique": "Username sudah terdaftar. Silakan gunakan username lain",
|
||||
},
|
||||
)
|
||||
email = serializers.CharField(
|
||||
error_messages={
|
||||
"required": "Email harus diisi",
|
||||
"invalid": "Email yang diisi tidak sesuai dengan format",
|
||||
"unique": "Email sudah terdaftar. Silakan gunakan email lain.",
|
||||
},
|
||||
)
|
||||
phone = serializers.CharField(max_length=20, required=False, error_messages={
|
||||
"unique": "Nomor telepon sudah terdaftar. Silakan gunakan nomor lain.",
|
||||
},)
|
||||
|
||||
password = serializers.CharField(write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['first_name', 'last_name',
|
||||
'username', 'email', 'phone', 'password']
|
||||
|
||||
def create(self, validated_data):
|
||||
phone = validated_data.pop('phone', None)
|
||||
first_name = validated_data.get('first_name', '')
|
||||
words = first_name.split()
|
||||
|
||||
if len(words) > 1 and 'last_name' not in validated_data:
|
||||
first_name = words[0]
|
||||
last_name = ' '.join(words[1:])
|
||||
validated_data['first_name'] = first_name
|
||||
validated_data['last_name'] = last_name
|
||||
|
||||
try:
|
||||
user = User.objects.create_user(**validated_data)
|
||||
Account.objects.create(user=user, phone=phone)
|
||||
except IntegrityError:
|
||||
error_message = "Username atau email sudah pernah digunakan"
|
||||
raise serializers.ValidationError(
|
||||
{"message": error_message}, code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class AuthTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(
|
||||
label=_("Username"),
|
||||
error_messages={'required': 'Username harus diisi'}
|
||||
)
|
||||
password = serializers.CharField(
|
||||
label=_("Password"),
|
||||
style={'input_type': 'password'},
|
||||
trim_whitespace=False,
|
||||
write_only=True,
|
||||
error_messages={'required': 'Password harus diisi'}
|
||||
)
|
||||
token = serializers.CharField(
|
||||
label=_("Token"),
|
||||
read_only=True
|
||||
)
|
||||
|
||||
extra_kwargs = {
|
||||
"username": {
|
||||
"error_messages": {
|
||||
"required": "Username harus diisi",
|
||||
"invalid": "Username yang diisi tidak sesuai dengan format"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def validate(self, attrs):
|
||||
username = attrs.get('username')
|
||||
password = attrs.get('password')
|
||||
|
||||
if username and password:
|
||||
user = authenticate(request=self.context.get('request'),
|
||||
username=username, password=password)
|
||||
|
||||
if not user:
|
||||
msg = _('Username atau password salah')
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
else:
|
||||
msg = _('Harap masukan "username" dan "password"')
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
|
||||
attrs['user'] = user
|
||||
return attrs
|
||||
11
backend/easycookapi-main/accounts/signals.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from profiles.models import Account
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_or_update_account(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
Account.objects.create(user=instance)
|
||||
else:
|
||||
instance.account.save()
|
||||
3
backend/easycookapi-main/accounts/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
8
backend/easycookapi-main/accounts/urls.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from accounts.views import RegisterView, LoginView, LogoutView
|
||||
|
||||
urlpatterns = [
|
||||
path('register/', RegisterView.as_view(), name='account-register'),
|
||||
path('login/', LoginView.as_view(), name='account-login'),
|
||||
path('logout/', LogoutView.as_view(), name='account-logout'),
|
||||
]
|
||||
7
backend/easycookapi-main/accounts/utils.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.contrib.auth. tokens import PasswordResetTokenGenerator
|
||||
|
||||
class TokenGenerator(PasswordResetTokenGenerator):
|
||||
def _make_hash_value(self, user, timestamp):
|
||||
return str(user.pk)+str(timestamp)
|
||||
|
||||
generate_token = TokenGenerator()
|
||||
51
backend/easycookapi-main/accounts/views.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from django.contrib.auth import authenticate, logout
|
||||
from rest_framework import generics, permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from profiles.models import Account
|
||||
from .serializers import RegisterSerializer
|
||||
|
||||
|
||||
class RegisterView(generics.CreateAPIView):
|
||||
serializer_class = RegisterSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
return Response(serializer.errors, status=400)
|
||||
|
||||
|
||||
class LoginView(APIView):
|
||||
def post(self, request):
|
||||
username = request.data.get('username')
|
||||
password = request.data.get('password')
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
return Response({'token': token.key})
|
||||
else:
|
||||
return Response({'message': 'Username atau password salah.'}, status=400)
|
||||
|
||||
|
||||
class LogoutView(generics.CreateAPIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def post(self, request):
|
||||
request.user.auth_token.delete()
|
||||
logout(request)
|
||||
return Response({'message': 'Berhasil log out.'})
|
||||
|
||||
|
||||
class ForgotPasswordView(generics.GenericAPIView):
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
# Implementation for forgot password feature
|
||||
return Response("Forgot password implementation goes here")
|
||||
22
backend/easycookapi-main/manage.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'EasyCookAPI.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
backend/easycookapi-main/media/default.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
backend/easycookapi-main/media/default_profpics.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
0
backend/easycookapi-main/pantry/__init__.py
Normal file
3
backend/easycookapi-main/pantry/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/easycookapi-main/pantry/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PantryConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'pantry'
|
||||
38
backend/easycookapi-main/pantry/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 4.1 on 2023-06-28 09:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('id_product', models.IntegerField(null=True)),
|
||||
('nama_bahan', models.CharField(max_length=255)),
|
||||
('harga', models.FloatField(null=True)),
|
||||
('berat', models.IntegerField(null=True)),
|
||||
('satuan', models.CharField(max_length=50)),
|
||||
('nama_supplier', models.CharField(max_length=155)),
|
||||
('alamat', models.TextField(null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rekomendasi',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pantry.product')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1 on 2023-07-18 10:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pantry', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rekomendasi',
|
||||
name='supplier',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
20
backend/easycookapi-main/pantry/models.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
id_product = models.IntegerField(null=True)
|
||||
nama_bahan = models.CharField(max_length=255)
|
||||
harga = models.FloatField(null=True)
|
||||
berat = models.IntegerField(null=True)
|
||||
satuan = models.CharField(max_length=50)
|
||||
nama_supplier = models.CharField(max_length=155)
|
||||
alamat = models.TextField(null=True)
|
||||
|
||||
|
||||
class Rekomendasi(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
supplier = models.CharField(max_length=255, null=True)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
8
backend/easycookapi-main/pantry/serializers.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Product
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = '__all__'
|
||||
3
backend/easycookapi-main/pantry/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
15
backend/easycookapi-main/pantry/urls.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from django.urls import path
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from .views import RekomendasiView, RecipeRecomendationView, ProductView
|
||||
|
||||
urlpatterns = [
|
||||
path('product/', ProductView.as_view(), name="add-product"),
|
||||
|
||||
path("<int:id>/rekomendasi/", RekomendasiView.as_view(),
|
||||
name="rekomendasi-supplier"),
|
||||
|
||||
path("rekomendasi-resep/", RecipeRecomendationView.as_view(),
|
||||
name="rekomendasi-resep"),
|
||||
]
|
||||
153
backend/easycookapi-main/pantry/views.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
from django.shortcuts import render
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics, status
|
||||
from django.http import JsonResponse
|
||||
import json
|
||||
import requests
|
||||
from django.db.models import Count
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from .models import Product, Rekomendasi
|
||||
from recipes.models import Information, Ingredient, Unit, RawIngredient, Tag
|
||||
from profiles.models import Pantry
|
||||
from django.db.models import Q, Subquery, OuterRef, Func, F, Count
|
||||
from .serializers import ProductSerializer
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class ProductView(generics.GenericAPIView):
|
||||
def post(self, request):
|
||||
Product.objects.all().delete()
|
||||
panenpanen_data = requests.get(
|
||||
'http://127.0.0.1:8080/api/v1/easycook/products')
|
||||
json_data = panenpanen_data.json()
|
||||
products = json_data.get('data', [])
|
||||
for product in products:
|
||||
id_product = product.get('id')
|
||||
nama_bahan = product.get('nama_bahan')
|
||||
harga = product.get('harga')
|
||||
berat = product.get('berat')
|
||||
satuan = product.get('satuan')
|
||||
nama_supplier = product.get('nama_supplier')
|
||||
alamat = product.get('alamat-supplier')
|
||||
data = Product(id_product=id_product, nama_bahan=nama_bahan, harga=harga,
|
||||
berat=berat, satuan=satuan, nama_supplier=nama_supplier, alamat=alamat)
|
||||
# data.save()
|
||||
|
||||
response_data = {
|
||||
'data': 'Data berhasil disimpan',
|
||||
'status': 'success',
|
||||
}
|
||||
return JsonResponse(response_data)
|
||||
|
||||
|
||||
class RekomendasiView(generics.GenericAPIView):
|
||||
serializer_class = ProductSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Information.objects.filter(pk=self.kwargs['id']).prefetch_related('ingredient_set')
|
||||
|
||||
def get(self, request, id):
|
||||
user = request.user
|
||||
Rekomendasi.objects.filter(user_id=user.id).delete()
|
||||
# Product.objects.all().delete()
|
||||
recipe = Information.objects.filter(pk=self.kwargs['id'])
|
||||
recipe_id = recipe.get().id
|
||||
|
||||
# panenpanen_data = requests.get(
|
||||
# 'http://152.69.206.130:80/api/v1/easycook/products')
|
||||
# json_data = panenpanen_data.json()
|
||||
# products = json_data.get('data', [])
|
||||
# for product in products:
|
||||
# id_product = product.get('id')
|
||||
# nama_bahan = product.get('nama_bahan')
|
||||
# harga = product.get('harga')
|
||||
# berat = product.get('berat')
|
||||
# satuan = product.get('satuan')
|
||||
# nama_supplier = product.get('nama_supplier')
|
||||
# alamat = product.get('alamat-supplier')
|
||||
# data = Product(id_product=id_product, nama_bahan=nama_bahan, harga=harga,
|
||||
# berat=berat, satuan=satuan, nama_supplier=nama_supplier, alamat=alamat)
|
||||
# data.save()
|
||||
|
||||
owned_raw_materials = Pantry.objects.filter(
|
||||
user=request.user).values_list('raw_ingredient', flat=True)
|
||||
|
||||
owned_raw_ingredient_ids = Pantry.objects.filter(
|
||||
user=user).values_list('raw_ingredient_id', flat=True)
|
||||
not_owned_ingredients = Ingredient.objects.filter(
|
||||
recipe_id=recipe_id).exclude(raw_ingredient__in=owned_raw_ingredient_ids)
|
||||
|
||||
for bahan in not_owned_ingredients:
|
||||
nama_bahan = bahan.name
|
||||
products = Product.objects.filter(
|
||||
nama_bahan__icontains=nama_bahan)
|
||||
for product in products:
|
||||
rekomendasi = Rekomendasi(
|
||||
product_id=product.id, user_id=user.id, supplier=product.nama_supplier)
|
||||
rekomendasi.save()
|
||||
|
||||
rangking = Rekomendasi.objects.values('supplier').annotate(
|
||||
count=Count('supplier')).order_by('-count')
|
||||
data = Rekomendasi.objects.filter(user_id=user.id)
|
||||
datas = list(data.values())
|
||||
|
||||
supplier_data = {}
|
||||
rekomendasi_bahan = []
|
||||
for data in datas:
|
||||
product = Product.objects.get(id=data['product_id'])
|
||||
supplier = data['supplier']
|
||||
if supplier not in supplier_data:
|
||||
supplier_data[supplier] = []
|
||||
supplier_data[supplier].append({
|
||||
'nama_bahan': product.nama_bahan,
|
||||
'harga': product.harga,
|
||||
'satuan': product.satuan,
|
||||
'alamat': product.alamat,
|
||||
'berat': product.berat,
|
||||
'id_product': product.id_product,
|
||||
'nama_supplier': product.nama_supplier,
|
||||
|
||||
})
|
||||
|
||||
rangking_list = []
|
||||
for rank in rangking:
|
||||
supplier = rank['supplier']
|
||||
count = rank['count']
|
||||
products = supplier_data.get(supplier, [])
|
||||
rangking_list.append({
|
||||
'nama_supplier': supplier,
|
||||
'jumlah_kesesuaian_bahan': count,
|
||||
'produk': products
|
||||
})
|
||||
|
||||
return JsonResponse(rangking_list, safe=False)
|
||||
|
||||
|
||||
class RecipeRecomendationView(generics.GenericAPIView):
|
||||
def get_owned_ingredients(self, user):
|
||||
pantry_items = Pantry.objects.filter(
|
||||
user=user).values_list('raw_material_id', flat=True)
|
||||
owned_ingredients = IngredientTag.objects.filter(
|
||||
raw_material_id__in=pantry_items).values_list('ingredient_id', flat=True).distinct()
|
||||
return list(owned_ingredients)
|
||||
|
||||
def get_recipes_with_completeness_percentage(self, request):
|
||||
user_count_subquery = IngredientTag.objects.filter(ingredient__recipe_id=OuterRef(
|
||||
'pk'), raw_material__pantry__user_id=request.user.id).annotate(count=Func(F("raw_material__pantry__id"), function="Count")).values("count")
|
||||
recipes = Information.objects.annotate(completeness_percentage=Subquery(
|
||||
user_count_subquery) / Count('ingredient') * 100,)
|
||||
return recipes
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
raw_meterials = RawMaterial.objects.all()
|
||||
pantry_raw_materials = Pantry.objects.filter(
|
||||
user=user).values_list('raw_material_id', flat=True)
|
||||
owned_ingredients = self.get_owned_ingredients(user)
|
||||
recipes = self.get_recipes_with_completeness_percentage(request).filter(
|
||||
ingredient__pk__in=owned_ingredients).order_by('-completeness_percentage').distinct()
|
||||
|
||||
recipe_data = list(recipes.values())
|
||||
|
||||
return JsonResponse({"Rekomendasi resep": recipe_data}, safe=False)
|
||||
0
backend/easycookapi-main/profiles/__init__.py
Normal file
3
backend/easycookapi-main/profiles/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
backend/easycookapi-main/profiles/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class ProfilesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'profiles'
|
||||
70
backend/easycookapi-main/profiles/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Generated by Django 4.1 on 2023-06-28 09:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import profiles.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('recipes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Preference',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_at', models.DateTimeField(auto_now=True)),
|
||||
('preference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.restriction')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Pantry',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('stock', models.IntegerField(default=None, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_at', models.DateTimeField(auto_now=True)),
|
||||
('raw_material', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.rawingredient')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='History',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_at', models.DateTimeField(auto_now=True)),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.information')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Favorite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.information')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_picture', models.ImageField(default='default_profpics.png', upload_to=profiles.models.user_directory_path)),
|
||||
('phone', models.CharField(max_length=20, null=True, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('modified_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1 on 2023-06-28 11:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='pantry',
|
||||
old_name='raw_material',
|
||||
new_name='raw_ingredient',
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 4.1 on 2023-07-04 06:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0002_rename_raw_material_pantry_raw_ingredient'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='history',
|
||||
name='modified_at',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='favorite',
|
||||
name='source',
|
||||
field=models.CharField(max_length=30, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='history',
|
||||
name='source',
|
||||
field=models.CharField(max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 20:23
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('profiles', '0003_remove_history_modified_at_favorite_source_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Restriction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('base_ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.baseingredient')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Preference',
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 21:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0006_alter_unit_type'),
|
||||
('profiles', '0004_restriction_delete_preference'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='pantry',
|
||||
name='modified_at',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pantry',
|
||||
name='unit',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='recipes.unit'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1 on 2023-07-16 16:25
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0005_remove_pantry_modified_at_pantry_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='pantry',
|
||||
old_name='stock',
|
||||
new_name='amount',
|
||||
),
|
||||
]
|
||||
48
backend/easycookapi-main/profiles/models.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from django.db import connections, models
|
||||
from django.contrib.auth.models import User
|
||||
from recipes.models import Information, RawIngredient, BaseIngredient, Unit
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def user_directory_path(instance, filename):
|
||||
return 'profile_picts/{user_id}_{filename}'.format(user_id=instance.user.id, filename=str(datetime.now()) + ".png")
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
profile_picture = models.ImageField(
|
||||
default='default_profpics.png', upload_to=user_directory_path)
|
||||
phone = models.CharField(max_length=20, unique=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
modified_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user.username} Profile'
|
||||
|
||||
|
||||
class Favorite(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
recipe = models.ForeignKey(Information, on_delete=models.CASCADE)
|
||||
source = models.CharField(max_length=30, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class History(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
recipe = models.ForeignKey(Information, on_delete=models.CASCADE)
|
||||
source = models.CharField(max_length=20, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Pantry(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
raw_ingredient = models.ForeignKey(RawIngredient, on_delete=models.CASCADE)
|
||||
amount = models.IntegerField(null=True, default=None)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Restriction(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
base_ingredient = models.ForeignKey(
|
||||
BaseIngredient, on_delete=models.CASCADE)
|
||||
43
backend/easycookapi-main/profiles/serializers.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
from profiles.models import Account, Favorite, History, Pantry
|
||||
|
||||
|
||||
class FavoriteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Favorite
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class HistorySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = History
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'first_name', 'last_name', 'username', 'email']
|
||||
|
||||
|
||||
class AccountSerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ['user', 'profile_picture', 'phone']
|
||||
|
||||
|
||||
class ProfilePictureSerializer(AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields = ['profile_picture']
|
||||
|
||||
|
||||
class PantrySerializer(serializers.ModelSerializer):
|
||||
unit = serializers.CharField(source='unit.code', allow_null=True)
|
||||
raw_ingredient = serializers.CharField(source='raw_ingredient.name')
|
||||
|
||||
class Meta:
|
||||
model = Pantry
|
||||
fields = '__all__'
|
||||
3
backend/easycookapi-main/profiles/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
36
backend/easycookapi-main/profiles/urls.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from django.urls import path
|
||||
from profiles.views import (AccountView, HistoryView, FavoriteView, PantryView,
|
||||
UpdatePasswordView, PantryRecommendationView, CFRecommendationView, HybridRecommendationView, MostViewedRecommendationView, FavoriteRecommendationView)
|
||||
|
||||
urlpatterns = [
|
||||
# URL's for Favorite
|
||||
path('favorites/', FavoriteView.as_view(), name="favorites"),
|
||||
path('favorites/<int:recipe_id>/', FavoriteView.as_view(), name="favorite"),
|
||||
|
||||
# URL's for History
|
||||
path('history/', HistoryView.as_view(), name="histories"),
|
||||
path('history/<int:recipe_id>/', HistoryView.as_view(), name="history"),
|
||||
|
||||
|
||||
# URL's for the Account
|
||||
path('account/', AccountView.as_view(), name='account'),
|
||||
path('account/update/', AccountView.as_view(), name='account-update'),
|
||||
path('account/password/update',
|
||||
UpdatePasswordView.as_view(), name='password-update'),
|
||||
|
||||
# URL's for the Pantry
|
||||
path('pantry/', PantryView.as_view(), name='pantry-list'),
|
||||
path('pantry/<int:pantry_id>',
|
||||
PantryView.as_view(), name='pantry-list'),
|
||||
path('pantry/recommendations/', PantryRecommendationView.as_view(),
|
||||
name='pantry-recommendation'),
|
||||
path('cf_recommendations/',
|
||||
CFRecommendationView.as_view(), name='recommendations'),
|
||||
path('hybrid_recommendations/',
|
||||
HybridRecommendationView.as_view(), name='recommendations'),
|
||||
path('most_viewed_recommendations/',
|
||||
MostViewedRecommendationView.as_view(), name='recommendations'),
|
||||
path('favorited_recommendations/',
|
||||
FavoriteRecommendationView.as_view(), name='recommendations'),
|
||||
|
||||
]
|
||||
361
backend/easycookapi-main/profiles/utils.py
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
from django.db.models import F
|
||||
from profiles.models import Pantry
|
||||
from recipes.models import Ingredient, UnitConversion
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
from .models import Favorite
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def get_owned_raw_ingredients(user):
|
||||
pantry_ids = Pantry.objects.filter(user=user).values_list('id', flat=True)
|
||||
unit_conversions = UnitConversion.objects.select_related(
|
||||
'higher_unit', 'lower_unit').all()
|
||||
|
||||
owned_raw_ingredient_ids = set()
|
||||
|
||||
for pantry_id in pantry_ids:
|
||||
pantry = Pantry.objects.select_related(
|
||||
'unit', 'raw_ingredient').get(id=pantry_id)
|
||||
raw_ingredient_id = pantry.raw_ingredient_id
|
||||
|
||||
ingredients = Ingredient.objects.filter(
|
||||
Q(raw_ingredient_id=raw_ingredient_id) &
|
||||
Q(raw_ingredient__pantry__user=user)
|
||||
).select_related('unit')
|
||||
|
||||
for ingredient in ingredients:
|
||||
if pantry.unit is None:
|
||||
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
||||
elif ingredient.unit is None:
|
||||
continue
|
||||
else:
|
||||
if ingredient.unit == pantry.unit:
|
||||
if pantry.amount >= ingredient.amount:
|
||||
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
||||
else:
|
||||
if ingredient.unit.type == "measured" and pantry.unit.type == "measured":
|
||||
if pantry.unit.hierarchy <= ingredient.unit.hierarchy:
|
||||
higher = pantry
|
||||
lower = ingredient
|
||||
else:
|
||||
higher = ingredient
|
||||
lower = pantry
|
||||
|
||||
conversion = UnitConversion.objects.get(
|
||||
higher_unit_id=higher.unit.pk, lower_unit_id=lower.unit.pk)
|
||||
if conversion:
|
||||
converted_amount = conversion.value * higher.amount
|
||||
if higher == pantry and converted_amount >= lower.amount:
|
||||
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
||||
elif higher == ingredient and lower.amount >= converted_amount:
|
||||
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
||||
else:
|
||||
common_unit_id = find_common_unit(
|
||||
higher.unit.id, lower.unit.id, unit_conversions
|
||||
)
|
||||
if common_unit_id:
|
||||
converted_higher_amount = convert_amount(
|
||||
higher.amount, higher.unit.id, common_unit_id, unit_conversions
|
||||
)
|
||||
converted_lower_amount = convert_amount(
|
||||
lower.amount, lower.unit.id, common_unit_id, unit_conversions
|
||||
)
|
||||
if converted_higher_amount >= converted_lower_amount:
|
||||
owned_raw_ingredient_ids.add(
|
||||
raw_ingredient_id)
|
||||
else:
|
||||
if ingredient.unit.category == pantry.unit.category:
|
||||
if pantry.unit.hierarchy < ingredient.unit.hierarchy:
|
||||
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
||||
elif pantry.unit.hierarchy == ingredient.unit.hierarchy:
|
||||
if pantry.amount >= ingredient.amount:
|
||||
owned_raw_ingredient_ids.add(
|
||||
raw_ingredient_id)
|
||||
elif pantry.unit.category == 'item':
|
||||
owned_raw_ingredient_ids.add(raw_ingredient_id)
|
||||
|
||||
return owned_raw_ingredient_ids
|
||||
|
||||
|
||||
def find_common_unit(higher_unit_id, lower_unit_id, unit_conversions):
|
||||
if higher_unit_id == lower_unit_id:
|
||||
return higher_unit_id
|
||||
|
||||
conversion = unit_conversions.filter(
|
||||
higher_unit_id=higher_unit_id, lower_unit_id=lower_unit_id
|
||||
).first()
|
||||
if conversion:
|
||||
return lower_unit_id
|
||||
|
||||
conversion = unit_conversions.filter(
|
||||
higher_unit_id=lower_unit_id, lower_unit_id=higher_unit_id
|
||||
).first()
|
||||
if conversion:
|
||||
return higher_unit_id
|
||||
|
||||
for conversion in unit_conversions:
|
||||
if conversion.higher_unit_id == higher_unit_id:
|
||||
common_unit = find_common_unit(
|
||||
conversion.lower_unit_id, lower_unit_id, unit_conversions
|
||||
)
|
||||
if common_unit:
|
||||
return common_unit
|
||||
return None
|
||||
|
||||
|
||||
def convert_amount(amount, from_unit_id, to_unit_id, unit_conversions):
|
||||
if from_unit_id == to_unit_id:
|
||||
return amount
|
||||
|
||||
conversion = unit_conversions.filter(
|
||||
higher_unit_id=from_unit_id, lower_unit_id=to_unit_id
|
||||
).first()
|
||||
if conversion:
|
||||
return amount * conversion.value
|
||||
|
||||
conversion = unit_conversions.filter(
|
||||
higher_unit_id=to_unit_id, lower_unit_id=from_unit_id
|
||||
).first()
|
||||
if conversion:
|
||||
return amount / conversion.value
|
||||
|
||||
common_unit_id = find_common_unit(
|
||||
from_unit_id, to_unit_id, unit_conversions)
|
||||
if common_unit_id:
|
||||
return convert_amount(
|
||||
convert_amount(amount, from_unit_id,
|
||||
common_unit_id, unit_conversions),
|
||||
common_unit_id, to_unit_id, unit_conversions
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_owned_recipe_ingredients(user, recipe_id):
|
||||
pantry_ids = Pantry.objects.filter(user=user).values_list('id', flat=True)
|
||||
unit_conversions = UnitConversion.objects.select_related(
|
||||
'higher_unit', 'lower_unit').all()
|
||||
|
||||
owned_ingredient_ids = set()
|
||||
|
||||
for pantry_id in pantry_ids:
|
||||
pantry = Pantry.objects.select_related(
|
||||
'unit', 'raw_ingredient').get(id=pantry_id)
|
||||
raw_ingredient_id = pantry.raw_ingredient_id
|
||||
|
||||
ingredients = Ingredient.objects.filter(
|
||||
recipe_id=recipe_id, raw_ingredient_id=raw_ingredient_id, raw_ingredient__pantry__user=user).select_related('unit')
|
||||
|
||||
for ingredient in ingredients:
|
||||
if pantry.unit is None:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
elif ingredient.unit is None:
|
||||
continue
|
||||
else:
|
||||
if ingredient.unit == pantry.unit:
|
||||
if pantry.amount >= ingredient.amount:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
else:
|
||||
if ingredient.unit.type == "measured" and pantry.unit.type == "measured":
|
||||
if pantry.unit.hierarchy <= ingredient.unit.hierarchy:
|
||||
higher = pantry
|
||||
lower = ingredient
|
||||
else:
|
||||
higher = ingredient
|
||||
lower = pantry
|
||||
|
||||
conversion = UnitConversion.objects.get(
|
||||
higher_unit_id=higher.unit.pk, lower_unit_id=lower.unit.pk)
|
||||
if conversion:
|
||||
converted_amount = conversion.value * higher.amount
|
||||
if higher == pantry and converted_amount >= lower.amount:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
elif higher == ingredient and lower.amount >= converted_amount:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
else:
|
||||
common_unit_id = find_common_unit(
|
||||
higher.unit.id, lower.unit.id, unit_conversions
|
||||
)
|
||||
if common_unit_id:
|
||||
converted_higher_amount = convert_amount(
|
||||
higher.amount, higher.unit.id, common_unit_id, unit_conversions
|
||||
)
|
||||
converted_lower_amount = convert_amount(
|
||||
lower.amount, lower.unit.id, common_unit_id, unit_conversions
|
||||
)
|
||||
if converted_higher_amount >= converted_lower_amount:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
else:
|
||||
if ingredient.unit.category == pantry.unit.category:
|
||||
if pantry.unit.hierarchy < ingredient.unit.hierarchy:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
elif pantry.unit.hierarchy == ingredient.unit.hierarchy:
|
||||
if pantry.amount >= ingredient.amount:
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
elif pantry.unit.category == 'item':
|
||||
owned_ingredient_ids.add(ingredient.id)
|
||||
|
||||
return owned_ingredient_ids
|
||||
|
||||
|
||||
def jaccard_similarity(owned, set1, set2):
|
||||
intersection = len(owned)
|
||||
union = len(set1.union(set2))
|
||||
similarity = intersection / union
|
||||
return similarity
|
||||
|
||||
|
||||
def calculate_top_recipes_by_ingredients(user):
|
||||
pantry = set(Pantry.objects.filter(user=user).values_list('raw_ingredient_id', flat=True))
|
||||
# Get valid recipes
|
||||
recipe_ingredients = Ingredient.objects.filter(
|
||||
raw_ingredient__pantry__user=user
|
||||
).values('recipe_id', 'raw_ingredient_id', 'id', user_id=F('raw_ingredient__pantry__user_id'))
|
||||
|
||||
data = {}
|
||||
for ingredient in recipe_ingredients:
|
||||
user_id = ingredient['user_id']
|
||||
recipe_id = ingredient['recipe_id']
|
||||
raw_ingredient_id = ingredient['raw_ingredient_id']
|
||||
# ingredient_id = ingredient['id']
|
||||
|
||||
if recipe_id in data:
|
||||
data[recipe_id]['raw_ingredient_ids'].add(raw_ingredient_id)
|
||||
else:
|
||||
data[recipe_id] = {
|
||||
'user_id': user_id,
|
||||
'recipe_id': recipe_id,
|
||||
'raw_ingredient_ids': {raw_ingredient_id}
|
||||
}
|
||||
|
||||
for recipe_id, recipe_data in data.items():
|
||||
raw_ingredient_ids = set(Ingredient.objects.filter(recipe_id=recipe_id).values_list('raw_ingredient_id', flat=True))
|
||||
owned_raw_ingredient = set(get_owned_recipe_ingredients(user, recipe_id))
|
||||
similarity = jaccard_similarity(
|
||||
owned_raw_ingredient, pantry, raw_ingredient_ids)
|
||||
|
||||
recipe_data['similarity'] = similarity * 100
|
||||
|
||||
sorted_recipes = sorted(
|
||||
data.values(), key=lambda x: x['similarity'], reverse=True)
|
||||
|
||||
return sorted_recipes
|
||||
|
||||
|
||||
def create_interaction_matrix():
|
||||
favorites = Favorite.objects.all()
|
||||
user_ids = []
|
||||
recipe_ids = []
|
||||
|
||||
for favorite in favorites:
|
||||
user_ids.append(favorite.user_id)
|
||||
recipe_ids.append(favorite.recipe_id)
|
||||
|
||||
unique_user_ids = list(set(user_ids))
|
||||
unique_recipe_ids = list(set(recipe_ids))
|
||||
|
||||
interaction_matrix = pd.DataFrame(
|
||||
0, index=unique_user_ids, columns=unique_recipe_ids)
|
||||
|
||||
for i in range(len(user_ids)):
|
||||
user_id = user_ids[i]
|
||||
recipe_id = recipe_ids[i]
|
||||
interaction_matrix.loc[user_id, recipe_id] = 1
|
||||
|
||||
return interaction_matrix
|
||||
|
||||
|
||||
def create_interaction_matrix_hybrid():
|
||||
favorites = Favorite.objects.all()
|
||||
user_ids = []
|
||||
recipe_ids = []
|
||||
ratings = []
|
||||
|
||||
for favorite in favorites:
|
||||
user_ids.append(favorite.user_id)
|
||||
recipe_ids.append(favorite.recipe_id)
|
||||
ratings.append(favorite.rating) # Menambahkan rating
|
||||
|
||||
unique_user_ids = list(set(user_ids))
|
||||
unique_recipe_ids = list(set(recipe_ids))
|
||||
|
||||
interaction_matrix = pd.DataFrame(
|
||||
0, index=unique_user_ids, columns=unique_recipe_ids)
|
||||
|
||||
for i in range(len(user_ids)):
|
||||
user_id = user_ids[i]
|
||||
recipe_id = recipe_ids[i]
|
||||
rating = ratings[i]
|
||||
# Menggunakan rating sebagai nilai
|
||||
interaction_matrix.loc[user_id, recipe_id] = rating
|
||||
|
||||
return interaction_matrix
|
||||
|
||||
|
||||
def calculate_cosine_similarity():
|
||||
interaction_matrix = create_interaction_matrix()
|
||||
similarity_matrix = cosine_similarity(interaction_matrix.T)
|
||||
similarity_df = pd.DataFrame(
|
||||
similarity_matrix, index=interaction_matrix.columns, columns=interaction_matrix.columns)
|
||||
return similarity_df, interaction_matrix
|
||||
|
||||
|
||||
def get_recommendations_for_user(user_id):
|
||||
similarity_matrix, interaction_matrix = calculate_cosine_similarity()
|
||||
|
||||
if user_id in interaction_matrix.index:
|
||||
scores = np.dot(interaction_matrix.values, similarity_matrix)
|
||||
|
||||
recommended_items = [interaction_matrix.columns[i]
|
||||
for i in np.argsort(scores[interaction_matrix.index == user_id, :])[0][::-1]]
|
||||
|
||||
recommendations = []
|
||||
|
||||
min_score = np.min(scores)
|
||||
max_score = np.max(scores)
|
||||
|
||||
if min_score == max_score:
|
||||
return []
|
||||
|
||||
# Normalize the scores to a range of 0-100
|
||||
normalized_scores = 100 * \
|
||||
(scores - min_score) / (max_score - min_score)
|
||||
|
||||
for item in recommended_items:
|
||||
score = scores[interaction_matrix.index ==
|
||||
user_id, interaction_matrix.columns == item][0]
|
||||
similarity_score = normalized_scores[interaction_matrix.index ==
|
||||
user_id, interaction_matrix.columns == item][0]
|
||||
if score > 0:
|
||||
recommendation = {
|
||||
'recipe_id': item,
|
||||
'similarity': round(similarity_score, 2),
|
||||
'score': round(score, 2)
|
||||
}
|
||||
recommendations.append(recommendation)
|
||||
return recommendations
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def get_hybrid_recommendation(user_id):
|
||||
similarity_matrix_cbf, _ = calculate_cosine_similarity()
|
||||
|
||||
interaction_matrix = create_interaction_matrix() # Move this line here
|
||||
|
||||
if user_id in interaction_matrix.index:
|
||||
similarity_matrix_ibcf = cosine_similarity(interaction_matrix.T)
|
||||
similarity_matrix = 0.6 * similarity_matrix_cbf + 0.4 * similarity_matrix_ibcf
|
||||
|
||||
recommended_items = list(interaction_matrix.columns)
|
||||
scores = np.dot(interaction_matrix.values, similarity_matrix)
|
||||
scores_filtered = scores[interaction_matrix.index == user_id, :]
|
||||
sorted_indices = np.argsort(scores_filtered[0])[::-1]
|
||||
recommended_items_sorted = [interaction_matrix.columns[i]
|
||||
for i in sorted_indices if interaction_matrix.columns[i] in recommended_items]
|
||||
|
||||
return recommended_items_sorted
|
||||
else:
|
||||
return []
|
||||
301
backend/easycookapi-main/profiles/views.py
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
from .models import Information
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from django.db.models import Count, Max
|
||||
|
||||
from .models import Account, Pantry, History, Favorite
|
||||
from .serializers import (
|
||||
AccountSerializer, FavoriteSerializer, HistorySerializer, PantrySerializer, ProfilePictureSerializer
|
||||
)
|
||||
from recipes.models import Information, RawIngredient
|
||||
from recipes.serializers import RecipeInformationSerializer, RawIngredientSerializer
|
||||
|
||||
from .utils import calculate_top_recipes_by_ingredients, get_hybrid_recommendation, get_recommendations_for_user
|
||||
|
||||
|
||||
class AccountView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
serializer_class = AccountSerializer
|
||||
|
||||
def get(self, request):
|
||||
account = Account.objects.get(user_id=request.user.id)
|
||||
serializer = self.get_serializer(account)
|
||||
return Response(serializer.data)
|
||||
|
||||
def post(self, request):
|
||||
account = User.objects.get(id=request.user.id)
|
||||
account_serializer = self.get_serializer(
|
||||
instance=account, data=request.data)
|
||||
profile_picture_serializer = ProfilePictureSerializer(
|
||||
instance=account.account, data=request.data)
|
||||
|
||||
if profile_picture_serializer.is_valid(raise_exception=True):
|
||||
profile_picture_serializer.save()
|
||||
|
||||
if account_serializer.is_valid(raise_exception=True):
|
||||
account_serializer.save()
|
||||
|
||||
return Response(account_serializer.data)
|
||||
|
||||
|
||||
class FavoriteView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
|
||||
recipes = Information.objects.filter(
|
||||
favorite__user=user).order_by("-favorite__created_at")
|
||||
|
||||
recipes_serializer = RecipeInformationSerializer(
|
||||
recipes, context={'request': request}, many=True)
|
||||
return Response(recipes_serializer.data)
|
||||
|
||||
def post(self, request, recipe_id):
|
||||
user = request.user
|
||||
|
||||
try:
|
||||
recipe = Information.objects.get(id=recipe_id)
|
||||
except Information.DoesNotExist:
|
||||
return Response({'message': 'Resep tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
source = request.data.get('source', '')
|
||||
|
||||
favorite, created = Favorite.objects.get_or_create(
|
||||
user=user, recipe=recipe, source=source)
|
||||
|
||||
if created:
|
||||
favorite_serializer = FavoriteSerializer(
|
||||
favorite, context={'request': request})
|
||||
return Response(favorite_serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response({'message': 'Anda sudah menambahkan resep ini ke favorit'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, recipe_id):
|
||||
favorite = Favorite.objects.filter(
|
||||
recipe_id=recipe_id, user=request.user).first()
|
||||
|
||||
if favorite:
|
||||
favorite.delete()
|
||||
return Response({'message': 'Berhasil menghapus resep dari daftar favorit'}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({'message': 'Data tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class HistoryView(generics.GenericAPIView):
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
recipes = Information.objects.filter(history__user=user).annotate(
|
||||
newest_created_at=Max('history__created_at')).order_by('-newest_created_at')
|
||||
recipes_serializer = RecipeInformationSerializer(
|
||||
recipes, context={'request': request}, many=True)
|
||||
return Response(recipes_serializer.data)
|
||||
|
||||
def post(self, request, recipe_id):
|
||||
if request.user.is_authenticated:
|
||||
user = request.user
|
||||
else:
|
||||
user = User.objects.get(id=4)
|
||||
|
||||
try:
|
||||
recipe = Information.objects.get(id=recipe_id)
|
||||
except Information.DoesNotExist:
|
||||
return Response({'message': 'Resep tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
source = request.data.get('source', '')
|
||||
|
||||
history = History(
|
||||
user=user, recipe=recipe, source=source)
|
||||
history.save()
|
||||
|
||||
history_serializer = HistorySerializer(history)
|
||||
return Response(history_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def delete(self, request, recipe_id):
|
||||
history = History.objects.filter(
|
||||
recipe_id=recipe_id, user=request.user).first()
|
||||
|
||||
if history:
|
||||
history.delete()
|
||||
return Response({'message': 'History berhasil dihapus'}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({'message': 'History tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class PantryView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = PantrySerializer
|
||||
|
||||
def get(self, request):
|
||||
pantry = Pantry.objects.filter(user=request.user)
|
||||
if pantry.exists():
|
||||
serializer = PantrySerializer(pantry, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({"message": "Penyimpanan kosong."}, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def post(self, request):
|
||||
user = request.user
|
||||
|
||||
raw_ingredient_id = request.data.get('id')
|
||||
amount = request.data.get('amount')
|
||||
unit_id = request.data.get('unit_id')
|
||||
raw_ingredient_ids = request.data.get('raw_ingredient_id', [])
|
||||
|
||||
if (len(raw_ingredient_ids) != 0):
|
||||
Pantry.objects.filter(user=user).delete()
|
||||
raw_ingredients = RawIngredient.objects.filter(
|
||||
pk__in=raw_ingredient_ids)
|
||||
for raw_ingredient in raw_ingredients:
|
||||
pantry = Pantry(user=user, raw_ingredient=raw_ingredient)
|
||||
pantry.save()
|
||||
|
||||
user = request.user
|
||||
|
||||
top_recipes = calculate_top_recipes_by_ingredients(user)[:12]
|
||||
top_recipe_ids = [recipe['recipe_id'] for recipe in top_recipes]
|
||||
recipes = Information.objects.filter(id__in=top_recipe_ids).annotate()
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request, 'top_recipes': top_recipes}, many=True).data
|
||||
sorted_recipes = sorted(
|
||||
recipes_list,
|
||||
key=lambda x: (0.8 * next(
|
||||
(item['similarity'] for item in top_recipes if item['recipe_id'] == x['id']), 0)) +
|
||||
(0.2 * x['completeness_percentage']),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
return Response(sorted_recipes, status=status.HTTP_200_OK)
|
||||
else:
|
||||
try:
|
||||
pantry = Pantry.objects.get(
|
||||
user=user, raw_ingredient_id=raw_ingredient_id)
|
||||
pantry.amount = amount
|
||||
pantry.unit_id = unit_id
|
||||
pantry.save()
|
||||
pantry_serializer = PantrySerializer(pantry).data
|
||||
return Response(pantry_serializer, status=status.HTTP_200_OK)
|
||||
except Pantry.DoesNotExist:
|
||||
pantry = Pantry.objects.create(
|
||||
user=user, raw_ingredient_id=raw_ingredient_id, amount=amount, unit_id=unit_id)
|
||||
pantry_serializer = PantrySerializer(pantry).data
|
||||
return Response(pantry_serializer, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, pantry_id):
|
||||
pantry = Pantry.objects.filter(
|
||||
user=request.user, id=pantry_id)
|
||||
pantry.delete()
|
||||
return Response('Berhasil menghapus data', status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class UpdatePasswordView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
user = request.user
|
||||
current_password = request.data.get('current_password')
|
||||
new_password = request.data.get('new_password')
|
||||
if user.check_password(current_password):
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
return Response("Password berhasil diperbarui", status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response("Password saat ini salah", status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class PantryRecommendationView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
|
||||
top_recipes = calculate_top_recipes_by_ingredients(user)[:20]
|
||||
top_recipe_ids = [recipe['recipe_id'] for recipe in top_recipes]
|
||||
recipes = Information.objects.filter(id__in=top_recipe_ids).annotate()
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request, 'top_recipes': top_recipes}, many=True).data
|
||||
|
||||
filtered_recipes = [
|
||||
recipe for recipe in recipes_list if recipe['completeness_percentage'] > 0]
|
||||
|
||||
sorted_recipes = sorted(
|
||||
filtered_recipes,
|
||||
key=lambda x: (0.8 * next(
|
||||
(item['similarity'] for item in top_recipes if item['recipe_id'] == x['id']), 0)) +
|
||||
(0.2 * x['completeness_percentage']),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
sorted_recipes = sorted_recipes[:12]
|
||||
return Response(sorted_recipes, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class CFRecommendationView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = RecipeInformationSerializer
|
||||
|
||||
def get(self, request):
|
||||
user_id = self.request.user.id
|
||||
top_recipes = get_recommendations_for_user(user_id)[:20]
|
||||
top_recipes_ids = [recipe['recipe_id']
|
||||
for recipe in top_recipes]
|
||||
|
||||
recipes = Information.objects.filter(
|
||||
id__in=top_recipes_ids)
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request, 'top_recipes': top_recipes}, many=True).data
|
||||
sorted_recipes = sorted(
|
||||
recipes_list, key=lambda x: x['similarity'], reverse=True)
|
||||
return Response(sorted_recipes, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class MostViewedRecommendationView(generics.GenericAPIView):
|
||||
|
||||
def get(self, request):
|
||||
recipe_counts = History.objects.values('recipe_id').annotate(
|
||||
count=Count('recipe_id')).order_by('-count')[:9]
|
||||
|
||||
recipe_ids = [count['recipe_id'] for count in recipe_counts]
|
||||
|
||||
recipes = Information.objects.filter(
|
||||
id__in=recipe_ids).annotate(history_count=Count('history')).order_by('-history_count')
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request}, many=True).data
|
||||
return Response(recipes_list, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class FavoriteRecommendationView(generics.GenericAPIView):
|
||||
|
||||
def get(self, request):
|
||||
recipe_counts = Favorite.objects.values('recipe_id').annotate(
|
||||
count=Count('recipe_id')).order_by('-count')[:9]
|
||||
|
||||
recipe_ids = [count['recipe_id'] for count in recipe_counts]
|
||||
|
||||
recipes = Information.objects.filter(
|
||||
id__in=recipe_ids).annotate(favorite_count=Count('favorite')).order_by('-favorite_count')
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request}, many=True).data
|
||||
return Response(recipes_list, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
# Unused
|
||||
class HybridRecommendationView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = RecipeInformationSerializer
|
||||
|
||||
def get(self, request):
|
||||
user_id = self.request.user.id
|
||||
recommended_recipe_ids = get_hybrid_recommendation(user_id)[:9]
|
||||
|
||||
recipes = Information.objects.filter(id__in=recommended_recipe_ids)
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request}, many=True).data
|
||||
sorted_recipes = sorted(
|
||||
recipes_list, key=lambda x: x['completeness_percentage'], reverse=True)[:9]
|
||||
|
||||
return Response(sorted_recipes, status=status.HTTP_200_OK)
|
||||
0
backend/easycookapi-main/recipes/__init__.py
Normal file
3
backend/easycookapi-main/recipes/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/easycookapi-main/recipes/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RecipesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'recipes'
|
||||
104
backend/easycookapi-main/recipes/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Generated by Django 4.1 on 2023-06-28 09:11
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BaseIngredient',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Information',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('image_url', models.CharField(max_length=255)),
|
||||
('time', models.IntegerField(null=True)),
|
||||
('servings', models.IntegerField(null=True)),
|
||||
('calories', models.IntegerField(null=True)),
|
||||
('difficulty', models.CharField(max_length=255, null=True)),
|
||||
('instructions', models.TextField()),
|
||||
('source_url', models.CharField(max_length=255)),
|
||||
('slug', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Restriction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('category', models.CharField(max_length=255)),
|
||||
('image', models.ImageField(default='default_profpics.png', upload_to='profile_picts')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Review',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('comment', models.TextField(blank=True, default='', null=True)),
|
||||
('rating', models.FloatField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='recipes.information')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Unit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(max_length=255)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tag', models.CharField(max_length=255)),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.information')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReviewImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('image', models.ImageField(upload_to='reviews')),
|
||||
('review', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.review')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RawIngredient',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.baseingredient')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ingredient',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('amount', models.FloatField(null=True)),
|
||||
('state', models.CharField(max_length=255, null=True)),
|
||||
('raw_ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='recipes.rawingredient')),
|
||||
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='recipes.information')),
|
||||
('unit', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='recipes.unit')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 20:23
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0004_restriction_delete_preference'),
|
||||
('recipes', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UnitConversion',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.IntegerField(null=True)),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Restriction',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unit',
|
||||
name='category',
|
||||
field=models.CharField(default='apakek', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unit',
|
||||
name='hierarchy',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unitconversion',
|
||||
name='higher_unit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='higher_unit', to='recipes.unit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='unitconversion',
|
||||
name='lower_unit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lower_unit', to='recipes.unit'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 20:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0002_unitconversion_delete_restriction_unit_category_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='unit',
|
||||
name='category',
|
||||
field=models.CharField(max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 20:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0003_alter_unit_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='unit',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('calculated', 'calculated'), ('informal', 'informal')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unit',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('calculated', 'calculated'), ('informal', 'informal')], max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 20:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0004_unit_type_alter_unit_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='unit',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('weight', 'weight'), ('volume', 'volume'), ('length', 'length'), ('item', 'item')], max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.1 on 2023-07-12 20:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recipes', '0005_alter_unit_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='unit',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('measured', 'measured'), ('informal', 'informal')], max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
112
backend/easycookapi-main/recipes/models.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseIngredient(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Unit(models.Model):
|
||||
UNIT_TYPES = [
|
||||
('measured', 'measured'),
|
||||
('informal', 'informal'),
|
||||
]
|
||||
|
||||
UNIT_CATEGORIES = [
|
||||
('weight', 'weight'),
|
||||
('volume', 'volume'),
|
||||
('length', 'length'),
|
||||
('item', 'item'),
|
||||
]
|
||||
|
||||
code = models.CharField(max_length=255)
|
||||
name = models.CharField(max_length=255)
|
||||
hierarchy = models.IntegerField(null=True)
|
||||
category = models.CharField(
|
||||
max_length=255, choices=UNIT_CATEGORIES, null=True)
|
||||
type = models.CharField(max_length=20, choices=UNIT_TYPES, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UnitConversion(models.Model):
|
||||
higher_unit = models.ForeignKey(
|
||||
Unit, on_delete=models.CASCADE, related_name='higher_unit')
|
||||
lower_unit = models.ForeignKey(
|
||||
Unit, on_delete=models.CASCADE, related_name='lower_unit')
|
||||
value = models.IntegerField(null=True)
|
||||
|
||||
|
||||
class Information(models.Model):
|
||||
title = models.CharField(max_length=255)
|
||||
image_url = models.CharField(max_length=255)
|
||||
time = models.IntegerField(null=True)
|
||||
servings = models.IntegerField(null=True)
|
||||
calories = models.IntegerField(null=True)
|
||||
difficulty = models.CharField(max_length=255, null=True)
|
||||
instructions = models.TextField()
|
||||
source_url = models.CharField(max_length=255)
|
||||
slug = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class RawIngredient(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
base = models.ForeignKey(BaseIngredient, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Ingredient(models.Model):
|
||||
recipe = models.ForeignKey(
|
||||
Information, on_delete=models.CASCADE, related_name='ingredients')
|
||||
name = models.CharField(max_length=255)
|
||||
amount = models.FloatField(null=True)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, null=True)
|
||||
state = models.CharField(max_length=255, null=True)
|
||||
raw_ingredient = models.ForeignKey(RawIngredient, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
recipe = models.ForeignKey(Information, on_delete=models.CASCADE)
|
||||
tag = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.tag
|
||||
|
||||
|
||||
class Review(models.Model):
|
||||
recipe = models.ForeignKey(
|
||||
Information, on_delete=models.CASCADE, related_name='reviews')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
comment = models.TextField(null=True, blank=True, default='')
|
||||
rating = models.FloatField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user.username} - {self.recipe.title}'
|
||||
|
||||
|
||||
class ReviewImage(models.Model):
|
||||
review = models.ForeignKey(Review, on_delete=models.CASCADE)
|
||||
image = models.ImageField(upload_to='reviews')
|
||||
|
||||
def __str__(self):
|
||||
return f'Image for {self.review}'
|
||||
|
||||
|
||||
def user_directory_path(instance, filename):
|
||||
filename = str(datetime.now()) + ".png"
|
||||
return 'profile_picts/{1}'.format(instance.user.id, filename)
|
||||
214
backend/easycookapi-main/recipes/serializers.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
from rest_framework import serializers
|
||||
from recipes.models import Information, Review, Ingredient, RawIngredient, Unit
|
||||
from profiles.models import Account, Favorite, Pantry
|
||||
from django.db.models import Avg, Count
|
||||
from django.template.defaultfilters import date
|
||||
from decimal import Decimal
|
||||
import ast
|
||||
import math
|
||||
from profiles.utils import get_owned_raw_ingredients, get_owned_recipe_ingredients
|
||||
|
||||
|
||||
class RawIngredientSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RawIngredient
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RecipeIngredientsSerializer(serializers.ModelSerializer):
|
||||
unit = serializers.CharField(source='unit.code')
|
||||
|
||||
class Meta:
|
||||
model = Ingredient
|
||||
fields = ('id', 'name', 'amount', 'state', 'recipe', 'unit')
|
||||
|
||||
|
||||
class ReviewSummarySerializer(serializers.ModelSerializer):
|
||||
score = serializers.SerializerMethodField()
|
||||
amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Review
|
||||
fields = ('score', 'amount')
|
||||
|
||||
def get_score(self, instance):
|
||||
ratings = Review.objects.filter(recipe_id=instance.id).aggregate(
|
||||
avg_rating=Avg('rating'),
|
||||
review_count=Count('id')
|
||||
)
|
||||
avg_rating = ratings['avg_rating'] or 0
|
||||
review_count = ratings['review_count'] or 0
|
||||
|
||||
total_rating = avg_rating * review_count
|
||||
total_review_count = review_count
|
||||
|
||||
return Decimal(total_rating / total_review_count).quantize(Decimal('0.00')) if total_review_count != 0 else 0.0
|
||||
|
||||
def get_amount(self, instance):
|
||||
review_counts = Review.objects.filter(recipe_id=instance.id).aggregate(
|
||||
user_review_count=Count('id')
|
||||
)
|
||||
user_review_count = review_counts['user_review_count'] or 0
|
||||
|
||||
total_review_count = user_review_count
|
||||
|
||||
return total_review_count
|
||||
|
||||
|
||||
class RecipeInformationSerializer(serializers.ModelSerializer):
|
||||
completeness_percentage = serializers.SerializerMethodField()
|
||||
is_favorited = serializers.SerializerMethodField()
|
||||
similarity = serializers.SerializerMethodField()
|
||||
rating = ReviewSummarySerializer(source='*')
|
||||
|
||||
def get_is_favorited(self, instance):
|
||||
user = self.context['request'].user
|
||||
if user.is_authenticated:
|
||||
return Favorite.objects.filter(recipe_id=instance.pk, user=user).exists()
|
||||
return False
|
||||
|
||||
def get_completeness_percentage(self, instance):
|
||||
user = self.context['request'].user
|
||||
|
||||
if user.is_authenticated:
|
||||
owned_ingredient_ids = len(get_owned_recipe_ingredients(
|
||||
user, instance.pk))
|
||||
|
||||
total_ingredient_count = instance.ingredients.count()
|
||||
# print(instance.title + " " + str(owned_ingredient_ids) + " " + str(total_ingredient_count))
|
||||
|
||||
if total_ingredient_count > 0:
|
||||
completeness_percentage = (
|
||||
owned_ingredient_ids / total_ingredient_count * 100
|
||||
)
|
||||
completeness_percentage = round(
|
||||
completeness_percentage, 2)
|
||||
return completeness_percentage
|
||||
|
||||
return 0
|
||||
|
||||
def get_similarity(self, instance): # Add this method
|
||||
recipe_id = instance.pk
|
||||
top_recipes = self.context.get('top_recipes')
|
||||
similarity = 0
|
||||
|
||||
if top_recipes:
|
||||
similarity = next(
|
||||
(item['similarity']
|
||||
for item in top_recipes if item['recipe_id'] == recipe_id),
|
||||
0
|
||||
)
|
||||
similarity = round(Decimal(similarity), 2)
|
||||
return similarity
|
||||
|
||||
class Meta:
|
||||
model = Information
|
||||
fields = ('id', 'title', 'image_url', 'difficulty', 'source_url',
|
||||
'calories', 'completeness_percentage', 'is_favorited', 'rating', 'similarity')
|
||||
|
||||
|
||||
class RecipeDetailSerializer(serializers.ModelSerializer):
|
||||
ingredients = serializers.SerializerMethodField()
|
||||
owned_ingredients = serializers.SerializerMethodField()
|
||||
not_owned_ingredients = serializers.SerializerMethodField()
|
||||
instructions = serializers.CharField()
|
||||
rating = ReviewSummarySerializer(source='*')
|
||||
is_favorited = serializers.SerializerMethodField()
|
||||
|
||||
def get_ingredients(self, instance):
|
||||
user = self.context['request'].user
|
||||
|
||||
all_ingredients = Ingredient.objects.filter(recipe=instance)
|
||||
serialized_data = RecipeIngredientsSerializer(
|
||||
all_ingredients, many=True).data
|
||||
|
||||
if user.is_authenticated:
|
||||
owned_ingredient_ids = get_owned_recipe_ingredients(
|
||||
user, instance.pk)
|
||||
owned_ingredients = Ingredient.objects.filter(
|
||||
pk__in=owned_ingredient_ids).distinct()
|
||||
|
||||
owned_ids = [ingredient.id for ingredient in owned_ingredients]
|
||||
for ingredient in serialized_data:
|
||||
if ingredient['id'] in owned_ids:
|
||||
|
||||
ingredient['is_owned'] = True
|
||||
else:
|
||||
ingredient['is_owned'] = False
|
||||
else:
|
||||
for ingredient in serialized_data:
|
||||
ingredient['is_owned'] = False
|
||||
|
||||
return serialized_data
|
||||
|
||||
def get_owned_ingredients(self, instance):
|
||||
user = self.context['request'].user
|
||||
|
||||
if not user.is_authenticated:
|
||||
return []
|
||||
|
||||
owned_ingredient_ids = get_owned_recipe_ingredients(
|
||||
user, instance.pk)
|
||||
owned_ingredients = Ingredient.objects.filter(
|
||||
id__in=owned_ingredient_ids).distinct()
|
||||
return RecipeIngredientsSerializer(owned_ingredients, many=True).data
|
||||
|
||||
def get_not_owned_ingredients(self, instance):
|
||||
user = self.context['request'].user
|
||||
|
||||
if not user.is_authenticated:
|
||||
all_ingredients = Ingredient.objects.filter(recipe=instance)
|
||||
return RecipeIngredientsSerializer(all_ingredients, many=True).data
|
||||
|
||||
owned_raw_ingredient_ids = Pantry.objects.filter(
|
||||
user=user).values_list('raw_ingredient_id', flat=True)
|
||||
not_owned_ingredients = Ingredient.objects.filter(
|
||||
recipe=instance).exclude(raw_ingredient__in=owned_raw_ingredient_ids)
|
||||
|
||||
return RecipeIngredientsSerializer(not_owned_ingredients, many=True).data
|
||||
|
||||
def get_is_favorited(self, instance):
|
||||
user = self.context['request'].user
|
||||
if user.is_authenticated:
|
||||
return Favorite.objects.filter(recipe_id=instance.pk, user=user).exists()
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
model = Information
|
||||
fields = ('id', 'title', 'image_url', 'time', 'servings', 'difficulty', 'source_url',
|
||||
'rating', 'is_favorited', 'ingredients', 'instructions', 'owned_ingredients', 'not_owned_ingredients')
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
string_array = representation['instructions']
|
||||
array = ast.literal_eval(string_array)
|
||||
representation['instructions'] = array
|
||||
return representation
|
||||
|
||||
|
||||
class ReviewSerializer(serializers.ModelSerializer):
|
||||
user = serializers.SerializerMethodField()
|
||||
modified_date = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Review
|
||||
fields = ('id', 'user', 'rating', 'comment', 'modified_date')
|
||||
|
||||
def get_user(self, instance):
|
||||
account = Account.objects.get(user=instance.user)
|
||||
user_data = {
|
||||
'id': account.user.id,
|
||||
'profile_picture': account.profile_picture.url,
|
||||
'name': f"{account.user.first_name} {account.user.last_name}"
|
||||
}
|
||||
return user_data
|
||||
|
||||
def get_modified_date(self, instance):
|
||||
modified_date = date(instance.updated_at, "j F Y")
|
||||
return modified_date
|
||||
|
||||
|
||||
class UnitSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Unit
|
||||
fields = '__all__'
|
||||
3
backend/easycookapi-main/recipes/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
backend/easycookapi-main/recipes/urls.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from django.urls import path
|
||||
from .views import RecipeDetailView, SearchRecipeView, ReviewView, RecipeListView, OwnedIngredientView, NotOwnedIngredientViews, RawIngredientView, UnitView
|
||||
|
||||
urlpatterns = [
|
||||
path('', RecipeListView.as_view(), name='recipe_list'),
|
||||
path('raw-material/', RawIngredientView.as_view(), name='get_raw_material'),
|
||||
path('<int:recipe_id>/own/',
|
||||
OwnedIngredientView.as_view(), name='own_ingredient'),
|
||||
path('<int:recipe_id>/not-own/', NotOwnedIngredientViews.as_view(),
|
||||
name='not_own_ingredient'),
|
||||
path('<int:recipe_id>/', RecipeDetailView.as_view(), name='recipe_detail'),
|
||||
path('<int:recipe_id>/reviews/', ReviewView.as_view(), name='recipe_reviews'),
|
||||
path('<int:recipe_id>/reviews/<int:review_id>/',
|
||||
ReviewView.as_view(), name='recipe_review'),
|
||||
path('search/', SearchRecipeView.as_view(), name='recipe_search'),
|
||||
path('units/', UnitView.as_view(), name='recipe_search'),
|
||||
]
|
||||
190
backend/easycookapi-main/recipes/views.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
from recipes.models import Information, Review, RawIngredient, Unit
|
||||
from profiles.models import Pantry
|
||||
from rest_framework import generics, status, permissions
|
||||
from rest_framework.response import Response
|
||||
from .serializers import (
|
||||
RecipeInformationSerializer,
|
||||
ReviewSerializer,
|
||||
RecipeDetailSerializer,
|
||||
RecipeIngredientsSerializer,
|
||||
RawIngredientSerializer,
|
||||
UnitSerializer
|
||||
)
|
||||
from .models import Ingredient
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
class IsAuthenticatedOrReadOnly(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if request.method == 'GET' or request.user.is_authenticated:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class RecipeListView(generics.GenericAPIView):
|
||||
serializer_class = RecipeInformationSerializer
|
||||
|
||||
def get(self, request):
|
||||
recipes = Information.objects.all()[:30]
|
||||
recipes_serializer = self.serializer_class(
|
||||
recipes, context={'request': request}, many=True)
|
||||
return Response(recipes_serializer.data)
|
||||
|
||||
|
||||
class OwnedIngredientView(generics.GenericAPIView):
|
||||
serializer_class = RecipeIngredientsSerializer
|
||||
|
||||
def get(self, request, recipe_id):
|
||||
user = request.user
|
||||
|
||||
if not user.is_authenticated:
|
||||
return Response([])
|
||||
|
||||
owned_raw_ingredient_ids = Pantry.objects.filter(
|
||||
user=user).values_list('raw_ingredient_id', flat=True)
|
||||
owned_ingredients = Ingredient.objects.filter(
|
||||
raw_ingredient__in=owned_raw_ingredient_ids, recipe_id=recipe_id).distinct()
|
||||
serializer = RecipeIngredientsSerializer(owned_ingredients, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class NotOwnedIngredientViews(generics.GenericAPIView):
|
||||
serializer_class = RecipeIngredientsSerializer
|
||||
|
||||
def get(self, request, recipe_id):
|
||||
user = request.user
|
||||
|
||||
if not user.is_authenticated:
|
||||
all_ingredients = Ingredient.objects.filter(recipe_id=recipe_id)
|
||||
serializer = RecipeIngredientsSerializer(
|
||||
all_ingredients, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
owned_raw_ingredient_ids = Pantry.objects.filter(
|
||||
user=user).values_list('raw_ingredient_id', flat=True)
|
||||
not_owned_ingredients = Ingredient.objects.filter(
|
||||
recipe_id=recipe_id).exclude(raw_ingredient__in=owned_raw_ingredient_ids)
|
||||
serializer = RecipeIngredientsSerializer(
|
||||
not_owned_ingredients, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RecipeDetailView(generics.GenericAPIView):
|
||||
serializer_class = RecipeDetailSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request, recipe_id):
|
||||
recipes = Information.objects.get(pk=recipe_id)
|
||||
recipes_serializer = self.serializer_class(
|
||||
recipes, context={'request': request})
|
||||
return Response(recipes_serializer.data)
|
||||
|
||||
|
||||
class SearchRecipeView(generics.GenericAPIView):
|
||||
def get(self, request):
|
||||
keyword = request.GET.get('keyword')
|
||||
|
||||
if not keyword or not keyword.strip():
|
||||
return Response({'message': 'Kata kunci yang Anda masukkan tidak valid'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
recipes = Information.objects.filter(title__icontains=keyword)
|
||||
|
||||
if not recipes:
|
||||
return Response({'message': 'Resep tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
recipes_list = RecipeInformationSerializer(
|
||||
recipes, context={'request': request}, many=True).data
|
||||
sorted_recipes = sorted(
|
||||
recipes_list, key=lambda x: x['completeness_percentage'], reverse=True)
|
||||
return Response(sorted_recipes, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ReviewView(generics.GenericAPIView):
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
serializer_class = ReviewSerializer
|
||||
|
||||
def get(self, request, recipe_id, review_id=None):
|
||||
if review_id is not None:
|
||||
try:
|
||||
review = Review.objects.get(
|
||||
recipe_id=recipe_id, pk=review_id, user=request.user)
|
||||
except Review.DoesNotExist:
|
||||
return Response({"message": "Ulasan tidak ditemukan"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
review_serializer = self.serializer_class(review)
|
||||
return Response(review_serializer.data)
|
||||
else:
|
||||
reviews = Review.objects.filter(recipe_id=recipe_id)
|
||||
|
||||
if not reviews:
|
||||
return Response({"message": "Tidak ada ulasan"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
review_serializer = self.serializer_class(reviews, many=True)
|
||||
return Response(review_serializer.data)
|
||||
|
||||
def post(self, request, recipe_id):
|
||||
try:
|
||||
recipe = Information.objects.get(pk=recipe_id)
|
||||
except Information.DoesNotExist:
|
||||
return JsonResponse({'message': 'Resep tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
review_serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if Review.objects.filter(user=request.user, recipe=recipe).exists():
|
||||
return Response({'message': 'Anda sudah memberikan ulasan'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if review_serializer.is_valid():
|
||||
saved_review = review_serializer.save(
|
||||
user=request.user, recipe=recipe)
|
||||
return Response(self.serializer_class(saved_review).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(review_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def put(self, request, recipe_id, review_id):
|
||||
review = Review.objects.get(
|
||||
pk=review_id, user=request.user, recipe_id=recipe_id)
|
||||
|
||||
if not review:
|
||||
return Response({"message": "Ulasan tidak ditemukan"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
review_serializer = self.serializer_class(
|
||||
instance=review, data=request.data)
|
||||
|
||||
if review_serializer.is_valid():
|
||||
review_serializer.save()
|
||||
return Response(review_serializer.data)
|
||||
else:
|
||||
return Response(review_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request, recipe_id, review_id):
|
||||
try:
|
||||
review = Review.objects.get(
|
||||
pk=review_id, user=request.user)
|
||||
review.delete()
|
||||
return Response({'message': 'Berhasil menghapus ulasan'}, status=status.HTTP_200_OK)
|
||||
except Review.DoesNotExist:
|
||||
return Response({'message': 'Ulasan tidak ditemukan'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class RawIngredientView(generics.GenericAPIView):
|
||||
serializer_class = RawIngredientSerializer
|
||||
|
||||
def get(self, request):
|
||||
if request.GET.get('name'):
|
||||
raw_materials = RawIngredient.objects.filter(
|
||||
name__icontains=request.GET.get('name'))
|
||||
else:
|
||||
raw_materials = RawIngredient.objects.all()
|
||||
serializer = self.get_serializer(raw_materials, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UnitView(generics.GenericAPIView):
|
||||
def get(self, request):
|
||||
raw_ingredient_id = request.GET.get('raw_ingredient_id')
|
||||
|
||||
units = Unit.objects.filter(
|
||||
ingredient__raw_ingredient_id=raw_ingredient_id).distinct()
|
||||
|
||||
units_list = UnitSerializer(units, many=True).data
|
||||
return Response(units_list, status=status.HTTP_200_OK)
|
||||
BIN
backend/easycookapi-main/requirements.txt
Normal file
5
backend/easycookapi-main/restart-server.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
git pull
|
||||
sudo systemctl restart easycookapi
|
||||
sudo systemctl restart nginx
|
||||
275
backend/easycookapi-main/static/admin/css/autocomplete.css
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
select.admin-autocomplete {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single,
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
|
||||
border-color: var(--body-quiet-color);
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
|
||||
color: var(--body-fg);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: #888 transparent transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 4px 0 4px;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
margin-top: -2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple {
|
||||
background-color: var(--body-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 10px 5px 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
|
||||
color: var(--body-quiet-color);
|
||||
margin-top: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: var(--darkened-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: var(--body-quiet-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
|
||||
border: solid var(--body-quiet-color) 1px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: var(--darkened-bg);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown {
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
|
||||
background: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
color: var(--body-fg);
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
color: var(--body-fg);
|
||||
background: var(--body-bg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[role=group] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
|
||||
background-color: var(--selected-bg);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -4em;
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -5em;
|
||||
padding-left: 6em;
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.select2-container--admin-autocomplete .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px;
|
||||
}
|
||||
1089
backend/easycookapi-main/static/admin/css/base.css
Normal file
325
backend/easycookapi-main/static/admin/css/changelists.css
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
/* 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: 19px;
|
||||
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;
|
||||
}
|
||||
|
||||
#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-clear a {
|
||||
font-size: 0.8125rem;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
|
||||
/* DATE DRILLDOWN */
|
||||
|
||||
.change-list ul.toplinks {
|
||||
display: block;
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks li {
|
||||
padding: 3px 6px;
|
||||
font-weight: bold;
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.change-list ul.toplinks .date-back a {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.change-list ul.toplinks .date-back a:focus,
|
||||
.change-list ul.toplinks .date-back a:hover {
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
.filtered .actions {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#changelist table input {
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
#changelist table tbody tr.selected {
|
||||
background-color: var(--selected-row);
|
||||
}
|
||||
|
||||
#changelist .actions {
|
||||
padding: 10px;
|
||||
background: var(--body-bg);
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
line-height: 24px;
|
||||
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: 24px;
|
||||
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: 24px;
|
||||
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);
|
||||
}
|
||||
33
backend/easycookapi-main/static/admin/css/dark_mode.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
@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;
|
||||
}
|
||||
}
|
||||
26
backend/easycookapi-main/static/admin/css/dashboard.css
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* DASHBOARD */
|
||||
|
||||
.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;
|
||||
}
|
||||
20
backend/easycookapi-main/static/admin/css/fonts.css
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Bold-webfont.woff');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Regular-webfont.woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Light-webfont.woff');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
528
backend/easycookapi-main/static/admin/css/forms.css
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
@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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
float: left;
|
||||
width: 160px;
|
||||
word-wrap: break-word;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.aligned label:not(.vCheckboxLabel):after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.aligned label + p, .aligned label + div.help, .aligned label + div.readonly {
|
||||
padding: 6px 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 170px;
|
||||
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 {
|
||||
clear: left;
|
||||
margin-top: 0;
|
||||
margin-left: 160px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
form .aligned label + p.help,
|
||||
form .aligned label + div.help {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.checkbox-row p.help,
|
||||
.checkbox-row div.help {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
fieldset .fieldBox {
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* WIDE FIELDSETS */
|
||||
|
||||
.wide label {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
form .wide p,
|
||||
form .wide input + p.help,
|
||||
form .wide input + div.help {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
form .wide p.help,
|
||||
form .wide div.help {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
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: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
/* SUBMIT ROW */
|
||||
|
||||
.submit-row {
|
||||
padding: 12px 14px 7px;
|
||||
margin: 0 0 20px;
|
||||
background: var(--darkened-bg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.popup .submit-row {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.submit-row input {
|
||||
height: 35px;
|
||||
line-height: 15px;
|
||||
margin: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.submit-row input.default {
|
||||
margin: 0 0 5px 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.submit-row p {
|
||||
margin: 0.3em;
|
||||
}
|
||||
|
||||
.submit-row p.deletelink-box {
|
||||
float: left;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.submit-row a.deletelink {
|
||||
display: block;
|
||||
background: var(--delete-button-bg);
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
margin-bottom: 5px;
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.submit-row a.closelink {
|
||||
display: inline-block;
|
||||
background: var(--close-button-bg);
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
margin: 0 0 0 5px;
|
||||
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);
|
||||
}
|
||||
|
||||
.submit-row a.closelink:focus,
|
||||
.submit-row a.closelink:hover,
|
||||
.submit-row a.closelink:active {
|
||||
background: var(--close-button-hover-bg);
|
||||
}
|
||||
|
||||
/* 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: 16px;
|
||||
height: 16px;
|
||||
background-image: url(../img/search.svg);
|
||||
}
|
||||
|
||||
form .related-widget-wrapper ul {
|
||||
display: inline-block;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.clearable-file-input input {
|
||||
margin-top: 0;
|
||||
}
|
||||
61
backend/easycookapi-main/static/admin/css/login.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/* LOGIN FORM */
|
||||
|
||||
.login {
|
||||
background: var(--darkened-bg);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login #header {
|
||||
height: auto;
|
||||
padding: 15px 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login #header h1 {
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login #header h1 a {
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
.login #content {
|
||||
padding: 20px 20px 0;
|
||||
}
|
||||
|
||||
.login #container {
|
||||
background: var(--body-bg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
width: 28em;
|
||||
min-width: 300px;
|
||||
margin: 100px auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login .form-row {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.login .form-row label {
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.login .form-row #id_username, .login .form-row #id_password {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login .submit-row {
|
||||
padding: 1em 0 0 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login .password-reset-link {
|
||||
text-align: center;
|
||||
}
|
||||
139
backend/easycookapi-main/static/admin/css/nav_sidebar.css
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
.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.shifted > #nav-sidebar {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
1015
backend/easycookapi-main/static/admin/css/responsive.css
Normal file
80
backend/easycookapi-main/static/admin/css/responsive_rtl.css
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/* 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 {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
[dir="rtl"] #changelist-filter {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
239
backend/easycookapi-main/static/admin/css/rtl.css
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/* 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 {
|
||||
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;
|
||||
}
|
||||
|
||||
/* FORMS */
|
||||
|
||||
.aligned label {
|
||||
padding: 0 0 3px 1em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.submit-row {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.submit-row p.deletelink-box {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.submit-row input.default {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.vDateField, .vTimeField {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.aligned .form-row input {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
form .aligned p.help, form .aligned div.help {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
form .aligned ul {
|
||||
margin-right: 163px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
form ul.inline li {
|
||||
float: right;
|
||||
padding-right: 0;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
input[type=submit].default, .submit-row input.default {
|
||||
float: left;
|
||||
}
|
||||
|
||||
fieldset .fieldBox {
|
||||
float: right;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
21
backend/easycookapi-main/static/admin/css/vendor/select2/LICENSE-SELECT2.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
481
backend/easycookapi-main/static/admin/css/vendor/select2/select2.css
vendored
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
.select2-container {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
vertical-align: middle; }
|
||||
.select2-container .select2-selection--single {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 28px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||
display: block;
|
||||
padding-left: 8px;
|
||||
padding-right: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-selection--single .select2-selection__clear {
|
||||
position: relative; }
|
||||
.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered {
|
||||
padding-right: 8px;
|
||||
padding-left: 20px; }
|
||||
.select2-container .select2-selection--multiple {
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
min-height: 32px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-container .select2-selection--multiple .select2-selection__rendered {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
padding-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
.select2-container .select2-search--inline {
|
||||
float: left; }
|
||||
.select2-container .select2-search--inline .select2-search__field {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
font-size: 100%;
|
||||
margin-top: 5px;
|
||||
padding: 0; }
|
||||
.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
|
||||
.select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -100000px;
|
||||
width: 100%;
|
||||
z-index: 1051; }
|
||||
|
||||
.select2-results {
|
||||
display: block; }
|
||||
|
||||
.select2-results__options {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
.select2-results__option {
|
||||
padding: 6px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none; }
|
||||
.select2-results__option[aria-selected] {
|
||||
cursor: pointer; }
|
||||
|
||||
.select2-container--open .select2-dropdown {
|
||||
left: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--above {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--open .select2-dropdown--below {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-search--dropdown {
|
||||
display: block;
|
||||
padding: 4px; }
|
||||
.select2-search--dropdown .select2-search__field {
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none; }
|
||||
.select2-search--dropdown.select2-search--hide {
|
||||
display: none; }
|
||||
|
||||
.select2-close-mask {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
z-index: 99;
|
||||
background-color: #fff;
|
||||
filter: alpha(opacity=0); }
|
||||
|
||||
.select2-hidden-accessible {
|
||||
border: 0 !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
-webkit-clip-path: inset(50%) !important;
|
||||
clip-path: inset(50%) !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
white-space: nowrap !important; }
|
||||
|
||||
.select2-container--default .select2-selection--single {
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 28px; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px; }
|
||||
.select2-container--default .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--default[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: text; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered {
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
width: 100%; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__rendered li {
|
||||
list-style: none; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
margin-right: 10px;
|
||||
padding: 1px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #333; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline {
|
||||
float: right; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||
border: solid black 1px;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection--multiple {
|
||||
background-color: #eee;
|
||||
cursor: default; }
|
||||
|
||||
.select2-container--default.select2-container--disabled .select2-selection__choice__remove {
|
||||
display: none; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--default .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa; }
|
||||
|
||||
.select2-container--default .select2-search--inline .select2-search__field {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
-webkit-appearance: textfield; }
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--default .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-disabled=true] {
|
||||
color: #999; }
|
||||
|
||||
.select2-container--default .select2-results__option[aria-selected=true] {
|
||||
background-color: #ddd; }
|
||||
|
||||
.select2-container--default .select2-results__option .select2-results__option {
|
||||
padding-left: 1em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__group {
|
||||
padding-left: 0; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -1em;
|
||||
padding-left: 2em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -2em;
|
||||
padding-left: 3em; }
|
||||
.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
|
||||
margin-left: -3em;
|
||||
padding-left: 4em; }
|
||||
.select2-container--default .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--default .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--default .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #5897fb;
|
||||
color: white; }
|
||||
|
||||
.select2-container--default .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic .select2-selection--single {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
outline: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%);
|
||||
background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
.select2-container--classic .select2-selection--single:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__rendered {
|
||||
color: #444;
|
||||
line-height: 28px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__clear {
|
||||
cursor: pointer;
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
margin-right: 10px; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__placeholder {
|
||||
color: #999; }
|
||||
.select2-container--classic .select2-selection--single .select2-selection__arrow {
|
||||
background-color: #ddd;
|
||||
border: none;
|
||||
border-left: 1px solid #aaa;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 20px;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); }
|
||||
.select2-container--classic .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--classic[dir="rtl"] .select2-selection--single .select2-selection__clear {
|
||||
float: left; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow {
|
||||
border: none;
|
||||
border-right: 1px solid #aaa;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
left: 1px;
|
||||
right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--single {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow {
|
||||
background: transparent;
|
||||
border: none; }
|
||||
.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent #888 transparent;
|
||||
border-width: 0 4px 5px 4px; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
|
||||
background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%);
|
||||
background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%);
|
||||
background-repeat: repeat-x;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); }
|
||||
|
||||
.select2-container--classic .select2-selection--multiple {
|
||||
background-color: white;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: text;
|
||||
outline: 0; }
|
||||
.select2-container--classic .select2-selection--multiple:focus {
|
||||
border: 1px solid #5897fb; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__rendered {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__clear {
|
||||
display: none; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #e4e4e4;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
cursor: default;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
padding: 0 5px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-right: 2px; }
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #555; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
|
||||
margin-left: 2px;
|
||||
margin-right: auto; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-selection--multiple {
|
||||
border: 1px solid #5897fb; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple {
|
||||
border-top: none;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0; }
|
||||
|
||||
.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple {
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--dropdown .select2-search__field {
|
||||
border: 1px solid #aaa;
|
||||
outline: 0; }
|
||||
|
||||
.select2-container--classic .select2-search--inline .select2-search__field {
|
||||
outline: 0;
|
||||
box-shadow: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown {
|
||||
background-color: white;
|
||||
border: 1px solid transparent; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--above {
|
||||
border-bottom: none; }
|
||||
|
||||
.select2-container--classic .select2-dropdown--below {
|
||||
border-top: none; }
|
||||
|
||||
.select2-container--classic .select2-results > .select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto; }
|
||||
|
||||
.select2-container--classic .select2-results__option[role=group] {
|
||||
padding: 0; }
|
||||
|
||||
.select2-container--classic .select2-results__option[aria-disabled=true] {
|
||||
color: grey; }
|
||||
|
||||
.select2-container--classic .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #3875d7;
|
||||
color: white; }
|
||||
|
||||
.select2-container--classic .select2-results__group {
|
||||
cursor: default;
|
||||
display: block;
|
||||
padding: 6px; }
|
||||
|
||||
.select2-container--classic.select2-container--open .select2-dropdown {
|
||||
border-color: #5897fb; }
|
||||
1
backend/easycookapi-main/static/admin/css/vendor/select2/select2.min.css
vendored
Normal file
580
backend/easycookapi-main/static/admin/css/widgets.css
Normal file
|
|
@ -0,0 +1,580 @@
|
|||
/* SELECTOR (FILTER INTERFACE) */
|
||||
|
||||
.selector {
|
||||
width: 800px;
|
||||
float: left;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
width: 380px;
|
||||
height: 17.2em;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.selector-available, .selector-chosen {
|
||||
width: 380px;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selector-chosen select {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.selector-available h2, .selector-chosen h2 {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.selector-chosen h2 {
|
||||
background: var(--primary);
|
||||
color: var(--header-link-color);
|
||||
}
|
||||
|
||||
.selector .selector-available h2 {
|
||||
background: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.selector .selector-filter {
|
||||
border: 1px solid var(--border-color);
|
||||
border-width: 0 1px;
|
||||
padding: 8px;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.625rem;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.selector .selector-filter label,
|
||||
.inline-group .aligned .selector .selector-filter label {
|
||||
float: left;
|
||||
margin: 7px 0 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selector .selector-available input {
|
||||
width: 320px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.selector ul.selector-chooser {
|
||||
align-self: center;
|
||||
width: 22px;
|
||||
background-color: var(--selected-bg);
|
||||
border-radius: 10px;
|
||||
margin: 0 5px;
|
||||
padding: 0;
|
||||
transform: translateY(-17px);
|
||||
}
|
||||
|
||||
.selector-chooser li {
|
||||
margin: 0;
|
||||
padding: 3px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.selector select {
|
||||
padding: 0 10px;
|
||||
margin: 0 0 10px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.selector-add, .selector-remove {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
text-indent: -3000px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.active.selector-add, .active.selector-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.active.selector-add:hover, .active.selector-remove:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -96px no-repeat;
|
||||
}
|
||||
|
||||
.active.selector-add:focus, .active.selector-add:hover {
|
||||
background-position: 0 -112px;
|
||||
}
|
||||
|
||||
.selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 -64px no-repeat;
|
||||
}
|
||||
|
||||
.active.selector-remove:focus, .active.selector-remove:hover {
|
||||
background-position: 0 -80px;
|
||||
}
|
||||
|
||||
a.selector-chooseall, a.selector-clearall {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
text-align: left;
|
||||
margin: 1px auto 3px;
|
||||
overflow: hidden;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
color: var(--body-quiet-color);
|
||||
text-decoration: none;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-clearall:focus,
|
||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||
color: var(--link-fg);
|
||||
}
|
||||
|
||||
a.active.selector-chooseall, a.active.selector-clearall {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.selector-chooseall {
|
||||
padding: 0 18px 0 0;
|
||||
background: url(../img/selector-icons.svg) right -160px no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a.active.selector-chooseall:focus, a.active.selector-chooseall:hover {
|
||||
background-position: 100% -176px;
|
||||
}
|
||||
|
||||
a.selector-clearall {
|
||||
padding: 0 0 0 18px;
|
||||
background: url(../img/selector-icons.svg) 0 -128px no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
a.active.selector-clearall:focus, a.active.selector-clearall:hover {
|
||||
background-position: 0 -144px;
|
||||
}
|
||||
|
||||
/* STACKED SELECTORS */
|
||||
|
||||
.stacked {
|
||||
float: left;
|
||||
width: 490px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stacked select {
|
||||
width: 480px;
|
||||
height: 10.1em;
|
||||
}
|
||||
|
||||
.stacked .selector-available, .stacked .selector-chosen {
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.stacked .selector-available {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stacked .selector-available input {
|
||||
width: 422px;
|
||||
}
|
||||
|
||||
.stacked ul.selector-chooser {
|
||||
height: 22px;
|
||||
width: 50px;
|
||||
margin: 0 0 10px 40%;
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stacked .selector-chooser li {
|
||||
float: left;
|
||||
padding: 3px 3px 3px 5px;
|
||||
}
|
||||
|
||||
.stacked .selector-chooseall, .stacked .selector-clearall {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stacked .selector-add {
|
||||
background: url(../img/selector-icons.svg) 0 -32px no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add {
|
||||
background-position: 0 -32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .active.selector-add:focus, .stacked .active.selector-add:hover {
|
||||
background-position: 0 -48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .selector-remove {
|
||||
background: url(../img/selector-icons.svg) 0 0 no-repeat;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove {
|
||||
background-position: 0 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stacked .active.selector-remove:focus, .stacked .active.selector-remove:hover {
|
||||
background-position: 0 -16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector .help-icon {
|
||||
background: url(../img/icon-unknown.svg) 0 0 no-repeat;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: -2px 0 0 2px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.selector .selector-chosen .help-icon {
|
||||
background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.selector .search-label-icon {
|
||||
background: url(../img/search.svg) 0 0 no-repeat;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
/* DATE AND TIME */
|
||||
|
||||
p.datetime {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--body-quiet-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.datetime span {
|
||||
white-space: nowrap;
|
||||
font-weight: normal;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField {
|
||||
margin-left: 5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
table p.datetime {
|
||||
font-size: 0.6875rem;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .clock-icon {
|
||||
background: url(../img/icon-clock.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.datetimeshortcuts a:focus .clock-icon,
|
||||
.datetimeshortcuts a:hover .clock-icon {
|
||||
background-position: 0 -16px;
|
||||
}
|
||||
|
||||
.datetimeshortcuts .date-icon {
|
||||
background: url(../img/icon-calendar.svg) 0 0 no-repeat;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.datetimeshortcuts a:focus .date-icon,
|
||||
.datetimeshortcuts a:hover .date-icon {
|
||||
background-position: 0 -16px;
|
||||
}
|
||||
|
||||
.timezonewarning {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
/* URL */
|
||||
|
||||
p.url {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.url a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* FILE UPLOADS */
|
||||
|
||||
p.file-upload {
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.aligned p.file-upload {
|
||||
margin-left: 170px;
|
||||
}
|
||||
|
||||
.file-upload a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.file-upload .deletelink {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
span.clearable-file-input label {
|
||||
color: var(--body-fg);
|
||||
font-size: 0.6875rem;
|
||||
display: inline;
|
||||
float: none;
|
||||
}
|
||||
|
||||
/* CALENDARS & CLOCKS */
|
||||
|
||||
.calendarbox, .clockbox {
|
||||
margin: 5px auto;
|
||||
font-size: 0.75rem;
|
||||
width: 19em;
|
||||
text-align: center;
|
||||
background: var(--body-bg);
|
||||
color: var(--body-fg);
|
||||
border: 1px solid var(--hairline-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clockbox {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendar table {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calendar caption, .calendarbox h2 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
border-top: none;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
color: #333;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.calendar th {
|
||||
padding: 8px 5px;
|
||||
background: var(--darkened-bg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.calendar td {
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calendar td.selected a {
|
||||
background: var(--primary);
|
||||
color: var(--button-fg);
|
||||
}
|
||||
|
||||
.calendar td.nonday {
|
||||
background: var(--darkened-bg);
|
||||
}
|
||||
|
||||
.calendar td.today a {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.calendar td a, .timelist a {
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 6px;
|
||||
text-decoration: none;
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.calendar td a:focus, .timelist a:focus,
|
||||
.calendar td a:hover, .timelist a:hover {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendar td a:active, .timelist a:active {
|
||||
background: var(--header-bg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.calendarnav {
|
||||
font-size: 0.625rem;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin: 0;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
.calendarnav a:link, #calendarnav a:visited,
|
||||
#calendarnav a:focus, #calendarnav a:hover {
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
.calendar-shortcuts {
|
||||
background: var(--body-bg);
|
||||
color: var(--body-quiet-color);
|
||||
font-size: 0.6875rem;
|
||||
line-height: 11px;
|
||||
border-top: 1px solid var(--hairline-color);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
text-indent: -9999px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calendarnav-previous {
|
||||
left: 10px;
|
||||
background: url(../img/calendar-icons.svg) 0 0 no-repeat;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-previous:focus,
|
||||
.calendarbox .calendarnav-previous:hover {
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
|
||||
.calendarnav-next {
|
||||
right: 10px;
|
||||
background: url(../img/calendar-icons.svg) 0 -30px no-repeat;
|
||||
}
|
||||
|
||||
.calendarbox .calendarnav-next:focus,
|
||||
.calendarbox .calendarnav-next:hover {
|
||||
background-position: 0 -45px;
|
||||
}
|
||||
|
||||
.calendar-cancel {
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
font-size: 0.75rem;
|
||||
background: #eee;
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--body-fg);
|
||||
}
|
||||
|
||||
.calendar-cancel:focus, .calendar-cancel:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.calendar-cancel a {
|
||||
color: black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
ul.timelist, .timelist li {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.timelist a {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* EDIT INLINE */
|
||||
|
||||
.inline-deletelink {
|
||||
float: right;
|
||||
text-indent: -9999px;
|
||||
background: url(../img/inline-delete.svg) 0 0 no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 0px none;
|
||||
}
|
||||
|
||||
.inline-deletelink:focus, .inline-deletelink:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* RELATED WIDGET WRAPPER */
|
||||
.related-widget-wrapper {
|
||||
float: left; /* display properly in form rows with multiple fields */
|
||||
overflow: hidden; /* clear floated contents */
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link:link {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.related-widget-wrapper-link:link:focus,
|
||||
.related-widget-wrapper-link:link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
select + .related-widget-wrapper-link,
|
||||
.related-widget-wrapper-link + .related-widget-wrapper-link {
|
||||
margin-left: 7px;
|
||||
}
|
||||
407
backend/easycookapi-main/static/admin/fonts/LICENSE.txt
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
<<<<<<< HEAD
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
=======
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
>>>>>>> c1853c95ed45e4f6a5582e70519ce634365806af
|
||||
3
backend/easycookapi-main/static/admin/fonts/README.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Roboto webfont source: https://www.google.com/fonts/specimen/Roboto
|
||||
WOFF files extracted using https://github.com/majodev/google-webfonts-helper
|
||||
Weights used in this project: Light (300), Regular (400), Bold (700)
|
||||
20
backend/easycookapi-main/static/admin/img/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Code Charm Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
7
backend/easycookapi-main/static/admin/img/README.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
All icons are taken from Font Awesome (http://fontawesome.io/) project.
|
||||
The Font Awesome font is licensed under the SIL OFL 1.1:
|
||||
- https://scripts.sil.org/OFL
|
||||
|
||||
SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG
|
||||
Font-Awesome-SVG-PNG is licensed under the MIT license (see file license
|
||||
in current folder).
|
||||
14
backend/easycookapi-main/static/admin/img/calendar-icons.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<svg width="15" height="60" viewBox="0 0 1792 7168" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<g id="previous">
|
||||
<path d="M1037 1395l102-102q19-19 19-45t-19-45l-307-307 307-307q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-454 454q-19 19-19 45t19 45l454 454q19 19 45 19t45-19zm627-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</g>
|
||||
<g id="next">
|
||||
<path d="M845 1395l454-454q19-19 19-45t-19-45l-454-454q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l307 307-307 307q-19 19-19 45t19 45l102 102q19 19 45 19t45-19zm819-499q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<use xlink:href="#previous" x="0" y="0" fill="#333333" />
|
||||
<use xlink:href="#previous" x="0" y="1792" fill="#000000" />
|
||||
<use xlink:href="#next" x="0" y="3584" fill="#333333" />
|
||||
<use xlink:href="#next" x="0" y="5376" fill="#000000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#EBECE6" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9C9C9" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="24" height="22" viewBox="0 0 847 779" xmlns="http://www.w3.org/2000/svg"><g><path fill="#F1C02A" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120z"/><path fill="#9E9E93" d="M120 1h607c66 0 120 54 120 120v536c0 66-54 120-120 120h-607c-66 0-120-54-120-120v-536c0-66 54-120 120-120zm607 25h-607c-26 0-50 11-67 28-17 18-28 41-28 67v536c0 27 11 50 28 68 17 17 41 27 67 27h607c26 0 49-10 67-27 17-18 28-41 28-68v-536c0-26-11-49-28-67-18-17-41-28-67-28z"/><path stroke="#A9A8A4" stroke-width="20" d="M706 295l-68 281"/><path stroke="#E47474" stroke-width="20" d="M316 648l390-353M141 435l175 213"/><path stroke="#C9A741" stroke-width="20" d="M319 151l-178 284M706 295l-387-144"/><g fill="#040405"><path d="M319 111c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40zM141 395c22 0 40 18 40 40s-18 40-40 40c-23 0-41-18-41-40s18-40 41-40zM316 608c22 0 40 18 40 40 0 23-18 41-40 41s-40-18-40-41c0-22 18-40 40-40zM706 254c22 0 40 18 40 41 0 22-18 40-40 40s-40-18-40-40c0-23 18-41 40-41zM638 536c22 0 40 18 40 40s-18 40-40 40-40-18-40-40 18-40 40-40z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#70bf2b" d="M1600 796v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 331 B |
3
backend/easycookapi-main/static/admin/img/icon-alert.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#efb80b" d="M1024 1375v-190q0-14-9.5-23.5t-22.5-9.5h-192q-13 0-22.5 9.5t-9.5 23.5v190q0 14 9.5 23.5t22.5 9.5h192q13 0 22.5-9.5t9.5-23.5zm-2-374l18-459q0-12-10-19-13-11-24-11h-220q-11 0-24 11-10 7-10 21l17 457q0 10 10 16.5t24 6.5h185q14 0 23.5-6.5t10.5-16.5zm-14-934l768 1408q35 63-2 126-17 29-46.5 46t-63.5 17h-1536q-34 0-63.5-17t-46.5-46q-37-63-2-126l768-1408q17-31 47-49t65-18 65 18 47 49z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<g id="icon">
|
||||
<path d="M192 1664h288v-288h-288v288zm352 0h320v-288h-320v288zm-352-352h288v-320h-288v320zm352 0h320v-320h-320v320zm-352-384h288v-288h-288v288zm736 736h320v-288h-320v288zm-384-736h320v-288h-320v288zm768 736h288v-288h-288v288zm-384-352h320v-320h-320v320zm-352-864v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm736 864h288v-320h-288v320zm-384-384h320v-288h-320v288zm384 0h288v-288h-288v288zm32-480v-288q0-13-9.5-22.5t-22.5-9.5h-64q-13 0-22.5 9.5t-9.5 22.5v288q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5-9.5t9.5-22.5zm384-64v1280q0 52-38 90t-90 38h-1408q-52 0-90-38t-38-90v-1280q0-52 38-90t90-38h128v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h384v-96q0-66 47-113t113-47h64q66 0 113 47t47 113v96h128q52 0 90 38t38 90z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
|
||||
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#efb80b" d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 380 B |
9
backend/easycookapi-main/static/admin/img/icon-clock.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="16" height="32" viewBox="0 0 1792 3584" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<g id="icon">
|
||||
<path d="M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-14 9-23t23-9h64q14 0 23 9t9 23zm416 352q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</g>
|
||||
</defs>
|
||||
<use xlink:href="#icon" x="0" y="0" fill="#447e9b" />
|
||||
<use xlink:href="#icon" x="0" y="1792" fill="#003366" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 677 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="14" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#dd4646" d="M1490 1322q0 40-28 68l-136 136q-28 28-68 28t-68-28l-294-294-294 294q-28 28-68 28t-68-28l-136-136q-28-28-28-68t28-68l294-294-294-294q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 294 294-294q28-28 68-28t68 28l136 136q28 28 28 68t-28 68l-294 294 294 294q28 28 28 68z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 392 B |
3
backend/easycookapi-main/static/admin/img/icon-no.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#dd4646" d="M1277 1122q0-26-19-45l-181-181 181-181q19-19 19-45 0-27-19-46l-90-90q-19-19-46-19-26 0-45 19l-181 181-181-181q-19-19-45-19-27 0-46 19l-90 90q-19 19-19 46 0 26 19 45l181 181-181 181q-19 19-19 45 0 27 19 46l90 90q19 19 46 19 26 0 45-19l181-181 181 181q19 19 45 19 27 0 46-19l90-90q19-19 19-46zm387-226q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 560 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#ffffff" d="M1024 1376v-192q0-14-9-23t-23-9h-192q-14 0-23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23-9t9-23zm256-672q0-88-55.5-163t-138.5-116-170-41q-243 0-371 213-15 24 8 42l132 100q7 6 19 6 16 0 25-12 53-68 86-92 34-24 86-24 48 0 85.5 26t37.5 59q0 38-20 61t-68 45q-63 28-115.5 86.5t-52.5 125.5v36q0 14 9 23t23 9h192q14 0 23-9t9-23q0-19 21.5-49.5t54.5-49.5q32-18 49-28.5t46-35 44.5-48 28-60.5 12.5-81zm384 192q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 655 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#666666" d="M1024 1376v-192q0-14-9-23t-23-9h-192q-14 0-23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23-9t9-23zm256-672q0-88-55.5-163t-138.5-116-170-41q-243 0-371 213-15 24 8 42l132 100q7 6 19 6 16 0 25-12 53-68 86-92 34-24 86-24 48 0 85.5 26t37.5 59q0 38-20 61t-68 45q-63 28-115.5 86.5t-52.5 125.5v36q0 14 9 23t23 9h192q14 0 23-9t9-23q0-19 21.5-49.5t54.5-49.5q32-18 49-28.5t46-35 44.5-48 28-60.5 12.5-81zm384 192q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 655 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2b70bf" d="M1664 960q-152-236-381-353 61 104 61 225 0 185-131.5 316.5t-316.5 131.5-316.5-131.5-131.5-316.5q0-121 61-225-229 117-381 353 133 205 333.5 326.5t434.5 121.5 434.5-121.5 333.5-326.5zm-720-384q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm848 384q0 34-20 69-140 230-376.5 368.5t-499.5 138.5-499.5-139-376.5-368q-20-35-20-69t20-69q140-229 376.5-368t499.5-139 499.5 139 376.5 368q20 35 20 69z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 581 B |
3
backend/easycookapi-main/static/admin/img/icon-yes.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="13" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#70bf2b" d="M1412 734q0-28-18-46l-91-90q-19-19-45-19t-45 19l-408 407-226-226q-19-19-45-19t-45 19l-91 90q-18 18-18 46 0 27 18 45l362 362q19 19 45 19 27 0 46-19l543-543q18-18 18-45zm252 162q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |