diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..bf67d43 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Remote Attach", + "type": "python", + "request": "attach", + "host": "localhost", + "port": 5678, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}/rcamp", + "remoteRoot": "/opt/rcamp" + } + ] + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9f17ef7..01e419a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,77 +1,86 @@ -FROM quay.io/rockylinux/rockylinux:8 -LABEL maintainer="Adam W Zheng " - -# Define s6 overlay process supervisor version -#ARG S6_OVERLAY_VERSION=3.1.5.0 +# Use Python 3.11 as the base image +FROM python:3.11-slim # Define gosu version ARG GOSU_VERSION=1.16 -# Install dependencies -RUN dnf -y install 'dnf-command(config-manager)' \ - && dnf config-manager --set-enabled powertools \ - && dnf -y install epel-release \ - && dnf -y groupinstall "Development Tools" \ - && dnf -y install xz dpkg which sssd pam_radius sqlite pam-devel openssl-devel python3-devel openldap-devel mysql-devel pcre-devel +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gnupg2 \ + xz-utils \ + dpkg \ + which \ + sssd \ + libpam0g-dev \ + libsqlite3-dev \ + libssl-dev \ + python3-dev \ + libldap2-dev \ + default-libmysqlclient-dev \ + curl \ + lsb-release \ + ca-certificates \ + build-essential \ + libssl-dev \ + libffi-dev \ + libpq-dev \ + libmariadb-dev \ + libmariadb-dev-compat \ + default-libmysqlclient-dev \ + libldap2-dev \ + libsasl2-dev \ + pkg-config \ + python3-dev \ + python3-pip \ + python3-venv \ + sqlite3 \ + git \ + && rm -rf /var/lib/apt/lists/* # Install gosu to drop user and chown shared volumes at runtime -ADD ["https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-amd64", "/usr/bin/gosu"] -ADD ["https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-amd64.asc", "/tmp/gosu.asc"] -RUN gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ - && gpg --batch --verify /tmp/gosu.asc /usr/bin/gosu -RUN chmod +x /usr/bin/gosu \ - && gosu nobody true - -# Cleanup -RUN dnf -y update && dnf clean all && rm -rf /var/cache/dnf && > /var/log/dnf.log - -# Add s6-overlay process supervisor -#ADD ["https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz", "/tmp"] -#RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz -#ADD ["https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz", "/tmp"] -#RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz +RUN curl -fsSL https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-amd64 -o /usr/bin/gosu \ + && chmod +x /usr/bin/gosu \ + && gosu nobody true -# Copy s6-supervisor source definition directory into container -#COPY ["etc/s6-overlay/", "/etc/s6-overlay/"] - -# Set Workdir +# Set working directory WORKDIR /opt/rcamp -# Add requirements +# Add requirements and other necessary files ADD ["requirements.txt", "/opt/requirements.txt"] - -# Add uwsgi conf ADD ["uwsgi.ini", "/opt/uwsgi.ini"] - -# Add codebase to container ADD ["rcamp", "/opt/rcamp"] -# From old dockerfile +# Set up virtual environment WORKDIR /opt ENV VIRTUAL_ENV=/opt/rcamp_venv RUN python3 -m venv $VIRTUAL_ENV ENV PATH="$VIRTUAL_ENV/bin:$PATH" +# Install the dependencies from requirements.txt COPY requirements.txt /opt/ RUN pip install --upgrade pip && \ pip install wheel && \ pip install -r requirements.txt -RUN git clone -b python3 https://github.com/ResearchComputing/django-ldapdb-test-env -WORKDIR /opt/django-ldapdb-test-env -RUN python3 setup.py install +# Clone and install the django-ldapdb-test-env repository WORKDIR /opt/rcamp -#Port Metadata +# Cleanup +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +# Expose necessary ports EXPOSE 80/tcp EXPOSE 443/tcp -#Simple Healthcheck +# Simple Healthcheck HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD curl http://localhost:8000/ || exit 1 -# s6-overlay entrypoint -#ENTRYPOINT ["/init"] - +# Copy entrypoint script COPY ["docker-entrypoint.sh", "/usr/local/bin/"] -ENTRYPOINT ["sh","/usr/local/bin/docker-entrypoint.sh"] + +# Set entrypoint +ENTRYPOINT ["sh", "/usr/local/bin/docker-entrypoint.sh"] + +# Default command to start the application CMD ["/opt/rcamp_venv/bin/uwsgi", "/opt/uwsgi.ini"] + diff --git a/README.md b/README.md index 90de287..bc4f780 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ # RCAMP Research Computing Administrative & Management Portal -## Overview and App Structure +# Table of Contents +1. [Overview](#overview) +2. [Installation](#installation) +3. [Tests](#tests) +4. [API](#api) + +## Overview **accounts** - The accounts app contains all code for the creation, review, and approval of account requests. Also contained in this app is all code necessary for managing users and groups. @@ -15,7 +21,7 @@ Research Computing Administrative & Management Portal **rcamp** - The rcamp directory contains site code and, most importantly, settings. -## Setting up your dev environment +## Installation You will need Docker 18.03+ and Compose 1.21+ before you begin. Documentation for Docker can be found here: https://docs.docker.com/install/. Start by cloning RCAMP. @@ -47,9 +53,129 @@ $ docker-compose run --rm --entrypoint "python3" rcamp-uwsgi manage.py createsup ``` The name, password, and email needed by the createsuperuser script can be whatever you like. You should now be able to view the webpage at localhost:8000. -## Writing and Running Tests +## Tests Documentation on use of the RCAMP test framework can be found in the RCAMP Wiki [Test Framework page](https://github.com/ResearchComputing/RCAMP/wiki/Test-Framework). ``` $ docker-compose run --rm --entrypoint "python3" rcamp-uwsgi manage.py test ``` + +## API + +If your user has superuser permissions, the endpoints can be used for queries. Here are examples of hitting the account requests endpoint + +```bash +$ curl -u : "http://rcamp2.rc.int.colorado.edu/api/accountrequests/?min_approve_date=2025-03-15&max_approve_date=2025-03-16" +``` +```json +[ + { + "username": "rmaccuser23", + "first_name": "rmacc", + "last_name": "user", + "email": "rmacc23@user.com", + "organization": "xsede", + "discipline": "Visual & Performing Arts", + "course_number": null, + "sponsor_email": null, + "resources_requested": null, + "status": "a", + "approved_on": "2025-03-15T18:14:14Z", + "request_date": "2025-03-15T18:14:14Z", + "notes": "" + } +] +``` + +If I want to request all account requests on 2025-03-14 +```bash +$ curl -u : "http://rcamp2.rc.int.colorado.edu/api/accountrequests/?min_approve_date=2025-03-14&max_approve_date=2025-03-15" +``` +```json +[ + { + "username": "rmaccuser14", + "first_name": "rmacc", + "last_name": "user", + "email": "rmaccuser14@cool.com", + "organization": "xsede", + "discipline": "Hum", + "course_number": null, + "sponsor_email": null, + "resources_requested": null, + "status": "a", + "approved_on": "2025-03-14T11:13:15Z", + "request_date": "2025-03-14T11:13:15Z", + "notes": "" + }, + { + "username": "dahdahdah", + "first_name": "rmacc", + "last_name": "user", + "email": "rmacc255@user.com", + "organization": "xsede", + "discipline": "Law", + "course_number": null, + "sponsor_email": null, + "resources_requested": null, + "status": "a", + "approved_on": "2025-03-14T12:27:11Z", + "request_date": "2025-03-14T12:27:19Z", + "notes": "" + }, + { + "username": "rmaccuser", + "first_name": "rmacc", + "last_name": "user", + "email": "rmacc@Colorado.EDU", + "organization": "ucb", + "discipline": "Physical Sciences", + "course_number": null, + "sponsor_email": null, + "resources_requested": null, + "status": "a", + "approved_on": "2025-03-14T12:29:28Z", + "request_date": "2025-03-14T12:29:29Z", + "notes": null + } +] +``` + +If you interested in a specific discipline you can use a substring to get results: +```bash +curl -u : "http://rcamp2.rc.int.colorado.edu/api/accountrequests/?discipline=Visual" +``` +```json +[ + { + "username": "rmaccuser23", + "first_name": "rmacc", + "last_name": "user", + "email": "rmacc23@user.com", + "organization": "xsede", + "discipline": "Visual & Performing Arts", + "course_number": null, + "sponsor_email": null, + "resources_requested": null, + "status": "a", + "approved_on": "2025-03-15T18:14:14Z", + "request_date": "2025-03-15T18:14:14Z", + "notes": "" + }, + { + "username": "rmacc-user67", + "first_name": "rmacc67", + "last_name": "user67", + "email": "rmacc67@user.com", + "organization": "xsede", + "discipline": "Visual & Performing Arts", + "course_number": null, + "sponsor_email": null, + "resources_requested": null, + "status": "a", + "approved_on": "2025-03-16T16:36:00Z", + "request_date": "2025-03-16T16:36:00Z", + "notes": "" + } +] +``` diff --git a/cmds b/cmds new file mode 100644 index 0000000..8092e1e --- /dev/null +++ b/cmds @@ -0,0 +1,2 @@ +docker-compose run --rm --entrypoint "python3" rcamp-uwsgi manage.py migrate +docker-compose run --rm --entrypoint "python3" rcamp-uwsgi manage.py createsuperuser diff --git a/docker-compose.yml b/docker-compose.yml index 2b9ef13..9b1bc7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,40 +1,70 @@ -version: "3.6" services: rcamp-uwsgi: + image: rcamp-uwsgi2 build: context: . container_name: rcamp-uwsgi - command: ["python3", "manage.py", "runserver", "0.0.0.0:8000"] + hostname: ${HOSTNAME} + #command: ["python3", "manage.py", "runserver", "0.0.0.0:8000"] env_file: - - dev-environment.env + - .env environment: - UWSGI_UID=${UWSGI_UID} - UWSGI_GID=${UWSGI_GID} volumes: - ./rcamp:/opt/rcamp - ./ldapdb:/opt/ldapdb - - static-content:/opt/static - - media-uploads:/opt/media - - rcamp-logs:/opt/logs - ports: - - "80:8000" + - /var/lib/sss/pipes:/var/lib/sss/pipes:rw + - ./rcamp-pam/nsswitch.conf:/etc/nsswitch.conf + - ./rcamp-pam/pam.d:/etc/pam.d + - ./rcamp-pam/raddb:/etc/raddb + - ./static-content:/opt/static + - ${MEDIA_DIR}:/opt/media + - ./rcamp-logs:/opt/logs +# ports: +# - "80:8000" +# - "443:443" + #- "5678:5678" # Debugger port depends_on: - - database - - ldap + database: + condition: service_healthy + logging: + driver: syslog database: - image: mysql:5.7 + image: mysql:8.4 container_name: database environment: - MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=rcamp1712 volumes: - database:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -ppassword"] + interval: 10s + timeout: 5s + retries: 5 - ldap: - image: researchcomputing/rc-test-ldap - container_name: ldap + nginx: + hostname: ${HOSTNAME} + image: nginx:1.20.0 + container_name: nginx + volumes: + - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params:ro + - ${SSL_CERT}:/etc/nginx/certs/rcamp.crt:ro + - ${SSL_KEY}:/etc/nginx/certs/rcamp.key:ro + - ${MEDIA_DIR}:/var/www/media:ro + - ./static-content:/var/www/static:ro + - ./nginx-logs:/var/logs/nginx + ports: + - "80:80" + - "443:443" + logging: + driver: syslog + depends_on: + - rcamp-uwsgi volumes: static-content: diff --git a/rcamp/accounts/admin.py b/rcamp/accounts/admin.py index e9314bf..8b979bf 100644 --- a/rcamp/accounts/admin.py +++ b/rcamp/accounts/admin.py @@ -1,6 +1,6 @@ import logging -from django.contrib import admin +from django.contrib import admin, messages from django.contrib.auth import admin as auth_admin from django import forms from lib.fields import LdapCsvField @@ -14,6 +14,18 @@ Intent, ORGANIZATIONS ) +from django.urls import reverse_lazy +from django.shortcuts import render +#from .forms import ComanageSyncForm +#from .models import ComanageUser +#from comanage.lib import UserCO +# from lib.utils import get_user_and_groups, get_comanage_users_by_org +from django.urls import path +from django.utils.html import format_html +from django.urls import reverse +from django.shortcuts import redirect +from django.http import HttpResponseRedirect +from django.db.models import Q @admin.register(User) @@ -57,6 +69,7 @@ def approve_requests(modeladmin, request, queryset): 'email', 'role', 'organization', + 'discipline', 'request_date', 'status' ] @@ -204,4 +217,136 @@ class RcLdapGroupAdmin(RcLdapModelAdmin): search_fields = ['name'] form = RcLdapGroupForm +# # Custom action to sync users from Comanage +# def sync_users_from_comanage(modeladmin, request, queryset): +# """ +# Sync the selected users from Comanage. +# """ +# if not queryset: +# users = ["kyre0001_amc", "etda0001", kr] +# for user in users: +# try: +# ComanageUser.sync_from_comanage(user) # Call the sync method on the user +# modeladmin.message_user(request, f"User {user} synced successfully!") +# except Exception as e: +# modeladmin.message_user(request, f"Error syncing user {user}: {str(e)}", level='debug') + +# # Action to sync users in bulk +# def sync_users_from_comanage(modeladmin, request, queryset): +# """ +# Sync the selected users from Comanage. +# """ +# if queryset.count() == 0: +# modeladmin.message_user(request, "No users selected to sync.", level=messages.WARNING) +# return + +# for user in queryset: +# try: +# # Assuming ComanageUser.sync_from_comanage expects user ID +# ComanageUser.sync_from_comanage(user.user_id) # Sync logic here +# modeladmin.message_user(request, f"User {user.name} synced successfully!", level=messages.SUCCESS) +# except Exception as e: +# modeladmin.message_user(request, f"Error syncing user {user.name}: {str(e)}", level=messages.ERROR) + +# # ComanageUserAdmin with additional customization and sync functionality +# class ComanageUserAdmin(admin.ModelAdmin): +# list_display = ['user_id', 'name', 'email', 'co_person_id'] +# search_fields = ['user_id', 'name', 'email', 'co_person_id'] +# readonly_fields = ['user_id', 'name', 'email', 'co_person_id', 'group_names', 'created_at', 'modified'] +# actions = [sync_users_from_comanage] + +# # Disable the "Add" button in the list view +# def has_add_permission(self, request): +# return False + +# # Disable the delete button for individual objects +# def has_delete_permission(self, request, obj=None): +# return False + +# def changelist_view(self, request, extra_context=None): +# """ +# Override the changelist view to trigger a function when the list of users is accessed. +# """ +# # Call your custom function here (e.g., sync users from Comanage) +# try: +# user = "kyre0001_amc" # Or you can filter users as needed +# ComanageUser.sync_from_comanage(user) # Call your sync function here +# self.message_user(request, "Users synced successfully!", level=messages.SUCCESS) +# except Exception as e: +# self.message_user(request, f"Error syncing users: {str(e)}", level=messages.ERROR) + +# # Proceed with the normal changelist view rendering +# return super().changelist_view(request, extra_context) + +# # Override the change_view method to remove the "Save" button and show custom sync button +# def change_view(self, request, object_id, form_url='', extra_context=None): +# extra_context = extra_context or {} +# extra_context['show_save_and_add_another'] = False +# extra_context['show_save_and_continue'] = False +# extra_context['show_save'] = False + +# return super().change_view(request, object_id, form_url, extra_context) + +# class ComanageGroupForm(forms.ModelForm): +# def __init__(self, *args, **kwargs): +# super(ComanageGroupForm, self).__init__(*args, **kwargs) +# instance = getattr(self, 'instance', None) +# group_members = None + +# if instance and instance.pk: +# # If the group instance exists, filter out the members already in the group +# group_members = instance.members.all() +# available_users = ComanageUser.objects.all() +# else: +# # If this is a new group, show all users +# available_users = ComanageUser.objects.all() + +# # Prepare choices for the 'members' field (users not already in the group) +# user_choices = [ +# (user.id, f'{user.name} ({user.user_id})') for user in available_users +# ] + +# # Set pre-selected members based on the group instance +# self.fields['members'].initial = group_members + +# # Set the required fields +# self.fields['gid'].required = False +# self.fields['members'].required = False + +# # Apply the filtered multi-select widget +# self.fields['members'].widget = admin.widgets.FilteredSelectMultiple( +# verbose_name='Members', +# is_stacked=False +# ) + +# # Assign the available choices dynamically +# self.fields['members'].choices = user_choices + +# class Meta: +# model = ComanageGroup +# fields = ['name', 'group_id', 'gid', 'created_at', 'modified', 'members'] + +# class ComanageGroupAdmin(admin.ModelAdmin): +# list_display = ['name', 'gid', 'get_members'] +# search_fields = ['name', 'group_id', 'gid'] +# readonly_fields = ['group_id', 'created_at', 'modified'] +# form = ComanageGroupForm + +# # Custom method to display group members +# def get_members(self, obj): +# """ +# Custom method to display all members of the group in the admin list view. +# """ +# return ", ".join([user.name for user in obj.members.all()]) + +# get_members.short_description = 'Group Members' + +# def get_queryset(self, request): +# queryset = super().get_queryset(request) +# # Optionally, modify the queryset if needed +# return queryset + +# admin.site.register(ComanageGroup, ComanageGroupAdmin) +# admin.site.register(ComanageUser, ComanageUserAdmin) + admin.site.register(IdTracker) diff --git a/rcamp/accounts/forms.py b/rcamp/accounts/forms.py index 2944561..7d0ad52 100644 --- a/rcamp/accounts/forms.py +++ b/rcamp/accounts/forms.py @@ -9,9 +9,11 @@ CsuLdapUser, RcLdapUser, AccountRequest, - REQUEST_ROLES + REQUEST_ROLES, + NSF_DISCIPLINES ) + class AccountRequestVerifyForm(forms.Form): """ An abstract form for verifying user credentials against a configured authority @@ -24,6 +26,7 @@ class AccountRequestVerifyForm(forms.Form): password = forms.CharField(max_length=255,widget=forms.PasswordInput,required=True) department = forms.CharField(max_length=128,required=True) role = forms.ChoiceField(choices=REQUEST_ROLES,required=True) + discipline = forms.ChoiceField(choices=NSF_DISCIPLINES, required=True) @sensitive_variables('password') def clean(self): diff --git a/rcamp/accounts/migrations/0004_accountrequest_discipline_alter_accountrequest_id_and_more.py b/rcamp/accounts/migrations/0004_accountrequest_discipline_alter_accountrequest_id_and_more.py new file mode 100644 index 0000000..3459823 --- /dev/null +++ b/rcamp/accounts/migrations/0004_accountrequest_discipline_alter_accountrequest_id_and_more.py @@ -0,0 +1,74 @@ +# Generated by Django 4.2.20 on 2025-04-02 13:58 + +import django.contrib.auth.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_auto_20180228_1158'), + ] + + operations = [ + migrations.AddField( + model_name='accountrequest', + name='discipline', + field=models.CharField(blank=True, choices=[('Computer & Information Services', 'Computer & Information Services'), ('Engineering', 'Engineering'), ('Geosciences, Atmospheric Sciences & Ocean Sciences', 'Geosciences, Atmospheric Sciences & Ocean Sciences'), ('Life Sciences', 'Life Sciences'), ('Mathematics and Statistics', 'Mathematics and Statistics'), ('Physical Sciences', 'Physical Sciences'), ('Psychology', 'Psychology'), ('Social Sciences', 'Social Sciences'), ('Other Sciences (not elsewhere classified)', 'Other Sciences (not elsewhere classified)'), ('Education', 'Education'), ('Law', 'Law'), ('Humanities', 'Humanities'), ('Visual & Performing Arts', 'Visual & Performing Arts'), ('Business Management and Business Administration', 'Business Management and Business Administration'), ('Communications, Communications Technologies, Journalism (Library Science is considered “Other Non-Science & Engineering Fields”)', 'Communications, Communications Technologies, Journalism (Library Science is considered “Other Non-Science & Engineering Fields”)'), ('Social Work', 'Social Work'), ('Other Non-Science & Engineering Fields', 'Other Non-Science & Engineering Fields')], max_length=256, null=True), + ), + migrations.AlterField( + model_name='accountrequest', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='accountrequest', + name='login_shell', + field=models.CharField(choices=[('/bin/bash', 'bash'), ('/bin/tcsh', 'tcsh')], default='/bin/bash', max_length=24), + ), + migrations.AlterField( + model_name='accountrequest', + name='organization', + field=models.CharField(choices=[('ucb', 'University of Colorado Boulder'), ('csu', 'Colorado State University'), ('xsede', 'XSEDE'), ('amc', 'University of Colorado Denver | Anschutz Medical Campus'), ('ncar', 'NCAR'), ('internal', 'Research Computing - Administrative')], max_length=128), + ), + migrations.AlterField( + model_name='accountrequest', + name='role', + field=models.CharField(choices=[('undergraduate', 'Undergraduate'), ('graduate', 'Graduate'), ('postdoc', 'Post Doc'), ('instructor', 'Instructor'), ('faculty', 'Faculty'), ('affiliated_faculty', 'Affiliated Faculty'), ('staff', 'Staff'), ('sponsored', 'Sponsored Affiliate')], default='undergraduate', max_length=24), + ), + migrations.AlterField( + model_name='accountrequest', + name='status', + field=models.CharField(choices=[('p', 'Pending'), ('a', 'Approved'), ('d', 'Denied'), ('i', 'Incomplete')], default='p', max_length=16), + ), + migrations.AlterField( + model_name='idtracker', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='intent', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='user', + name='last_name', + field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/rcamp/accounts/models.py b/rcamp/accounts/models.py index 807bfea..b8f8843 100644 --- a/rcamp/accounts/models.py +++ b/rcamp/accounts/models.py @@ -1,9 +1,11 @@ from django.db import models from django.conf import settings from django.utils import timezone +from django.core.exceptions import ValidationError from django.views.decorators.debug import sensitive_variables from django.contrib.auth.models import AbstractUser from lib import ldap_utils +#from lib.utils import get_user_and_groups, get_comanage_users_by_org, sync_group_to_comanage, get_group_id import ldapdb.models.fields as ldap_fields import ldapdb.models import logging @@ -29,6 +31,26 @@ ('sponsored','Sponsored Affiliate',), ) +NSF_DISCIPLINES = ( + ('Computer & Information Services', 'Computer & Information Services'), + ('Engineering', 'Engineering'), + ('Geosciences, Atmospheric Sciences & Ocean Sciences', 'Geosciences, Atmospheric Sciences & Ocean Sciences'), + ('Life Sciences', 'Life Sciences'), + ('Mathematics and Statistics', 'Mathematics and Statistics'), + ('Physical Sciences', 'Physical Sciences'), + ('Psychology', 'Psychology'), + ('Social Sciences', 'Social Sciences'), + ('Other Sciences (not elsewhere classified)', 'Other Sciences (not elsewhere classified)'), + ('Education', 'Education'), + ('Law', 'Law'), + ('Humanities', 'Humanities'), + ('Visual & Performing Arts', 'Visual & Performing Arts'), + ('Business Management and Business Administration', 'Business Management and Business Administration'), + ('Communications, Communications Technologies, Journalism (Library Science is considered “Other Non-Science & Engineering Fields”)', 'Communications, Communications Technologies, Journalism (Library Science is considered “Other Non-Science & Engineering Fields”)'), + ('Social Work', 'Social Work'), + ('Other Non-Science & Engineering Fields', 'Other Non-Science & Engineering Fields'), +) + ROLES = REQUEST_ROLES + ( ('pi','Principal Investigator',), ('admin','Admin',), @@ -75,7 +97,7 @@ class Meta: organization = models.CharField(max_length=128,choices=ORGANIZATIONS,blank=False,null=False) department = models.CharField(max_length=128,blank=True,null=True) role = models.CharField(max_length=24,choices=REQUEST_ROLES,default='undergraduate') - + discipline = models.CharField(max_length=256,choices=NSF_DISCIPLINES,blank=True, null=True) status = models.CharField(max_length=16,choices=STATUSES,default='p') approved_on = models.DateTimeField(null=True,blank=True) notes = models.TextField(null=True,blank=True) @@ -106,7 +128,8 @@ def save(self,*args,**kwargs): last_name=self.last_name, email=self.email, organization=self.organization, - role=self.role + role=self.role, + discipline=self.discipline, ) # Create associated auth user auth_user_defaults = dict( @@ -125,7 +148,16 @@ def save(self,*args,**kwargs): super(AccountRequest,self).save(*args,**kwargs) if manually_approved: - account_request_approved.send(sender=self.__class__,account_request=self) + logger = logging.getLogger('accounts') + logger.info('Sending account_request_approved signal') + account_request_approved.send(sender=self.__class__, account_request=self) + logger.info('Signal sent: account_request_approved') + + def clean(self): + # Custom validation to make discipline required if organization is 'xsede' + if not self.discipline: + raise ValidationError({'discipline': 'This field is required.'}) + super(AccountRequest, self).clean() class Intent(models.Model): account_request = models.OneToOneField( @@ -177,6 +209,7 @@ def get_next_id(self): class LdapUser(ldapdb.models.Model): class Meta: managed = False + abstract=True rdn_keys = ['username'] @@ -186,7 +219,7 @@ class Meta: full_name = ldap_fields.CharField(db_column='cn') email = ldap_fields.CharField(db_column='mail') # posixAccount - username = ldap_fields.CharField(db_column='uid') + username = ldap_fields.CharField(db_column='uid', primary_key=True) # ldap specific modified_date = ldap_fields.DateTimeField(db_column='modifytimestamp',blank=True) @@ -200,9 +233,6 @@ def save(self,*args,**kwargs): force_insert = kwargs.pop('force_insert',None) super(LdapUser,self).save(*args,**kwargs) - class Meta: - abstract=True - class RcLdapUserManager(models.Manager): def get_user_from_suffixed_username(self,suffixed_username): username, organization = ldap_utils.get_ldap_username_and_org(suffixed_username) @@ -439,7 +469,7 @@ def __init__(self,*args,**kwargs): # posixGroup attributes # gid = ldap_fields.IntegerField(db_column='gidNumber', unique=True) gid = ldap_fields.IntegerField(db_column='gidNumber',null=True,blank=True) - name = ldap_fields.CharField(db_column='cn', max_length=200) + name = ldap_fields.CharField(db_column='cn', max_length=200, primary_key=True) members = ldap_fields.ListField(db_column='memberUid',blank=True,null=True) def __str__(self): @@ -483,6 +513,104 @@ def save(self,*args,**kwargs): super(RcLdapGroup,self).save(*args,**kwargs) +# Uncomment when working on comanage integration again +# class ComanageUser(models.Model): +# class Meta: +# verbose_name = 'Comanage User' +# verbose_name_plural = 'Comanage Users' + +# user_id = models.CharField(max_length=255, unique=True) +# co_person_id = models.CharField(max_length=255, unique=True, blank=True, null=True) +# name = models.CharField(max_length=255) +# email = models.EmailField() +# created_at = models.DateTimeField() +# modified = models.DateTimeField() + +# # Fallback to store group names as a comma-separated list +# group_names = models.TextField(blank=True, null=True) + +# def __str__(self): +# return self.name + +# # Method to fetch and update data from Comanage +# @classmethod +# def sync_from_comanage(cls, user_id): +# # Fetch user data and groups from Comanage +# user_data, group_data = get_user_and_groups(user_id) + +# # Get or create the user +# user, created = cls.objects.update_or_create( +# user_id=user_data['user_id'], +# defaults={ +# 'co_person_id': user_data['co_person_id'], +# 'name': user_data['name'], +# 'email': user_data['email'], +# 'created_at': user_data['created_at'], +# 'modified': user_data['modified'] +# } +# ) + +# # Sync the groups +# if group_data: +# group_instances = [] +# for group in group_data: +# # Create or get the group instance +# group_instance, _ = ComanageGroup.objects.get_or_create( +# group_id=group['Id'], +# defaults={ +# 'name': group['Name'], +# 'created_at': group['Created'], +# 'modified': group['Modified'], +# 'gid': int(group['gidNumber']), +# } +# ) +# group_instance.gid = int(group['gidNumber']) +# group_instance.created_at = group['Created'] +# group_instance.modified = group['Modified'] +# group_instance.members.add(user) +# group_instance.member_uids = ', '.join([group['Name'] for group in group_data]) +# group_instance.save(immutable=True) +# group_instances.append(group_instance) + +# # You can also store the group names as a comma-separated list +# user.group_names = ', '.join([group['Name'] for group in group_data]) + +# # Save the user after syncing the groups +# user.save() + +# return user + +# Uncomment when working on comanage integration again +# class ComanageGroup(models.Model): +# class Meta: +# verbose_name = 'Comanage Group' +# verbose_name_plural = 'Comanage Groups' + +# name = models.CharField(max_length=255) +# group_id = models.CharField(max_length=255, unique=True) +# created_at = models.DateTimeField(null=True, blank=True) +# modified = models.DateTimeField(null=True, blank=True) +# members = models.ManyToManyField(ComanageUser, blank=True) +# gid = models.IntegerField(unique=True) + +# def __str__(self): +# return self.name + +# def save(self,*args,**kwargs): +# immutable = kwargs.pop('immutable', False) + +# if not immutable: +# # If no GID specified, auto-assign +# if self.gid is None: +# id_tracker = IdTracker.objects.get(category='posix') +# gid = id_tracker.get_next_id() +# self.gid = gid +# logger = logging.getLogger('accounts') +# logger.info('Auto-assigned GID to group: {}, {}'.format(gid, self.name)) +# if self.group_id is None or self.group_id == '': +# self.group_id = get_group_id(self) + +# super(ComanageGroup,self).save(*args,**kwargs) def date_to_sp_expire (date_, epoch=datetime.date(year=1970, day=1, month=1)): return (date_ - epoch).days diff --git a/rcamp/accounts/templates/account-request-review.html b/rcamp/accounts/templates/account-request-review.html index 6a1ae50..2d50b2a 100644 --- a/rcamp/accounts/templates/account-request-review.html +++ b/rcamp/accounts/templates/account-request-review.html @@ -67,6 +67,10 @@

