Added agents app and compose tweaks
This commit is contained in:
parent
3c6e5e091c
commit
ddcbfbfdd8
20 changed files with 474 additions and 9 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -49,3 +49,7 @@ vitest.config.*.timestamp*
|
||||||
|
|
||||||
.env
|
.env
|
||||||
static
|
static
|
||||||
|
.github
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
*.sqlite3
|
||||||
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/agents/__init__.py
Normal file
0
apps/agents/__init__.py
Normal file
6
apps/agents/apps.py
Normal file
6
apps/agents/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AgentsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.agents'
|
||||||
29
apps/agents/langgraph_adapter.py
Normal file
29
apps/agents/langgraph_adapter.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .llm import get_llm_for_domain
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleAgent:
|
||||||
|
"""Minimal agent abstraction that calls a local LLM and returns responses."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, domain: str, system_message: str | None = None):
|
||||||
|
self.name = name
|
||||||
|
self.domain = domain
|
||||||
|
self.system_message = system_message or "You are an assistant."
|
||||||
|
self._llm = get_llm_for_domain(domain)
|
||||||
|
|
||||||
|
def run(self, prompt: str, **kwargs: Any) -> str:
|
||||||
|
full_prompt = f"{self.system_message}\n\nUser: {prompt}"
|
||||||
|
logger.debug("Agent %s running prompt: %s", self.name, prompt)
|
||||||
|
return self._llm.generate(full_prompt)
|
||||||
|
|
||||||
|
|
||||||
|
def build_agents_for_domains(domains: list[str]) -> dict[str, SimpleAgent]:
|
||||||
|
agents = {}
|
||||||
|
for d in domains:
|
||||||
|
agents[d] = SimpleAgent(name=f"agent-{d}", domain=d, system_message=f"You are a tutor for {d}.")
|
||||||
|
return agents
|
||||||
63
apps/agents/llm.py
Normal file
63
apps/agents/llm.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
"""Lightweight local LLM wrappers.
|
||||||
|
|
||||||
|
This file provides simple wrappers for `llama_cpp` and `transformers` backends.
|
||||||
|
They are intentionally minimal — adapt to your runtime and model formats.
|
||||||
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseLLM:
|
||||||
|
def generate(self, prompt: str) -> str:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class LlamaCPPWrapper(BaseLLM):
|
||||||
|
def __init__(self, model_path: str):
|
||||||
|
try:
|
||||||
|
from llama_cpp import Llama
|
||||||
|
|
||||||
|
self._llm = Llama(model_path=model_path)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("llama_cpp is unavailable or model failed to load")
|
||||||
|
self._llm = None
|
||||||
|
|
||||||
|
def generate(self, prompt: str) -> str:
|
||||||
|
if self._llm is None:
|
||||||
|
raise RuntimeError("Llama model not available")
|
||||||
|
resp = self._llm(prompt)
|
||||||
|
return resp.get("text") if isinstance(resp, dict) else str(resp)
|
||||||
|
|
||||||
|
|
||||||
|
class TransformersWrapper(BaseLLM):
|
||||||
|
def __init__(self, model_name_or_path: str):
|
||||||
|
try:
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||||
|
import torch
|
||||||
|
|
||||||
|
self.tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)
|
||||||
|
self.model = AutoModelForCausalLM.from_pretrained(model_name_or_path, torch_dtype=torch.float16, device_map="auto")
|
||||||
|
except Exception:
|
||||||
|
logger.exception("transformers not available or model failed to load")
|
||||||
|
self.model = None
|
||||||
|
self.tokenizer = None
|
||||||
|
|
||||||
|
def generate(self, prompt: str) -> str:
|
||||||
|
if self.model is None or self.tokenizer is None:
|
||||||
|
raise RuntimeError("Transformers model not available")
|
||||||
|
inputs = self.tokenizer(prompt, return_tensors="pt")
|
||||||
|
outputs = self.model.generate(**inputs, max_new_tokens=256)
|
||||||
|
return self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_llm_for_domain(domain: str, prefer: str | None = None) -> BaseLLM:
|
||||||
|
# Basic loader: choose Llama (gguf) if file exists, else fall back to transformers
|
||||||
|
model_dir = "models" / domain
|
||||||
|
gguf = model_dir / "model.gguf"
|
||||||
|
if gguf.exists():
|
||||||
|
return LlamaCPPWrapper(str(gguf))
|
||||||
|
# fallback: try transformers
|
||||||
|
return TransformersWrapper(str(model_dir))
|
||||||
22
apps/agents/models.py
Normal file
22
apps/agents/models.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class AgentRun(models.Model):
|
||||||
|
agent_name = models.CharField(max_length=255)
|
||||||
|
input_text = models.TextField()
|
||||||
|
output_text = models.TextField(blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class ModelMeta(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
path = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
framework = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
def __str__(self) -> str: # pragma: no cover - trivial
|
||||||
|
return self.name
|
||||||
16
apps/agents/service.py
Normal file
16
apps/agents/service.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .models import AgentRun
|
||||||
|
from .langgraph_adapter import SimpleAgent
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_agent(agent: SimpleAgent, prompt: str) -> str:
|
||||||
|
"""Run the agent and store an AgentRun record using the Django ORM."""
|
||||||
|
out = agent.run(prompt)
|
||||||
|
try:
|
||||||
|
AgentRun.objects.create(agent_name=agent.name, input_text=prompt, output_text=out)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to persist agent run via Django ORM")
|
||||||
|
return out
|
||||||
|
|
@ -3,4 +3,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
class DomainsConfig(AppConfig):
|
class DomainsConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'domains'
|
name = 'apps.domains'
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||||
|
from .models import UserProfile, User
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
@admin.register(UserProfile)
|
||||||
|
class UserProfileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'display_name', 'created_at')
|
||||||
|
search_fields = ('user__username', 'display_name')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(DjangoUserAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('username', 'password')}),
|
||||||
|
('Personal info', {'fields': ('first_name', 'last_name', 'email_address')}),
|
||||||
|
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
|
||||||
|
('Important dates', {'fields': ('last_login', 'password_reset_at')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('username', 'email_address', 'first_name', 'last_name', 'password1', 'password2'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
list_display = ('username', 'email_address', 'first_name', 'last_name', 'is_staff')
|
||||||
|
search_fields = ('username', 'email_address', 'first_name', 'last_name')
|
||||||
|
ordering = ('username',)
|
||||||
|
|
|
||||||
41
apps/users/managers.py
Normal file
41
apps/users/managers.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.contrib.auth.models import BaseUserManager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from apps.users.models import User # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(BaseUserManager["User"]):
|
||||||
|
"""Custom manager for the User model."""
|
||||||
|
|
||||||
|
def _create_user(self, email_address: str, password: str | None, **extra_fields):
|
||||||
|
"""
|
||||||
|
Create and save a user with the given email and password.
|
||||||
|
"""
|
||||||
|
if not email_address:
|
||||||
|
msg = "The given email must be set"
|
||||||
|
raise ValueError(msg)
|
||||||
|
email_address = self.normalize_email(email_address)
|
||||||
|
user: User = self.model(email_address=email_address, **extra_fields)
|
||||||
|
user.password = make_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_user(self, email_address: str, password: str | None = None, **extra_fields): # type: ignore[override]
|
||||||
|
"""
|
||||||
|
Create and save a regular user with the given email and password.
|
||||||
|
"""
|
||||||
|
extra_fields.setdefault("is_staff", False)
|
||||||
|
return self._create_user(email_address, password, **extra_fields)
|
||||||
|
|
||||||
|
def create_superuser(self, email_address: str, password: str | None = None, **extra_fields): # type: ignore[override]
|
||||||
|
"""
|
||||||
|
Create and save a superuser with the given email and password.
|
||||||
|
"""
|
||||||
|
extra_fields.setdefault("is_staff", True)
|
||||||
|
if extra_fields.get("is_staff") is not True:
|
||||||
|
msg = "Superuser must have is_staff=True."
|
||||||
|
raise ValueError(msg)
|
||||||
|
return self._create_user(email_address, password, **extra_fields)
|
||||||
61
apps/users/migrations/0001_initial.py
Normal file
61
apps/users/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Generated by Django 5.2.8 on 2025-11-18 23:09
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('user_uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='User UUID')),
|
||||||
|
('email_address', models.EmailField(max_length=255, unique=True, verbose_name='Email Address')),
|
||||||
|
('username', models.CharField(max_length=25, unique=True, verbose_name='Username')),
|
||||||
|
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||||
|
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||||
|
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Account Active')),
|
||||||
|
('is_staff', models.BooleanField(default=False, verbose_name='Account Admin')),
|
||||||
|
('is_verified', models.BooleanField(default=False, verbose_name='Account Verified')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User',
|
||||||
|
'verbose_name_plural': 'Users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('display_name', models.CharField(blank=True, max_length=150)),
|
||||||
|
('bio', models.TextField(blank=True)),
|
||||||
|
('timezone', models.CharField(blank=True, max_length=64)),
|
||||||
|
('avatar_url', models.URLField(blank=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='user',
|
||||||
|
constraint=models.UniqueConstraint(fields=('username',), name='unique_username'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,108 @@
|
||||||
from django.db import models
|
from typing import ClassVar
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
# Create your models here.
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
|
from django.db.models import (
|
||||||
|
AutoField,
|
||||||
|
BooleanField,
|
||||||
|
CASCADE,
|
||||||
|
CharField,
|
||||||
|
DateField,
|
||||||
|
DateTimeField,
|
||||||
|
EmailField,
|
||||||
|
UUIDField,
|
||||||
|
Model,
|
||||||
|
OneToOneField,
|
||||||
|
TextField,
|
||||||
|
UniqueConstraint,
|
||||||
|
URLField,
|
||||||
|
)
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from apps.users.managers import UserManager
|
||||||
|
|
||||||
|
|
||||||
|
class TimeStampMixin(Model):
|
||||||
|
"""Abstract model that provides created_at and updated_at fields."""
|
||||||
|
|
||||||
|
created_at = DateTimeField(verbose_name="Created At", auto_now_add=True)
|
||||||
|
updated_at = DateTimeField(verbose_name="Updated At", auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractBaseUser, TimeStampMixin, PermissionsMixin):
|
||||||
|
"""Default custom user model for viswamedha.com (adapted).
|
||||||
|
|
||||||
|
Uses username as the USERNAME_FIELD and maintains an email_address
|
||||||
|
field used as the primary contact address.
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = AutoField(primary_key=True)
|
||||||
|
user_uuid = UUIDField(verbose_name=_("User UUID"), default=uuid4, editable=False)
|
||||||
|
|
||||||
|
# Required fields
|
||||||
|
email_address = EmailField(verbose_name=_("Email Address"), max_length=255, unique=True)
|
||||||
|
username = CharField(verbose_name=_("Username"), max_length=25, unique=True)
|
||||||
|
first_name = CharField(verbose_name=_("First Name"), max_length=255)
|
||||||
|
last_name = CharField(verbose_name=_("Last Name"), max_length=255)
|
||||||
|
date_of_birth = DateField(verbose_name=_("Date of Birth"), null=True, blank=True)
|
||||||
|
|
||||||
|
is_active = BooleanField(verbose_name=_("Account Active"), default=True)
|
||||||
|
is_staff = BooleanField(verbose_name=_("Account Admin"), default=False)
|
||||||
|
is_verified = BooleanField(verbose_name=_("Account Verified"), default=False)
|
||||||
|
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'username'
|
||||||
|
EMAIL_FIELD = 'email_address'
|
||||||
|
REQUIRED_FIELDS = ['email_address', 'first_name', 'last_name', 'date_of_birth']
|
||||||
|
|
||||||
|
objects: ClassVar[UserManager] = UserManager()
|
||||||
|
|
||||||
|
def has_perm(self, perm, obj=None):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_module_perms(self, app_label):
|
||||||
|
return True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('User')
|
||||||
|
verbose_name_plural = _('Users')
|
||||||
|
constraints = [
|
||||||
|
UniqueConstraint(fields=['username'], name='unique_username'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self):
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_student(self):
|
||||||
|
return hasattr(self, 'student')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.full_name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.username:
|
||||||
|
self.username = self.username.lower()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(Model):
|
||||||
|
"""A lightweight profile attached to the project's `AUTH_USER_MODEL`."""
|
||||||
|
|
||||||
|
user = OneToOneField(
|
||||||
|
settings.AUTH_USER_MODEL, on_delete=CASCADE, related_name='profile'
|
||||||
|
)
|
||||||
|
display_name = CharField(max_length=150, blank=True)
|
||||||
|
bio = TextField(blank=True)
|
||||||
|
timezone = CharField(max_length=64, blank=True)
|
||||||
|
avatar_url = URLField(blank=True)
|
||||||
|
created_at = DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self) -> str: # pragma: no cover - trivial
|
||||||
|
return self.display_name or getattr(self.user, 'username', str(self.user))
|
||||||
|
|
|
||||||
20
apps/users/serializers.py
Normal file
20
apps/users/serializers.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from apps.users.models import UserProfile
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UserProfile
|
||||||
|
fields = ['display_name', 'bio', 'timezone', 'avatar_url']
|
||||||
|
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer):
|
||||||
|
profile = UserProfileSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'user_uuid', 'username', 'email_address', 'first_name', 'last_name', 'profile']
|
||||||
|
|
@ -1,3 +1,56 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserModelTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user_data = {
|
||||||
|
'email_address': 'Test@Example.com',
|
||||||
|
'username': 'TestUser',
|
||||||
|
'first_name': 'Test',
|
||||||
|
'last_name': 'User',
|
||||||
|
'date_of_birth': '1990-01-01',
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create_user_and_properties(self):
|
||||||
|
user = User.objects.create_user(password='pass1234', **self.user_data)
|
||||||
|
self.assertIsNotNone(user.pk)
|
||||||
|
# email should be normalized by the manager
|
||||||
|
self.assertEqual(user.email_address.lower(), 'test@example.com')
|
||||||
|
# username should be saved lowercase by model.save()
|
||||||
|
self.assertEqual(user.username, 'testuser')
|
||||||
|
# full_name property
|
||||||
|
self.assertEqual(user.full_name, 'Test User')
|
||||||
|
|
||||||
|
def test_create_superuser(self):
|
||||||
|
su = User.objects.create_superuser(password='adminpass', **self.user_data)
|
||||||
|
self.assertTrue(su.is_staff)
|
||||||
|
self.assertTrue(su.pk)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAPITests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = APIClient()
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
password='pass1234',
|
||||||
|
email_address='apiuser@example.com',
|
||||||
|
username='apiuser',
|
||||||
|
first_name='API',
|
||||||
|
last_name='User',
|
||||||
|
date_of_birth='1995-05-05',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_list_users(self):
|
||||||
|
url = '/api/users/'
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||||
|
# Should contain at least the created user
|
||||||
|
usernames = [u.get('username') for u in resp.json()]
|
||||||
|
self.assertIn(self.user.username, usernames)
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
|
||||||
16
apps/users/viewsets.py
Normal file
16
apps/users/viewsets.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from .serializers import UserSerializer
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""Read-only viewset exposing users for the POC."""
|
||||||
|
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
@ -31,9 +31,6 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --omit=dev
|
|
||||||
|
|
||||||
COPY --from=node /app/build ./build
|
COPY --from=node /app/build ./build
|
||||||
|
|
||||||
COPY ./compose/prod/start /start
|
COPY ./compose/prod/start /start
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@ services:
|
||||||
deploy:
|
deploy:
|
||||||
mode: replicated
|
mode: replicated
|
||||||
replicas: ${REPLICAS}
|
replicas: ${REPLICAS}
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.fyp-web.rule=Host(`${DOMAIN}`)"
|
- "traefik.http.routers.fyp-web.rule=Host(`${DOMAIN}`)"
|
||||||
- "traefik.http.routers.fyp-web.entrypoints=${ENTRYPOINT}"
|
- "traefik.http.routers.fyp-web.entrypoints=${ENTRYPOINT}"
|
||||||
- "traefik.http.routers.fyp-web.tls.certresolver=${CERTRESOLVER}"
|
- "traefik.http.routers.fyp-web.tls.certresolver=${CERTRESOLVER}"
|
||||||
|
- "traefik.http.services.fyp-web.loadbalancer.server.port=${PORT}"
|
||||||
- "com.centurylinklabs.watchtower.enable=true"
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,7 @@ set -o errexit
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
set -o nounset
|
set -o nounset
|
||||||
|
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate
|
||||||
python manage.py collectstatic --noinput
|
python manage.py collectstatic --noinput
|
||||||
exec /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:8000
|
exec /usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:8000
|
||||||
|
|
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Loading…
Reference in a new issue