diff --git a/.makim.yaml b/.makim.yaml index 23ef7b1..9f401fa 100644 --- a/.makim.yaml +++ b/.makim.yaml @@ -121,7 +121,7 @@ groups: CWD=$(pwd) CMD_FLAG="{{ '--check' if args.check else '' }} --no-input" - for app in 'sites' 'growth_plan_linker' ''; do + for app in 'sites' 'growth_plan_linker' 'projects' 'linker' ''; do echo -e "\nMaking migration files for: '${app}' ...\n" CMD="python manage.py makemigrations ${CMD_FLAG} ${app}" sugar run --service app --cmd $CMD diff --git a/src/config/settings/base.py b/src/config/settings/base.py index c16590a..4725433 100644 --- a/src/config/settings/base.py +++ b/src/config/settings/base.py @@ -89,7 +89,13 @@ 'drf_spectacular', ] -LOCAL_APPS = ['growth_plan_linker.users', 'growth_plan_linker'] +LOCAL_APPS = [ + 'growth_plan_linker.users', + 'growth_plan_linker', + 'projects.apps.ProjectsConfig', + 'linker.apps.LinkerConfig', + 'growth_plan.apps.GrowthPlanConfig', +] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/src/config/urls.py b/src/config/urls.py index 357f32e..5a6849e 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -11,6 +11,11 @@ from drf_spectacular.views import SpectacularSwaggerView from rest_framework.authtoken.views import obtain_auth_token +from growth_plan_linker.urls import ( + urlpatterns as urlpatterns_growth_plan_linker, +) + + urlpatterns = [ path( '', TemplateView.as_view(template_name='pages/home.html'), name='home' @@ -31,7 +36,8 @@ # ... # Media files *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), -] +] + urlpatterns_growth_plan_linker + if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development urlpatterns += staticfiles_urlpatterns() diff --git a/src/growth_plan/__init__.py b/src/growth_plan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/growth_plan/admin.py b/src/growth_plan/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/growth_plan/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/growth_plan/apps.py b/src/growth_plan/apps.py new file mode 100644 index 0000000..e177afb --- /dev/null +++ b/src/growth_plan/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GrowthPlanConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'growth_plan' diff --git a/src/growth_plan/forms.py b/src/growth_plan/forms.py new file mode 100644 index 0000000..89422e5 --- /dev/null +++ b/src/growth_plan/forms.py @@ -0,0 +1,31 @@ +from django import forms + +from .models import GrowthPlanItem + + +class GrowthPlanItemForm(forms.ModelForm): + class Meta: + model = GrowthPlanItem + fields = [ + 'title', + 'description', + 'start_date', + 'end_date', + 'progress_percentage', + ] + widgets = { + 'title': forms.TextInput(attrs={'class': 'form-control'}), + 'description': forms.Textarea(attrs={'class': 'form-control'}), + # Specify the date format for start_date and end_date fields + 'start_date': forms.DateInput( + format='%Y-%m-%d', + attrs={'class': 'form-control', 'type': 'date'}, + ), + 'end_date': forms.DateInput( + format='%Y-%m-%d', + attrs={'class': 'form-control', 'type': 'date'}, + ), + 'progress_percentage': forms.NumberInput( + attrs={'class': 'form-control'} + ), + } diff --git a/src/growth_plan/migrations/0001_initial.py b/src/growth_plan/migrations/0001_initial.py new file mode 100644 index 0000000..3c3dd4a --- /dev/null +++ b/src/growth_plan/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.2 on 2024-02-21 01:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='GrowthPlanItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Title')), + ('description', models.TextField(verbose_name='Description')), + ('start_date', models.DateField(verbose_name='Start Date')), + ('end_date', models.DateField(verbose_name='End Date')), + ('progress_percentage', models.PositiveIntegerField(default=0, help_text='Enter a number between 0 and 100.', verbose_name='Progress Percentage')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='growth_plan_items', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'Growth Plan Item', + 'verbose_name_plural': 'Growth Plan Items', + }, + ), + ] diff --git a/src/growth_plan/migrations/__init__.py b/src/growth_plan/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/growth_plan/models.py b/src/growth_plan/models.py new file mode 100644 index 0000000..cf8f315 --- /dev/null +++ b/src/growth_plan/models.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class GrowthPlanItem(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='growth_plan_items', + verbose_name=_('User'), + ) + title = models.CharField(max_length=255, verbose_name=_('Title')) + description = models.TextField(verbose_name=_('Description')) + start_date = models.DateField(verbose_name=_('Start Date')) + end_date = models.DateField(verbose_name=_('End Date')) + progress_percentage = models.PositiveIntegerField( + default=0, + verbose_name=_('Progress Percentage'), + help_text=_('Enter a number between 0 and 100.'), + ) + + class Meta: + verbose_name = _('Growth Plan Item') + verbose_name_plural = _('Growth Plan Items') + + def __str__(self): + return self.title diff --git a/src/growth_plan/templates/growth-plan-item-confirm-delete.html b/src/growth_plan/templates/growth-plan-item-confirm-delete.html new file mode 100644 index 0000000..f46edf3 --- /dev/null +++ b/src/growth_plan/templates/growth-plan-item-confirm-delete.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} + +{% block content %} +
+

+ Confirm Delete +

+

+ Are you sure you want to delete "{{ object.title }}"? +

+
+ {% csrf_token %} + + Cancel +
+
+{% endblock content %} diff --git a/src/growth_plan/templates/growth-plan-item-form.html b/src/growth_plan/templates/growth-plan-item-form.html new file mode 100644 index 0000000..a4928b0 --- /dev/null +++ b/src/growth_plan/templates/growth-plan-item-form.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} + +{% block content %} + +
+

+ {{ title }} Growth Plan Item +

+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} + {% for error in field.errors %} +
+ {{ error }} +
+ {% endfor %} +
+ {% endfor %} + + Back to My Growth Plan +
+
+{% endblock content %} diff --git a/src/growth_plan/templates/my-growth-plan.html b/src/growth_plan/templates/my-growth-plan.html new file mode 100644 index 0000000..c8b9de8 --- /dev/null +++ b/src/growth_plan/templates/my-growth-plan.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} + +{% block content %} +
+