Request Details

Organization {{ account_request.get_organization_display }} + + Discipline + {{ account_request.get_discipline_display }} + {% endblock %} diff --git a/rcamp/accounts/templates/account-request-verify-csu.html b/rcamp/accounts/templates/account-request-verify-csu.html index fb2190d..d26d422 100644 --- a/rcamp/accounts/templates/account-request-verify-csu.html +++ b/rcamp/accounts/templates/account-request-verify-csu.html @@ -122,6 +122,28 @@ {% endif %} + + {% if form.discipline.errors %} +
+ {% else %} +
+ {% endif %} + +
+ + {% if form.discipline.errors %} +
+ + {{ form.discipline.errors }} +
+ {% endif %} +
+
+
diff --git a/rcamp/accounts/templates/account-request-verify-ucb.html b/rcamp/accounts/templates/account-request-verify-ucb.html index 92ce6ea..43cb2c9 100644 --- a/rcamp/accounts/templates/account-request-verify-ucb.html +++ b/rcamp/accounts/templates/account-request-verify-ucb.html @@ -122,6 +122,27 @@ {% endif %}
+ + {% if form.discipline.errors %} +
+ {% else %} +
+ {% endif %} + +
+ + {% if form.discipline.errors %} +
+ + {{ form.discipline.errors }} +
+ {% endif %} +
+
diff --git a/rcamp/accounts/templates/change_list.html b/rcamp/accounts/templates/change_list.html new file mode 100644 index 0000000..52e02d2 --- /dev/null +++ b/rcamp/accounts/templates/change_list.html @@ -0,0 +1,22 @@ +{% extends "admin/change_list.html" %} + +{% block content %} +
+

User Change View

+ + +

{{ original.name }}

+

User ID: {{ original.user_id }}

+

Email: {{ original.email }}

+ + + {% if sync_button %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/rcamp/accounts/templates/comanage_sync_detail.html b/rcamp/accounts/templates/comanage_sync_detail.html new file mode 100644 index 0000000..005410e --- /dev/null +++ b/rcamp/accounts/templates/comanage_sync_detail.html @@ -0,0 +1,36 @@ +{% extends "admin/base.html" %} + +{% block extrahead %} + +{% endblock %} + +{% block content %} +

User Groups for {{ user_data.name }}

+ +

User ID: {{ user_data.user_id }}

+

Email: {{ user_data.email }}

+

Created At: {{ user_data.created_at }}

+

Modified: {{ user_data.modified }}

+ +

Groups

+ + + + + + + + + + + {% for group in groups %} + + + + + + {% endfor %} + +
Group NameCreated AtModified
{{ group.Name }}{{ group.Created }}{{ group.Modified }}
+ +{% endblock %} \ No newline at end of file diff --git a/rcamp/accounts/templates/comanage_sync_form.html b/rcamp/accounts/templates/comanage_sync_form.html new file mode 100644 index 0000000..a4d9632 --- /dev/null +++ b/rcamp/accounts/templates/comanage_sync_form.html @@ -0,0 +1,11 @@ + +{% extends "admin/base_site.html" %} + +{% block content %} +

Sync Comanage User

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/rcamp/accounts/templates/comanage_sync_list.html b/rcamp/accounts/templates/comanage_sync_list.html new file mode 100644 index 0000000..1924825 --- /dev/null +++ b/rcamp/accounts/templates/comanage_sync_list.html @@ -0,0 +1,50 @@ +{% extends 'admin/change_list.html' %} + +{% block extrahead %} + +{% endblock %} + +{% block content %} + + + +{% block search %} +
+
+ + +
+ +
+{% endblock %} + + + + + + + + + + + + + + {% for user in user_data %} + + + + + + + + + {% endfor %} + +
User IDNameEmailCreated AtModifiedActions
{{ user.user_id }}{{ user.name }}{{ user.email }}{{ user.created_at }}{{ user.modified }} + View Groups +
+ +{% endblock %} diff --git a/rcamp/accounts/urls.py b/rcamp/accounts/urls.py index 6ac6bdd..d5d791e 100644 --- a/rcamp/accounts/urls.py +++ b/rcamp/accounts/urls.py @@ -1,38 +1,40 @@ -from django.conf.urls import include, url +from django.urls import include, path, re_path +from . import admin from accounts.views import ( AccountRequestOrgSelectView, AccountRequestVerifyUcbView, AccountRequestVerifyCsuView, AccountRequestIntentView, - AccountRequestReviewView + AccountRequestReviewView, ) - urlpatterns = [ - url( + re_path( r'^account-request/create/organization$', AccountRequestOrgSelectView.as_view(), name='account-request-org-select' ), - url( + re_path( r'^account-request/create/verify/ucb$', AccountRequestVerifyUcbView.as_view(), name='account-request-verify-ucb' ), - url( + re_path( r'^account-request/create/verify/csu$', AccountRequestVerifyCsuView.as_view(), name='account-request-verify-csu' ), - url( + re_path( r'^account-request/create/intent$', AccountRequestIntentView.as_view(), name='account-request-intent' ), - url( + re_path( r'^account-request/review$', AccountRequestReviewView.as_view(), name='account-request-review' ), ] + +# removed path('admin/accounts/comanageuser//sync/', sync_user_from_comanage, name='accounts_comanageuser_sync'), \ No newline at end of file diff --git a/rcamp/accounts/views.py b/rcamp/accounts/views.py index 302caf3..906a03d 100644 --- a/rcamp/accounts/views.py +++ b/rcamp/accounts/views.py @@ -7,7 +7,7 @@ AccountRequest, Intent, CuLdapUser, - CsuLdapUser + CsuLdapUser, ) from accounts.forms import ( AccountRequestVerifyUcbForm, @@ -18,7 +18,7 @@ account_request_received, account_request_approved ) - +from django.shortcuts import render, get_object_or_404 class AccountRequestOrgSelectView(TemplateView): @@ -51,6 +51,7 @@ def form_valid(self, form): cu_user = CuLdapUser.objects.get(username=username) department = form.cleaned_data.get('department') role = form.cleaned_data.get('role') + discipline = form.cleaned_data.get('discipline') account_request_data = dict( username = cu_user.username, @@ -60,6 +61,7 @@ def form_valid(self, form): department = department, role = role, organization = organization, + discipline = discipline, ) # Auto-approve eligible users @@ -81,6 +83,7 @@ def form_valid(self, form): csu_user = CsuLdapUser.objects.get(username=username) department = form.cleaned_data.get('department') role = form.cleaned_data.get('role') + discipline = form.cleaned_data.get('discipline') account_request_data = dict( username = csu_user.username, @@ -89,7 +92,8 @@ def form_valid(self, form): email = csu_user.email, department = department, role = role, - organization = organization + organization = organization, + discipline = discipline, ) # Auto-approve all CSU users @@ -146,3 +150,27 @@ def get_context_data(self, **kwargs): return context except AccountRequest.DoesNotExist: raise Http404('Account Request not found.') + +# # View to display detailed group information +# def comanage_user_detail(request, user_id): +# user = get_object_or_404(ComanageUser, user_id=user_id) +# # Assume get_groups is a function that returns the groups for a user +# groups = get_groups(user_id) +# return render(request, 'comanage_sync_detail.html', { +# 'user_data': user, +# 'groups': groups +# }) + +# def sync_user_from_comanage(request, user_id): +# """ +# A custom view to sync user data from Comanage. +# """ +# try: +# user = ComanageUser.objects.get(id=user_id) +# user.sync_from_comanage(user_id=user.user_id) # Implement the logic to sync from Comanage +# message = "User synced successfully!" +# except ComanageUser.DoesNotExist: +# message = "User not found." + +# # Redirect back to the change page +# return redirect('admin:accounts_comanageuser_change', object_id=user_id) \ No newline at end of file diff --git a/rcamp/endpoints/filters.py b/rcamp/endpoints/filters.py index d4e37ae..f5f6a50 100644 --- a/rcamp/endpoints/filters.py +++ b/rcamp/endpoints/filters.py @@ -1,5 +1,6 @@ import django_filters import rest_framework +import pytz from accounts.models import AccountRequest from projects.models import Project @@ -11,6 +12,8 @@ class AccountRequestFilter(django_filters.rest_framework.FilterSet): max_request_date = django_filters.DateTimeFilter(field_name="request_date", lookup_expr='lte') min_approve_date = django_filters.DateTimeFilter(field_name="approved_on", lookup_expr='gte') max_approve_date = django_filters.DateTimeFilter(field_name="approved_on", lookup_expr='lte') + discipline = django_filters.CharFilter(field_name="discipline", lookup_expr='icontains') + class Meta: model = AccountRequest fields = [ @@ -19,6 +22,7 @@ class Meta: 'last_name', 'email', 'organization', + 'discipline', 'course_number', 'sponsor_email', 'resources_requested', diff --git a/rcamp/endpoints/serializers.py b/rcamp/endpoints/serializers.py index 339cb9a..4cc5b17 100644 --- a/rcamp/endpoints/serializers.py +++ b/rcamp/endpoints/serializers.py @@ -19,6 +19,7 @@ class Meta: 'last_name', 'email', 'organization', + 'discipline', 'course_number', 'sponsor_email', 'resources_requested', diff --git a/rcamp/endpoints/urls.py b/rcamp/endpoints/urls.py index e623f6e..839c0a1 100644 --- a/rcamp/endpoints/urls.py +++ b/rcamp/endpoints/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.urls import include, re_path from rest_framework import routers from endpoints.viewsets import AccountRequestList @@ -16,6 +16,6 @@ router.register(r'allocations', AllocationList) urlpatterns = [ - url(r'^', include(router.urls)), - url(r'^auth/',include('rest_framework.urls', namespace='rest_framework')), + re_path(r'^', include(router.urls)), + re_path(r'^auth/',include('rest_framework.urls', namespace='rest_framework')), ] diff --git a/rcamp/endpoints/viewsets.py b/rcamp/endpoints/viewsets.py index 3654099..da1a20d 100644 --- a/rcamp/endpoints/viewsets.py +++ b/rcamp/endpoints/viewsets.py @@ -22,14 +22,13 @@ from projects.models import Allocation -# class AccountRequestList(generics.ListAPIView): class AccountRequestList(viewsets.ModelViewSet): queryset = AccountRequest.objects.all() serializer_class = AccountRequestSerializer - filter_backends = (django_filters.rest_framework.DjangoFilterBackend,filters.SearchFilter,) permission_classes = (permissions.IsAuthenticated,) - search_fields = ('username','first_name','last_name','email',) - filter_class = AccountRequestFilter + search_fields = ('username', 'first_name', 'last_name', 'email',) + filter_backends = (django_filters.rest_framework.DjangoFilterBackend, filters.SearchFilter,) + filterset_class = AccountRequestFilter lookup_field = 'username' class ProjectList(viewsets.ModelViewSet): @@ -38,7 +37,7 @@ class ProjectList(viewsets.ModelViewSet): filter_backends = (django_filters.rest_framework.DjangoFilterBackend,filters.SearchFilter,) permission_classes = (permissions.IsAuthenticated,) search_fields = ('project_id','pi_emails') - filter_class = ProjectFilter + filterset_class = ProjectFilter lookup_field = 'project_id' class AllocationList(viewsets.ReadOnlyModelViewSet): @@ -47,5 +46,5 @@ class AllocationList(viewsets.ReadOnlyModelViewSet): filter_backends = (django_filters.rest_framework.DjangoFilterBackend,filters.SearchFilter,) permission_classes = (permissions.IsAuthenticated,) search_fields = ('allocation_id','start_date','end_date','created_on',) - filter_class = AllocationFilter + filterset_class = AllocationFilter lookup_field = 'allocation_id' diff --git a/rcamp/lib/fields.py b/rcamp/lib/fields.py index 5ddb373..23f227a 100644 --- a/rcamp/lib/fields.py +++ b/rcamp/lib/fields.py @@ -1,7 +1,8 @@ from django.db import models from django import forms from django.core.validators import validate_email -from django.utils.translation import ugettext_lazy as _, ungettext_lazy +from django.utils.translation import gettext_lazy as _, ngettext_lazy + import ast @@ -13,7 +14,7 @@ def __init__(self, *args, **kwargs): self.delimiter = kwargs.pop('delimiter',',') super(ListField, self).__init__(*args, **kwargs) - def from_db_value(self, value, expression, connection, context): + def from_db_value(self, value, expression, connection, context=None): if not value: return [] else: diff --git a/rcamp/lib/ldap_utils.py b/rcamp/lib/ldap_utils.py index bed1173..cf3e468 100644 --- a/rcamp/lib/ldap_utils.py +++ b/rcamp/lib/ldap_utils.py @@ -1,5 +1,4 @@ from django.conf import settings -from ldapdb import escape_ldap_filter import ldap diff --git a/rcamp/lib/utils.py b/rcamp/lib/utils.py new file mode 100644 index 0000000..815105e --- /dev/null +++ b/rcamp/lib/utils.py @@ -0,0 +1,47 @@ +# from comanage.sync_ldap_to_cilogon import get_co_user_and_groups, get_co_users, sync_co_group, get_co_group_id +# import logging + +# def get_comanage_users_by_org(org="amc"): +# users = get_co_users(org) +# return users + +# def get_comanage_user(user_id): +# # This is a placeholder for the actual API call to Comanage +# # Replace this with your actual function that fetches data from Comanage +# # For example, it could make an HTTP request to a Comanage API or use an SDK. + +# # Simulating user data returned from Comanage +# user_data = { +# 'user_id': user_id, +# 'name': f'User {user_id}', +# 'email': f'user{user_id}@example.com', +# 'role': 'Admin' if int(user_id) % 2 == 0 else 'User', +# 'created_at': '2025-02-15T10:00:00' +# } +# return user_data + +# def get_user_and_groups(uid): +# """ +# Fetch user data from Comanage using the UID. +# Replace this with your actual logic for fetching data. +# """ +# user_data, user_groups = get_co_user_and_groups(uid) + +# return user_data, user_groups + +# # def get_groups(uid): +# # """ +# # Fetch the groups for a given user. +# # Replace this with your actual logic to retrieve the user's groups. +# # """ +# # # Example mock data, replace with actual logic +# # return [{'name': 'Group A', 'role': 'Member'}, {'name': 'Group B', role: 'Member'}] if uid.endswith('1') else [{'name': 'Group C', 'role': 'Member'}] + + +# def sync_group_to_comanage(group): +# co_group_id = sync_co_group(group) +# return co_group_id + +# def get_group_id(group): +# co_group_id = get_co_group_id(group) +# return co_group_id \ No newline at end of file diff --git a/rcamp/lib/views.py b/rcamp/lib/views.py index 9453c87..a5cd517 100644 --- a/rcamp/lib/views.py +++ b/rcamp/lib/views.py @@ -1,4 +1,3 @@ -from django.shortcuts import render_to_response from django.shortcuts import render from django.template import RequestContext from django.shortcuts import redirect diff --git a/rcamp/mailer/migrations/0003_alter_maillog_id_alter_mailnotifier_event_and_more.py b/rcamp/mailer/migrations/0003_alter_maillog_id_alter_mailnotifier_event_and_more.py new file mode 100644 index 0000000..515f851 --- /dev/null +++ b/rcamp/mailer/migrations/0003_alter_maillog_id_alter_mailnotifier_event_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.20 on 2025-04-02 13:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0002_allocation_expiration_mailer_field_choices'), + ] + + operations = [ + migrations.AlterField( + model_name='maillog', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='mailnotifier', + name='event', + field=models.CharField(choices=[('account_created_from_request', 'account_created_from_request'), ('account_request_approved', 'account_request_approved'), ('account_request_received', 'account_request_received'), ('allocation_created_from_request', 'allocation_created_from_request'), ('allocation_expired', 'allocation_expired'), ('allocation_expiring', 'allocation_expiring'), ('allocation_request_created_by_user', 'allocation_request_created_by_user'), ('project_created_by_user', 'project_created_by_user')], max_length=128), + ), + migrations.AlterField( + model_name='mailnotifier', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/rcamp/mailer/receivers.py b/rcamp/mailer/receivers.py index e9a1a20..dda95e3 100644 --- a/rcamp/mailer/receivers.py +++ b/rcamp/mailer/receivers.py @@ -4,77 +4,60 @@ @receiver(account_request_received) -def notify_account_request_received(sender, **kwargs): - account_request = kwargs.get('account_request') - +def notify_account_request_received(sender, account_request, **kwargs): notifiers = MailNotifier.objects.filter(event='account_request_received') for notifier in notifiers: ctx = {'account_request':account_request} msg = notifier.send(context=ctx) @receiver(account_request_approved) -def notify_account_request_approved(sender, **kwargs): - account_request = kwargs.get('account_request') - +def notify_account_request_approved(sender, account_request, **kwargs): notifiers = MailNotifier.objects.filter(event='account_request_approved') for notifier in notifiers: ctx = {'account_request':account_request} msg = notifier.send(context=ctx) @receiver(account_created_from_request) -def notify_account_created_from_request(sender, **kwargs): - account = kwargs.get('account') - +def notify_account_created_from_request(sender, account, **kwargs): notifiers = MailNotifier.objects.filter(event='account_created_from_request') for notifier in notifiers: ctx = {'account':account} msg = notifier.send(context=ctx) @receiver(project_created_by_user) -def notify_project_created_by_user(sender, **kwargs): - project = kwargs.get('project') - +def notify_project_created_by_user(sender, project, **kwargs): notifiers = MailNotifier.objects.filter(event='project_created_by_user') for notifier in notifiers: ctx = {'project':project} msg = notifier.send(context=ctx) @receiver(allocation_request_created_by_user) -def notify_allocation_request_created_by_user(sender, **kwargs): - ar = kwargs.get('allocation_request') - requester = kwargs.get('requester') - +def notify_allocation_request_created_by_user(sender, allocation_request, requester, **kwargs): notifiers = MailNotifier.objects.filter(event='allocation_request_created_by_user') for notifier in notifiers: ctx = { - 'allocation_request': ar, + 'allocation_request': allocation_request, 'requester': requester } msg = notifier.send(context=ctx) @receiver(allocation_created_from_request) -def notify_allocation_created_from_request(sender, **kwargs): - alloc = kwargs.get('allocation') - +def notify_allocation_created_from_request(sender, allocation, **kwargs): notifiers = MailNotifier.objects.filter(event='allocation_created_from_request') for notifier in notifiers: - ctx = {'allocation':alloc} + ctx = {'allocation':allocation} msg = notifier.send(context=ctx) @receiver(allocation_expiring) -def notify_allocation_expiring(sender, **kwargs): - alloc = kwargs.get('allocation') - +def notify_allocation_expiring(sender, allocation, **kwargs): notifiers = MailNotifier.objects.filter(event='allocation_expiring') for notifier in notifiers: - ctx = {'allocation':alloc} + ctx = {'allocation':allocation} msg = notifier.send(context=ctx) @receiver(allocation_expired) -def notify_allocation_expired(sender, **kwargs): - alloc = kwargs.get('allocation') - +def notify_allocation_expired(sender, allocation, **kwargs): notifiers = MailNotifier.objects.filter(event='allocation_expired') for notifier in notifiers: - ctx = {'allocation':alloc} + ctx = {'allocation':allocation} msg = notifier.send(context=ctx) diff --git a/rcamp/mailer/signals.py b/rcamp/mailer/signals.py index c6e1570..b84fc86 100644 --- a/rcamp/mailer/signals.py +++ b/rcamp/mailer/signals.py @@ -1,15 +1,14 @@ import django.dispatch +account_request_received = django.dispatch.Signal() +account_request_approved = django.dispatch.Signal() +account_created_from_request = django.dispatch.Signal() -account_request_received = django.dispatch.Signal(providing_args=['account_request']) -account_request_approved = django.dispatch.Signal(providing_args=['account_request']) -account_created_from_request = django.dispatch.Signal(providing_args=['account']) +project_created_by_user = django.dispatch.Signal() -project_created_by_user = django.dispatch.Signal(providing_args=['project']) +allocation_request_created_by_user = django.dispatch.Signal() -allocation_request_created_by_user = django.dispatch.Signal(providing_args=['allocation_request']) +allocation_created_from_request = django.dispatch.Signal() -allocation_created_from_request = django.dispatch.Signal(providing_args=['allocation']) - -allocation_expiring = django.dispatch.Signal(providing_args=['allocation']) -allocation_expired = django.dispatch.Signal(providing_args=['allocation']) +allocation_expiring = django.dispatch.Signal() +allocation_expired = django.dispatch.Signal() diff --git a/rcamp/manage.py b/rcamp/manage.py index f27dc4d..6e7f288 100755 --- a/rcamp/manage.py +++ b/rcamp/manage.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 import os import sys +import django if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rcamp.settings") from django.core.management import execute_from_command_line + + print("Django version:", django.get_version()) execute_from_command_line(sys.argv) diff --git a/rcamp/projects/migrations/0006_alter_allocation_id_alter_allocationrequest_id_and_more.py b/rcamp/projects/migrations/0006_alter_allocation_id_alter_allocationrequest_id_and_more.py new file mode 100644 index 0000000..eec3cc9 --- /dev/null +++ b/rcamp/projects/migrations/0006_alter_allocation_id_alter_allocationrequest_id_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.20 on 2025-04-02 13:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0005_allocation_expiration_notice_sent'), + ] + + operations = [ + migrations.AlterField( + model_name='allocation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='allocationrequest', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='allocationrequest', + name='proposal', + field=models.FileField(blank=True, null=True, upload_to='proposals/%Y/%m/%d'), + ), + migrations.AlterField( + model_name='allocationrequest', + name='status', + field=models.CharField(choices=[('a', 'Approved'), ('d', 'Denied'), ('w', 'Waiting'), ('h', 'Hold'), ('r', 'Ready For Review'), ('q', 'Response Requested'), ('i', 'Denied - Insufficient Resources'), ('x', 'Denied - Proposal Incomplete'), ('f', 'Approved - Fully Funded'), ('p', 'Approved - Partially Funded')], default='w', max_length=16), + ), + migrations.AlterField( + model_name='project', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='project', + name='organization', + field=models.CharField(choices=[('ucb', 'University of Colorado Boulder'), ('csu', 'Colorado State University'), ('xsede', 'XSEDE'), ('amc', 'AMC')], max_length=128), + ), + migrations.AlterField( + model_name='reference', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/rcamp/projects/templates/project-list.html b/rcamp/projects/templates/project-list.html index 9ed5ef3..2a5b917 100644 --- a/rcamp/projects/templates/project-list.html +++ b/rcamp/projects/templates/project-list.html @@ -45,9 +45,9 @@

{{ project.title }}

{% endfor %} - +

Create a new project

-

Create a new Research Computing project.

+

Create a new Research Computing project by requesting an allocation.

diff --git a/rcamp/projects/urls.py b/rcamp/projects/urls.py index 8dd36f5..02860db 100644 --- a/rcamp/projects/urls.py +++ b/rcamp/projects/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.urls import include, re_path from django.contrib.auth.decorators import login_required from projects.views import ProjectListView from projects.views import ProjectDetailView @@ -11,13 +11,13 @@ from projects.views import AllocationRequestCreateView urlpatterns = [ - url(r'^list$', login_required(ProjectListView.as_view()), name='project-list'), - url(r'^list/(?P[-\w]+)/$', login_required(ProjectDetailView.as_view()), name='project-detail'), - url(r'^list/(?P[-\w]+)/edit$', login_required(ProjectEditView.as_view()), name='project-edit'), - url(r'^create$', login_required(ProjectCreateView.as_view()), name='project-create'), - url(r'^list/(?P[-\w]+)/references/(?P[-\w]+)/$', login_required(ReferenceDetailView.as_view()), name='reference-detail'), - url(r'^list/(?P[-\w]+)/references/(?P[-\w]+)/edit$', login_required(ReferenceEditView.as_view()), name='reference-edit'), - url(r'^list/(?P[-\w]+)/references/create$', login_required(ReferenceCreateView.as_view()), name='reference-create'), - url(r'^list/(?P[-\w]+)/allocationrequests/(?P[-\w]+)/$', login_required(AllocationRequestDetailView.as_view()), name='allocation-request-detail'), - url(r'^list/(?P[-\w]+)/allocationrequests/create$', login_required(AllocationRequestCreateView.as_view()), name='allocation-request-create'), + re_path(r'^list$', login_required(ProjectListView.as_view()), name='project-list'), + re_path(r'^list/(?P[-\w]+)/$', login_required(ProjectDetailView.as_view()), name='project-detail'), + re_path(r'^list/(?P[-\w]+)/edit$', login_required(ProjectEditView.as_view()), name='project-edit'), + re_path(r'^create$', login_required(ProjectCreateView.as_view()), name='project-create'), + re_path(r'^list/(?P[-\w]+)/references/(?P[-\w]+)/$', login_required(ReferenceDetailView.as_view()), name='reference-detail'), + re_path(r'^list/(?P[-\w]+)/references/(?P[-\w]+)/edit$', login_required(ReferenceEditView.as_view()), name='reference-edit'), + re_path(r'^list/(?P[-\w]+)/references/create$', login_required(ReferenceCreateView.as_view()), name='reference-create'), + re_path(r'^list/(?P[-\w]+)/allocationrequests/(?P[-\w]+)/$', login_required(AllocationRequestDetailView.as_view()), name='allocation-request-detail'), + re_path(r'^list/(?P[-\w]+)/allocationrequests/create$', login_required(AllocationRequestCreateView.as_view()), name='allocation-request-create'), ] diff --git a/rcamp/rcamp/settings/databases.py b/rcamp/rcamp/settings/databases.py index e198d67..cf35804 100644 --- a/rcamp/rcamp/settings/databases.py +++ b/rcamp/rcamp/settings/databases.py @@ -29,6 +29,12 @@ 'USER': str(os.environ.get('RCAMP_CSU_LDAP_USER')), 'PASSWORD': str(os.environ.get('RCAMP_CSU_LDAP_PASSWORD')), }, + 'ldapci': { + 'ENGINE': 'ldapdb.backends.ldap', + 'NAME': str(os.environ.get('RCAMP_RC_CI_LDAP_URI')), + 'USER': str(os.environ.get('RCAMP_RC_CI_LDAP_USER')), + 'PASSWORD': str(os.environ.get('RCAMP_RC_CI_LDAP_PASSWORD')), + } } LDAPCONFS = { @@ -61,6 +67,19 @@ 'group_dn': 'ou=eIdentityUsers,dc=ColoState,dc=edu', 'people_dn': 'ou=eIdentityUsers,dc=ColoState,dc=edu', }, + 'ldapci': { + 'server': str(DATABASES['ldapci']['NAME']), + 'bind_dn': str(DATABASES['ldapci']['USER']), + 'bind_pw': str(DATABASES['ldapci']['PASSWORD']), + 'base_dn': 'dc=rc,dc=int,dc=colorado,dc=edu', + 'people_dn': 'ou=people,dc=rc,dc=int,dc=colorado,dc=edu', + 'ucb_dn': 'ou=ucb,ou=people,dc=rc,dc=int,dc=colorado,dc=edu', + 'csu_dn': 'ou=csu,ou=people,dc=rc,dc=int,dc=colorado,dc=edu', + 'xsede_dn': 'ou=xsede,ou=people,dc=rc,dc=int,dc=colorado,dc=edu', + 'amc_dn': 'ou=amc,ou=people,dc=rc,dc=int,dc=colorado,dc=edu', + 'internal_dn': 'ou=internal,ou=people,dc=rc,dc=int,dc=colorado,dc=edu', + 'group_dn': 'ou=groups,dc=rc,dc=int,dc=colorado,dc=edu', + } } DATABASE_ROUTERS = ['lib.router.LdapRouter',] diff --git a/rcamp/rcamp/settings/main.py b/rcamp/rcamp/settings/main.py index 33e6673..48f7061 100644 --- a/rcamp/rcamp/settings/main.py +++ b/rcamp/rcamp/settings/main.py @@ -6,13 +6,13 @@ SECRET_KEY = os.environ.get('RCAMP_SECRET_KEY') +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' hosts = os.environ.get('RCAMP_ALLOWEDHOSTS') ALLOWED_HOSTS = hosts.split(',') INSTALLED_APPS = [ - 'grappelli', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -29,6 +29,7 @@ 'mailer', 'accounts', 'projects', + 'comanage', ] if DEBUG: diff --git a/rcamp/rcamp/static/css/custom-admin.css b/rcamp/rcamp/static/css/custom-admin.css new file mode 100644 index 0000000..8c1b68f --- /dev/null +++ b/rcamp/rcamp/static/css/custom-admin.css @@ -0,0 +1,129 @@ +/* Ensuring the custom page inherits basic admin styles */ +@import url('/static/admin/css/widgets.css'); /* Include the admin widgets */ +@import url('/static/admin/css/base.css'); /* Include the base admin styles */ + +/* Custom styling for the search form */ +.search-form { + margin-bottom: 20px; + padding: 10px; + border: 1px solid #ddd; + background-color: #f9f9f9; + border-radius: 8px; + max-width: 600px; /* Adjust the width to your liking */ + margin-left: auto; + margin-right: auto; +} + +.search-form input { + width: 100%; + padding: 10px; + border-radius: 4px; + border: 1px solid #ccc; + font-size: 14px; /* Smaller font for the input */ +} + +.search-form button { + width: 100%; + padding: 10px; + margin-top: 10px; + font-size: 16px; + border-radius: 4px; +} + +/* Optional: Add some spacing around the form */ +.search-form .form-group { + margin-bottom: 15px; +} + +/* Optional: Style for the label */ +.search-form label { + font-weight: bold; +} + +/* Add some spacing between the div and the table */ +div { + padding-bottom: 20px; +} + +/* Style for the button */ +.button { + display: inline-block; + padding: 15px 30px; /* Larger padding for a bigger button */ + font-size: 18px; /* Adjust font size to make it larger */ + background-color: #007bff; /* Blue background color */ + color: white; /* White text */ + text-align: center; + border-radius: 5px; /* Rounded corners */ + text-decoration: none; /* Remove underline */ + transition: background-color 0.3s; /* Smooth hover effect */ +} + +.button:hover { + background-color: #0056b3; /* Darker blue on hover */ +} + +/* Optional: Add some space between the button and other elements */ +.button { + margin-top: 10px; /* Adjust as needed */ +} + +/* Table Styling */ +.table { + width: 100%; + margin-bottom: 1rem; + background-color: transparent; + border: 1px solid #ddd; +} + +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #ddd; +} + +.table th { + background-color: #f9f9f9; + font-weight: bold; +} + +.table-bordered { + border: 1px solid #ddd; +} + +.table-striped tbody tr:nth-child(odd) { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover { + background-color: #f1f1f1; +} + +/* Action Links Styling */ +table td a { + color: #007bff; + text-decoration: none; +} + +table td a:hover { + color: #0056b3; + text-decoration: underline; +} + +/* Header Styling */ +h1 { + font-size: 1.75rem; + margin-bottom: 5px; + color: #333; +} + +/* Responsive adjustments */ +@media screen and (max-width: 768px) { + .table th, .table td { + font-size: 0.875rem; + } + + .table td a { + font-size: 0.875rem; + } +} diff --git a/rcamp/rcamp/urls.py b/rcamp/rcamp/urls.py index 608ea0c..3fbc24d 100644 --- a/rcamp/rcamp/urls.py +++ b/rcamp/rcamp/urls.py @@ -13,7 +13,7 @@ 1. Add an import: from blog import urls as blog_urls 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) """ -from django.conf.urls import include, url +from django.urls import include, re_path from django.contrib import admin from django.contrib.auth import views as auth_views from django.views.generic import TemplateView @@ -31,14 +31,14 @@ handler500 = 'lib.views.handler500' urlpatterns = [ - url(r'^grappelli/', include('grappelli.urls')), # grappelli URLS - url(r'^$', index_view, name='index'), - url(r'^login', auth_views.LoginView.as_view(template_name='registration/login.html')), - url(r'^logout', auth_views.LogoutView.as_view(template_name='registration/logout.html')), - url(r'^admin/', admin.site.urls), - url(r'^api/', include('endpoints.urls')), - url(r'^accounts/', include(('accounts.urls', 'accounts'), namespace='accounts')), - url(r'^projects/', include(('projects.urls', 'projects'), namespace='projects')), + re_path(r'^grappelli/', include('grappelli.urls')), # grappelli URLS + re_path(r'^$', index_view, name='index'), + re_path(r'^login', auth_views.LoginView.as_view(template_name='registration/login.html')), + re_path(r'^logout', auth_views.LogoutView.as_view(template_name='registration/logout.html')), + re_path(r'^admin/', admin.site.urls), + re_path(r'^api/', include('endpoints.urls')), + re_path(r'^accounts/', include(('accounts.urls', 'accounts'), namespace='accounts')), + re_path(r'^projects/', include(('projects.urls', 'projects'), namespace='projects')), ] diff --git a/rcamp/rcamp/wsgi.py b/rcamp/rcamp/wsgi.py index 0d88abf..aca0a59 100644 --- a/rcamp/rcamp/wsgi.py +++ b/rcamp/rcamp/wsgi.py @@ -8,9 +8,19 @@ """ import os +import debugpy +import logging from django.core.wsgi import get_wsgi_application +# Uncomment for remote debugging in container +# debugpy.listen(('0.0.0.0', 5678)) # 5678 is the default debug port +# logger = logging.getLogger('admin') +# logger.info("Debugger is listening on port 5678...") + +# # Optionally, you can make the process wait for a debugger to attach +# debugpy.wait_for_client() + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rcamp.settings") application = get_wsgi_application() diff --git a/rcamp/templates/admin/accounts/comanageuser/change_form.html b/rcamp/templates/admin/accounts/comanageuser/change_form.html new file mode 100644 index 0000000..fce0415 --- /dev/null +++ b/rcamp/templates/admin/accounts/comanageuser/change_form.html @@ -0,0 +1,23 @@ +{% extends "admin/change_form.html" %} + +{% block content %} +
+

User Change View

+ + +

{{ original.name }}

+

User ID: {{ original.user_id }}

+

Email: {{ original.email }}

+ + + {% if sync_button %} +
+ {% csrf_token %} + +
+ {% endif %} +
+ + {{ block.super }} +{% endblock %} + diff --git a/rcamp/tests/test_accounts_forms.py b/rcamp/tests/test_accounts_forms.py index e98808e..fba0262 100644 --- a/rcamp/tests/test_accounts_forms.py +++ b/rcamp/tests/test_accounts_forms.py @@ -29,13 +29,15 @@ first_name = 'Test', last_name = 'User', email = 'testuser@test.org', - edu_affiliation = 'faculty' + edu_affiliation = 'faculty', + discipline = 'Social Sciences', ) mock_csu_user_defaults = dict( username = 'testuser', first_name = 'Test', last_name = 'User', email = 'testuser@test.org', + discipline = 'Social Sciences', ) @@ -48,6 +50,7 @@ def test_form_valid(self): 'password': 'testpass', 'role': 'faculty', 'department': 'physics', + 'discipline': 'Social Sciences', } form = AccountRequestVerifyUcbForm(data=form_data) with mock.patch('accounts.models.CuLdapUser.objects.get',return_value=mock_cu_user): @@ -61,6 +64,7 @@ def test_form_invalid_bad_user(self): 'password': 'testpass', 'role': 'faculty', 'department': 'physics', + 'discipline': 'Social Sciences', } form = AccountRequestVerifyUcbForm(data=form_data) with mock.patch('accounts.models.CuLdapUser.objects.get',side_effect=[CuLdapUser.DoesNotExist]): @@ -154,6 +158,7 @@ def test_csu_form_valid(self): 'password': 'testpass', 'role': 'faculty', 'department': 'physics', + 'discipline': 'Social Sciences', } form = AccountRequestVerifyCsuForm(data=form_data) with mock.patch('accounts.models.CsuLdapUser.objects.get',return_value=mock_csu_user): @@ -167,6 +172,7 @@ def test_csu_form_invalid_bad_creds(self): 'password': 'testpass', 'role': 'faculty', 'department': 'physics', + 'discipline': 'Social Sciences', } form = AccountRequestVerifyCsuForm(data=form_data) with mock.patch('accounts.models.CsuLdapUser.objects.get',return_value=mock_csu_user): @@ -184,6 +190,7 @@ def setUp(self): 'last_name': 'user', 'email': 'testuser@test.org', 'login_shell': '/bin/bash', + 'discipline': 'Law', 'status': 'p' } @@ -198,6 +205,7 @@ def test_form_valid_create_approve_request(self): 'role': 'faculty', 'department': 'physics', 'login_shell': '/bin/bash', + 'discipline': 'Law', 'status': 'p' } with mock.patch('accounts.models.CuLdapUser.objects.get',return_value=mock_cu_user): diff --git a/rcamp/tests/test_accounts_models.py b/rcamp/tests/test_accounts_models.py index 5d50c6b..c6607ed 100644 --- a/rcamp/tests/test_accounts_models.py +++ b/rcamp/tests/test_accounts_models.py @@ -168,6 +168,7 @@ def get_account_request_defaults(): email = 'testuser@colorado.edu', role = 'faculty', department = 'physics', + discipline = 'Law', organization = 'ucb' ) return account_request_defaults diff --git a/rcamp/tests/test_accounts_views.py b/rcamp/tests/test_accounts_views.py index f82cf96..f9c763a 100644 --- a/rcamp/tests/test_accounts_views.py +++ b/rcamp/tests/test_accounts_views.py @@ -33,7 +33,8 @@ def get_account_request_verify_defaults(): username = 'testuser', password = 'testpass', role = 'faculty', - department = 'physics' + department = 'physics', + discipline = 'Education', ) return verification_defaults diff --git a/rcamp/tests/test_endpoints.py b/rcamp/tests/test_endpoints.py index f287503..bf8b348 100644 --- a/rcamp/tests/test_endpoints.py +++ b/rcamp/tests/test_endpoints.py @@ -1,5 +1,6 @@ from django.test import TestCase from django.test import override_settings +from django.utils import timezone import pytz import json import datetime @@ -45,6 +46,8 @@ def setUp(self): resources_requested='summit', organization='ucb', role='staff', + discipline='Law', + approved_on=timezone.make_aware(datetime.datetime(2016, 5, 1), timezone=pytz.timezone('America/Denver')), status='p' ) self.ar1 = AccountRequest.objects.create(**ar_dict) @@ -53,11 +56,11 @@ def setUp(self): username='testuser2', email='tu2@tu.org', role='faculty', - approved_on=pytz.timezone('America/Denver').localize(datetime.datetime(2016,4,1)), + approved_on=timezone.make_aware(datetime.datetime(2016, 3, 1), timezone=pytz.timezone('America/Denver')), status='a' )) self.ar2 = AccountRequest.objects.create(**ar_dict) - + del ar_dict['resources_requested'] ar_dict.update(dict( username='testuser3', @@ -65,7 +68,7 @@ def setUp(self): status='a', notes='approved!', id_verified_by='admin', - approved_on=pytz.timezone('America/Denver').localize(datetime.datetime(2016,5,1)), + approved_on=timezone.make_aware(datetime.datetime(2016, 5, 1), timezone=pytz.timezone('America/Denver')) )) self.ar3 = AccountRequest.objects.create(**ar_dict) @@ -82,6 +85,7 @@ def test_ar_list(self): 'last_name': 'user', 'resources_requested': 'summit', 'organization': 'ucb', + 'discipline': 'Law', 'email': 'tu@tu.org', }, { @@ -92,7 +96,8 @@ def test_ar_list(self): 'resources_requested': 'summit', 'organization': 'ucb', 'email': 'tu2@tu.org', - 'approved_on': '2016-04-01T00:00:00Z' + 'discipline': 'Law', + 'approved_on': '2016-03-01T00:00:00Z' }, { 'username': 'testuser3', @@ -102,6 +107,7 @@ def test_ar_list(self): 'resources_requested': None, 'notes': 'approved!', 'organization': 'ucb', + 'discipline': 'Law', 'email': 'tu3@tu.org', } ] @@ -117,6 +123,7 @@ def test_ar_post(self): last_name = 'user', resources_requested = 'summit', organization = 'ucb', + discipline = 'Law', email = 'newtu@tu.org', ) res = self.client.post('/api/accountrequests/', data=post_data) @@ -133,6 +140,7 @@ def test_ar_detail(self): 'last_name': 'user', 'resources_requested': 'summit', 'organization': 'ucb', + 'discipline': 'Law', 'email': 'tu@tu.org', } self.assertDictContainsSubset(expected_content,res_content) @@ -140,7 +148,7 @@ def test_ar_detail(self): def test_ar_filter_dates(self): res = self.client.get( '/api/accountrequests/?min_approve_date={}&max_approve_date={}'.format( - '2016-03-31', + '2016-03-29', '2016-04-01' ) ) @@ -154,7 +162,8 @@ def test_ar_filter_dates(self): 'resources_requested': 'summit', 'organization': 'ucb', 'email': 'tu2@tu.org', - 'approved_on': '2016-04-01T00:00:00Z' + 'discipline': 'Law', + 'approved_on': '2016-03-01T00:00:00Z' } ] res_content = json.loads(res.content) @@ -177,7 +186,8 @@ def test_ar_search(self): 'resources_requested': 'summit', 'organization': 'ucb', 'email': 'tu2@tu.org', - 'approved_on': '2016-04-01T00:00:00Z' + 'discipline': 'Law', + 'approved_on': '2016-03-01T00:00:00Z' } ] res_content = json.loads(res.content) diff --git a/rcamp/tests/utilities/ldap.py b/rcamp/tests/utilities/ldap.py index 39cadfc..65dd945 100644 --- a/rcamp/tests/utilities/ldap.py +++ b/rcamp/tests/utilities/ldap.py @@ -16,7 +16,6 @@ def get_ldap_user_defaults(): last_name = 'User', full_name = 'User, Test', email = 'testuser@colorado.edu', - modified_date=timezone.make_aware(datetime.datetime(2015,11,0o6,0o3,43,24), timezone.get_default_timezone()), uid = 1010, gid = 1010, gecos='Test User,,,', diff --git a/rcamp/tests/utilities/utils.py b/rcamp/tests/utilities/utils.py index 04d21c0..b44e95a 100644 --- a/rcamp/tests/utilities/utils.py +++ b/rcamp/tests/utilities/utils.py @@ -20,10 +20,10 @@ def assert_test_env(): # We can reasonably assume that no production resource will satisfy this criteria, so # this is one of several safeguards against running the functional tests against prod. assert os.environ.get('RCAMP_DEBUG') == 'True' - assert settings.DATABASES['rcldap']['PASSWORD'] == 'password' + assert settings.DATABASES['rcldap']['PASSWORD'] == 'admin' # In an abundance of caution, also make sure that the LDAP and MySQL connections are configured # to use the test services. - assert 'ldap' in settings.DATABASES['rcldap']['NAME'] + assert 'rcamp' in settings.DATABASES['rcldap']['NAME'] assert 'database' in settings.DATABASES['default']['HOST'] # Probably not running against prod backends. return True diff --git a/requirements.txt b/requirements.txt index 15e4af0..3e913d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,34 @@ -Django>=2.2,<3 -django-crispy-forms==1.8.1 +asgiref==3.8.1 +certifi==2025.1.31 +charset-normalizer==3.4.1 +debugpy==1.8.13 +Django==5.2 +django-auth-ldap==5.1.0 +django-crispy-forms==1.13.0 django-easy-pjax==1.2.0 -django-filter==2.2.0 -django-grappelli==2.13.4 -django-material==1.7.1 -djangorestframework==3.10.3 -funcparserlib==0.3.6 +django-extensions==3.2.3 +django-filter==22.1 +django-grappelli==4.0.1 +django-ldapdb @ git+https://oauth2:glpat-T4wkcqXufHvA53DClNJp_W86MQp1OjE5CA.01.0y13pb1es@gitlab.rc.int.colorado.edu/rc-ops/django-ldap-rc.git@feature/python3.11-update +django-material==1.11.3 +django-mysql==4.16.0 +django-utils==0.0.2 +djangorestframework==3.15.2 funcsigs==0.4 -mock==1.3.0 -mockldap==0.2.6 -mysqlclient==1.3.13 -pbr==1.8.1 -python-dateutil==2.5.3 -python-ldap==3.2.0 +idna==3.10 +mock==4.0.3 +mysqlclient==2.2.7 +pbr==5.8.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 +python-dateutil==2.8.2 +python-dotenv==1.0.1 +python-ldap==3.4.4 python-pam==1.8.4 -pytz==2020.1 -six==1.14.0 -uWSGI==2.0.19 +pytz==2025.1 +requests==2.32.3 +six==1.16.0 +sqlparse==0.5.3 +typing_extensions==4.12.2 +urllib3==2.3.0 +uWSGI==2.0.31