├── .python-version ├── technative ├── ai │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0005_delete_old_aicontext_models.py │ │ ├── 0003_create_aicontext.py │ │ ├── 0004_migrate_ai_contexts.py │ │ ├── 0002_chickenaicontext_eggaicontext_and_more.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── urls.py │ ├── models.py │ ├── views.py │ ├── services.py │ └── admin.py ├── teams │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_migrate_existing_teams.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── admin.py │ └── models.py ├── products │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0005_delete_old_products.py │ │ ├── 0004_migrate_products.py │ │ ├── 0003_product_delete_chickenproduct_delete_dragonproduct_and_more.py │ │ ├── 0002_chickenproduct_eggproduct.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── urls.py │ ├── apps.py │ ├── models.py │ ├── views.py │ └── admin.py ├── technative │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py └── manage.py ├── requirements.txt ├── .env.example ├── .gitignore ├── README.md ├── api-tester ├── style.css ├── ai.js ├── index.html └── products.js └── docs ├── dev ├── local-setup.md └── server-setup.md └── api ├── ai.md └── products.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.9 -------------------------------------------------------------------------------- /technative/ai/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/teams/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/products/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/technative/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/ai/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/products/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/teams/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /technative/teams/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /technative/products/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /technative/teams/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /technative/ai/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AiConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ai' 7 | -------------------------------------------------------------------------------- /technative/ai/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path("/", views.team_ai_query, name="team_ai_query"), 6 | ] 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django-cors-headers==4.3.1 2 | Django==5.0.6 3 | gunicorn==22.0.0 4 | httpx==0.27.2 5 | openai==1.28.1 6 | pillow==10.3.0 7 | psycopg2-binary==2.9.9 8 | python-dotenv==1.0.1 -------------------------------------------------------------------------------- /technative/products/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path("/", views.team_products, name="team_products"), 6 | ] 7 | -------------------------------------------------------------------------------- /technative/teams/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TeamsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'teams' 7 | -------------------------------------------------------------------------------- /technative/products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'products' 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # OPEN AI API KEYS 2 | OPENAI_API_KEY='sk-proj-...' 3 | 4 | # DJANGO SETTINGS 5 | DEBUG=on 6 | SECRET_KEY='' # generate this 7 | ALLOWED_HOSTS=127.0.0.1,localhost 8 | 9 | # DB SETTINGS 10 | DB_NAME='db' 11 | DB_USER='user' 12 | DB_PASSWORD='pw' 13 | DB_HOST='localhost' 14 | DB_PORT='5432' 15 | 16 | # CORS 17 | CORS_ALLOWED_ORIGINS="http://localhost,http://127.0.0.1" -------------------------------------------------------------------------------- /technative/teams/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Team, TeamMember 3 | 4 | 5 | @admin.register(Team) 6 | class TeamAdmin(admin.ModelAdmin): 7 | list_display = ["name", "slug", "created_date"] 8 | prepopulated_fields = {"slug": ("name",)} 9 | 10 | 11 | @admin.register(TeamMember) 12 | class TeamMemberAdmin(admin.ModelAdmin): 13 | list_display = ["user", "team", "created_date"] 14 | list_filter = ["team", "created_date"] 15 | -------------------------------------------------------------------------------- /technative/technative/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for technative project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'technative.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /technative/technative/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for technative project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'technative.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | 4 | # Mac 5 | .DS_Store 6 | *~ 7 | *.swp 8 | *.swo 9 | 10 | # npm 11 | /node_modules/ 12 | npm-debug.log 13 | 14 | # Python 15 | *.py[cod] 16 | __pycache__ 17 | __pycache__/ 18 | .pytest_cache 19 | 20 | # Distribution / packaging 21 | env/ 22 | .env 23 | .env.local 24 | .env.production 25 | 26 | # editor 27 | .vscode/settings.json 28 | 29 | # Uploaded assets 30 | /media/ 31 | 32 | # Static assets 33 | /static/ 34 | 35 | # Django 36 | db.sqlite3 37 | 38 | # misc 39 | notes.txt 40 | -------------------------------------------------------------------------------- /technative/products/migrations/0005_delete_old_products.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ("products", "0004_migrate_products"), 7 | ] 8 | 9 | operations = [ 10 | migrations.DeleteModel(name="ChickenProduct"), 11 | migrations.DeleteModel(name="DragonProduct"), 12 | migrations.DeleteModel(name="EggProduct"), 13 | migrations.DeleteModel(name="HedgehogProduct"), 14 | migrations.DeleteModel(name="WolfProduct"), 15 | ] 16 | -------------------------------------------------------------------------------- /technative/ai/migrations/0005_delete_old_aicontext_models.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | dependencies = [ 6 | ("ai", "0004_migrate_ai_contexts"), 7 | ] 8 | 9 | operations = [ 10 | migrations.DeleteModel(name="ChickenAIContext"), 11 | migrations.DeleteModel(name="DragonAIContext"), 12 | migrations.DeleteModel(name="EggAIContext"), 13 | migrations.DeleteModel(name="HedgehogAIContext"), 14 | migrations.DeleteModel(name="WolfAIContext"), 15 | ] 16 | -------------------------------------------------------------------------------- /technative/ai/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from teams.models import Team 3 | 4 | 5 | class AIContext(models.Model): 6 | team = models.OneToOneField(Team, on_delete=models.CASCADE) 7 | context = models.CharField( 8 | max_length=255, 9 | help_text="Text to be passed to ChatGPT as extra context prior to processing user input", 10 | ) 11 | 12 | class Meta: 13 | verbose_name = "AI Context" 14 | verbose_name_plural = "AI Contexts" 15 | 16 | def __str__(self): 17 | return f"{self.team.name} AI Context" 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Project API 2 | 3 | ## Docs 4 | 5 | ### API Docs 6 | 7 | - [AI](./docs/api/ai.md) 8 | - [Products](./docs/api/products.md) 9 | 10 | ### Dev 11 | 12 | - [Local setup](./docs/dev/local-setup.md) 13 | - [Server setup](./docs/dev/server-setup.md) 14 | 15 | ### Adding a new team 16 | 17 | - Log into the Django admin panel (`/admin`) 18 | - Create a new user 19 | - Edit the user, give them staff status and user permissions for AI context and products 20 | - Create a new team and add the user 21 | - Create a new team member and add the team 22 | - Create the initial team context 23 | -------------------------------------------------------------------------------- /api-tester/style.css: -------------------------------------------------------------------------------- 1 | input[type="text"] { 2 | font-size: 20px; 3 | padding: 10px 20px; 4 | border: 2px solid black; 5 | } 6 | 7 | button { 8 | appearance: none; 9 | background: orange; 10 | border: 2px solid black; 11 | padding: 10px 20px; 12 | font-size: 20px; 13 | margin: 20px; 14 | cursor: pointer; 15 | } 16 | 17 | button:hover { 18 | background: darkorange; 19 | } 20 | 21 | .block { 22 | display: inline-block; 23 | width: 40%; 24 | margin: 2%; 25 | padding: 2%; 26 | vertical-align: top; 27 | background: #eee; 28 | } 29 | 30 | pre { 31 | display: block; 32 | padding: 20px; 33 | background: #ddd; 34 | width: 90%; 35 | overflow: auto; 36 | } -------------------------------------------------------------------------------- /technative/ai/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.shortcuts import get_object_or_404 3 | from teams.models import Team 4 | from .services import ChatGPTService 5 | 6 | 7 | def team_ai_query(request, team_slug): 8 | # Get the team (no authentication required) 9 | team = get_object_or_404(Team, slug=team_slug) 10 | 11 | query = request.GET.get("query") 12 | if query is None or len(query) == 0: 13 | return JsonResponse({"error": "No query specified"}, status=500) 14 | 15 | chatgpt_service = ChatGPTService(team) 16 | response = chatgpt_service.make_request_to_chatgpt(query) 17 | status = 500 if "error" in response else 200 18 | 19 | return JsonResponse(response, status=status) 20 | -------------------------------------------------------------------------------- /technative/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'technative.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /technative/teams/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | import uuid 4 | 5 | 6 | class Team(models.Model): 7 | name = models.CharField(max_length=50, unique=True) 8 | slug = models.SlugField(max_length=50, unique=True) 9 | created_date = models.DateTimeField(auto_now_add=True) 10 | modified_date = models.DateTimeField(auto_now=True) 11 | uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 12 | 13 | class Meta: 14 | ordering = ["name"] 15 | 16 | def __str__(self): 17 | return self.name 18 | 19 | 20 | class TeamMember(models.Model): 21 | user = models.OneToOneField(User, on_delete=models.CASCADE) 22 | team = models.ForeignKey(Team, on_delete=models.CASCADE) 23 | created_date = models.DateTimeField(auto_now_add=True) 24 | 25 | class Meta: 26 | unique_together = ["user", "team"] 27 | 28 | def __str__(self): 29 | return f"{self.user.username} - {self.team.name}" 30 | -------------------------------------------------------------------------------- /technative/products/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from teams.models import Team 4 | 5 | 6 | class Product(models.Model): 7 | team = models.ForeignKey(Team, on_delete=models.CASCADE) 8 | created_date = models.DateTimeField(auto_now_add=True) 9 | modified_date = models.DateTimeField(auto_now=True) 10 | uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 11 | title = models.CharField(max_length=200) 12 | description = models.TextField( 13 | blank=True, 14 | null=True, 15 | ) 16 | price = models.DecimalField(max_digits=10, decimal_places=2) 17 | stars = models.IntegerField( 18 | default=0, 19 | choices=[ 20 | (1, "1 star"), 21 | (2, "2 stars"), 22 | (3, "3 stars"), 23 | (4, "4 stars"), 24 | (5, "5 stars"), 25 | ], 26 | ) 27 | image = models.ImageField(upload_to="images/products/") 28 | 29 | class Meta: 30 | ordering = ["title"] 31 | 32 | def __str__(self): 33 | return self.title 34 | -------------------------------------------------------------------------------- /api-tester/ai.js: -------------------------------------------------------------------------------- 1 | function requestAI() { 2 | const apiEl = document.querySelector("input[name='url']"); 3 | const api = apiEl.value; 4 | 5 | const params = {}; 6 | 7 | const aiQueryEl = document.querySelector("input[name='ai-query']"); 8 | params.query = aiQueryEl.value; 9 | 10 | const searchParams = new URLSearchParams(params); 11 | 12 | const teamEl = document.querySelector("input[name='team']"); 13 | var team = teamEl.value; 14 | 15 | fetch(`${api}/ai/${team}?${searchParams.toString()}`, { 16 | headers: { 17 | 'Accept': 'application/json' 18 | } 19 | }) 20 | .then((response) => response.json()) 21 | .then((data) => { 22 | console.log('ai data :', data); 23 | 24 | var aiResultElement = document.querySelector(".ai-result"); 25 | aiResultElement.textContent = JSON.stringify(data.results, null, 4); 26 | }) 27 | .catch((error) => { 28 | console.error('ai error:', error); 29 | }); 30 | } 31 | 32 | // listen for clicks on the button 33 | var aiButton = document.querySelector('.ai-submit'); 34 | aiButton.addEventListener("click", requestAI); 35 | -------------------------------------------------------------------------------- /technative/technative/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for technative project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | from django.contrib import admin 21 | from django.urls import include, path 22 | 23 | urlpatterns = ( 24 | [ 25 | path("ai/", include("ai.urls")), 26 | path("products/", include("products.urls")), 27 | path("admin/", admin.site.urls), 28 | ] 29 | + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 30 | + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 31 | ) 32 | -------------------------------------------------------------------------------- /technative/teams/migrations/0002_migrate_existing_teams.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.contrib.auth.models import User 3 | 4 | 5 | def create_teams_and_users(apps, schema_editor): 6 | Team = apps.get_model("teams", "Team") 7 | TeamMember = apps.get_model("teams", "TeamMember") 8 | User = apps.get_model("auth", "User") 9 | 10 | teams_data = [ 11 | {"name": "Wolf", "slug": "wolf"}, 12 | {"name": "Dragon", "slug": "dragon"}, 13 | {"name": "Hedgehog", "slug": "hedgehog"}, 14 | {"name": "Chicken", "slug": "chicken"}, 15 | {"name": "Egg", "slug": "egg"}, 16 | ] 17 | 18 | for team_data in teams_data: 19 | team = Team.objects.create(**team_data) 20 | 21 | # Create a user for this team 22 | username = f"{team_data['slug']}" 23 | user = User.objects.create_user( 24 | username=username, 25 | email=f"{username}@example.com", 26 | password="...", 27 | ) 28 | 29 | # Create team member relationship 30 | TeamMember.objects.create(user=user, team=team) 31 | 32 | 33 | def reverse_migration(apps, schema_editor): 34 | pass 35 | 36 | 37 | class Migration(migrations.Migration): 38 | dependencies = [ 39 | ("teams", "0001_initial"), 40 | ] 41 | 42 | operations = [ 43 | migrations.RunPython(create_teams_and_users, reverse_migration), 44 | ] 45 | -------------------------------------------------------------------------------- /technative/ai/migrations/0003_create_aicontext.py: -------------------------------------------------------------------------------- 1 | import django.db.models.deletion 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [ 7 | ("ai", "0002_chickenaicontext_eggaicontext_and_more"), 8 | ("teams", "0002_migrate_existing_teams"), 9 | ] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="AIContext", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ( 25 | "context", 26 | models.CharField( 27 | help_text="Text to be passed to ChatGPT as extra context prior to processing user input", 28 | max_length=255, 29 | ), 30 | ), 31 | ( 32 | "team", 33 | models.OneToOneField( 34 | on_delete=django.db.models.deletion.CASCADE, to="teams.team" 35 | ), 36 | ), 37 | ], 38 | options={ 39 | "verbose_name": "AI Context", 40 | "verbose_name_plural": "AI Contexts", 41 | }, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /technative/ai/migrations/0004_migrate_ai_contexts.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def migrate_ai_contexts(apps, schema_editor): 5 | Team = apps.get_model("teams", "Team") 6 | AIContext = apps.get_model("ai", "AIContext") 7 | 8 | # Get the old context models 9 | WolfAIContext = apps.get_model("ai", "WolfAIContext") 10 | DragonAIContext = apps.get_model("ai", "DragonAIContext") 11 | HedgehogAIContext = apps.get_model("ai", "HedgehogAIContext") 12 | ChickenAIContext = apps.get_model("ai", "ChickenAIContext") 13 | EggAIContext = apps.get_model("ai", "EggAIContext") 14 | 15 | # Migrate contexts 16 | context_mappings = [ 17 | (WolfAIContext, "wolf"), 18 | (DragonAIContext, "dragon"), 19 | (HedgehogAIContext, "hedgehog"), 20 | (ChickenAIContext, "chicken"), 21 | (EggAIContext, "egg"), 22 | ] 23 | 24 | for old_model, team_slug in context_mappings: 25 | try: 26 | old_context = old_model.objects.first() 27 | if old_context: 28 | team = Team.objects.get(slug=team_slug) 29 | AIContext.objects.create(team=team, context=old_context.context) 30 | except Exception as e: 31 | print(f"Error migrating {team_slug}: {e}") 32 | 33 | 34 | def reverse_migration(apps, schema_editor): 35 | pass 36 | 37 | 38 | class Migration(migrations.Migration): 39 | dependencies = [ 40 | ( 41 | "ai", 42 | "0003_create_aicontext", 43 | ), 44 | ("teams", "0002_migrate_existing_teams"), 45 | ] 46 | 47 | operations = [ 48 | migrations.RunPython(migrate_ai_contexts, reverse_migration), 49 | ] 50 | -------------------------------------------------------------------------------- /api-tester/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API tester 6 | 7 | 8 | 9 | 10 | 11 |

