Production latest code push
This commit is contained in:
commit
2fc5bb1b21
30
.env
Normal file
30
.env
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Database Configuration
|
||||||
|
POSTGRES_DB=polisplexity
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=mysecretpassword
|
||||||
|
|
||||||
|
# Django Environment Variables
|
||||||
|
DEBUG=True
|
||||||
|
SECRET_KEY=django-insecure-%*=%u3gv38cv*2iwy)m^)flo3p4w7ol*n5*-7lr*i4^u+(v=#q
|
||||||
|
ALLOWED_HOSTS=127.0.0.1,localhost,app.polisplexity.tech,191.101.233.39
|
||||||
|
|
||||||
|
# Database URL
|
||||||
|
DATABASE_URL=postgres://postgres:mysecretpassword@db:5432/polisplexity
|
||||||
|
|
||||||
|
# Static and Media Files
|
||||||
|
STATIC_URL=/static/
|
||||||
|
STATIC_ROOT=/app/static
|
||||||
|
MEDIA_URL=/media/
|
||||||
|
MEDIA_ROOT=/app/media
|
||||||
|
|
||||||
|
# Neo4j Database Configuration
|
||||||
|
NEO4J_URI=neo4j+s://74d433fb.databases.neo4j.io
|
||||||
|
NEO4J_USERNAME=neo4j
|
||||||
|
NEO4J_PASSWORD=4Y5-ppefHkgEiLr-l0qzbf8wNJw0zkOmRmk7cSkSrTg
|
||||||
|
|
||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
|
# Facebook API Tokens
|
||||||
|
PAGE_ACCESS_TOKEN=EAAIq9z4rVPIBOxJxRnmbjIUsqJ9ZB5hZC9MF4qN64VNpxUCYguMCqUNKSsAjQZAcD9hlhZCv2RcV4GOIFC3Ni6VGoMp3rTFlLwtXxFIklj0FqZAVqSh7i0QT3Kwt9SCx9V9iioSsyFhUQrnpTXZCoDPJy0i2kMkzkY5ZA58hieSeQZBZARz3ZC7XeZCi5uSZBXYCeatGuAZDZD
|
||||||
|
VERIFY_TOKEN=YzQ2VWcODWO922j30HZ9AV113kAisTjcacc3wzURPvFjHCOWjcYP39ThgCWlPQ1w
|
0
.env:Zone.Identifier
Normal file
0
.env:Zone.Identifier
Normal file
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Use official Python image
|
||||||
|
FROM python:3.10
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Run Gunicorn server
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "polisplexity.wsgi:application"]
|
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
BIN
core/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/admin.cpython-310.pyc
Normal file
BIN
core/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/apps.cpython-310.pyc
Normal file
BIN
core/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/models.cpython-310.pyc
Normal file
BIN
core/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/urls.cpython-310.pyc
Normal file
BIN
core/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/views.cpython-310.pyc
Normal file
BIN
core/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
5
core/admin.py
Normal file
5
core/admin.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Category, UserMenu
|
||||||
|
|
||||||
|
admin.site.register(Category)
|
||||||
|
admin.site.register(UserMenu)
|
6
core/apps.py
Normal file
6
core/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "core"
|
29
core/migrations/0001_initial.py
Normal file
29
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-03-16 20:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Category",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
51
core/migrations/0002_usermenu.py
Normal file
51
core/migrations/0002_usermenu.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-03-16 20:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("core", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserMenu",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=100)),
|
||||||
|
("order", models.IntegerField(default=0)),
|
||||||
|
("description", models.TextField()),
|
||||||
|
("icon", models.CharField(blank=True, max_length=50, null=True)),
|
||||||
|
(
|
||||||
|
"image",
|
||||||
|
models.ImageField(
|
||||||
|
blank=True, null=True, upload_to="media/images/menu/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("url", models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
("url_text", models.CharField(blank=True, max_length=50, null=True)),
|
||||||
|
(
|
||||||
|
"category",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="user_menus",
|
||||||
|
to="core.category",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["category", "order"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
core/migrations/__init__.py
Normal file
0
core/migrations/__init__.py
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0002_usermenu.cpython-310.pyc
Normal file
BIN
core/migrations/__pycache__/0002_usermenu.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
core/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
28
core/models.py
Normal file
28
core/models.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class Category(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class UserMenu(models.Model):
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='user_menus') # Assuming each menu belongs to one category
|
||||||
|
order = models.IntegerField(default=0)
|
||||||
|
description = models.TextField()
|
||||||
|
icon = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
image = models.ImageField(upload_to='media/images/menu/', null=True, blank=True) # Changed 'media/images/' to 'user_menu_images/' for clarity
|
||||||
|
url = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
url_text = models.CharField(max_length=50, blank=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['category', 'order'] # Orders by category first, then by the order field within each category
|
||||||
|
|
||||||
|
|
66
core/templates/core/index.html
Normal file
66
core/templates/core/index.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Polisplexity Portal{% endblock title %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.card-img-top {
|
||||||
|
height: 200px; /* Fixed height for consistency */
|
||||||
|
object-fit: cover; /* Ensures image covers the area nicely */
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: scale(1.05); /* Slight zoom effect on hover for interactivity */
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock extra_css %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2 class="text-center mb-4">Welcome to Polisplexity</h2>
|
||||||
|
<p class="text-center mb-4">Select one of the options Bellow</p>
|
||||||
|
|
||||||
|
{% for category, items in grouped_menu_items.items %}
|
||||||
|
{% if not forloop.first %}
|
||||||
|
<hr> <!-- Horizontal separator for every new category except the first one -->
|
||||||
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-3 mb-4">
|
||||||
|
<h3> {{ category.name }}</h3>
|
||||||
|
<small> {{ category.description }}</small>
|
||||||
|
</div>
|
||||||
|
{% for menu_item in items %}
|
||||||
|
<div class="col-12 col-md-3 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
{% if menu_item.image %}
|
||||||
|
<!-- Display Image if available -->
|
||||||
|
<img src="{{ menu_item.image.url }}" class="card-img-top" alt="{{ menu_item.title }}">
|
||||||
|
{% elif menu_item.icon %}
|
||||||
|
<!-- Display Icon if image is not available but icon is -->
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<span class="material-symbols-rounded md-48">{{ menu_item.icon }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ menu_item.title }}</h5>
|
||||||
|
<p class="card-text">{{ menu_item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white">
|
||||||
|
<a href="{{ menu_item.url }}" class="btn btn-primary">{{ menu_item.url_text }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$('[data-bs-toggle="tooltip"]').tooltip() // Initialize Bootstrap tooltips
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock extra_js %}
|
3
core/tests.py
Normal file
3
core/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
6
core/urls.py
Normal file
6
core/urls.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import index
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', index, name='index'),
|
||||||
|
]
|
11
core/views.py
Normal file
11
core/views.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from .models import UserMenu
|
||||||
|
from itertools import groupby
|
||||||
|
|
||||||
|
|
||||||
|
def index(request):
|
||||||
|
menu_items = UserMenu.objects.all().order_by('category', 'order')
|
||||||
|
# Group by category
|
||||||
|
grouped_menu_items = {k: list(v) for k, v in groupby(menu_items, lambda x: x.category)}
|
||||||
|
return render(request, 'core/index.html', {'grouped_menu_items': grouped_menu_items})
|
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
39
docker-compose.yml
Normal file
39
docker-compose.yml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: polisplexity_postgres
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "5434:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
retries: 5
|
||||||
|
timeout: 5s
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: polisplexity_django
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- .:/app # Ensure correct project structure
|
||||||
|
ports:
|
||||||
|
- "8010:8001"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
command: >
|
||||||
|
sh -c "python manage.py migrate &&
|
||||||
|
python manage.py collectstatic --noinput &&
|
||||||
|
exec gunicorn --bind 0.0.0.0:8001 polisplexity.wsgi:application"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
22
manage.py
Normal file
22
manage.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polisplexity.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
BIN
media/images/logos/logo_U4MX.png
Normal file
BIN
media/images/logos/logo_U4MX.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
media/media/images/menu/01_Forms_image.webp
Normal file
BIN
media/media/images/menu/01_Forms_image.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 708 KiB |
11
pgloader.load
Normal file
11
pgloader.load
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
LOAD DATABASE
|
||||||
|
FROM sqlite:///app/db.sqlite3
|
||||||
|
INTO postgresql://postgres:mysecretpassword@localhost:5434/polisplexity
|
||||||
|
|
||||||
|
WITH include no drop, create tables, create indexes, reset sequences
|
||||||
|
|
||||||
|
SET work_mem to '16MB', maintenance_work_mem to '512 MB'
|
||||||
|
|
||||||
|
ALTER SCHEMA 'main' RENAME TO 'public'
|
||||||
|
;
|
||||||
|
|
0
polisplexity/__init__.py
Normal file
0
polisplexity/__init__.py
Normal file
BIN
polisplexity/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
polisplexity/__pycache__/settings.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
polisplexity/__pycache__/urls.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
polisplexity/__pycache__/wsgi.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/wsgi.cpython-310.pyc
Normal file
Binary file not shown.
16
polisplexity/asgi.py
Normal file
16
polisplexity/asgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for polisplexity project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polisplexity.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
127
polisplexity/settings.py
Normal file
127
polisplexity/settings.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
Django settings for polisplexity project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.0.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import dj_database_url
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
BASE_URL = "https://app.polisplexity.tech"
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv("DEBUG") == "True"
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"core",
|
||||||
|
"pxy_de",
|
||||||
|
"pxy_cr",
|
||||||
|
"pxy_whatsapp",
|
||||||
|
"pxy_city_digital_twins",
|
||||||
|
"pxy_bots",
|
||||||
|
"pxy_openai",
|
||||||
|
"pxy_meta_pages",
|
||||||
|
"pxy_langchain",
|
||||||
|
"pxy_neo4j",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "polisplexity.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "polisplexity.wsgi.application"
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DATABASES = {
|
||||||
|
"default": dj_database_url.config(default=os.getenv("DATABASE_URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
|
||||||
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
STATIC_URL = "/static/"
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "static") # Ensure this line is correct
|
||||||
|
|
||||||
|
# Add this if missing
|
||||||
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # Fixes mixed content issues
|
||||||
|
CSRF_TRUSTED_ORIGINS = ['https://app.polisplexity.tech'] # Allow CSRF over HTTPS
|
||||||
|
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
MEDIA_ROOT = os.getenv("MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
# Facebook API Tokens
|
||||||
|
PAGE_ACCESS_TOKEN = os.getenv("PAGE_ACCESS_TOKEN")
|
||||||
|
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
|
||||||
|
|
||||||
|
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||||
|
|
||||||
|
# Neo4j Database Configuration
|
||||||
|
NEO4J_URI = os.getenv("NEO4J_URI")
|
||||||
|
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
|
||||||
|
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
|
||||||
|
|
||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
36
polisplexity/urls.py
Normal file
36
polisplexity/urls.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for polisplexity project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
admin.site.site_header = "Polisplexity City Technologies"
|
||||||
|
admin.site.site_title = "Polisplexity Admin Portal"
|
||||||
|
admin.site.index_title = "Welcome to Polisplexity City Technologies Portal"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path('', include('core.urls')),
|
||||||
|
path('', include('pxy_city_digital_twins.urls')),
|
||||||
|
path('pxy_whatsapp/', include('pxy_whatsapp.urls')),
|
||||||
|
path('bots/', include('pxy_bots.urls')), # Webhook URL: /bots/webhook/<bot_name>/
|
||||||
|
path('pxy_meta_pages/', include('pxy_meta_pages.urls', namespace='pxy_meta_pages')),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
16
polisplexity/wsgi.py
Normal file
16
polisplexity/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for polisplexity project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "polisplexity.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
2149
polisplexity_dump.sql
Normal file
2149
polisplexity_dump.sql
Normal file
File diff suppressed because it is too large
Load Diff
0
pxy_bots/__init__.py
Normal file
0
pxy_bots/__init__.py
Normal file
BIN
pxy_bots/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/admin.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/apps.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/handlers.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/handlers.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/models.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/urls.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/utils.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/views.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
52
pxy_bots/admin.py
Normal file
52
pxy_bots/admin.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import TelegramBot
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
|
@admin.register(TelegramBot)
|
||||||
|
class TelegramBotAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "username", "is_active", "get_assistant_name", "set_webhook_action")
|
||||||
|
search_fields = ("name", "username")
|
||||||
|
list_filter = ("is_active",)
|
||||||
|
actions = ["set_webhooks"]
|
||||||
|
|
||||||
|
@admin.action(description="Set webhooks for selected bots")
|
||||||
|
def set_webhooks(self, request, queryset):
|
||||||
|
base_url = request.build_absolute_uri("/")[:-1] # Get base server URL
|
||||||
|
results = []
|
||||||
|
for bot in queryset:
|
||||||
|
if bot.is_active:
|
||||||
|
try:
|
||||||
|
result = bot.set_webhook(base_url)
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Webhook set for {bot.name}: {result}",
|
||||||
|
level="success",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Failed to set webhook for {bot.name}: {str(e)}",
|
||||||
|
level="error",
|
||||||
|
)
|
||||||
|
results.append(result)
|
||||||
|
else:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"Skipped inactive bot: {bot.name}",
|
||||||
|
level="warning",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_assistant_name(self, obj):
|
||||||
|
"""Show the name of the assistant linked to the bot."""
|
||||||
|
return obj.assistant.name if obj.assistant else "None"
|
||||||
|
|
||||||
|
get_assistant_name.short_description = "Assistant Name"
|
||||||
|
|
||||||
|
def set_webhook_action(self, obj):
|
||||||
|
"""Button in the Django admin to manually trigger webhook setup."""
|
||||||
|
return format_html(
|
||||||
|
'<a class="button" href="{}">Set Webhook</a>',
|
||||||
|
f"/admin/pxy_bots/set_webhook/{obj.id}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
set_webhook_action.short_description = "Webhook"
|
6
pxy_bots/apps.py
Normal file
6
pxy_bots/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PxyBotsConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "pxy_bots"
|
73
pxy_bots/handlers.py
Normal file
73
pxy_bots/handlers.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
from telegram import Update, ForceReply
|
||||||
|
import logging
|
||||||
|
from pxy_openai.assistants import OpenAIAssistant
|
||||||
|
from .models import TelegramBot
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
from pxy_langchain.models import AIAssistant
|
||||||
|
from pxy_langchain.services import LangchainAIService
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def dream_city_command(update: Update):
|
||||||
|
"""Send a message with a link to the random city generator."""
|
||||||
|
message = (
|
||||||
|
"Descubre la ciudad de tus sueños! Haz clic en el enlace para explorar una ciudad generada aleatoriamente que podría reflejar tus aspiraciones urbanas: "
|
||||||
|
"https://app.polisplexity.tech/city/digital/twin/dream/?innovation=30&technology=30&science=40"
|
||||||
|
)
|
||||||
|
await update.message.reply_text(message)
|
||||||
|
|
||||||
|
|
||||||
|
async def start(update: Update):
|
||||||
|
"""Send a message when the command /start is issued."""
|
||||||
|
user = update.effective_user
|
||||||
|
await update.message.reply_html(
|
||||||
|
rf"Hi {user.mention_html()}!",
|
||||||
|
reply_markup=ForceReply(selective=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def help_command(update: Update):
|
||||||
|
"""Send a message when the command /help is issued."""
|
||||||
|
user = update.effective_user
|
||||||
|
await update.message.reply_text(f"Help! How can I assist you, {user.first_name}?")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_location(update: Update):
|
||||||
|
"""Respond to a location message."""
|
||||||
|
location = update.message.location
|
||||||
|
if location:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"Thanks for sharing your location! Latitude: {location.latitude}, Longitude: {location.longitude}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text("Please share your location.")
|
||||||
|
|
||||||
|
|
||||||
|
async def respond(update, bot_name):
|
||||||
|
"""Respond to user messages using the LangChain AI service."""
|
||||||
|
try:
|
||||||
|
user_message = update.message.text
|
||||||
|
|
||||||
|
# Fetch the Telegram bot and its assigned AI assistant asynchronously
|
||||||
|
telegram_bot = await sync_to_async(TelegramBot.objects.get)(name=bot_name, is_active=True)
|
||||||
|
|
||||||
|
if not telegram_bot.assistant:
|
||||||
|
raise ValueError(f"No assistant configured for bot '{bot_name}'.")
|
||||||
|
|
||||||
|
# Fetch the AI assistant linked to this bot
|
||||||
|
assistant = await sync_to_async(AIAssistant.objects.get)(name=telegram_bot.assistant.name)
|
||||||
|
|
||||||
|
# Initialize AI service
|
||||||
|
ai_service = LangchainAIService(assistant)
|
||||||
|
|
||||||
|
# Generate response asynchronously
|
||||||
|
bot_response = await sync_to_async(ai_service.generate_response)(user_message)
|
||||||
|
|
||||||
|
await update.message.reply_text(bot_response)
|
||||||
|
|
||||||
|
except TelegramBot.DoesNotExist:
|
||||||
|
await update.message.reply_text(f"Bot '{bot_name}' not found or inactive.")
|
||||||
|
except Exception as e:
|
||||||
|
await update.message.reply_text(f"Oops! Something went wrong: {e}")
|
24
pxy_bots/migrations/0001_initial.py
Normal file
24
pxy_bots/migrations/0001_initial.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 4.0.6 on 2025-01-16 12:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TelegramBot',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text="Name of the bot (e.g., 'SupportBot').", max_length=50, unique=True)),
|
||||||
|
('username', models.CharField(help_text="The bot's Telegram username (e.g., 'SupportBot').", max_length=50, unique=True)),
|
||||||
|
('token', models.CharField(help_text='Telegram Bot API token.', max_length=200, unique=True)),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Is this bot active?')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2025-01-16 19:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pxy_bots", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="is_active",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=True, help_text="Indicates if this bot is active."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(
|
||||||
|
help_text="Bot name (e.g., 'SupportBot').", max_length=50, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="token",
|
||||||
|
field=models.CharField(
|
||||||
|
help_text="Telegram bot token.", max_length=200, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="username",
|
||||||
|
field=models.CharField(
|
||||||
|
help_text="Bot username (e.g., 'SupportBot').",
|
||||||
|
max_length=50,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
30
pxy_bots/migrations/0003_telegrambot_assistant.py
Normal file
30
pxy_bots/migrations/0003_telegrambot_assistant.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2025-01-28 21:47
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"pxy_bots",
|
||||||
|
"0002_alter_telegrambot_is_active_alter_telegrambot_name_and_more",
|
||||||
|
),
|
||||||
|
("pxy_openai", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=1,
|
||||||
|
help_text="The OpenAI assistant associated with this Telegram bot.",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="telegram_bots",
|
||||||
|
to="pxy_openai.openaiassistant",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2025-01-29 02:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("pxy_bots", "0003_telegrambot_assistant"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant_id",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
default=1, help_text="The ID of the associated assistant."
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=1,
|
||||||
|
help_text="The assistant model type.",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2025-01-29 03:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pxy_bots", "0004_remove_telegrambot_assistant_and_more"),
|
||||||
|
(
|
||||||
|
"pxy_langchain",
|
||||||
|
"0002_remove_aiprovider_base_url_aiassistant_created_at_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant_id",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant_type",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="telegrambot",
|
||||||
|
name="assistant",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=1,
|
||||||
|
help_text="The LangChain AI assistant associated with this Telegram bot.",
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="telegram_bots",
|
||||||
|
to="pxy_langchain.aiassistant",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
0
pxy_bots/migrations/__init__.py
Normal file
0
pxy_bots/migrations/__init__.py
Normal file
BIN
pxy_bots/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
pxy_bots/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
pxy_bots/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_bots/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
47
pxy_bots/models.py
Normal file
47
pxy_bots/models.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from django.db import models
|
||||||
|
from pxy_langchain.models import AIAssistant # Now referencing LangChain AI assistants
|
||||||
|
|
||||||
|
class TelegramBot(models.Model):
|
||||||
|
"""
|
||||||
|
Represents a Telegram bot that interacts with users using a LangChain AI assistant.
|
||||||
|
"""
|
||||||
|
name = models.CharField(max_length=50, unique=True, help_text="Bot name (e.g., 'SupportBot').")
|
||||||
|
username = models.CharField(max_length=50, unique=True, help_text="Bot username (e.g., 'SupportBot').")
|
||||||
|
token = models.CharField(max_length=200, unique=True, help_text="Telegram bot token.")
|
||||||
|
is_active = models.BooleanField(default=True, help_text="Indicates if this bot is active.")
|
||||||
|
assistant = models.ForeignKey(
|
||||||
|
AIAssistant,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="telegram_bots",
|
||||||
|
help_text="The LangChain AI assistant associated with this Telegram bot.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} (@{self.username})"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_bot_token(bot_name):
|
||||||
|
"""Retrieve the token for the given bot name."""
|
||||||
|
try:
|
||||||
|
bot = TelegramBot.objects.get(name=bot_name, is_active=True)
|
||||||
|
return bot.token
|
||||||
|
except TelegramBot.DoesNotExist:
|
||||||
|
raise ValueError(f"Bot with name '{bot_name}' not found or inactive.")
|
||||||
|
|
||||||
|
def set_webhook(self, base_url):
|
||||||
|
"""
|
||||||
|
Set the webhook for this bot dynamically based on the server's base URL.
|
||||||
|
"""
|
||||||
|
if not self.is_active:
|
||||||
|
raise ValueError(f"Bot '{self.name}' is inactive. Activate it before setting the webhook.")
|
||||||
|
|
||||||
|
webhook_url = f"{base_url}/bots/webhook/{self.name}/"
|
||||||
|
response = requests.post(
|
||||||
|
f"https://api.telegram.org/bot{self.token}/setWebhook",
|
||||||
|
data={"url": webhook_url}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Failed to set webhook for {self.name}: {response.json()}")
|
16
pxy_bots/set_webhook.py
Normal file
16
pxy_bots/set_webhook.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import requests
|
||||||
|
from pxy_bots.models import TelegramBot
|
||||||
|
|
||||||
|
BASE_URL = "https://your-domain.com/bots/webhook/"
|
||||||
|
|
||||||
|
def set_telegram_webhooks():
|
||||||
|
"""Sets webhooks for all active bots."""
|
||||||
|
bots = TelegramBot.objects.filter(is_active=True)
|
||||||
|
for bot in bots:
|
||||||
|
webhook_url = f"{BASE_URL}{bot.name}/"
|
||||||
|
response = requests.post(
|
||||||
|
f"https://api.telegram.org/bot{bot.token}/setWebhook",
|
||||||
|
data={"url": webhook_url}
|
||||||
|
)
|
||||||
|
print(f"Webhook for {bot.name} ({bot.username}) set to: {webhook_url}")
|
||||||
|
print("Response:", response.json())
|
209
pxy_bots/telgram_bot.py
Normal file
209
pxy_bots/telgram_bot.py
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import logging
|
||||||
|
from openai import OpenAI
|
||||||
|
from telegram import ForceReply, Update
|
||||||
|
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def dream_city_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Send a message with a link to the random city generator."""
|
||||||
|
# Construct the message
|
||||||
|
message = (
|
||||||
|
"Descubre la ciudad de tus sueños! Haz clic en el enlace para explorar una ciudad generada aleatoriamente que podría reflejar tus aspiraciones urbanas: "
|
||||||
|
"https://app.polisplexity.tech/city/digital/twin/dream/?innovation=30&technology=30&science=40"
|
||||||
|
)
|
||||||
|
# Send the message
|
||||||
|
await update.message.reply_text(message)
|
||||||
|
|
||||||
|
|
||||||
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Send a message when the command /start is issued."""
|
||||||
|
user = update.effective_user
|
||||||
|
await update.message.reply_html(
|
||||||
|
rf"Hi {user.mention_html()}!",
|
||||||
|
reply_markup=ForceReply(selective=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Send a message when the command /help is issued."""
|
||||||
|
user = update.effective_user
|
||||||
|
location = update.message.location
|
||||||
|
await update.message.reply_text(rf"Help! {user.mention_html()} in {location}")
|
||||||
|
|
||||||
|
async def handle_location(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Respond to a location message."""
|
||||||
|
location = update.message.location
|
||||||
|
|
||||||
|
if location:
|
||||||
|
# Extract latitude and longitude
|
||||||
|
latitude = location.latitude
|
||||||
|
longitude = location.longitude
|
||||||
|
|
||||||
|
# You can now use the latitude and longitude for your bot's purposes
|
||||||
|
await update.message.reply_text(f"Thanks for sharing your location! Latitude: {latitude}, Longitude: {longitude}")
|
||||||
|
else:
|
||||||
|
# Respond if no location is found in the message
|
||||||
|
await update.message.reply_text("Please share your location.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def assistant(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Respond to user message using OpenAI."""
|
||||||
|
bot_response = "Thinking..."
|
||||||
|
try:
|
||||||
|
user_message = update.message.text
|
||||||
|
client = OpenAI(api_key='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',)
|
||||||
|
|
||||||
|
|
||||||
|
neurolitiks_assistant = client.beta.assistants.retrieve("asst_LcyxtrwgxgdHoVwSsuTUf5Ec")
|
||||||
|
|
||||||
|
my_thread = client.beta.threads.create()
|
||||||
|
|
||||||
|
my_thread_message = client.beta.threads.messages.create(
|
||||||
|
thread_id=my_thread.id,
|
||||||
|
role="user",
|
||||||
|
content=user_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
my_run = client.beta.threads.runs.create(
|
||||||
|
thread_id=my_thread.id,
|
||||||
|
assistant_id=neurolitiks_assistant.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Step 6: Periodically retrieve the Run to check on its status to see if it has moved to completed
|
||||||
|
while my_run.status in ["queued", "in_progress"]:
|
||||||
|
keep_retrieving_run = client.beta.threads.runs.retrieve(
|
||||||
|
thread_id=my_thread.id,
|
||||||
|
run_id=my_run.id
|
||||||
|
)
|
||||||
|
|
||||||
|
print(keep_retrieving_run)
|
||||||
|
|
||||||
|
if keep_retrieving_run.status == "completed":
|
||||||
|
|
||||||
|
# Step 7: Retrieve the Messages added by the Assistant to the Thread
|
||||||
|
all_messages = client.beta.threads.messages.list(
|
||||||
|
thread_id=my_thread.id
|
||||||
|
)
|
||||||
|
bot_response = all_messages.data[0].content[0].text.value #openai_response.choices[0].message.content
|
||||||
|
break
|
||||||
|
elif keep_retrieving_run.status == "queued" or keep_retrieving_run.status == "in_progress":
|
||||||
|
# bot_response += ' ' + my_run.status
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
bot_response += ' ' + keep_retrieving_run.status
|
||||||
|
break
|
||||||
|
|
||||||
|
if bot_response:
|
||||||
|
await update.message.reply_text(bot_response)
|
||||||
|
else:
|
||||||
|
# In case the response is empty or only whitespace
|
||||||
|
await update.message.reply_text("I'm not sure how to respond to that. 🤔")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error while processing the message: {e}")
|
||||||
|
# Send a default response in case of an error
|
||||||
|
await update.message.reply_text(f"Oops, I encountered an issue. Please try again later. 😓 {e}")
|
||||||
|
|
||||||
|
def prepare_persona_feedback(user_query):
|
||||||
|
# Example function to fetch and format feedback based on user query
|
||||||
|
# This could query a database or API for more dynamic data
|
||||||
|
persona_feedback = {
|
||||||
|
"Other Citizens": "Will analyze and propose community driven improvements.",
|
||||||
|
"Technology Makers": "Will analyze integration with technologies, and will say what technologies and how.",
|
||||||
|
"Scientific Innovators": "Will analyze from the urbanist persepective first, and then will recommen what scientific or urban models to use"
|
||||||
|
}
|
||||||
|
return persona_feedback
|
||||||
|
|
||||||
|
|
||||||
|
def generate_system_prompt(persona_feedback):
|
||||||
|
return (f"Based on the urban improvement proposal, seek feedback from various perspectives: "
|
||||||
|
f"Other Citizens suggest: {persona_feedback['Other Citizens']}; "
|
||||||
|
f"Technology Makers recommend: {persona_feedback['Technology Makers']}; "
|
||||||
|
f"Scientific Innovators advise: {persona_feedback['Scientific Innovators']}. "
|
||||||
|
"Evaluate and compile these suggestions to enhance the proposal.")
|
||||||
|
|
||||||
|
|
||||||
|
async def respond(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""Respond to user message using OpenAI."""
|
||||||
|
bot_response = "Thinking..."
|
||||||
|
try:
|
||||||
|
user_query = update.message.text
|
||||||
|
client = OpenAI(api_key='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')
|
||||||
|
|
||||||
|
# Assuming prepare_persona_feedback and generate_system_prompt are defined elsewhere
|
||||||
|
persona_feedback = prepare_persona_feedback(user_query)
|
||||||
|
system_prompt = generate_system_prompt(persona_feedback)
|
||||||
|
|
||||||
|
openai_response = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo-0125",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "Act as a citizen technology advisor. You will receive enquiries from citizens and will advise on what technologies they could use to resolve them."},
|
||||||
|
{"role": "user", "content": user_query}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
bot_response = openai_response.choices[0].message.content
|
||||||
|
|
||||||
|
# Send the main response
|
||||||
|
if bot_response:
|
||||||
|
await update.message.reply_text(bot_response)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text("I'm not sure how to respond to that. 🤔")
|
||||||
|
|
||||||
|
# Generate random percentages for the URL
|
||||||
|
innovation = random.randint(1, 100)
|
||||||
|
technology = random.randint(1, 100)
|
||||||
|
science = random.randint(1, 100)
|
||||||
|
|
||||||
|
# Prepare the promotional message
|
||||||
|
dream_city_link = (
|
||||||
|
f"Descubre la ciudad de tus sueños! Haz clic en el enlace para explorar una ciudad matemática generada en VR "
|
||||||
|
f"que podría reflejar tus aspiraciones urbanas: https://app.polisplexity.tech/city/digital/twin/dream/?"
|
||||||
|
f"innovation={innovation}&technology={technology}&science={science}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the promotional message in a separate follow-up message
|
||||||
|
await update.message.reply_text(dream_city_link)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error while processing the message: {e}")
|
||||||
|
await update.message.reply_text("Oops, I encountered an issue. Please try again later. 😓")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Start the bot."""
|
||||||
|
application = Application.builder().token("6474402815:AAHCSXM7VKwyj5-lVh8p3365eRQ5Nj94H4I").build()
|
||||||
|
|
||||||
|
application.add_handler(CommandHandler("start", start))
|
||||||
|
application.add_handler(CommandHandler("help", help_command))
|
||||||
|
application.add_handler(CommandHandler("dream_city", dream_city_command))
|
||||||
|
|
||||||
|
debate_filter_private = filters.ChatType.PRIVATE & filters.Regex(r'\bmi ciudad de los sueños\b.*:\s*')
|
||||||
|
application.add_handler(MessageHandler(debate_filter_private, dream_city_command))
|
||||||
|
application.add_handler(MessageHandler(filters.ChatType.PRIVATE, respond))
|
||||||
|
|
||||||
|
# Replace 'your_bot_username' with your bot's username without '@'
|
||||||
|
bot_username = 'PolisplexityBot'
|
||||||
|
mention_pattern = fr'@{bot_username}\b'
|
||||||
|
|
||||||
|
# Use the regex filter to check for mentions of the bot
|
||||||
|
mention_filter = filters.Regex(mention_pattern)
|
||||||
|
|
||||||
|
debate_filter_groups = filters.ChatType.GROUPS & filters.Regex(r'\bmi ciudad de los sueños\b.*:\s*')
|
||||||
|
application.add_handler(MessageHandler(mention_filter & debate_filter_groups, dream_city_command))
|
||||||
|
application.add_handler(MessageHandler(mention_filter & filters.ChatType.GROUPS, respond))
|
||||||
|
|
||||||
|
# application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, respond))
|
||||||
|
application.add_handler(MessageHandler(filters.LOCATION, handle_location))
|
||||||
|
|
||||||
|
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
3
pxy_bots/tests.py
Normal file
3
pxy_bots/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
6
pxy_bots/urls.py
Normal file
6
pxy_bots/urls.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import telegram_webhook
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('webhook/<str:bot_name>/', telegram_webhook, name='telegram_webhook'),
|
||||||
|
]
|
20
pxy_bots/utils.py
Normal file
20
pxy_bots/utils.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from .models import TelegramBot
|
||||||
|
|
||||||
|
def get_bot_token(bot_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Retrieve the Telegram bot token by bot name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bot_name (str): The name of the bot to fetch.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The Telegram bot token.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no active bot with the given name is found.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
bot = TelegramBot.objects.get(name=bot_name, is_active=True)
|
||||||
|
return bot.token
|
||||||
|
except TelegramBot.DoesNotExist:
|
||||||
|
raise ValueError(f"No active bot found with the name '{bot_name}'")
|
69
pxy_bots/views.py
Normal file
69
pxy_bots/views.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import json
|
||||||
|
from telegram import Update, Bot
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
from .models import TelegramBot
|
||||||
|
from pxy_langchain.services import LangchainAIService
|
||||||
|
from .handlers import dream_city_command, start, help_command, handle_location
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
async def telegram_webhook(request, bot_name):
|
||||||
|
"""
|
||||||
|
Webhook view that handles Telegram updates asynchronously and only uses LangChain.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Webhook called for bot: {bot_name}")
|
||||||
|
|
||||||
|
# Step 1: Fetch the bot instance asynchronously
|
||||||
|
try:
|
||||||
|
bot_instance = await sync_to_async(TelegramBot.objects.get)(name=bot_name, is_active=True)
|
||||||
|
logger.info(f"Loaded bot configuration: {bot_instance}")
|
||||||
|
except TelegramBot.DoesNotExist:
|
||||||
|
logger.error(f"Bot '{bot_name}' not found or inactive.")
|
||||||
|
return JsonResponse({"error": f"Bot '{bot_name}' not found."}, status=400)
|
||||||
|
|
||||||
|
# Step 2: Ensure the bot has a LangChain assistant
|
||||||
|
if not bot_instance.assistant:
|
||||||
|
logger.error(f"No assistant configured for bot '{bot_name}'.")
|
||||||
|
return JsonResponse({"error": "Assistant not configured."}, status=400)
|
||||||
|
|
||||||
|
# Step 3: Process POST request from Telegram
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
request_body = json.loads(request.body.decode("utf-8"))
|
||||||
|
update = Update.de_json(request_body, Bot(token=bot_instance.token))
|
||||||
|
logger.info(f"Update received: {update}")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Failed to decode JSON: {e}")
|
||||||
|
return JsonResponse({"error": "Invalid JSON payload"}, status=400)
|
||||||
|
|
||||||
|
# Step 4: Route commands to the appropriate handlers
|
||||||
|
if update.message:
|
||||||
|
if update.message.text == "/start":
|
||||||
|
await start(update)
|
||||||
|
elif update.message.text == "/help":
|
||||||
|
await help_command(update)
|
||||||
|
elif update.message.text == "/dream_city":
|
||||||
|
await dream_city_command(update)
|
||||||
|
elif update.message.location:
|
||||||
|
await handle_location(update)
|
||||||
|
else:
|
||||||
|
# Step 5: Process AI-generated response using LangChain
|
||||||
|
assistant_instance = await sync_to_async(LangchainAIService)(bot_instance.assistant)
|
||||||
|
bot_response = await sync_to_async(assistant_instance.generate_response)(update.message.text)
|
||||||
|
|
||||||
|
# Step 6: Send the response back to Telegram
|
||||||
|
await update.message.reply_text(bot_response)
|
||||||
|
|
||||||
|
return JsonResponse({"status": "ok"})
|
||||||
|
|
||||||
|
logger.warning("Received non-POST request")
|
||||||
|
return JsonResponse({"error": "Invalid request method"}, status=400)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in webhook: {e}")
|
||||||
|
return JsonResponse({"error": f"Unexpected error: {str(e)}"}, status=500)
|
1
pxy_city_digital_twins
Submodule
1
pxy_city_digital_twins
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit bf4767d280cb31a73b62ebb6ff912c604423de52
|
0
pxy_cr/__init__.py
Normal file
0
pxy_cr/__init__.py
Normal file
BIN
pxy_cr/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/__pycache__/admin.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/__pycache__/apps.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/__pycache__/models.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
3
pxy_cr/admin.py
Normal file
3
pxy_cr/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
pxy_cr/apps.py
Normal file
6
pxy_cr/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PxyCrConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "pxy_cr"
|
0
pxy_cr/migrations/__init__.py
Normal file
0
pxy_cr/migrations/__init__.py
Normal file
BIN
pxy_cr/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_cr/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
3
pxy_cr/models.py
Normal file
3
pxy_cr/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
3
pxy_cr/tests.py
Normal file
3
pxy_cr/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
pxy_cr/views.py
Normal file
3
pxy_cr/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
0
pxy_de/__init__.py
Normal file
0
pxy_de/__init__.py
Normal file
BIN
pxy_de/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_de/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_de/__pycache__/admin.cpython-310.pyc
Normal file
BIN
pxy_de/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_de/__pycache__/apps.cpython-310.pyc
Normal file
BIN
pxy_de/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_de/__pycache__/models.cpython-310.pyc
Normal file
BIN
pxy_de/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
36
pxy_de/admin.py
Normal file
36
pxy_de/admin.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import PipelineStep, PipelineRun, PipelineProductRun
|
||||||
|
|
||||||
|
class PipelineStepAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'description') # Specify the fields to display
|
||||||
|
|
||||||
|
class PipelineProductRunAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('pipeline_run', 'step', 'status', 'date')
|
||||||
|
list_filter = ('status', 'step') # Add filtering by status and step
|
||||||
|
search_fields = ('pipeline_run__run_identifier', 'step__name') # Enable search by pipeline_run identifier and step name
|
||||||
|
|
||||||
|
# Register your models with custom ModelAdmin
|
||||||
|
admin.site.register(PipelineStep, PipelineStepAdmin)
|
||||||
|
admin.site.register(PipelineProductRun, PipelineProductRunAdmin)
|
||||||
|
|
||||||
|
# pxy_de/admin.py (or wherever your PipelineRun admin is)
|
||||||
|
from django.contrib import messages
|
||||||
|
from pxy_de.pipelines.pxy_products.run_products import run_pipeline
|
||||||
|
|
||||||
|
def run_pipeline_action(modeladmin, request, queryset):
|
||||||
|
try:
|
||||||
|
run_pipeline()
|
||||||
|
messages.success(request, "Pipeline executed successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"Pipeline failed to execute: {str(e)}")
|
||||||
|
|
||||||
|
run_pipeline_action.short_description = "Run Products Pipeline"
|
||||||
|
|
||||||
|
class PipelineRunAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['run_identifier', 'overall_status', 'date']
|
||||||
|
ordering = ['-date']
|
||||||
|
actions = [run_pipeline_action]
|
||||||
|
|
||||||
|
admin.site.register(PipelineRun, PipelineRunAdmin)
|
||||||
|
|
||||||
|
|
6
pxy_de/apps.py
Normal file
6
pxy_de/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PxyDeConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "pxy_de"
|
85
pxy_de/migrations/0001_initial.py
Normal file
85
pxy_de/migrations/0001_initial.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-03-16 23:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PipelineRun",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"run_identifier",
|
||||||
|
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"overall_status",
|
||||||
|
models.CharField(blank=True, max_length=20, null=True),
|
||||||
|
),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PipelineStep",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PipelineProductRun",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("status", models.CharField(max_length=10)),
|
||||||
|
("date", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"pipeline_run",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="product_runs",
|
||||||
|
to="pxy_de.pipelinerun",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"step",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="pxy_de.pipelinestep",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
pxy_de/migrations/__init__.py
Normal file
0
pxy_de/migrations/__init__.py
Normal file
BIN
pxy_de/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
pxy_de/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_de/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_de/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
30
pxy_de/models.py
Normal file
30
pxy_de/models.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# pxy_de/pipelines/models.py
|
||||||
|
import uuid
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class PipelineStep(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class PipelineRun(models.Model):
|
||||||
|
run_identifier = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||||
|
overall_status = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.run_identifier)
|
||||||
|
|
||||||
|
class PipelineProductRun(models.Model):
|
||||||
|
pipeline_run = models.ForeignKey(PipelineRun, on_delete=models.CASCADE, related_name='product_runs')
|
||||||
|
step = models.ForeignKey(PipelineStep, on_delete=models.CASCADE)
|
||||||
|
status = models.CharField(max_length=10)
|
||||||
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.pipeline_run.run_identifier} - {self.step.name}"
|
||||||
|
|
||||||
|
|
||||||
|
|
0
pxy_de/pipelines/__init__.py
Normal file
0
pxy_de/pipelines/__init__.py
Normal file
BIN
pxy_de/pipelines/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_de/pipelines/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_de/pipelines/__pycache__/models.cpython-310.pyc
Normal file
BIN
pxy_de/pipelines/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
0
pxy_de/pipelines/pxy_products/__init__.py
Normal file
0
pxy_de/pipelines/pxy_products/__init__.py
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user