commit e40c9e3d8f6c41e74251b25492a238edb841f298 Author: = Date: Sun Sep 8 08:51:55 2024 -0400 first commit diff --git a/Read.me b/Read.me new file mode 100644 index 0000000..ea62c9b --- /dev/null +++ b/Read.me @@ -0,0 +1,67 @@ +Read.me +Provide code for following program + +Program runs on 5 containers using podman-dockerise + +First container is latest postgresql with pgadmin +Second container is latest couchdb +Third container is latest django +Fourth container is latest nodejs with latest react and latest redux +Fifth container is latest nginx which contains the landing page and serves static content + +All dynamic data is stored in an external nfs server defined in a configuration file for main django application. +Initial data that containers need is stored locally. + +Program starts by welcoming user and displaying login or register page served by nginx. +Registration is by email which by default is noone@nowhere.net +Program will use a certificate system to secure communication between containers where root certificate passkey is the secret in settings page of the main django application + +Django main application and apis it serves: +Djano main application is the user directory, project directory, and RBAC definitions directory. +First time running the django application default user is admin and password is "GoodMorning123!" which has full administrative rights. +First time admin login to the django application default landing page is the program configuration page which has following required fields to complete: + -Company Name (only one company can exist but subsidieries can be created - 256 characters maximum) + -First subsidiary name (organization unit responsible for administring the program - 256 characters maximum) + -Three letter for initial subsidiary + -Contact email address for issues (must have proper email form) which will be the email that will receive the login certificate for admin user + -email address for registration (which replaces the default noone@nowhere.net - must have proper email form) + -Address NFS server (test button to make sure program can create a test folder, create a file in test folder, delete test file, and delete test folder. Test must be done with success at least once for submit button to become usuable - until that it should be grayed out) +submit button will submit the form. Once form is submitted following will happen: + -Login for django environment will become certificate based + -Will replace default registration email with one submitted in the nginx landing page + -Will create initial database in postgresql for the subsidiary with three letter provided + -Will set admin with certificate as default postgresql database administrator (this certificate will be used to exchange data with other containers) + -Will create initial couchdb database for subsidieary and default project + -Will set admin with certificate as couchdb default administrator (this certificate will be used to exhange data with other containers) + -Initial landing page will switch to project dashboard displaying default project which will have project number made of three letters submitted and 0000001. For example if ABC is the three letter submitted project number will be ABC-0000001. More details on initial projects later. + -Will apply initial project template to the django database (more on project templates later) +What users can see on the landing page will be based on their role but admin user can see and modify everything. +Clicking on the project will open a new web page served by nginx but uses nodejs container. This is project landing page. What can be done in project will be done through this react application which will use couchdb to manage its data. + +Here is what project page will have: +It is a progressive web application page. It contains all the work packages. How work packages relate to each other is defined through template. +Web application uses graphql to get all the data it needs from postgresql and couchdb. +By default it contains all the work packages that come from default template. Template uses hashtaged object pairs with relation between them in curly braket. +Default template contains following lines at the start for subsidiary defined with letters ABC: +Vertices: +#WP0000001 {childof} #ABC0000001 +#T0000001 {childof} #WP0000001 +#T0000002 {childof} #T0000001 +#T0000002 {end-before-start} #T0000001 +#T0000001-TaskName {field-of} #T0000001 +#WP0000001-WorkPackageName {field-of} #WP0000001 +List of relations: +{childof} +{depends-on} +{end-before-start} +{end-after-start} +{field-of} + +CouchDB will store different fields which can accept pre-defined type of data for each hashtaged object. When defining a hashtag object user will be asked to provide type of data it should be. By default character. Provide pull down list of any type of data Couchdb can store that can be captured through the intake form. +All default initial hashtaged objects are CHAR type. +List of Vertices and list of relations are stored in CouchDB database for the project. List of relations is editable. Nodes for vertices are created through work package manager react application. +work package manager also is responsible for creating vertices. But nodes and relation they will use between two nodes must exist before use. That is vertice builder need to rely on existing searchable data. + +Hashtaged objects need to be unique and they are not case sensitive. + +Another react application shows the graph view based on vertices one or more relations (use filter to apply which relations to use to graph). Do not show unlinked nodes. \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..fbce924 --- /dev/null +++ b/app.js @@ -0,0 +1,35 @@ +import React, { useEffect, useState } from 'react'; + +function App() { + const [projects, setProjects] = useState([]); + const [tasks, setTasks] = useState([]); + + useEffect(() => { + fetch('/api/projects/') + .then(response => response.json()) + .then(data => setProjects(data)); + + fetch('/api/tasks/') + .then(response => response.json()) + .then(data => setTasks(data)); + }, []); + + return ( +
+

Projects

+ +

Tasks

+ +
+ ); +} + +export default App; \ No newline at end of file diff --git a/main/__init__.py b/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/admin.py b/main/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/main/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/main/api_views.py b/main/api_views.py new file mode 100644 index 0000000..f3ff637 --- /dev/null +++ b/main/api_views.py @@ -0,0 +1,12 @@ +# main/api_views.py +from rest_framework import viewsets +from .models import Project, Task +from .serializers import ProjectSerializer, TaskSerializer + +class ProjectViewSet(viewsets.ModelViewSet): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + +class TaskViewSet(viewsets.ModelViewSet): + queryset = Task.objects.all() + serializer_class = TaskSerializer diff --git a/main/apps.py b/main/apps.py new file mode 100644 index 0000000..5b1947b --- /dev/null +++ b/main/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + +class MainConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'main' + + def ready(self): + import main.signals diff --git a/main/forms.py b/main/forms.py new file mode 100644 index 0000000..9af62ea --- /dev/null +++ b/main/forms.py @@ -0,0 +1,12 @@ +from django import forms +from .models import Project, Task + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = ['name', 'description'] + +class TaskForm(forms.ModelForm): + class Meta: + model = Task + fields = ['name', 'description', 'status'] \ No newline at end of file diff --git a/main/migrations/__init__.py b/main/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/models.py b/main/models.py new file mode 100644 index 0000000..41249c4 --- /dev/null +++ b/main/models.py @@ -0,0 +1,48 @@ +from django.db import models + +# Create your models here. +from django.contrib.auth.models import User +from django.db import models +from django.conf import settings + +class Profile(models.Model): + ROLE_CHOICES = [ + ('Application Admin', 'Application Admin'), + ('Application Designer', 'Application Designer'), + ('Database Admin', 'Database Admin'), + ('PMO Admin', 'PMO Admin'), + ('Program Manager', 'Program Manager'), + ('Project Manager', 'Project Manager'), + ('Project Coordinator', 'Project Coordinator'), + ('Project User', 'Project User'), + ('Project Initiator', 'Project Initiator'), + ] + + user = models.OneToOneField(User, on_delete=models.CASCADE) + role = models.CharField(max_length=50, choices=ROLE_CHOICES) + + def __str__(self): + return f"{self.user.username} - {self.role}" + + +class Project(models.Model): + name = models.CharField(max_length=255) + description = models.TextField() + initiator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='initiated_projects') + manager = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_projects') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + +class Task(models.Model): + project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='tasks') + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + status = models.CharField(max_length=50, choices=[('todo', 'To Do'), ('in_progress', 'In Progress'), ('done', 'Done')], default='todo') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/main/serializers.py b/main/serializers.py new file mode 100644 index 0000000..17c24a1 --- /dev/null +++ b/main/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from .models import Project, Task + +class ProjectSerializer(serializers.ModelSerializer): + class Meta: + model = Project + fields = '__all__' + +class TaskSerializer(serializers.ModelSerializer): + class Meta: + model = Task + fields = '__all__' \ No newline at end of file diff --git a/main/signals.py b/main/signals.py new file mode 100644 index 0000000..ac06aea --- /dev/null +++ b/main/signals.py @@ -0,0 +1,10 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.contrib.auth.models import User +from .models import Profile + +@receiver(post_save, sender=User) +def create_or_update_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + instance.profile.save() \ No newline at end of file diff --git a/main/tests.py b/main/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/main/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/main/urls.py b/main/urls.py new file mode 100644 index 0000000..4ae88de --- /dev/null +++ b/main/urls.py @@ -0,0 +1,19 @@ +# main/urls.py +from .views import create_project, project_dashboard, create_task +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .api_views import ProjectViewSet, TaskViewSet + +router = DefaultRouter() +router.register(r'projects', ProjectViewSet) +router.register(r'tasks', TaskViewSet) + +urlpatterns = [ + path('api/', include(router.urls)), + path('login/', login_view, name='login'), + path('logout/', logout_view, name='logout'), + path('welcome/', welcome_view, name='welcome'), + path('projects/create/', create_project, name='create_project'), + path('projects//', project_dashboard, name='project_dashboard'), + path('projects//tasks/create/', create_task, name='create_task'), +] diff --git a/main/views.py b/main/views.py new file mode 100644 index 0000000..60a156b --- /dev/null +++ b/main/views.py @@ -0,0 +1,64 @@ +# main/views.py +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from .models import Project, Task +from .forms import ProjectForm, TaskForm + +@login_required +def create_project(request): + if request.method == 'POST': + form = ProjectForm(request.POST) + if form.is_valid(): + project = form.save(commit=False) + project.initiator = request.user + project.save() + # Notify PMO Admins + pmo_admins = User.objects.filter(profile__role='PMO Admin') + for admin in pmo_admins: + send_mail( + 'New Project Created', + f'A new project "{project.name}" has been created and requires a project manager assignment.', + 'admin@example.com', + [admin.email], + fail_silently=False, + ) + return redirect('project_dashboard', project_id=project.id) + else: + form = ProjectForm() + return render(request, 'create_project.html', {'form': form}) + +# main/views.py +@login_required +def load_project(request): + if request.method == 'POST': + project_id = request.POST['project_id'] + return redirect('project_dashboard', project_id=project_id) + projects = Project.objects.all() + return render(request, 'load_project.html', {'projects': projects}) + + +@login_required +def project_dashboard(request, project_id): + project = get_object_or_404(Project, id=project_id) + tasks = project.tasks.all() + return render(request, 'project_dashboard.html', {'project': project, 'tasks': tasks}) + +@login_required +def create_task(request, project_id): + project = get_object_or_404(Project, id=project_id) + if request.method == 'POST': + form = TaskForm(request.POST) + if form.is_valid(): + task = form.save(commit=False) + task.project = project + task.save() + return redirect('project_dashboard', project_id=project.id) + else: + form = TaskForm() + return render(request, 'create_task.html', {'form': form, 'project': project}) + +# main/views.py +from django.core.mail import send_mail +from django.contrib.auth.models import User + + diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..dff3903 --- /dev/null +++ b/manage.py @@ -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', 'project_management.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() diff --git a/project_management/__init__.py b/project_management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project_management/__pycache__/__init__.cpython-311.pyc b/project_management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8a822c3 Binary files /dev/null and b/project_management/__pycache__/__init__.cpython-311.pyc differ diff --git a/project_management/__pycache__/settings.cpython-311.pyc b/project_management/__pycache__/settings.cpython-311.pyc new file mode 100644 index 0000000..bdee51a Binary files /dev/null and b/project_management/__pycache__/settings.cpython-311.pyc differ diff --git a/project_management/__pycache__/urls.cpython-311.pyc b/project_management/__pycache__/urls.cpython-311.pyc new file mode 100644 index 0000000..2440023 Binary files /dev/null and b/project_management/__pycache__/urls.cpython-311.pyc differ diff --git a/project_management/__pycache__/wsgi.cpython-311.pyc b/project_management/__pycache__/wsgi.cpython-311.pyc new file mode 100644 index 0000000..97260be Binary files /dev/null and b/project_management/__pycache__/wsgi.cpython-311.pyc differ diff --git a/project_management/asgi.py b/project_management/asgi.py new file mode 100644 index 0000000..088c5dc --- /dev/null +++ b/project_management/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for wbsportal project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wbsportal.settings') + +application = get_asgi_application() diff --git a/project_management/settings.py b/project_management/settings.py new file mode 100644 index 0000000..17223cf --- /dev/null +++ b/project_management/settings.py @@ -0,0 +1,128 @@ +""" +Django settings for wbsportal project. + +Generated by 'django-admin startproject' using Django 4.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-e#$wxb+=br=3u(ju%$dzru!=6p4myj^!3fyb8-@d@y+9dx0od2' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', +] + +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 = 'wbsportal.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + '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 = 'wbsportal.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'p00003db', + 'USER': 'p00003dbuser', + 'PASSWORD': 'Express.123', + 'HOST': 'localhost', # Set to 'localhost' or your database server IP + 'PORT': '5432', # Default port for PostgreSQL + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +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 +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/project_management/urls.py b/project_management/urls.py new file mode 100644 index 0000000..e02d2b9 --- /dev/null +++ b/project_management/urls.py @@ -0,0 +1,8 @@ +# project_management/urls.py +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('main.urls')), +] \ No newline at end of file diff --git a/project_management/wsgi.py b/project_management/wsgi.py new file mode 100644 index 0000000..15bb940 --- /dev/null +++ b/project_management/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for wbsportal project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wbsportal.settings') + +application = get_wsgi_application() diff --git a/templates/create_project.html b/templates/create_project.html new file mode 100644 index 0000000..3fcda80 --- /dev/null +++ b/templates/create_project.html @@ -0,0 +1,6 @@ +

Create New Project

+
+ {% csrf_token %} + {{ form.as_p }} + +
\ No newline at end of file diff --git a/templates/create_task.html b/templates/create_task.html new file mode 100644 index 0000000..e407351 --- /dev/null +++ b/templates/create_task.html @@ -0,0 +1,6 @@ +

Create Task for Project: {{ project.name }}

+
+ {% csrf_token %} + {{ form.as_p }} + +
diff --git a/templates/load_project.html b/templates/load_project.html new file mode 100644 index 0000000..cfeacf0 --- /dev/null +++ b/templates/load_project.html @@ -0,0 +1,11 @@ + +

Load Existing Project

+
+ {% csrf_token %} + + +
diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..79eb645 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,9 @@ +

Login

+
+ {% csrf_token %} + + + + + +
\ No newline at end of file diff --git a/templates/project_dashboard.html b/templates/project_dashboard.html new file mode 100644 index 0000000..7d2eea4 --- /dev/null +++ b/templates/project_dashboard.html @@ -0,0 +1,12 @@ +

Project: {{ project.name }}

+

{{ project.description }}

+

Initiator: {{ project.initiator.username }}

+

Manager: {{ project.manager.username if project.manager else "Not assigned" }}

+ +

Tasks

+
    + {% for task in tasks %} +
  • {{ task.name }} - {{ task.status }}
  • + {% endfor %} +
+Add Task diff --git a/templates/welcome.html b/templates/welcome.html new file mode 100644 index 0000000..b72922d --- /dev/null +++ b/templates/welcome.html @@ -0,0 +1,9 @@ + +

Welcome, {{ user.username }}

+

Your role: {{ user.profile.role }}

+ + \ No newline at end of file