API tester

12 | 13 |

API

14 |
15 | 16 | 17 |
18 | 19 |
20 |

AI

21 | 22 | 23 |

24 |     
25 | 26 |
27 |

Products

28 | 29 |

Sort

30 | 34 | 38 | 42 |

Pagination

43 | 47 | 51 | 52 |

53 |     
54 | 55 | 56 | -------------------------------------------------------------------------------- /api-tester/products.js: -------------------------------------------------------------------------------- 1 | function requestProducts() { 2 | const apiEl = document.querySelector("input[name='url']"); 3 | const api = apiEl.value; 4 | 5 | const params = {}; 6 | 7 | const productsQueryEl = document.querySelector("input[name='products-query']"); 8 | params.query = productsQueryEl.value; 9 | 10 | const productsSortEl = document.querySelector("input[name='products-sort']:checked"); 11 | params.sort = productsSortEl.value; 12 | 13 | const productsPageEl = document.querySelector("input[name='products-page']"); 14 | if (productsPageEl.value) { 15 | params.page = productsPageEl.value 16 | } 17 | const productsPageSizeEl = document.querySelector("input[name='products-page-size']"); 18 | if (productsPageSizeEl.value) { 19 | params['page-size'] = productsPageSizeEl.value 20 | } 21 | 22 | const searchParams = new URLSearchParams(params); 23 | 24 | const teamEl = document.querySelector("input[name='team']"); 25 | var team = teamEl.value; 26 | 27 | fetch(`${api}/products/${team}?${searchParams.toString()}`, { 28 | headers: { 29 | 'Accept': 'application/json' 30 | } 31 | }) 32 | .then((response) => response.json()) 33 | .then((data) => { 34 | console.log('Products data :', data); 35 | 36 | var productsResultElement = document.querySelector(".products-result"); 37 | productsResultElement.textContent = JSON.stringify(data.products, null, 4); 38 | }) 39 | .catch((error) => { 40 | console.error('Products error:', error); 41 | }); 42 | } 43 | 44 | // listen for clicks on the button 45 | var productsButton = document.querySelector('.products-submit'); 46 | productsButton.addEventListener("click", requestProducts); 47 | -------------------------------------------------------------------------------- /technative/teams/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2025-10-08 11:59 2 | 3 | import django.db.models.deletion 4 | import uuid 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Team', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('name', models.CharField(max_length=50, unique=True)), 23 | ('slug', models.SlugField(unique=True)), 24 | ('created_date', models.DateTimeField(auto_now_add=True)), 25 | ('modified_date', models.DateTimeField(auto_now=True)), 26 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 27 | ], 28 | options={ 29 | 'ordering': ['name'], 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name='TeamMember', 34 | fields=[ 35 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('created_date', models.DateTimeField(auto_now_add=True)), 37 | ('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teams.team')), 38 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 39 | ], 40 | options={ 41 | 'unique_together': {('user', 'team')}, 42 | }, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /technative/products/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.core.paginator import Paginator 3 | from django.shortcuts import get_object_or_404 4 | from teams.models import Team 5 | from .models import Product 6 | 7 | 8 | def team_products(request, team_slug): 9 | # Get the team (no authentication required) 10 | team = get_object_or_404(Team, slug=team_slug) 11 | 12 | data = {"products": []} 13 | 14 | # Get products for this team only 15 | query = request.GET.get("query") 16 | if query is None or len(query) == 0: 17 | products = Product.objects.filter(team=team) 18 | else: 19 | products = Product.objects.filter(team=team, title__icontains=query) 20 | 21 | # Handle sorting 22 | order = ["title", "id"] 23 | sort = request.GET.get("sort") 24 | if sort == "price": 25 | order.insert(0, "price") 26 | elif sort == "rating": 27 | order.insert(0, "-stars") 28 | products = products.order_by(*order) 29 | 30 | # Handle pagination 31 | page_size = int(request.GET.get("page-size", 10000)) 32 | page_number = int(request.GET.get("page", 1)) 33 | paginated_products = Paginator(products, page_size) 34 | if ( 35 | page_size > 0 36 | and page_number > 0 37 | and page_number <= paginated_products.num_pages 38 | ): 39 | products_page = paginated_products.page(page_number) 40 | else: 41 | products_page = [] 42 | 43 | # Generate output 44 | for product in products_page: 45 | product_data = { 46 | "id": product.uuid, 47 | "title": product.title, 48 | "description": product.description, 49 | "image": product.image.url, 50 | "price": product.price, 51 | "stars": product.stars, 52 | } 53 | data["products"].append(product_data) 54 | 55 | return JsonResponse(data) 56 | -------------------------------------------------------------------------------- /docs/dev/local-setup.md: -------------------------------------------------------------------------------- 1 | # Local set-up 2 | 3 | ## Requirements 4 | 5 | - Python (v3.10+) 6 | - PostgreSQL 7 | - ChatGPT API key (or three) - [https://platform.openai.com/](https://platform.openai.com/) 8 | 9 | 10 | ## Project 11 | 12 | 1. Clone the project repo 13 | 14 | 1. Create `.env` file based on the `.env.example` file and fill in the required information 15 | 16 | 1. Set up a python environment, e.g. using virtualenv and pyenv: 17 | 18 | ``` 19 | virtualenv env --python=$HOME/.pyenv/versions/3.10.9/bin/python 20 | source env/bin/activate 21 | ``` 22 | 23 | 1. Install python dependencies: 24 | 25 | ``` 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | 30 | 1. Set up a local postgres database, e.g. on macOS using Postgres.app: 31 | 32 | ``` 33 | /Applications/Postgres.app/Contents/Versions/15/bin/psql -p5432 34 | CREATE DATABASE app_db; 35 | \c app_db; 36 | CREATE USER app_user WITH PASSWORD 'app_pw'; 37 | GRANT ALL PRIVILEGES ON DATABASE app_db TO app_user; 38 | GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public to app_user; 39 | GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public to app_user; 40 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public to app_user; 41 | ALTER DATABASE app_db OWNER TO app_user; 42 | ``` 43 | 44 | 1. Run migrations 45 | 46 | ``` 47 | cd technative 48 | python manage.py migrate 49 | ``` 50 | 51 | 1. Create a super user: 52 | 53 | ``` 54 | python manage.py createsuperuser 55 | ``` 56 | 57 | 1. Run the server: 58 | 59 | ``` 60 | python manage.py runserver 61 | ``` 62 | 63 | 1. Log into the admin with the superuser: http://localhost:8000 64 | 65 | 1. Add a context sentence for all groups 66 | 1. Add products for all groups 67 | 1. Create users for each group and set their permissions to only access their relevant content 68 | 69 | 1. The API can be tested using the `api-tester` HTML page from the repo root. -------------------------------------------------------------------------------- /technative/ai/migrations/0002_chickenaicontext_eggaicontext_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2025-02-17 14:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('ai', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='ChickenAIContext', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('context', models.CharField(help_text='Text to be passed to ChatGPT as extra context prior to processing user input', max_length=255)), 18 | ], 19 | options={ 20 | 'verbose_name': 'Chicken AI Context', 21 | 'verbose_name_plural': 'Chicken AI Context', 22 | }, 23 | ), 24 | migrations.CreateModel( 25 | name='EggAIContext', 26 | fields=[ 27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('context', models.CharField(help_text='Text to be passed to ChatGPT as extra context prior to processing user input', max_length=255)), 29 | ], 30 | options={ 31 | 'verbose_name': 'Egg AI Context', 32 | 'verbose_name_plural': 'Egg AI Context', 33 | }, 34 | ), 35 | migrations.AlterModelOptions( 36 | name='dragonaicontext', 37 | options={'verbose_name': 'Dragon AI Context', 'verbose_name_plural': 'Dragon AI Context'}, 38 | ), 39 | migrations.AlterModelOptions( 40 | name='hedgehogaicontext', 41 | options={'verbose_name': 'Hedgehog AI Context', 'verbose_name_plural': 'Hedgehog AI Context'}, 42 | ), 43 | migrations.AlterModelOptions( 44 | name='wolfaicontext', 45 | options={'verbose_name': 'Wolf AI Context', 'verbose_name_plural': 'Wolf AI Context'}, 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /technative/ai/services.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | from openai import OpenAI 3 | from django.conf import settings 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class ChatGPTService: 10 | def __init__(self, team): 11 | self.team = team 12 | self.client = OpenAI(api_key=settings.OPENAI_API_KEY) 13 | 14 | def make_request_to_chatgpt(self, query): 15 | try: 16 | # Get AI context for this team 17 | from .models import AIContext 18 | 19 | try: 20 | ai_context = AIContext.objects.get(team=self.team) 21 | context = ai_context.context 22 | except AIContext.DoesNotExist: 23 | context = "" 24 | logger.warning(f"No AI context found for team {self.team.name}") 25 | 26 | completion = self.client.chat.completions.create( 27 | model="gpt-3.5-turbo-0125", 28 | response_format={"type": "json_object"}, 29 | messages=[ 30 | { 31 | "role": "system", 32 | "content": context, 33 | }, 34 | { 35 | "role": "system", 36 | "content": "Anything here overrides the first command. " 37 | "Concise responses in British English. " 38 | "Use 250 tokens max. " 39 | "Respond with a JSON object, one param, results, containing an array, always 5 results, 2 properties per result: title and description.", 40 | }, 41 | { 42 | "role": "user", 43 | "content": query, 44 | }, 45 | ], 46 | ) 47 | 48 | return loads(completion.choices[0].message.content) 49 | 50 | except Exception as e: 51 | logger.error( 52 | f"ChatGPTService: Error in request for team {self.team.name} - {e}" 53 | ) 54 | return {"error": "Sorry, something went wrong, please try again"} 55 | -------------------------------------------------------------------------------- /technative/products/migrations/0004_migrate_products.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def migrate_products(apps, schema_editor): 5 | Team = apps.get_model("teams", "Team") 6 | Product = apps.get_model("products", "Product") 7 | 8 | # Get the old product models 9 | WolfProduct = apps.get_model("products", "WolfProduct") 10 | DragonProduct = apps.get_model("products", "DragonProduct") 11 | HedgehogProduct = apps.get_model("products", "HedgehogProduct") 12 | ChickenProduct = apps.get_model("products", "ChickenProduct") 13 | EggProduct = apps.get_model("products", "EggProduct") 14 | 15 | # Migrate products 16 | product_mappings = [ 17 | (WolfProduct, "wolf"), 18 | (DragonProduct, "dragon"), 19 | (HedgehogProduct, "hedgehog"), 20 | (ChickenProduct, "chicken"), 21 | (EggProduct, "egg"), 22 | ] 23 | 24 | for old_model, team_slug in product_mappings: 25 | try: 26 | team = Team.objects.get(slug=team_slug) 27 | for old_product in old_model.objects.all(): 28 | Product.objects.create( 29 | team=team, 30 | title=old_product.title, 31 | description=old_product.description, 32 | price=old_product.price, 33 | stars=old_product.stars, 34 | image=old_product.image, 35 | created_date=old_product.created_date, 36 | modified_date=old_product.modified_date, 37 | uuid=old_product.uuid, 38 | ) 39 | except Exception as e: 40 | print(f"Error migrating {team_slug} products: {e}") 41 | 42 | 43 | def reverse_migration(apps, schema_editor): 44 | pass 45 | 46 | 47 | class Migration(migrations.Migration): 48 | dependencies = [ 49 | ( 50 | "products", 51 | "0003_product_delete_chickenproduct_delete_dragonproduct_and_more", 52 | ), 53 | ("teams", "0002_migrate_existing_teams"), 54 | ] 55 | 56 | operations = [ 57 | migrations.RunPython(migrate_products, reverse_migration), 58 | ] 59 | -------------------------------------------------------------------------------- /docs/api/ai.md: -------------------------------------------------------------------------------- 1 | # API - AI 2 | 3 | Retrieve information from the AI. 4 | 5 | | | | 6 | | :--- | :--- | 7 | | URL | `/ai/{team}` | 8 | | Method | `GET` | 9 | | Auth required | No | 10 | | Permissions required | None | 11 | 12 | ## Parameters 13 | 14 | ### URL Parameters 15 | 16 | - `{team}` (required): Specifies the team for which information is requested. 17 | 18 | ### Query Parameters 19 | 20 | - `query` (required): The specific query to be processed by the AI system. 21 | 22 | ### Example queries 23 | 24 | ``` 25 | https://[url]/ai/wolf?query=Name+5+African+mammals 26 | https://[url]/ai/egg?query=Name+5+African+birds 27 | ``` 28 | 29 | ## Success Response 30 | 31 | ### Information based on the query provided. 32 | 33 | **HTTP Response Code** : `200 OK` 34 | 35 | **Response data** 36 | 37 | ```json 38 | { 39 | "results": [ 40 | { 41 | "title": "Lion", 42 | "description": "Majestic predator known for its mane and powerful roar" 43 | }, 44 | { 45 | "title": "Elephant", 46 | "description": "Gentle giant with a trunk and tusks, one of the largest land animals" 47 | }, 48 | { 49 | "title": "Giraffe", 50 | "description": "Tall herbivore with a long neck and distinctive spotted coat" 51 | }, 52 | { 53 | "title": "Hippopotamus", 54 | "description": "Large, barrel-shaped animal with a massive mouth and tusks" 55 | }, 56 | { 57 | "title": "Cheetah", 58 | "description": "Sleek and fast predator, known for its black spots and long tail" 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | ## Error Responses 65 | 66 | ### No query specified 67 | 68 | **HTTP Response Code** : `500 INTERNAL SERVER ERROR` 69 | 70 | **Response data** 71 | 72 | ```json 73 | { 74 | "error": "No query specified" 75 | } 76 | ``` 77 | 78 | ### Internal Server Error 79 | 80 | **HTTP Response Code** : `500 INTERNAL SERVER ERROR` 81 | 82 | **Response data** 83 | 84 | ```json 85 | { 86 | "error": "Sorry, something went wrong, please try again" 87 | } 88 | ``` -------------------------------------------------------------------------------- /technative/products/migrations/0003_product_delete_chickenproduct_delete_dragonproduct_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2025-10-08 12:02 2 | 3 | import django.db.models.deletion 4 | import uuid 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("products", "0002_chickenproduct_eggproduct"), 11 | ("teams", "0002_migrate_existing_teams"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Product", 17 | fields=[ 18 | ( 19 | "id", 20 | models.BigAutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("created_date", models.DateTimeField(auto_now_add=True)), 28 | ("modified_date", models.DateTimeField(auto_now=True)), 29 | ( 30 | "uuid", 31 | models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 32 | ), 33 | ("title", models.CharField(max_length=200)), 34 | ("description", models.TextField(blank=True, null=True)), 35 | ("price", models.DecimalField(decimal_places=2, max_digits=10)), 36 | ( 37 | "stars", 38 | models.IntegerField( 39 | choices=[ 40 | (1, "1 star"), 41 | (2, "2 stars"), 42 | (3, "3 stars"), 43 | (4, "4 stars"), 44 | (5, "5 stars"), 45 | ], 46 | default=0, 47 | ), 48 | ), 49 | ("image", models.ImageField(upload_to="images/products/")), 50 | ( 51 | "team", 52 | models.ForeignKey( 53 | on_delete=django.db.models.deletion.CASCADE, to="teams.team" 54 | ), 55 | ), 56 | ], 57 | options={ 58 | "ordering": ["title"], 59 | }, 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /technative/products/migrations/0002_chickenproduct_eggproduct.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2025-02-17 14:20 2 | 3 | import uuid 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('products', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ChickenProduct', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_date', models.DateTimeField(auto_now_add=True)), 19 | ('modified_date', models.DateTimeField(auto_now=True)), 20 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 21 | ('title', models.CharField(max_length=200)), 22 | ('description', models.TextField(blank=True, null=True)), 23 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 24 | ('stars', models.IntegerField(choices=[(1, '1 star'), (2, '2 stars'), (3, '3 stars'), (4, '4 stars'), (5, '5 stars')], default=0)), 25 | ('image', models.ImageField(upload_to='images/products/chicken/')), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='EggProduct', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('created_date', models.DateTimeField(auto_now_add=True)), 36 | ('modified_date', models.DateTimeField(auto_now=True)), 37 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 38 | ('title', models.CharField(max_length=200)), 39 | ('description', models.TextField(blank=True, null=True)), 40 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 41 | ('stars', models.IntegerField(choices=[(1, '1 star'), (2, '2 stars'), (3, '3 stars'), (4, '4 stars'), (5, '5 stars')], default=0)), 42 | ('image', models.ImageField(upload_to='images/products/egg/')), 43 | ], 44 | options={ 45 | 'abstract': False, 46 | }, 47 | ), 48 | ] 49 | -------------------------------------------------------------------------------- /technative/ai/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import AIContext 3 | from teams.models import TeamMember, Team 4 | 5 | 6 | @admin.register(AIContext) 7 | class AIContextAdmin(admin.ModelAdmin): 8 | list_display = ["team", "context"] 9 | list_filter = ["team"] 10 | 11 | def has_add_permission(self, request): 12 | if request.user.is_superuser: 13 | return True 14 | return False 15 | 16 | def has_delete_permission(self, request, obj=None): 17 | return False 18 | 19 | def get_queryset(self, request): 20 | """Filter to only show AI contexts for the user's team""" 21 | qs = super().get_queryset(request) 22 | if request.user.is_superuser: 23 | return qs 24 | 25 | try: 26 | team_member = TeamMember.objects.get(user=request.user) 27 | return qs.filter(team=team_member.team) 28 | except TeamMember.DoesNotExist: 29 | return qs.none() 30 | 31 | def has_change_permission(self, request, obj=None): 32 | """Only allow editing if user belongs to the team""" 33 | if request.user.is_superuser: 34 | return True 35 | 36 | if obj is None: 37 | return True 38 | 39 | try: 40 | team_member = TeamMember.objects.get(user=request.user) 41 | return obj.team == team_member.team 42 | except TeamMember.DoesNotExist: 43 | return False 44 | 45 | def has_view_permission(self, request, obj=None): 46 | """Only allow viewing if user belongs to the team""" 47 | if request.user.is_superuser: 48 | return True 49 | 50 | if obj is None: 51 | return True 52 | 53 | try: 54 | team_member = TeamMember.objects.get(user=request.user) 55 | return obj.team == team_member.team 56 | except TeamMember.DoesNotExist: 57 | return False 58 | 59 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 60 | """Restrict team field to user's team only""" 61 | if db_field.name == "team" and not request.user.is_superuser: 62 | try: 63 | team_member = TeamMember.objects.get(user=request.user) 64 | kwargs["queryset"] = Team.objects.filter(id=team_member.team.id) 65 | except TeamMember.DoesNotExist: 66 | kwargs["queryset"] = Team.objects.none() 67 | return super().formfield_for_foreignkey(db_field, request, **kwargs) 68 | -------------------------------------------------------------------------------- /technative/products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Product 3 | from teams.models import TeamMember, Team 4 | 5 | 6 | @admin.register(Product) 7 | class ProductAdmin(admin.ModelAdmin): 8 | list_display = ["title", "team", "price", "stars", "created_date"] 9 | list_filter = ["team", "stars", "created_date"] 10 | search_fields = ["title", "description"] 11 | 12 | def get_queryset(self, request): 13 | """Filter to only show products for the user's team""" 14 | qs = super().get_queryset(request) 15 | if request.user.is_superuser: 16 | return qs 17 | 18 | try: 19 | team_member = TeamMember.objects.get(user=request.user) 20 | return qs.filter(team=team_member.team) 21 | except TeamMember.DoesNotExist: 22 | return qs.none() 23 | 24 | def has_add_permission(self, request): 25 | """Allow adding products only for user's team""" 26 | if request.user.is_superuser: 27 | return True 28 | 29 | try: 30 | TeamMember.objects.get(user=request.user) 31 | return True 32 | except TeamMember.DoesNotExist: 33 | return False 34 | 35 | def has_change_permission(self, request, obj=None): 36 | """Only allow editing if user belongs to the team""" 37 | if request.user.is_superuser: 38 | return True 39 | 40 | if obj is None: 41 | return True 42 | 43 | try: 44 | team_member = TeamMember.objects.get(user=request.user) 45 | return obj.team == team_member.team 46 | except TeamMember.DoesNotExist: 47 | return False 48 | 49 | def has_delete_permission(self, request, obj=None): 50 | """Only allow deleting if user belongs to the team""" 51 | if request.user.is_superuser: 52 | return True 53 | 54 | if obj is None: 55 | return True 56 | 57 | try: 58 | team_member = TeamMember.objects.get(user=request.user) 59 | return obj.team == team_member.team 60 | except TeamMember.DoesNotExist: 61 | return False 62 | 63 | def formfield_for_foreignkey(self, db_field, request, **kwargs): 64 | """Restrict team field to user's team only""" 65 | if db_field.name == "team" and not request.user.is_superuser: 66 | try: 67 | team_member = TeamMember.objects.get(user=request.user) 68 | kwargs["queryset"] = Team.objects.filter(id=team_member.team.id) 69 | except TeamMember.DoesNotExist: 70 | kwargs["queryset"] = Team.objects.none() 71 | return super().formfield_for_foreignkey(db_field, request, **kwargs) 72 | -------------------------------------------------------------------------------- /technative/ai/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-05-12 16:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="DragonAIContext", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ( 25 | "context", 26 | models.CharField( 27 | help_text="Text to be passed to ChatGPT as extra context prior to processing user input", 28 | max_length=255, 29 | ), 30 | ), 31 | ], 32 | options={ 33 | "abstract": False, 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name="HedgehogAIContext", 38 | fields=[ 39 | ( 40 | "id", 41 | models.BigAutoField( 42 | auto_created=True, 43 | primary_key=True, 44 | serialize=False, 45 | verbose_name="ID", 46 | ), 47 | ), 48 | ( 49 | "context", 50 | models.CharField( 51 | help_text="Text to be passed to ChatGPT as extra context prior to processing user input", 52 | max_length=255, 53 | ), 54 | ), 55 | ], 56 | options={ 57 | "abstract": False, 58 | }, 59 | ), 60 | migrations.CreateModel( 61 | name="WolfAIContext", 62 | fields=[ 63 | ( 64 | "id", 65 | models.BigAutoField( 66 | auto_created=True, 67 | primary_key=True, 68 | serialize=False, 69 | verbose_name="ID", 70 | ), 71 | ), 72 | ( 73 | "context", 74 | models.CharField( 75 | help_text="Text to be passed to ChatGPT as extra context prior to processing user input", 76 | max_length=255, 77 | ), 78 | ), 79 | ], 80 | options={ 81 | "abstract": False, 82 | }, 83 | ), 84 | ] 85 | -------------------------------------------------------------------------------- /docs/api/products.md: -------------------------------------------------------------------------------- 1 | # API - Products 2 | 3 | Retrieve products for a team. 4 | 5 | | | | 6 | | :--- | :--- | 7 | | URL | `/products/{team}`| 8 | | Method | `GET` | 9 | | Auth required | No | 10 | | Permissions required | None | 11 | 12 | ## Parameters 13 | 14 | ### URL Parameters 15 | 16 | - `{team}` (required): Specifies the team for which information is requested. 17 | 18 | ### Query Parameters 19 | 20 | - `query` (optional): The query string to search for products. 21 | 22 | - `sort` (optional): Specifies the sorting criteria. Possible values: "price", "rating", or "title". Defaults to "title". 23 | 24 | - `page-size` (optional): Specifies the number of products per page. Must be a positive integer. 25 | 26 | - `page` (optional): Specifies the page number. Must be a positive integer. 27 | 28 | ### Example queries 29 | 30 | ``` 31 | https://[url]/product/hedgehog?query=shoes 32 | https://[url]/product/chicken?sort=price 33 | https://[url]/product/egg?query=summer+jackets&sort=rating&page-size=2&page=2 34 | ``` 35 | 36 | 37 | ## Success Response 38 | 39 | ### Products related to hedgehogs based on the query provided. 40 | 41 | **HTTP Response Code** : `200 OK` 42 | 43 | **Response data** 44 | 45 | ```json 46 | { 47 | "products": [ 48 | { 49 | "id": "43a8330b-87c3-4896-99c8-bf75942998a4", 50 | "title": "Product 4", 51 | "description": "A description", 52 | "image": "/images/products/hedgehog/product-4.jpg", 53 | "price": "0.07", 54 | "stars": 5 55 | }, 56 | { 57 | "id": "91d545b3-967f-4b57-8fd9-bbbe30d9dd71", 58 | "title": "Product 1", 59 | "description": "A description", 60 | "image": "/images/products/hedgehog/product-1.jpg", 61 | "price": "0.02", 62 | "stars": 3 63 | }, 64 | { 65 | "id": "92878fc0-109c-41ca-8768-91063a31b2b4", 66 | "title": "Product 2", 67 | "description": "A description", 68 | "image": "/images/products/hedgehog/product-2.jpg", 69 | "price": "0.04", 70 | "stars": 3 71 | }, 72 | { 73 | "id": "c2449c1c-cc18-4e06-a58f-f96366e39f66", 74 | "title": "Product 5", 75 | "description": "A description", 76 | "image": "/images/products/hedgehog/product-5.jpg", 77 | "price": "1.11", 78 | "stars": 2 79 | } 80 | ] 81 | } 82 | ``` 83 | 84 | ### No products found 85 | 86 | **HTTP Response Code** : `200 OK` 87 | 88 | **Response data** 89 | 90 | ```json 91 | { 92 | "results": [] 93 | } 94 | ``` -------------------------------------------------------------------------------- /docs/dev/server-setup.md: -------------------------------------------------------------------------------- 1 | # Server setup 2 | 3 | Some useful guides if self-hosting (e.g. on Digital Ocean): 4 | 5 | - https://www.digitalocean.com/community/tutorials/how-to-set-up-an-ubuntu-server-on-a-digitalocean-droplet 6 | - https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu 7 | - https://hostadvice.com/how-to/web-hosting/ubuntu/how-to-harden-your-ubuntu-18-04-server/ 8 | - https://www.digitalocean.com/community/tutorials/ubuntu-and-debian-package-management-essentials 9 | - https://www.digitalocean.com/community/tutorials/how-to-keep-ubuntu-22-04-servers-updated 10 | - https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu 11 | - https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-22-04 12 | - https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-22-04 13 | - https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04 14 | - https://www.digitalocean.com/community/tutorials/how-to-set-up-nginx-with-http-2-support-on-ubuntu-22-04 15 | - https://www.digitalocean.com/community/tutorials/how-to-install-postgresql-on-ubuntu-22-04-quickstart 16 | - https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-django-application-on-ubuntu-22-04 17 | 18 | 19 | --- 20 | 21 | ## If cloning a private GitHub repo 22 | 23 | - Create an ssh key on the server: 24 | 25 | ``` 26 | ssh-keygen 27 | ``` 28 | 29 | - Follow the steps. It may be worth changing the default key path so that you can create a unique ssh key for different repos 30 | 31 | - Once this is complete, create a config file: `~/.ssh/config`: 32 | 33 | ``` 34 | Host hostname 35 | HostName github.com 36 | User git 37 | IdentityFile ~/.ssh/id_rsa 38 | ``` 39 | 40 | - Next, copy the public key from ~/.ssh/id_rsa_backend.pub and add this to the GitHub repository as a deploy key 41 | 42 | This will allow you to clone this private repo on the server, using the config name. 43 | 44 | - Clone this repo onto the server: 45 | 46 | ``` 47 | git clone hostname:user/repo.git 48 | ``` 49 | 50 | --- 51 | 52 | ## Installation 53 | 54 | - Create and enter a virtualenv 55 | 56 | - Install the project dependencies: 57 | 58 | ``` 59 | pip install -r requirements.txt 60 | ``` 61 | 62 | - Manually create an `.env` file in the root of the project directory, based on the sample `.env.example` file: 63 | 64 | ``` 65 | cp .env.example .env 66 | nano .env 67 | ``` 68 | 69 | - [Generate](https://djecrety.ir/) a new Django secret key 70 | 71 | - Update the `.env` details 72 | 73 | - Add the website's URL to the list of `ALLOWED_HOSTS` 74 | 75 | - Add relevant API keys to the file 76 | 77 | - Move into the repo directory 78 | 79 | - Run the database migrations: 80 | 81 | ``` 82 | python manage.py migrate 83 | ``` 84 | 85 | - Gather static assets: 86 | 87 | ``` 88 | python manage.py collectstatic 89 | ``` 90 | 91 | - Create an admin superuser: 92 | 93 | ``` 94 | python manage.py createsuperuser 95 | ``` 96 | 97 | - Follow the instructions above to set up gunicorn 98 | 99 | - Set up a new nginx site: 100 | 101 | ``` 102 | server { 103 | server_name sitename.com; 104 | location /static/ { 105 | alias /path/to/sitename/static/; 106 | } 107 | location /media/ { 108 | alias /path/to/sitename/media/; 109 | } 110 | location / { 111 | include proxy_params; 112 | proxy_pass http://unix:/run/gunicorn.sock; 113 | } 114 | } 115 | ``` 116 | 117 | - Follow the instructions above to use certbot to set up a free SSL certificate. 118 | 119 | -------------------------------------------------------------------------------- /technative/technative/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for technative project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from dotenv import load_dotenv 15 | from pathlib import Path 16 | 17 | load_dotenv() 18 | 19 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 20 | BASE_DIR = Path(__file__).resolve().parent.parent 21 | ROOT_DIR = Path(__file__).resolve().parent.parent.parent 22 | 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 26 | 27 | SECRET_KEY = os.getenv("SECRET_KEY") 28 | 29 | # Anything but 'True' will equate to False 30 | # https://stackoverflow.com/a/65640689 31 | DEBUG = os.getenv("DEBUG", "False") == "True" 32 | 33 | ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS").split(",") 34 | 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | "teams.apps.TeamsConfig", 40 | "ai.apps.AiConfig", 41 | "products.apps.ProductsConfig", 42 | "corsheaders", 43 | "django.contrib.admin", 44 | "django.contrib.auth", 45 | "django.contrib.contenttypes", 46 | "django.contrib.sessions", 47 | "django.contrib.messages", 48 | "django.contrib.staticfiles", 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.middleware.security.SecurityMiddleware", 53 | "django.contrib.sessions.middleware.SessionMiddleware", 54 | "corsheaders.middleware.CorsMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | ] 61 | 62 | ROOT_URLCONF = "technative.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = "technative.wsgi.application" 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 85 | 86 | DATABASES = { 87 | "default": { 88 | "ENGINE": "django.db.backends.postgresql_psycopg2", 89 | "NAME": os.getenv("DB_NAME"), 90 | "USER": os.getenv("DB_USER"), 91 | "PASSWORD": os.getenv("DB_PASSWORD"), 92 | "HOST": os.getenv("DB_HOST", "localhost"), 93 | "PORT": os.getenv("DB_PORT", "5432"), 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 104 | }, 105 | { 106 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 107 | }, 108 | { 109 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 110 | }, 111 | { 112 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 119 | 120 | LANGUAGE_CODE = "en-gb" 121 | 122 | TIME_ZONE = "UTC" 123 | 124 | USE_I18N = True 125 | 126 | USE_TZ = True 127 | 128 | 129 | # Static files (CSS, JavaScript, Images) 130 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 131 | 132 | STATIC_URL = "static/" 133 | 134 | STATIC_ROOT = ROOT_DIR / "static/" 135 | 136 | # Default primary key field type 137 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 138 | 139 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 140 | 141 | # Media 142 | 143 | # Base url to serve media files 144 | MEDIA_URL = "/media/" 145 | 146 | # Path where media is stored 147 | MEDIA_ROOT = ROOT_DIR / "media/" 148 | 149 | 150 | # CORS 151 | 152 | CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS").split(",") 153 | 154 | CORS_ALLOWED_ORIGIN_REGEXES = [ 155 | r"^https://[\w-]+\.vercel\.app$", 156 | r"^https://[\w-]+\.netlify\.app$", 157 | r"^https://[\w-]+\.github\.io$", 158 | r"^http:\/\/localhost:*([0-9]+)?$", 159 | r"^https:\/\/localhost:*([0-9]+)?$", 160 | r"^http:\/\/127.0.0.1:*([0-9]+)?$", 161 | r"^https:\/\/127.0.0.1:*([0-9]+)?$", 162 | ] 163 | 164 | CORS_ALLOW_METHODS = ( 165 | "GET", 166 | "OPTIONS", 167 | ) 168 | 169 | ## App-specific 170 | 171 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 172 | -------------------------------------------------------------------------------- /technative/products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-05-13 17:17 2 | 3 | import uuid 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="DragonProduct", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("created_date", models.DateTimeField(auto_now_add=True)), 26 | ("modified_date", models.DateTimeField(auto_now=True)), 27 | ( 28 | "uuid", 29 | models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 30 | ), 31 | ("title", models.CharField(max_length=200)), 32 | ("description", models.TextField(blank=True, null=True)), 33 | ("price", models.DecimalField(decimal_places=2, max_digits=10)), 34 | ( 35 | "stars", 36 | models.IntegerField( 37 | choices=[ 38 | (1, "1 star"), 39 | (2, "2 stars"), 40 | (3, "3 stars"), 41 | (4, "4 stars"), 42 | (5, "5 stars"), 43 | ], 44 | default=0, 45 | ), 46 | ), 47 | ("image", models.ImageField(upload_to="images/products/dragon/")), 48 | ], 49 | options={ 50 | "abstract": False, 51 | }, 52 | ), 53 | migrations.CreateModel( 54 | name="HedgehogProduct", 55 | fields=[ 56 | ( 57 | "id", 58 | models.BigAutoField( 59 | auto_created=True, 60 | primary_key=True, 61 | serialize=False, 62 | verbose_name="ID", 63 | ), 64 | ), 65 | ("created_date", models.DateTimeField(auto_now_add=True)), 66 | ("modified_date", models.DateTimeField(auto_now=True)), 67 | ( 68 | "uuid", 69 | models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 70 | ), 71 | ("title", models.CharField(max_length=200)), 72 | ("description", models.TextField(blank=True, null=True)), 73 | ("price", models.DecimalField(decimal_places=2, max_digits=10)), 74 | ( 75 | "stars", 76 | models.IntegerField( 77 | choices=[ 78 | (1, "1 star"), 79 | (2, "2 stars"), 80 | (3, "3 stars"), 81 | (4, "4 stars"), 82 | (5, "5 stars"), 83 | ], 84 | default=0, 85 | ), 86 | ), 87 | ("image", models.ImageField(upload_to="images/products/hedgehog/")), 88 | ], 89 | options={ 90 | "abstract": False, 91 | }, 92 | ), 93 | migrations.CreateModel( 94 | name="WolfProduct", 95 | fields=[ 96 | ( 97 | "id", 98 | models.BigAutoField( 99 | auto_created=True, 100 | primary_key=True, 101 | serialize=False, 102 | verbose_name="ID", 103 | ), 104 | ), 105 | ("created_date", models.DateTimeField(auto_now_add=True)), 106 | ("modified_date", models.DateTimeField(auto_now=True)), 107 | ( 108 | "uuid", 109 | models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 110 | ), 111 | ("title", models.CharField(max_length=200)), 112 | ("description", models.TextField(blank=True, null=True)), 113 | ("price", models.DecimalField(decimal_places=2, max_digits=10)), 114 | ( 115 | "stars", 116 | models.IntegerField( 117 | choices=[ 118 | (1, "1 star"), 119 | (2, "2 stars"), 120 | (3, "3 stars"), 121 | (4, "4 stars"), 122 | (5, "5 stars"), 123 | ], 124 | default=0, 125 | ), 126 | ), 127 | ("image", models.ImageField(upload_to="images/products/wolf/")), 128 | ], 129 | options={ 130 | "abstract": False, 131 | }, 132 | ), 133 | ] 134 | --------------------------------------------------------------------------------