Skip to content
Snippets Groups Projects
Commit f4c8256b authored by Odin Johan Vatne's avatar Odin Johan Vatne
Browse files

Modify tags and project form

* Add project status
* Add tag type
* Change tag formatting
* Change tag filter module
* Migrate html content to templates
parent 1452e98e
No related branches found
No related tags found
No related merge requests found
Showing
with 338 additions and 55 deletions
...@@ -27,11 +27,15 @@ class NewUserForm(UserCreationForm): ...@@ -27,11 +27,15 @@ class NewUserForm(UserCreationForm):
class NewProjectForm(forms.Form): class NewProjectForm(forms.Form):
title = forms.CharField(label="Title", max_length=200) title = forms.CharField(label="Title", max_length=200, widget=forms.Textarea)
description = forms.CharField(label="Description", max_length=2048) description = forms.CharField(
label="Description", max_length=2048, widget=forms.Textarea)
status = forms.CharField( status = forms.CharField(
label="Status", max_length=128, initial="Accepting applications") label="Status", max_length=128, initial="Open", widget=forms.HiddenInput)
tags = forms.CharField(widget=forms.HiddenInput(), required=False) hidden = forms.BooleanField(
widget=forms.CheckboxInput(attrs={'class': 'hidden'}), # hidden checkbox cannot use hiddeninput
required=False, label="")
tags = forms.CharField(widget=forms.HiddenInput, required=False)
class ApplicationForm(forms.Form): class ApplicationForm(forms.Form):
......
...@@ -6,13 +6,13 @@ class Command(BaseCommand): ...@@ -6,13 +6,13 @@ class Command(BaseCommand):
# (category, ) # (category, )
dataset = [ dataset = [
('Prerequisite Class',), # academic. subject ID/name ('Prerequisite Class', True), # academic. subject ID/name
('Specialization',), # academic. e.g. "Software Development" or "Databases" ('Specialization', True), # academic. e.g. "Software Development" or "Databases"
('Subject',), # personal. e.g. "3D Rendering" or "Games", CS subject matters ('Subject', False), # personal. e.g. "3D Rendering" or "Games", CS subject matters
('Topic of Interest',), # personal. e.g. "Healthcare Systems" or "Sustainability", non-CS subject matter ('Topic of Interest', False), # personal. e.g. "Healthcare Systems" or "Sustainability", non-CS subject matter
('Method',) # personal. e.g. "User Testing", "Literature Review", "Circuit Design", or "Software Prototyping" ('Method', False) # personal. e.g. "User Testing", "Literature Review", "Circuit Design", or "Software Prototyping"
] ]
default_categories = [TagCategory(category=category) for (category,) in dataset] default_categories = [TagCategory(category=category, academic=is_academic) for (category, is_academic) in dataset]
def handle(self, *args, **options): def handle(self, *args, **options):
succeeded = TagCategory.objects.bulk_create(self.default_categories) succeeded = TagCategory.objects.bulk_create(self.default_categories)
......
...@@ -106,6 +106,7 @@ class Command(BaseCommand): ...@@ -106,6 +106,7 @@ class Command(BaseCommand):
'professor': 'celinec', 'professor': 'celinec',
'description': 'Develop a computer vision system to identify undersea life from research vessel photos.', 'description': 'Develop a computer vision system to identify undersea life from research vessel photos.',
'status': 'Open', 'status': 'Open',
'hidden': False,
'tags': ('Artificial Intelligence', 'Artificial Intelligence Programming', 'tags': ('Artificial Intelligence', 'Artificial Intelligence Programming',
'AI', 'Environment', 'Marine Life', 'Machine Learning', 'AI', 'Environment', 'Marine Life', 'Machine Learning',
'Computer Vision and Deep Learning', 'Computer Vision') 'Computer Vision and Deep Learning', 'Computer Vision')
...@@ -115,6 +116,7 @@ class Command(BaseCommand): ...@@ -115,6 +116,7 @@ class Command(BaseCommand):
'professor': 'damiand', 'professor': 'damiand',
'description': 'Explore new techniques in real-time 3D rendering of particle volumes.', 'description': 'Explore new techniques in real-time 3D rendering of particle volumes.',
'status': 'Open', 'status': 'Open',
'hidden': False,
'tags': ('3D Rendering', 'Algorithms and Computers', 'tags': ('3D Rendering', 'Algorithms and Computers',
'Literature Review', 'Programming', 'Mathematics 4', 'Literature Review', 'Programming', 'Mathematics 4',
'Graphics and Visualization', 'Physics') 'Graphics and Visualization', 'Physics')
...@@ -124,7 +126,8 @@ class Command(BaseCommand): ...@@ -124,7 +126,8 @@ class Command(BaseCommand):
projects = [Project(title=project['title'], projects = [Project(title=project['title'],
professor=User.objects.get(username=project['professor']), professor=User.objects.get(username=project['professor']),
description=project['description'], description=project['description'],
status=project['status']) status=project['status'],
hidden=project['hidden'])
for project in projectdata] for project in projectdata]
successful_projects = Project.objects.bulk_create(projects, ignore_conflicts=True) successful_projects = Project.objects.bulk_create(projects, ignore_conflicts=True)
if len(successful_projects) == 0: if len(successful_projects) == 0:
......
# Generated by Django 3.2.9 on 2022-04-13 05:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pasapp', '0009_auto_20220406_0610'),
]
operations = [
migrations.AddField(
model_name='tagcategory',
name='academic',
field=models.BooleanField(default=False),
),
]
# Generated by Django 3.2.9 on 2022-04-14 11:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pasapp', '0010_tagcategory_academic'),
]
operations = [
migrations.AlterField(
model_name='project',
name='status',
field=models.CharField(default='Open', max_length=128),
),
]
# Generated by Django 3.2.9 on 2022-04-15 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pasapp', '0011_alter_project_status'),
]
operations = [
migrations.AddField(
model_name='project',
name='hidden',
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name='project',
name='status',
field=models.CharField(default='Draft', max_length=128),
),
]
# Generated by Django 3.2.9 on 2022-04-15 17:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pasapp', '0012_auto_20220415_1838'),
]
operations = [
migrations.AlterField(
model_name='project',
name='group_size',
field=models.CharField(blank=True, max_length=128, null=True),
),
]
from random import choices
from tkinter import Widget
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -6,8 +8,9 @@ class Project(models.Model): ...@@ -6,8 +8,9 @@ class Project(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
professor = models.ForeignKey(User, on_delete=models.CASCADE) professor = models.ForeignKey(User, on_delete=models.CASCADE)
description = models.CharField(max_length=2048) description = models.CharField(max_length=2048)
group_size = models.CharField(max_length=128) # TODO: Not implemented group_size = models.CharField(max_length=128, blank=True, null=True) # TODO: Not implemented
status = models.CharField(max_length=128) status = models.CharField(max_length=128, default="Draft")
hidden = models.BooleanField(default=True) # only in effect if status is custom
date_created = models.DateTimeField( date_created = models.DateTimeField(
auto_now_add=True, name='date_created') # TODO: Not implemented auto_now_add=True, name='date_created') # TODO: Not implemented
last_updated = models.DateTimeField( last_updated = models.DateTimeField(
...@@ -23,6 +26,20 @@ class Project(models.Model): ...@@ -23,6 +26,20 @@ class Project(models.Model):
class Application(models.Model): class Application(models.Model):
class ProfessorStatus(models.TextChoices):
NONE = 'NONE'
OFFERED = 'OFFR'
DECLINED = 'DECL'
RETRACTED = 'RETR'
class StudentStatus(models.TextChoices):
APPLIED = 'APPL'
PREEMPTED = 'PRE', 'Preemptively Accepted'
ACCEPTED = 'ACC'
DECLINED = 'DECL'
RETRACTED = 'RETR'
project = models.ForeignKey(Project, on_delete=models.CASCADE) project = models.ForeignKey(Project, on_delete=models.CASCADE)
student = models.ForeignKey(User, on_delete=models.CASCADE) student = models.ForeignKey(User, on_delete=models.CASCADE)
message = models.CharField(max_length=2000) message = models.CharField(max_length=2000)
...@@ -30,8 +47,8 @@ class Application(models.Model): ...@@ -30,8 +47,8 @@ class Application(models.Model):
last_updated = models.DateTimeField( last_updated = models.DateTimeField(
auto_now_add=True, name='last_updated') # TODO: Not implemented auto_now_add=True, name='last_updated') # TODO: Not implemented
priority = models.IntegerField(default=None, blank=True, null=True) priority = models.IntegerField(default=None, blank=True, null=True)
professor_status = models.CharField(max_length=32) # TODO: Not implemented professor_status = models.CharField(max_length=4, choices=ProfessorStatus.choices, default=ProfessorStatus.NONE, null=False)
student_status = models.CharField(max_length=32) # TODO: Not implemented student_status = models.CharField(max_length=4, choices=StudentStatus.choices, default=StudentStatus.APPLIED, null=False)
class Meta: class Meta:
constraints = [ constraints = [
...@@ -46,6 +63,7 @@ class Application(models.Model): ...@@ -46,6 +63,7 @@ class Application(models.Model):
class TagCategory(models.Model): class TagCategory(models.Model):
category = models.CharField(max_length=128, unique=True) category = models.CharField(max_length=128, unique=True)
academic = models.BooleanField(default=False)
class Meta: class Meta:
verbose_name_plural = "tag categories" verbose_name_plural = "tag categories"
......
from django.db.models.query import QuerySet
from pasapp.models import Project, ProjectTag
OPEN_STATUS = "Open"
DRAFT_STATUS = "Draft"
CLOSED_STATUS = "Closed"
DEFAULT_STATUSES = (OPEN_STATUS, DRAFT_STATUS, CLOSED_STATUS)
def get_open_projects(inputSet: QuerySet = None) -> QuerySet:
# return Project.objects.filter(status=OPEN_STATUS)
if not inputSet:
inputSet = Project.objects
return inputSet.exclude(status=CLOSED_STATUS).exclude(status=DRAFT_STATUS).exclude(hidden=True) # Statuses are open by default
def get_open_tags(inputSet: QuerySet = None) -> QuerySet:
if not inputSet:
inputSet = ProjectTag.objects
open_project_ids = get_open_projects().values_list('id', flat=True)
return inputSet.filter(project__in=open_project_ids) # for performance, consider list(open_project_ids)
\ No newline at end of file
let initialStatus = "";
function setInitialStatus() {
const value = initialStatus;
if (value == "") {
return
}
const dropdown = document.getElementById("status-dropdown");
for (option of dropdown.options) {
if (option.value == value) {
dropdown.value = value;
updateStatus(value);
return;
}
}
const customCheckbox = document.getElementById("custom-status-checkbox");
const field = document.getElementById("custom-status-textfield");
const hiddenCheckboxValue = document.getElementById("id_hidden").checked;
const hiddenCheckbox = document.getElementById("custom-status-visibility-checkbox");
customCheckbox.checked = true;
field.value = value;
hiddenCheckbox.checked = hiddenCheckboxValue;
}
function setStatus(list) {
const checkbox = document.getElementById("custom-status-checkbox");
if (checkbox.checked) {
checkbox.checked = false;
}
const value = list.value;
updateStatus(value);
}
function toggleCustomStatus(checkbox) {
const isCustom = checkbox.checked;
if (isCustom) {
let value = document.getElementById("custom-status-textfield").value;
updateStatus(value);
} else {
let value = document.getElementById("status-dropdown").value;
updateStatus(value);
}
}
function toggleCustomStatusVisibility(checkbox) {
const isHidden = checkbox.checked;
const statusForm = document.getElementById("id_hidden");
statusForm.checked = isHidden;
}
function setCustomStatus(field) {
const checkbox = document.getElementById("custom-status-checkbox");
if (!checkbox.checked) {
checkbox.checked = true;
}
const value = field.value;
updateStatus(value);
}
function updateStatus(value) {
const statusForm = document.getElementById("id_status");
statusForm.value = value;
}
\ No newline at end of file
...@@ -2,8 +2,26 @@ ...@@ -2,8 +2,26 @@
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
} }
h5 { .hidden {
display: none;
}
.filter-column h5 {
margin-bottom: 0; margin-bottom: 0;
margin-block-start: 1em;
}
.filter-column h5:first-child {
margin-block-start: 0;
}
h5.nobreak {
display: inline-block;
}
#sidebar h3 {
margin-bottom: 0.5em;
} }
.floating-header { .floating-header {
...@@ -38,6 +56,12 @@ h5 { ...@@ -38,6 +56,12 @@ h5 {
color: inherit; color: inherit;
} }
#id_title {
width: 37em;
height: 1.4em;
vertical-align: text-top;
}
#tagBox { #tagBox {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
...@@ -46,18 +70,26 @@ h5 { ...@@ -46,18 +70,26 @@ h5 {
padding: 0; padding: 0;
} }
.tag { .tag-bubble {
display: inline; display: inline-block;
font-weight: 500;
background-color: lightpink; background-color: lightpink;
border: 1px indianred solid; border: 1px indianred solid;
border-radius: 4px; border-radius: 4px;
padding: 4px; padding: 4px;
margin: 4px 4px 0px; margin: 4px 4px 0px;
} }
.closable .tag:hover { #tagBox.closable .tag-bubble {
user-select: none;
}
#tagBox.closable .tag-bubble:hover {
background-color: white; background-color: white;
border: 1px indianred dashed; border-style: dashed;
} }
.dashed-box { .dashed-box {
...@@ -67,6 +99,16 @@ h5 { ...@@ -67,6 +99,16 @@ h5 {
padding: 4px; padding: 4px;
} }
.status-box {
border-radius: 4px;
border: 1px black dashed;
margin-bottom: 8px;
padding: 4px;
display: inline-block;
white-space: nowrap;
vertical-align: -110%;
}
.application { .application {
display: flex; display: flex;
} }
...@@ -88,16 +130,25 @@ h5 { ...@@ -88,16 +130,25 @@ h5 {
display: inline; display: inline;
} }
#split-content-container { .split-content-container {
display: flex; display: flex;
justify-content: center;
} }
#main-content { #main-content {
flex: 4; flex: 4;
margin-right: 8px; margin-right: 8px;
max-width: 80em;
} }
#sidebar { #sidebar {
flex: 1; flex: 1;
margin-left: 1px solid lightgray; border-left: 1px solid lightgray;
padding-left: 0.5em;
max-width: fit-content;
}
textarea#id_description {
display: block;
width: 40em;
} }
.tag.prerequisite-class { .tag.prerequisite-class {
background-color: rgba(255, 0, 0, 0.4); background-color: rgba(220, 38, 127, 0.6);
border: 1px rgba(255, 0, 0, 1) solid; border: 1px rgba(220, 38, 127, 1) solid;
} }
.tag.specialization { .tag.specialization {
background-color: rgba(255, 100, 100, 0.4); background-color: rgba(255, 97, 0, 0.6);
border: 1px rgba(255, 100, 100, 1) solid; border: 1px rgba(255, 97, 0, 1) solid;
} }
.tag.subject { .tag.subject {
background-color: rgba(100, 255, 0, 0.4); background-color: rgba(255, 176, 0, 0.6);
border: 1px rgba(100, 255, 0, 1) solid; border: 1px rgba(255, 176, 0, 1) solid;
} }
.tag.topic-of-interest { .tag.topic-of-interest {
background-color: rgba(0, 100, 255, 0.4); background-color: rgba(100, 143, 255, 0.6);
border: 1px rgba(0, 100, 255, 1) solid; border: 1px rgba(100, 143, 255, 1) solid;
} }
.tag.method { .tag.method {
background-color: rgba(100, 0, 200, 0.4); background-color: rgba(120, 94, 240, 0.6);
border: 1px rgba(100, 0, 200, 1) solid; border: 1px rgba(120, 94, 240, 1) solid;
} }
/* rgba(0, 77, 64, 0.4), rgba(170, 68, 153, 0.4), rgba(51, 34, 136, 0.4)*/
\ No newline at end of file
const tagSet = new Set(); const tagSet = new Set();
function addTag(item) { function addTag(list) {
const tagId = parseInt(item.value); const tagId = parseInt(list.value);
const tagName = item.options[item.selectedIndex].text; const tagName = list.options[list.selectedIndex].text;
const tagCategory = list.options[list.selectedIndex].attributes['meta'].value;
const tagCategorySlug = tagCategory.toLowerCase().replaceAll(' ', '-')
if (tagSet.has(tagId)) { if (tagSet.has(tagId)) {
return; return;
...@@ -10,16 +12,20 @@ function addTag(item) { ...@@ -10,16 +12,20 @@ function addTag(item) {
tagSet.add(tagId); tagSet.add(tagId);
const tagBox = document.getElementById("tagBox"); const tagBox = document.getElementById("tagBox");
tagBox.innerHTML += `<li class="tag" onclick="removeTag(this, ${tagId})">${tagName}</li>`; // <li class="tag {{ tag.category|lower|slugify }}" title="{{ tag.category}}">{{tag.name}}</li>
tagBox.innerHTML += `<li class="tag tag-bubble ${tagCategorySlug}" onclick="removeTag(this, ${tagId})">${tagName}</li>`;
const tagForm = document.getElementById("id_tags"); updateForm();
tagForm.value = [...tagSet].join(' ');
} }
function removeTag(item, tagId) { function removeTag(item, tagId) {
tagSet.delete(tagId); tagSet.delete(tagId);
item.remove(); item.remove();
updateForm();
}
function updateForm() {
const tagForm = document.getElementById("id_tags"); const tagForm = document.getElementById("id_tags");
tagForm.value = [...tagSet].join(' '); tagForm.value = [...tagSet].join(' ');
} }
\ No newline at end of file
<select id="status-dropdown" oninput="setStatus(this)" >
<option value="" disabled selected>Select status</option>
<option value="Open">Open</option>
<option value="Draft">Draft</option>
<option value="Closed">Closed</option>
</select>
<label for="custom-status-checkbox" id="custom-status-label">or enter custom:</label>
<input type="checkbox" id="custom-status-checkbox" onclick="toggleCustomStatus(this)">
<span class="status-box">
<input type="text" placeholder="Custom status" maxlength=128 id="custom-status-textfield" oninput="setCustomStatus(this)"><br/>
<label for="custom-status-visibility-checkbox">hidden from view:</label>
<input type="checkbox" id="custom-status-visibility-checkbox" onclick="toggleCustomStatusVisibility(this)" style="vertical-align:text-bottom;">
</span>
\ No newline at end of file
<li class="tag {{ tag.category|lower|slugify }}" title="{{ tag.category}}">{{tag.name}}</li> {% if onclick %}
\ No newline at end of file <li class="tag tag-bubble {{ tag.category|lower|slugify }}" title="{{ tag.category}}" onclick="removeTag(this, {{tag.id}})">{{tag.name}}</li>
{% else %}
<li class="tag tag-bubble {{ tag.category|lower|slugify }}" title="{{ tag.category}}">{{tag.name}}</li>
{% endif %}
\ No newline at end of file
{% for category, tags in tagsByCategory.items %}
<h5 class="nobreak">{{category}}</h5>
<div class="tag tag-bubble {{ category|lower|slugify }}"></div><br/>
{% for tag in tags %}
{% if tag.id in selectedTags %}
<input type="checkbox" name="tag{{tag.id}}" checked/>
{% else %}
<input type="checkbox" name="tag{{tag.id}}"/>
{% endif %}
<label for="tag{{tag.id}}" class="{{ tag.category|lower|slugify }}">{{tag.name}}</label>
<br/>
{% endfor %}
{% endfor %}
\ No newline at end of file
<select id="tagDropdown" oninput="addTag(this)" >
<option value="" disabled selected>Select tags</option>
{% for tag in tags %}
<option value="{{tag.id}}" meta="{{tag.category}}">{{tag.name}}</option>
{% endfor %}
</select>
\ No newline at end of file
...@@ -4,17 +4,17 @@ ...@@ -4,17 +4,17 @@
{% block imports %} {% block imports %}
<script src="{% static 'pasapp/tagedit.js' %}"></script> <script src="{% static 'pasapp/tagedit.js' %}"></script>
<script src="{% static 'pasapp/statusedit.js' %}"></script>
{% endblock %} {% endblock %}
{% block title %} New Project {% endblock %} {% block title %} New Project {% endblock %}
{% block content %} {% block content %}
<form action="/project/create/" method="post"> <form action="/project/create/" method="post">
{% csrf_token %} {{ form.as_p }} {% csrf_token %} {{ form.as_p }}
<select id="tagDropdown" oninput="addTag(this)" >
<option value="" disabled selected>Select tags</option> {% load html_components %}
{% for tag in tags %} <p>{% status_select %}</p>
<option value="{{tag.id}}">{{tag.name}}</option>
{% endfor %} {% tags_dropdown tags %}
</select>
<br> <br>
<ul id="tagBox" class="closable"> <ul id="tagBox" class="closable">
<!-- Generated tags from the dropdown go here --> <!-- Generated tags from the dropdown go here -->
......
...@@ -5,26 +5,29 @@ ...@@ -5,26 +5,29 @@
{% block imports %} {% block imports %}
<script src="{% static 'pasapp/tagedit.js' %}"></script> <script src="{% static 'pasapp/tagedit.js' %}"></script>
<script> <script>
console.log('hello world');
{% for project_tag in project_tags %} {% for project_tag in project_tags %}
tagSet.add({{project_tag.tag.id}}); tagSet.add({{project_tag.tag.id}});
{% endfor %} {% endfor %}
</script> </script>
<script src="{% static 'pasapp/statusedit.js' %}"></script>
<script>
initialStatus = "{{project.status}}";
</script>
{% endblock %} {% endblock %}
{% block title %} Editing {{project.title}} {% endblock %} {% block title %} Editing {{project.title}} {% endblock %}
{% block onload %} setInitialStatus() {% endblock %}
{% block content %} {% block content %}
<form action="/project/{{project.id}}/edit/" method="post"> <form action="/project/{{project.id}}/edit/" method="post">
{% csrf_token %} {{ form.as_p }} {% csrf_token %} {{ form.as_p }}
<select id="tagDropdown" oninput="addTag(this)" >
<option value="" disabled selected>Select tags</option> {% load html_components %}
{% for tag in tags %} <p>{% status_select %}</p>
<option value="{{tag.id}}">{{tag.name}}</option>
{% endfor %} {% tags_dropdown tags %}
</select>
<br> <br>
<ul id="tagBox" class="closable"> <ul id="tagBox" class="closable">
{% for project_tag in project_tags %} {% for project_tag in project_tags %}
<li class="tag" onclick="removeTag(this, {{project_tag.tag.id}})">{{project_tag.tag.name}}</li> {% tagbubble project_tag.tag True %}
{% endfor %} {% endfor %}
<!-- Generated tags from the dropdown go here --> <!-- Generated tags from the dropdown go here -->
</ul> </ul>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
{% if applications %} {% if applications %}
<ul> <ul>
{% for application in applications %} {% for application in applications %}
<li class="dashed-box"> <li class="dashed-box application-listing">
<b>{{application.student.first_name}} {{application.student.last_name}}</b> on {{application.date_created}} <b>{{application.student.first_name}} {{application.student.last_name}}</b> on {{application.date_created}}
<br> <br>
{% if application.priority %} {% if application.priority %}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment