diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index cd6f855..5fbdebe 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..34e02c6 Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 9aa598b..c3611c7 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1f807fa..90287b6 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 6867ddf..cf8d99b 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..72f18b7 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,18 @@ from django.contrib import admin +from .models import Program, Exercise -# Register your models here. +class ExerciseInline(admin.TabularInline): + model = Exercise + extra = 1 + +@admin.register(Program) +class ProgramAdmin(admin.ModelAdmin): + list_display = ('patient_name', 'clinician_name', 'created_at', 'updated_at') + search_fields = ('patient_name', 'clinician_name') + inlines = [ExerciseInline] + +@admin.register(Exercise) +class ExerciseAdmin(admin.ModelAdmin): + list_display = ('title', 'program') + list_filter = ('program',) + search_fields = ('title', 'notes') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..cd37356 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,12 @@ +from django import forms +from .models import Exercise + +class ExerciseForm(forms.ModelForm): + class Meta: + model = Exercise + fields = ['title', 'description', 'video_url'] + widgets = { + 'title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Exercise Title'}), + 'description': forms.Textarea(attrs={'class': 'form-control', 'placeholder': 'Exercise Notes', 'rows': 3}), + 'video_url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'YouTube Video URL'}), + } diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..c24f079 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.7 on 2026-01-19 00:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Program', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('patient_name', models.CharField(max_length=255)), + ('patient_email', models.EmailField(max_length=254)), + ('clinician_name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Exercise', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('youtube_url', models.URLField()), + ('title', models.CharField(blank=True, max_length=255)), + ('thumbnail_url', models.URLField(blank=True)), + ('notes', models.TextField(blank=True)), + ('order', models.PositiveIntegerField(default=0)), + ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='core.program')), + ], + options={ + 'ordering': ['order'], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..3f0016e Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..655838c 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,55 @@ from django.db import models -# Create your models here. +class Program(models.Model): + patient_name = models.CharField(max_length=255) + patient_email = models.EmailField() + clinician_name = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Program for {self.patient_name} by {self.clinician_name}" + +class Exercise(models.Model): + + program = models.ForeignKey(Program, on_delete=models.CASCADE, related_name='exercises') + + title = models.CharField(max_length=200) + + description = models.TextField() + + video_url = models.URLField(blank=True, null=True) + + + + def __str__(self): + + return self.title + + + + def get_video_id(self): + + if self.video_url: + + if 'youtube.com/watch' in self.video_url: + + return self.video_url.split('v=')[1].split('&')[0] + + elif 'youtu.be/' in self.video_url: + + return self.video_url.split('/')[-1] + + return None + + + + def get_video_thumbnail(self): + + video_id = self.get_video_id() + + if video_id: + + return f'https://img.youtube.com/vi/{video_id}/default.jpg' + + return None diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..a655a5b 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,40 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Back Clinic Rehab Builder{% endblock %} + + + + + - - {% block content %}{% endblock %} - +
+ +
+
+ {% block content %}{% endblock %} +
+ + + + + diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..81ba90d 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,37 @@ -{% extends "base.html" %} +{% extends 'base.html' %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}Dashboard - {{ block.super }}{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

Rehabilitation Program Builder

+

Create, manage, and share personalized recovery programs for your patients.

+ Create New Program
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file + + +
+

Existing Programs

+
+ {% for program in programs %} +
+
+
+
{{ program.patient_name }}
+
Clinician: {{ program.clinician_name }}
+

Last updated: {{ program.updated_at|date:"d M Y" }}

+ View Program +
+
+
+ {% empty %} +
+ +
+ {% endfor %} +
+
+{% endblock %} diff --git a/core/templates/core/program_detail.html b/core/templates/core/program_detail.html new file mode 100644 index 0000000..c00ec79 --- /dev/null +++ b/core/templates/core/program_detail.html @@ -0,0 +1,63 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

Program Details

+ Download PDF +
+
+

Patient: {{ program.patient_name }}

+

Patient Email: {{ program.patient_email }}

+

Clinician: {{ program.clinician_name }}

+

Created: {{ program.created_at|date:"d M Y" }}

+
+
+ +
+
+

Exercises

+
+
+
+ {% for exercise in program.exercises.all %} +
+
+ {% if exercise.get_video_thumbnail %} + + {{ exercise.title }} + + {% endif %} +
+
{{ exercise.title }}
+

{{ exercise.description }}

+
+
+
+ {% empty %} +

No exercises have been added to this program yet.

+ {% endfor %} +
+
+
+
+
+
+
+

Add New Exercise

+
+
+
+ {% csrf_token %} + {{ exercise_form.as_p }} + +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/program_form.html b/core/templates/core/program_form.html new file mode 100644 index 0000000..6d2275d --- /dev/null +++ b/core/templates/core/program_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} + +{% block content %} +
+

Create New Rehabilitation Program

+
+ {% csrf_token %} + {{ form.as_p }} + +
+
+{% endblock %} diff --git a/core/templates/core/program_pdf.html b/core/templates/core/program_pdf.html new file mode 100644 index 0000000..bfbae09 --- /dev/null +++ b/core/templates/core/program_pdf.html @@ -0,0 +1,43 @@ + + + + Rehabilitation Program + + + +