+ My Growth Plan +

+
+ {% for item in items %} +
+
+
+
+ {{ item.title }} +
+
+ {{ item.start_date }} to {{ item.end_date }} +
+

+ {{ item.description }} +

+
+ +
+
+ {% empty %} +

+ No growth plan items found. +

+ {% endfor %} +
+ Add New Item +
+{% endblock content %} diff --git a/src/growth_plan/templates/review-growth-plan-list.html b/src/growth_plan/templates/review-growth-plan-list.html new file mode 100644 index 0000000..507cf00 --- /dev/null +++ b/src/growth_plan/templates/review-growth-plan-list.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block content %} +
+

+ Growth Plan Review +

+
+ {% for item in items %} +
+
+
+
+ {{ item.title }} +
+
+ Assigned to: {{ item.user.get_full_name }} +
+

+ {{ item.description }} +

+

+ Progress: {{ item.progress_percentage }}% +

+

+ Duration: {{ item.start_date }} to {{ item.end_date }} +

+
+
+
+ {% empty %} +

+ No growth plan items to review. +

+ {% endfor %} +
+
+{% endblock content %} diff --git a/src/growth_plan/tests.py b/src/growth_plan/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/src/growth_plan/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/src/growth_plan/urls.py b/src/growth_plan/urls.py new file mode 100644 index 0000000..4724fa5 --- /dev/null +++ b/src/growth_plan/urls.py @@ -0,0 +1,35 @@ +from django.urls import path + +from .views import ( + GrowthPlanItemCreateView, + GrowthPlanItemDeleteView, + GrowthPlanItemUpdateView, + review_growth_plan_list, + user_growth_plan_list, +) + +urlpatterns = [ + path( + 'my-growth-plan/', user_growth_plan_list, name='user-growth-plan-list' + ), + path( + 'review-growth-plans/', + review_growth_plan_list, + name='review-growth-plan-list', + ), + path( + 'create/', + GrowthPlanItemCreateView.as_view(), + name='growth-plan-item-create', + ), + path( + 'edit//', + GrowthPlanItemUpdateView.as_view(), + name='growth-plan-item-edit', + ), + path( + 'delete//', + GrowthPlanItemDeleteView.as_view(), + name='growth-plan-item-delete', + ), +] diff --git a/src/growth_plan/views.py b/src/growth_plan/views.py new file mode 100644 index 0000000..be03660 --- /dev/null +++ b/src/growth_plan/views.py @@ -0,0 +1,82 @@ +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.http import HttpResponseForbidden +from django.shortcuts import render +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views.generic import DeleteView, UpdateView +from django.views.generic.edit import CreateView + +from .forms import GrowthPlanItemForm +from .models import GrowthPlanItem + + +@login_required +def user_growth_plan_list(request): + # List only the logged-in user's growth plan items + items = GrowthPlanItem.objects.filter(user=request.user) + return render(request, 'my-growth-plan.html', {'items': items}) + + +@login_required +def review_growth_plan_list(request): + # Accessible only to supervisors; list users' growth + # plan items linked through projects + if not request.user.supervised_projects.exists(): + return HttpResponseForbidden() + supervised_users = User.objects.filter( + projects__supervisors=request.user + ).distinct() + items = GrowthPlanItem.objects.filter( + user__in=supervised_users + ).select_related('user') + return render(request, 'review-growth-plan-list.html', {'items': items}) + + +@method_decorator(login_required, name='dispatch') +class GrowthPlanItemCreateView(CreateView): + model = GrowthPlanItem + form_class = GrowthPlanItemForm + template_name = 'growth-plan-item-form.html' + + def form_valid(self, form): + form.instance.user = ( + self.request.user + ) # Set the user to the current user + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy( + 'user-growth-plan-list' + ) # Redirect to the user's growth plan list + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = 'Create' + return context + + +class GrowthPlanItemUpdateView(UpdateView): + model = GrowthPlanItem + form_class = GrowthPlanItemForm + template_name = 'growth-plan-item-form.html' + success_url = reverse_lazy('user-growth-plan-list') + + def get_queryset(self): + # Users can only edit their own growth plan items + return GrowthPlanItem.objects.filter(user=self.request.user) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = 'Edit' + return context + + +class GrowthPlanItemDeleteView(DeleteView): + model = GrowthPlanItem + template_name = 'growth-plan-item-confirm-delete.html' + success_url = reverse_lazy('user-growth-plan-list') + + def get_queryset(self): + # Users can only delete their own growth plan items + return GrowthPlanItem.objects.filter(user=self.request.user) diff --git a/src/growth_plan_linker/forms.py b/src/growth_plan_linker/forms.py index 6375d0a..63f4650 100644 --- a/src/growth_plan_linker/forms.py +++ b/src/growth_plan_linker/forms.py @@ -2,7 +2,7 @@ from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User -from .models import Feedback, Link, Profile, Project +from .models import Profile class UserRegisterForm(UserCreationForm): @@ -27,46 +27,3 @@ class ProfileUpdateForm(forms.ModelForm): class Meta: model = Profile fields = [] # Specify the fields you want to include, e.g., [] - - -class ProjectForm(forms.ModelForm): - class Meta: - model = Project - fields = [ - 'name', - 'supervisors', - ] # Adjust according to your Project model fields - - -class LinkForm(forms.ModelForm): - class Meta: - model = Link - fields = [ - 'person_one', - 'person_two', - 'supervisor', - 'periodicity', - 'times', - ] - widgets = { - 'person_one': forms.Select(attrs={'class': 'form-control'}), - 'person_two': forms.Select(attrs={'class': 'form-control'}), - 'supervisor': forms.Select(attrs={'class': 'form-control'}), - 'periodicity': forms.Select( - choices=Link.PERIODICITY_CHOICES, - attrs={'class': 'form-control'}, - ), - 'times': forms.NumberInput(attrs={'class': 'form-control'}), - } - - -class FeedbackForm(forms.ModelForm): - class Meta: - model = Feedback - fields = ['content', 'link'] - widgets = { - 'content': forms.Textarea( - attrs={'class': 'form-control', 'rows': 3} - ), - 'link': forms.Select(attrs={'class': 'form-control'}), - } diff --git a/src/growth_plan_linker/migrations/0001_initial.py b/src/growth_plan_linker/migrations/0001_initial.py index 31523d2..0c2e984 100644 --- a/src/growth_plan_linker/migrations/0001_initial.py +++ b/src/growth_plan_linker/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.2 on 2024-02-20 01:13 +# Generated by Django 5.0.2 on 2024-02-20 20:34 import django.db.models.deletion from django.conf import settings @@ -10,44 +10,17 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('projects', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ - migrations.CreateModel( - name='Link', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('periodicity', models.CharField(max_length=50)), - ('times', models.IntegerField()), - ('person_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='person_one_links', to=settings.AUTH_USER_MODEL)), - ('person_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='person_two_links', to=settings.AUTH_USER_MODEL)), - ('supervisor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervised_links', to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Feedback', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField()), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('link', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='growth_plan_linker.link')), - ], - ), - migrations.CreateModel( - name='Project', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('supervisors', models.ManyToManyField(related_name='supervised_projects', to=settings.AUTH_USER_MODEL)), - ], - ), migrations.CreateModel( name='Profile', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('projects', models.ManyToManyField(related_name='participants', to='projects.project')), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('projects', models.ManyToManyField(related_name='participants', to='growth_plan_linker.project')), ], ), ] diff --git a/src/growth_plan_linker/models.py b/src/growth_plan_linker/models.py index 728b84a..00bdcce 100644 --- a/src/growth_plan_linker/models.py +++ b/src/growth_plan_linker/models.py @@ -8,62 +8,15 @@ class Profile(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE ) - projects = models.ManyToManyField('Project', related_name='participants') + projects = models.ManyToManyField( + 'projects.Project', related_name='participants' + ) # Add any additional fields for your Person model here def __str__(self): return self.user.username -class Project(models.Model): - name = models.CharField(max_length=100) - # Assuming you want to keep a relation to track project supervisors - supervisors = models.ManyToManyField( - settings.AUTH_USER_MODEL, related_name='supervised_projects' - ) - - def __str__(self): - return self.name - - -class Link(models.Model): - person_one = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='person_one_links', - ) - person_two = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='person_two_links', - ) - supervisor = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name='supervised_links', - ) - periodicity = models.CharField(max_length=50) - times = models.IntegerField() - - def __str__(self): - return f'{self.person_one.username} <-> {self.person_two.username}' - - -class Feedback(models.Model): - content = models.TextField() - link = models.ForeignKey( - Link, on_delete=models.CASCADE, related_name='feedback' - ) - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return ( - f'Feedback from {self.link.person_one.username} to ' - f'{self.link.person_two.username} on ' - f'{self.timestamp.strftime("%Y-%m-%d %H:%M:%S")}' - ) - - @receiver(post_save, sender=settings.AUTH_USER_MODEL) def create_user_profile(sender, instance, created, **kwargs): if created: diff --git a/src/growth_plan_linker/templates/account/email_confirm.html b/src/growth_plan_linker/templates/account/email_confirm.html index 2f2fe14..1996349 100644 --- a/src/growth_plan_linker/templates/account/email_confirm.html +++ b/src/growth_plan_linker/templates/account/email_confirm.html @@ -4,8 +4,7 @@ {% load account %} -{% block - head_title %} +{% block head_title %} {% translate "Confirm E-mail Address" %} {% endblock head_title %} @@ -14,8 +13,7 @@

{% translate "Confirm E-mail Address" %}

{% if confirmation %} - {% user_display confirmation.email_address.user as - user_display %} + {% user_display confirmation.email_address.user as user_display %}

{% blocktranslate with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address diff --git a/src/growth_plan_linker/templates/base.html b/src/growth_plan_linker/templates/base.html index 1f74938..9c1d592 100644 --- a/src/growth_plan_linker/templates/base.html +++ b/src/growth_plan_linker/templates/base.html @@ -38,6 +38,27 @@ type="text/css" /> + {% compress css %} @@ -95,7 +116,15 @@ href="{% url 'about' %}">About {% if request.user.is_authenticated %} -

+ + @@ -214,7 +243,7 @@

diff --git a/src/growth_plan_linker/urls.py b/src/growth_plan_linker/urls.py index f08ba89..03be4af 100644 --- a/src/growth_plan_linker/urls.py +++ b/src/growth_plan_linker/urls.py @@ -1,12 +1,12 @@ -from django.urls import path +from django.contrib import admin +from django.urls import include, path from . import views urlpatterns = [ + path('admin/', admin.site.urls), path('register/', views.user_register, name='register'), - path('projects/', views.project_list, name='project_list'), - path('project/create/', views.project_create, name='project_create'), - path('link/create/', views.link_create, name='link_create'), - path('feedback/create/', views.feedback_create, name='feedback_create'), - # Add other paths as needed + path('projects/', include('projects.urls')), + path('links/', include('linker.urls')), + path('growth-plan/', include('growth_plan.urls')), ] diff --git a/src/growth_plan_linker/views.py b/src/growth_plan_linker/views.py index 878f34b..c018e1e 100644 --- a/src/growth_plan_linker/views.py +++ b/src/growth_plan_linker/views.py @@ -1,13 +1,11 @@ -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect, render +from django.shortcuts import render -from .forms import FeedbackForm, LinkForm, ProjectForm, UserRegistrationForm -from .models import Project +from .forms import UserRegisterForm def user_register(request): if request.method == 'POST': - user_form = UserRegistrationForm(request.POST) + user_form = UserRegisterForm(request.POST) if user_form.is_valid(): # Create a new user object but avoid saving it yet new_user = user_form.save(commit=False) @@ -21,55 +19,7 @@ def user_register(request): {'new_user': new_user}, ) else: - user_form = UserRegistrationForm() + user_form = UserRegisterForm() return render( request, 'registration/register.html', {'user_form': user_form} ) - - -@login_required -def project_list(request): - projects = Project.objects.all() - return render(request, 'project/list.html', {'projects': projects}) - - -@login_required -def project_create(request): - if request.method == 'POST': - form = ProjectForm(request.POST) - if form.is_valid(): - form.save() - return redirect('project_list') - else: - form = ProjectForm() - return render(request, 'project/create.html', {'form': form}) - - -# Define similar views for Link and Feedback - - -@login_required -def link_create(request): - if request.method == 'POST': - form = LinkForm(request.POST) - if form.is_valid(): - form.save() - return redirect('link_list') - else: - form = LinkForm() - return render(request, 'link/create.html', {'form': form}) - - -@login_required -def feedback_create(request): - if request.method == 'POST': - form = FeedbackForm(request.POST) - if form.is_valid(): - form.save() - return redirect('feedback_list') - else: - form = FeedbackForm() - return render(request, 'feedback/create.html', {'form': form}) - - -# Add views for listing Links and Feedbacks, and any other operations you need diff --git a/src/linker/__init__.py b/src/linker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker/admin.py b/src/linker/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/linker/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/linker/apps.py b/src/linker/apps.py new file mode 100644 index 0000000..16010ec --- /dev/null +++ b/src/linker/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LinkerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'linker' diff --git a/src/linker/forms.py b/src/linker/forms.py new file mode 100644 index 0000000..ec388ca --- /dev/null +++ b/src/linker/forms.py @@ -0,0 +1,25 @@ +from django import forms + +from .models import LINK_PERIODICITY_CHOICES, Link + + +class LinkForm(forms.ModelForm): + class Meta: + model = Link + fields = [ + 'person_one', + 'person_two', + 'supervisor', + 'periodicity', + 'times', + ] + widgets = { + 'person_one': forms.Select(attrs={'class': 'form-control'}), + 'person_two': forms.Select(attrs={'class': 'form-control'}), + 'supervisor': forms.Select(attrs={'class': 'form-control'}), + 'periodicity': forms.Select( + choices=LINK_PERIODICITY_CHOICES, + attrs={'class': 'form-control'}, + ), + 'times': forms.NumberInput(attrs={'class': 'form-control'}), + } diff --git a/src/linker/migrations/0001_initial.py b/src/linker/migrations/0001_initial.py new file mode 100644 index 0000000..89fc896 --- /dev/null +++ b/src/linker/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.2 on 2024-02-20 20:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Link', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('periodicity', models.CharField(max_length=50)), + ('times', models.IntegerField()), + ('person_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='person_one_links', to=settings.AUTH_USER_MODEL)), + ('person_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='person_two_links', to=settings.AUTH_USER_MODEL)), + ('supervisor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supervised_links', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/linker/migrations/__init__.py b/src/linker/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker/models.py b/src/linker/models.py new file mode 100644 index 0000000..f67b9b7 --- /dev/null +++ b/src/linker/models.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.db import models + +LINK_PERIODICITY_CHOICES = ( + ('daily', 'daily'), + ('weekly', 'Weekly'), + ('monthly', 'Monthly'), + ('quarterly', 'Quarterly'), + ('semester', 'Semester'), + ('yearly', 'Yearly'), +) + + +class Link(models.Model): + person_one = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='person_one_links', + ) + person_two = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='person_two_links', + ) + supervisor = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='supervised_links', + ) + periodicity = models.CharField(max_length=50) + times = models.IntegerField() + + def __str__(self): + return f'{self.person_one.username} <-> {self.person_two.username}' diff --git a/src/linker/tests.py b/src/linker/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/src/linker/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/src/linker/urls.py b/src/linker/urls.py new file mode 100644 index 0000000..bb7d7ae --- /dev/null +++ b/src/linker/urls.py @@ -0,0 +1,10 @@ +# ruff: noqa +from django.urls import path +from .views import LinkListView, LinkCreateView, LinkUpdateView, LinkDeleteView + +urlpatterns = [ + path('', LinkListView.as_view(), name='link-list'), + path('create/', LinkCreateView.as_view(), name='link-create'), + path('edit//', LinkUpdateView.as_view(), name='link-edit'), + path('delete//', LinkDeleteView.as_view(), name='link-delete'), +] diff --git a/src/linker/views.py b/src/linker/views.py new file mode 100644 index 0000000..caa775f --- /dev/null +++ b/src/linker/views.py @@ -0,0 +1,39 @@ +# linker/views.py + +from django.urls import reverse_lazy +from django.views.generic import CreateView, DeleteView, ListView, UpdateView + +from .forms import LinkForm +from .models import Link + + +class LinkListView(ListView): + model = Link + context_object_name = 'links' + template_name = 'linker/link_list.html' + + +class LinkCreateView(CreateView): + model = Link + form_class = LinkForm + template_name = 'linker/link_form.html' + success_url = reverse_lazy('link-list') + + +class LinkUpdateView(UpdateView): + model = Link + form_class = LinkForm + template_name = 'linker/link_form.html' + success_url = reverse_lazy('link-list') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = 'Edit Link' + context['button_label'] = 'Update Link' + return context + + +class LinkDeleteView(DeleteView): + model = Link + template_name = 'linker/link_confirm_delete.html' + success_url = reverse_lazy('link-list') diff --git a/src/projects/__init__.py b/src/projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/projects/admin.py b/src/projects/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/src/projects/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/src/projects/apps.py b/src/projects/apps.py new file mode 100644 index 0000000..afae498 --- /dev/null +++ b/src/projects/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProjectsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'projects' diff --git a/src/projects/forms.py b/src/projects/forms.py new file mode 100644 index 0000000..6c59df9 --- /dev/null +++ b/src/projects/forms.py @@ -0,0 +1,12 @@ +from django import forms + +from .models import Project + + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = [ + 'name', + 'observers', + ] diff --git a/src/projects/migrations/0001_initial.py b/src/projects/migrations/0001_initial.py new file mode 100644 index 0000000..45af04e --- /dev/null +++ b/src/projects/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.2 on 2024-02-20 20:34 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Project', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('observers', models.ManyToManyField(related_name='supervised_projects', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/projects/migrations/__init__.py b/src/projects/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/projects/models.py b/src/projects/models.py new file mode 100644 index 0000000..ce1cb6b --- /dev/null +++ b/src/projects/models.py @@ -0,0 +1,13 @@ +from django.conf import settings +from django.db import models + + +class Project(models.Model): + name = models.CharField(max_length=100) + # Assuming you want to keep a relation to track project observers + observers = models.ManyToManyField( + settings.AUTH_USER_MODEL, related_name='supervised_projects' + ) + + def __str__(self): + return self.name diff --git a/src/projects/templates/delete.html b/src/projects/templates/delete.html new file mode 100644 index 0000000..efeee89 --- /dev/null +++ b/src/projects/templates/delete.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} + +{% block content %} +

+ Are you sure you want to delete "{{ project.name }}"? +

+
+ {% csrf_token %} + + Cancel +
+{% endblock content %} diff --git a/src/projects/templates/form.html b/src/projects/templates/form.html new file mode 100644 index 0000000..cef6efc --- /dev/null +++ b/src/projects/templates/form.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} + +{% block content %} +
+

+ {{ title }} +

+
+ {% csrf_token %} +
+ + +
+
+ + +
+ Hold down the Ctrl (windows) or Command (Mac) button to select multiple options. +
+
+ + Cancel +
+
+{% endblock content %} diff --git a/src/projects/templates/list.html b/src/projects/templates/list.html new file mode 100644 index 0000000..d2d93dd --- /dev/null +++ b/src/projects/templates/list.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} + +{% block content %} +

+ Projects +

+ Add New Project + + + + + + + + + + + {% for project in projects %} + + + + + + + {% endfor %} + +
+ # + + Name + + Observers + + Actions +
+ {{ forloop.counter }} + + {{ project.name }} + +
    + {% for observer in project.supervisors.all %} +
  • + {{ observer }} +
  • + {% endfor %} +
+
+ Edit + Delete +
+{% endblock content %} diff --git a/src/projects/tests.py b/src/projects/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/src/projects/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/src/projects/urls.py b/src/projects/urls.py new file mode 100644 index 0000000..4aa0053 --- /dev/null +++ b/src/projects/urls.py @@ -0,0 +1,17 @@ +# ruff: noqa +from django.urls import path +from .views import ( + ProjectListView, + ProjectCreateView, + ProjectUpdateView, + ProjectDeleteView, +) + +urlpatterns = [ + path('', ProjectListView.as_view(), name='project-list'), + path('create/', ProjectCreateView.as_view(), name='project-create'), + path('edit//', ProjectUpdateView.as_view(), name='project-edit'), + path( + 'delete//', ProjectDeleteView.as_view(), name='project-delete' + ), +] diff --git a/src/projects/views.py b/src/projects/views.py new file mode 100644 index 0000000..8e97415 --- /dev/null +++ b/src/projects/views.py @@ -0,0 +1,47 @@ +from django.urls import reverse_lazy +from django.views.generic import CreateView, DeleteView, ListView, UpdateView +from growth_plan_linker.users.models import User + +from .forms import ProjectForm +from .models import Project + + +class ProjectListView(ListView): + model = Project + context_object_name = 'projects' + template_name = 'list.html' + + +class ProjectCreateView(CreateView): + model = Project + form_class = ProjectForm + template_name = 'form.html' + success_url = reverse_lazy('project-list') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['users'] = User.objects.all() + context['title'] = 'Create Project' + return context + + +class ProjectUpdateView(UpdateView): + model = Project + form_class = ProjectForm + template_name = 'form.html' + success_url = reverse_lazy( + 'project-list' + ) # Adjust the URL name as necessary + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = 'Edit Project' + context['button_label'] = 'Update' + context['users'] = User.objects.all() + return context + + +class ProjectDeleteView(DeleteView): + model = Project + template_name = 'delete.html' + success_url = reverse_lazy('project-list')