Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use an official Python runtime as a parent image
FROM python:3.12-slim
FROM python:3.12-slim

# Set the working directory in the container
WORKDIR /app
Expand Down
16 changes: 9 additions & 7 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ services:
restart: always
container_name: db
environment:
POSTGRES_PASSWORD: ${POSTGRESS_ROOT_PASSWORD}
POSTGRES_PASSWORD: ${POSTGRES_ROOT_PASSWORD}
POSTGRES_DB: fablab
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- 5432:5432
healthcheck:
test: ["CMD", "pg_isready", "-U", "fablab"]
test: ["CMD", "pg_isready -d postgres -U fablab"]
interval: 10s
retries: 10
start_period: 60s # Give it more time to start
Expand All @@ -33,7 +34,7 @@ services:
- db
- rabbitmq
environment:
- DATABASE_URL=postgres://postgres:${POSTGRESS_ROOT_PASSWORD}@db:5432/fablab
- DATABASE_URL=postgres://postgres:${POSTGRES_ROOT_PASSWORD}@db:5432/fablab
- NINER_ENGAGE_COOKIE=${NINER_ENGAGE_COOKIE}
- NINER_ENGAGE_TOKEN=${NINER_ENGAGE_TOKEN}
- NINER_ENGAGE_PAYLOAD_TOKEN=${NINER_ENGAGE_PAYLOAD_TOKEN}
Expand All @@ -46,12 +47,12 @@ services:
restart: always
build:
context: .
command: celery -A superfablab worker -l info
entrypoint: celery -A superfablab worker -l info
volumes:
- ./superfablab:/app
environment:
- CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672
- DATABASE_URL=postgres://postgres:${POSTGRESS_ROOT_PASSWORD}@db:5432/fablab
- DATABASE_URL=postgres://postgres:${POSTGRES_ROOT_PASSWORD}@db:5432/fablab
- NINER_ENGAGE_COOKIE=${NINER_ENGAGE_COOKIE}
- NINER_ENGAGE_TOKEN=${NINER_ENGAGE_TOKEN}
- NINER_ENGAGE_PAYLOAD_TOKEN=${NINER_ENGAGE_PAYLOAD_TOKEN}
Expand All @@ -65,14 +66,14 @@ services:

beat:
restart: always
command: celery -A superfablab beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
entrypoint: celery -A superfablab beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler
build:
context: .
volumes:
- ./superfablab:/app
environment:
- CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672
- DATABASE_URL=postgres://postgres:${POSTGRESS_ROOT_PASSWORD}@db:5432/fablab
- DATABASE_URL=postgres://postgres:${POSTGRES_ROOT_PASSWORD}@db:5432/fablab
- NINER_ENGAGE_COOKIE=${NINER_ENGAGE_COOKIE}
- NINER_ENGAGE_TOKEN=${NINER_ENGAGE_TOKEN}
- NINER_ENGAGE_PAYLOAD_TOKEN=${NINER_ENGAGE_PAYLOAD_TOKEN}
Expand All @@ -82,6 +83,7 @@ services:
depends_on:
- web
- rabbitmq
- db

node:
image: node:18
Expand Down
19 changes: 12 additions & 7 deletions superfablab/superfablab/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'fablab',
'USER': 'postgres',
'PASSWORD': os.getenv('POSTGRESS_ROOT_PASSWORD'),
'PASSWORD': 'guest',
'HOST': 'db', # or use the container name if it's in a Docker network
'PORT': '5432',
}
Expand Down Expand Up @@ -180,12 +180,17 @@

from celery.schedules import crontab

# CELERY_BEAT_SCHEDULE = {
# 'run-my-task-daily': {
# 'task': 'users.tasks.check_for_needed_invites', # Replace your_app
# 'schedule': crontab(hour=15, minute=41), # 5 PM EST
# },
# }
CELERY_BEAT_SCHEDULE = {
#'run-my-task-daily': {
# 'task': 'users.tasks.check_for_needed_invites', # Replace your_app
# 'schedule': crontab(hour=15, minute=41), # 5 PM EST
#},

'update-certifications-every-hour': {
'task': 'users.tasks.canvas_quiz_status',
'schedule': crontab(minute=0, hour='*'), # every hour
}
}


ANYMAIL = {
Expand Down
7 changes: 6 additions & 1 deletion superfablab/superfablab/static/css/custom-bulma.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@charset "UTF-8";
/*! bulma.io v1.0.3 | MIT License | github.com/jgthms/bulma */
/*! bulma.io v1.0.4 | MIT License | github.com/jgthms/bulma */
/* Bulma Utilities */
:root {
--bulma-control-radius: var(--bulma-radius);
Expand Down Expand Up @@ -3776,6 +3776,7 @@ a.box:active {
}
.button.is-outlined {
--bulma-button-border-width: max(1px, 0.0625em);
--bulma-loading-color: hsl(var(--bulma-button-h), var(--bulma-button-s), var(--bulma-button-l));
background-color: transparent;
border-color: hsl(var(--bulma-button-h), var(--bulma-button-s), var(--bulma-button-l));
color: hsl(var(--bulma-button-h), var(--bulma-button-s), var(--bulma-button-l));
Expand Down Expand Up @@ -21040,6 +21041,10 @@ has-background-danger.is-hoverable:active {
font-weight: 700 !important;
}

.has-text-weight-extrabold {
font-weight: 800 !important;
}

.is-family-primary {
font-family: "Inter", "SF Pro", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Helvetica Neue", "Helvetica", "Arial", sans-serif !important;
}
Expand Down
2 changes: 1 addition & 1 deletion superfablab/superfablab/static/css/custom-bulma.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion superfablab/superfablab/static/css/custom-bulma.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@use "../../../node_modules/bulma/bulma.scss";

$primary: #A49665;
$primary: #005035;
$danger: #FF5722;

12 changes: 7 additions & 5 deletions superfablab/tools_and_trainings/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ def create_training(request):
certifier = request.user
training = Training.objects.create(user=user, category=category, training_level=request.POST['level'], certifier=certifier)

return redirect("home")

available_trainings = {}

return redirect("home")
for training in TrainingCategory.objects.all():
try:
to = Training.objects.get(category=training, user=request.user, training_level__gte=Training.TrainingLevels.TRAINER)
Expand All @@ -52,4 +49,9 @@ def create_training(request):
"available_trainings": available_trainings,
}

return render(request, "training_form.html", context)
return render(request, "training_form.html", context)

#version of create_training that is only used in the code. cannot be called from post.
# used to have system assign training to users automatically after they complete a training quiz.
def create_training_internal(user, category, level, certifier):
Training.objects.create(user=user, category=category, training_level=level, certifier=certifier)
46 changes: 41 additions & 5 deletions superfablab/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from django.utils.timezone import now, localtime, timedelta




from typing import Dict, Tuple

from canvasapi import Canvas
Expand Down Expand Up @@ -60,6 +58,8 @@ class SpaceUser(AbstractBaseUser, PermissionsMixin):
null=True,
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png'])]
)



class SpaceLevel(models.IntegerChoices):
USER = 0
Expand All @@ -82,7 +82,7 @@ class SpaceLevel(models.IntegerChoices):
objects = SpaceUserManager()

def get_full_name(self) -> str:
return f"{self.first_name}, {self.last_name}"
return f"{self.first_name} {self.last_name}"

def get_short_name(self) -> str:
return f"{self.first_name}"
Expand Down Expand Up @@ -154,8 +154,44 @@ def get_canvas_id_from_canvas(self) -> SpaceUser:
else:
self.save()
return self

#function that returns the canvas id without emailing the user
# This method attempts to retrieve and set the user's Canvas ID by matching their email with users in a specific Canvas course.
# Steps:
# 1. If the user already has a canvas_id, return self.
# 2. If the user does not have an email, return self.
# 3. If the email is not a charlotte.edu or uncc.edu address, return self.
# 4. Connect to the Canvas API using the API key from environment variables.
# 5. Get the specified course (hardcoded ID: 231237).
# 6. Iterate through all users in the course:
# a. Get each user's profile.
# b. Compare the primary email to the current user's email (charlotte.edu only).
# c. If a match is found, set self.canvas_id to the Canvas profile ID and break.
# 7. Save the user and return self.
def get_canvas_id(self) -> SpaceUser:
from .tasks import canvas_user_list
print("getting canvas id")
if self.canvas_id:
canvas_user_list.update({self.canvas_id: self}) #add to the canvas user list
return self
if not self.email:
return self
if not (self.email.endswith("@charlotte.edu") or self.email.endswith("@uncc.edu")):
return self
canvas = Canvas("https://instructure.charlotte.edu", os.getenv("CANVAS_API_KEY"))
course = canvas.get_course(231237)
for couse_user in course.get_users():
couse_user: CanvasUser
profile = couse_user.get_profile()
email_address = self.email[:self.email.find("@")]
if profile["primary_email"] == f"{email_address}@charlotte.edu":
self.canvas_id = profile["id"]
canvas_user_list.update({self.canvas_id: self}) #add to the canvas user list
break
self.save()
return self

class KeyolderHistoryManager(models.Manager):
class KeyholderHistoryManager(models.Manager):
def get_current_keyholder(self) -> KeyholderHistory:
return KeyholderHistory.objects.filter(exit_time__isnull=True).order_by('-start_time').first()
def is_keyholder(self, user:SpaceUser) -> bool:
Expand All @@ -172,7 +208,7 @@ class KeyholderHistory(models.Model):
keyholder = models.ForeignKey(SpaceUser, on_delete=models.CASCADE, related_name="keyholder_history")
start_time = models.DateTimeField()
exit_time = models.DateTimeField(null=True, blank=True)
objects = KeyolderHistoryManager()
objects = KeyholderHistoryManager()


from visit_tracking.models import Visit
89 changes: 88 additions & 1 deletion superfablab/users/tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
from celery import shared_task
from .models import SpaceUser
from canvasapi import Canvas
import os
from tools_and_trainings.models import Training, TrainingCategory, TrainingManager

def build_canvas_user_list():
global canvas_user_list
canvas_user_list = {}
users = SpaceUser.objects.exclude(canvas_id=None).all()
for user in users:
canvas_user_list[user.canvas_id] = {
"user": user,
"trainings": Training.objects.get_users_trainings(user)
}
print("Built canvas user list: \n", canvas_user_list)

@shared_task
def check_for_needed_invites():
Expand All @@ -14,4 +27,78 @@ def check_for_needed_invites():
@shared_task
def canvas_update(niner_id: int):
SpaceUser.objects.get(niner_id=niner_id).get_canvas_id_from_canvas()


# This task will check if users have completed Canvas certification quizzes
# and update their space access accordingly.
@shared_task
def canvas_quiz_status():
print("Starting canvas quiz status check...")
from tools_and_trainings.views import create_training_internal
canvas = Canvas("https://instructure.charlotte.edu", os.getenv("CANVAS_API_KEY"))
course = canvas.get_course(231237)
trainings = {
"Orientation":
{
"assignment": course.get_assignment(2633706),
"category": TrainingCategory.objects.get(name="Orientation"),
},
"Resin Printing":
{
"assignment": course.get_assignment(2691739),
"category": TrainingCategory.objects.get(name="Resin Printing"),
},
"Waterjet":
{
"assignment": course.get_assignment(2694817),
"category": TrainingCategory.objects.get(name="Waterjet"),
},
"Laser Cutter":
{
"assignment": course.get_assignment(2694816),
"category": TrainingCategory.objects.get(name="Laser Cutter"),
},
"3D Printer":
{
"assignment": course.get_assignment(2691733),
"category": TrainingCategory.objects.get(name="FDM Printing"),
},
}

training_level = Training.TrainingLevels.APPRENTICE #always set to apprentice for automated ceritification
certifier = SpaceUser.objects.get(niner_id=801380523)#set to a default certifier account (Bayli Wolfe aka me)

users = SpaceUser.objects.all()
#for user in users:
# if not user.canvas_id:
# user.get_canvas_id() #gets canvas id if not already set

users_with_ids = list(filter(lambda u: u.canvas_id is not None, users)) #returns users with canvas ids only
print("Users with canvas ids: \n", users_with_ids)
for user in users_with_ids:
trainings_list = Training.objects.get_users_trainings(user) #get list of trainings for user
user_training_categories = {t.category for t in trainings_list}

for training in trainings: #iterate through each training defined above
print(training)
try:
submissions = trainings[training]["assignment"].get_submission(user.canvas_id) #gets list of submissions for specific assignment
print(submissions)
training_category = trainings[training]['category']
if submissions is None or training_category in user_training_categories:
continue #skip if no submissions or user already has training
if submissions.workflow_state == "ungraded" or submissions.grade is None:
continue #skip if submission is ungraded or missing
create_training_internal(user, training_category, training_level, certifier) #create orientation training for user
print(f"Awarded {training} training to {user.get_full_name()}")
except Exception:
print(f"Error processing training {training} for user {user.get_full_name()}: unable to find canvas id for submission. Skipping.")
continue



print("Canvas quiz status check complete.")





2 changes: 1 addition & 1 deletion superfablab/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def index(request):
return HttpResponse("Hello, world. You're at the users index.")

def update_users_canvas_ID(request, user_id):
canvas_update.delay(user_id)
canvas_update(user_id)

if request.POST and request.POST['redirect']:
return redirect(request.POST['redirect'])
Expand Down
2 changes: 1 addition & 1 deletion superfablab/visit_tracking/templates/station.html
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ <h3 class="title has-text-weight-bold is-size-3"> Current {% if currentkeyholder

</style>
<div class="has-text-centered">
<p> Leaderboard of <strong>SHAME<strong> </p>
<p> You forgot to sign out. Mongo is <strong>appalled.<strong></p>
<ol>
{% for item in leaderboard_of_shame %}
<li> {{ item.user__first_name }}, {{ item.user__last_name }} - {{ item.forget_ratio | floatformat:2 }}</li>
Expand Down
Loading