Rehabilitation Program

+ +
+

Program Details

+

Patient: {{ program.patient_name }}

+

Clinician: {{ program.clinician_name }}

+

Date Created: {{ program.created_at|date:"F d, Y" }}

+
+ +
+

Exercises

+ +
+ + diff --git a/core/urls.py b/core/urls.py index 6299e3d..7f773b7 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,9 @@ from django.urls import path - -from .views import home +from .views import ProgramListView, ProgramCreateView, ProgramDetailView, ProgramPDFView urlpatterns = [ - path("", home, name="home"), -] + path('', ProgramListView.as_view(), name='program_list'), + path('programs/new/', ProgramCreateView.as_view(), name='program_create'), + path('programs//', ProgramDetailView.as_view(), name='program_detail'), + path('programs//pdf/', ProgramPDFView.as_view(), name='program_pdf'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..0614b8f 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,57 @@ -import os -import platform +from django.urls import reverse_lazy +from django.http import HttpResponse +from django.template.loader import get_template +from django.views import View +from xhtml2pdf import pisa -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone +from django.urls import reverse_lazy +from django.views.generic import CreateView, DetailView +from django.views.generic.list import ListView +from .models import Program, Exercise +from .forms import ExerciseForm +class ProgramListView(ListView): + model = Program + template_name = 'core/index.html' + context_object_name = 'programs' -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() +class ProgramCreateView(CreateView): + model = Program + template_name = 'core/program_form.html' + fields = ['patient_name', 'patient_email', 'clinician_name'] + + def get_success_url(self): + return reverse_lazy('program_detail', kwargs={'pk': self.object.pk}) - context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - } - return render(request, "core/index.html", context) +class ProgramDetailView(DetailView): + model = Program + template_name = 'core/program_detail.html' + context_object_name = 'program' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['exercise_form'] = ExerciseForm() + return context + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = ExerciseForm(request.POST) + if form.is_valid(): + exercise = form.save(commit=False) + exercise.program = self.object + exercise.save() + return self.render_to_response(self.get_context_data()) + return self.render_to_response(self.get_context_data(form=form)) + +class ProgramPDFView(View): + def get(self, request, *args, **kwargs): + program = Program.objects.get(pk=self.kwargs['pk']) + template = get_template('core/program_pdf.html') + context = {'program': program} + html = template.render(context) + response = HttpResponse(content_type='application/pdf') + response['Content-Disposition'] = f'attachment; filename="program_{program.pk}.pdf"' + pisa_status = pisa.CreatePDF(html, dest=response) + if pisa_status.err: + return HttpResponse('We had some errors
' + html + '
') + return response \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e22994c..fe74417 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +xhtml2pdf==0.2.11 +reportlab==3.6.12 diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..8993910 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,59 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +:root { + --primary-blue: #2A7F9E; + --accent-green: #4CAF50; + --neutral-gray: #F4F7F6; + --text-dark: #2D3748; + --font-headings: 'Poppins', sans-serif; + --font-body: 'Roboto', sans-serif; } + +body { + font-family: var(--font-body); + color: var(--text-dark); + background-color: #FBFCFE; +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-headings); + font-weight: 600; +} + +.navbar-brand { + font-family: var(--font-headings); + font-weight: 600; + color: var(--primary-blue) !important; +} + +.btn-primary { + background-color: var(--primary-blue); + border-color: var(--primary-blue); +} + +.btn-primary:hover { + background-color: #246B86; + border-color: #246B86; +} + +.btn-secondary { + background-color: var(--accent-green); + border-color: var(--accent-green); +} + +.btn-secondary:hover { + background-color: #45A049; + border-color: #45A049; +} + +.hero { + background: linear-gradient(135deg, var(--primary-blue), #5eb3d1); + color: white; +} + +.program-card { + border: 1px solid #e2e8f0; + transition: box-shadow .3s ease-in-out; +} + +.program-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} \ No newline at end of file diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..8993910 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,59 @@ - :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --primary-blue: #2A7F9E; + --accent-green: #4CAF50; + --neutral-gray: #F4F7F6; + --text-dark: #2D3748; + --font-headings: 'Poppins', sans-serif; + --font-body: 'Roboto', sans-serif; } + body { - margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + font-family: var(--font-body); + color: var(--text-dark); + background-color: #FBFCFE; } + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-headings); + font-weight: 600; +} + +.navbar-brand { + font-family: var(--font-headings); + font-weight: 600; + color: var(--primary-blue) !important; +} + +.btn-primary { + background-color: var(--primary-blue); + border-color: var(--primary-blue); +} + +.btn-primary:hover { + background-color: #246B86; + border-color: #246B86; +} + +.btn-secondary { + background-color: var(--accent-green); + border-color: var(--accent-green); +} + +.btn-secondary:hover { + background-color: #45A049; + border-color: #45A049; +} + +.hero { + background: linear-gradient(135deg, var(--primary-blue), #5eb3d1); + color: white; +} + +.program-card { + border: 1px solid #e2e8f0; + transition: box-shadow .3s ease-in-out; +} + +.program-card:hover { + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} \ No newline at end of file