create: new folder

This commit is contained in:
abiyasa05 2024-12-31 09:49:01 +07:00
parent 202d55bb00
commit 788ba8d006
277 changed files with 43908 additions and 0 deletions

13
backend/easycookapi-main/.gitignore vendored Normal file
View 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__/

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

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

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

View File

@ -0,0 +1 @@
from django.contrib import admin

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'

View 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

View 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

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

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PantryConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'pantry'

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

View File

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

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

View File

@ -0,0 +1,8 @@
from rest_framework import serializers
from .models import Product
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

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

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

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ProfilesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'profiles'

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

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

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

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

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class RecipesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'recipes'

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

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

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

Binary file not shown.

View File

@ -0,0 +1,5 @@
#!/bin/bash
git pull
sudo systemctl restart easycookapi
sudo systemctl restart nginx

View File

@ -0,0 +1,275 @@
select.admin-autocomplete {
width: 20em;
}
.select2-container--admin-autocomplete.select2-container {
min-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single,
.select2-container--admin-autocomplete .select2-selection--multiple {
min-height: 30px;
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection,
.select2-container--admin-autocomplete.select2-container--open .select2-selection {
border-color: var(--body-quiet-color);
min-height: 30px;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single {
padding: 0;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple,
.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple {
padding: 0;
}
.select2-container--admin-autocomplete .select2-selection--single {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered {
color: var(--body-fg);
line-height: 30px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
}
.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b {
border-color: #888 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
left: 50%;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
width: 0;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear {
float: left;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow {
left: 1px;
right: auto;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
.select2-container--admin-autocomplete .select2-selection--multiple {
background-color: var(--body-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: text;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered {
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0 10px 5px 5px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li {
list-style: none;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder {
color: var(--body-quiet-color);
margin-top: 5px;
float: left;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear {
cursor: pointer;
float: right;
font-weight: bold;
margin: 5px;
position: absolute;
right: 0;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice {
background-color: var(--darkened-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: default;
float: left;
margin-right: 5px;
margin-top: 5px;
padding: 0 5px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove {
color: var(--body-quiet-color);
cursor: pointer;
display: inline-block;
font-weight: bold;
margin-right: 2px;
}
.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover {
color: var(--body-fg);
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline {
float: right;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice {
margin-left: 5px;
margin-right: auto;
}
.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove {
margin-left: 2px;
margin-right: auto;
}
.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple {
border: solid var(--body-quiet-color) 1px;
outline: 0;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple {
background-color: var(--darkened-bg);
cursor: default;
}
.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove {
display: none;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.select2-container--admin-autocomplete .select2-search--dropdown {
background: var(--darkened-bg);
}
.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field {
background: var(--body-bg);
color: var(--body-fg);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.select2-container--admin-autocomplete .select2-search--inline .select2-search__field {
background: transparent;
color: var(--body-fg);
border: none;
outline: 0;
box-shadow: none;
-webkit-appearance: textfield;
}
.select2-container--admin-autocomplete .select2-results > .select2-results__options {
max-height: 200px;
overflow-y: auto;
color: var(--body-fg);
background: var(--body-bg);
}
.select2-container--admin-autocomplete .select2-results__option[role=group] {
padding: 0;
}
.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] {
color: var(--body-quiet-color);
}
.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] {
background-color: var(--selected-bg);
color: var(--body-fg);
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option {
padding-left: 1em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group {
padding-left: 0;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option {
margin-left: -1em;
padding-left: 2em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -2em;
padding-left: 3em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -3em;
padding-left: 4em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -4em;
padding-left: 5em;
}
.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option {
margin-left: -5em;
padding-left: 6em;
}
.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
background-color: var(--primary);
color: var(--primary-fg);
}
.select2-container--admin-autocomplete .select2-results__group {
cursor: default;
display: block;
padding: 6px;
}

File diff suppressed because it is too large Load Diff

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

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

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

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

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

View File

@ -0,0 +1,61 @@
/* LOGIN FORM */
.login {
background: var(--darkened-bg);
height: auto;
}
.login #header {
height: auto;
padding: 15px 16px;
justify-content: center;
}
.login #header h1 {
font-size: 1.125rem;
margin: 0;
}
.login #header h1 a {
color: var(--header-link-color);
}
.login #content {
padding: 20px 20px 0;
}
.login #container {
background: var(--body-bg);
border: 1px solid var(--hairline-color);
border-radius: 4px;
overflow: hidden;
width: 28em;
min-width: 300px;
margin: 100px auto;
height: auto;
}
.login .form-row {
padding: 4px 0;
}
.login .form-row label {
display: block;
line-height: 2em;
}
.login .form-row #id_username, .login .form-row #id_password {
padding: 8px;
width: 100%;
box-sizing: border-box;
}
.login .submit-row {
padding: 1em 0 0 0;
margin: 0;
text-align: center;
}
.login .password-reset-link {
text-align: center;
}

View File

@ -0,0 +1,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%;
}

File diff suppressed because it is too large Load Diff

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

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

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

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

File diff suppressed because one or more lines are too long

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

View 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

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

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

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

View 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

View File

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

View File

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

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

View 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

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

View File

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

View 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

View File

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

View 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

View File

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

View File

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

View File

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

View 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

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