Commit 8b55089d authored by Mirko Vucicevich's avatar Mirko Vucicevich
Browse files

Merge branch 'master' into 'p01'

New Overlay Model / Settings / JS

See merge request !18
parents ea0aedab a57ccbe8
# Generated by Django 3.1 on 2021-07-08 15:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('projector', '0042_auto_20201216_1604'),
]
operations = [
migrations.CreateModel(
name='Overlay',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('show_notifications', models.BooleanField(default=True, help_text='If true, notifications set here will pop up in your overlay')),
('show_contacts', models.BooleanField(default=True, help_text='Email addresses and names of contacts will show up on the overlay modal')),
('show_documentation_link', models.BooleanField(default=True, help_text='If the documentation url is set in your project, it will show up on the overlay modal')),
('show_ticket_link', models.BooleanField(default=True, help_text='If the ticketing system url is set in your project, it will show up on the overlay modal')),
('jira_issue_collector_url', models.TextField(blank=True, help_text='If you have a Jira Issue Collector configured, paste the URL for it here.')),
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='projector.project')),
],
),
]
# Generated by Django 3.1 on 2021-07-09 18:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('overlay', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='overlay',
name='title_override',
field=models.CharField(blank=True, help_text="Force a different title in the 'Get Help for X' part of the modal", max_length=256),
),
]
# Generated by Django 3.1 on 2021-07-09 20:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('overlay', '0002_overlay_title_override'),
]
operations = [
migrations.CreateModel(
name='AlertMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('highlight', models.CharField(choices=[('default (no color)', 'default'), ('success (green)', 'success'), ('info (blue)', 'info'), ('warning (yellow/orange)', 'warning'), ('danger (red)', 'danger')], default='default', max_length=64)),
('title', models.CharField(blank=True, max_length=256)),
('content', models.TextField()),
('active_start', models.DateTimeField(blank=True, help_text="[Optional] alert won't show until this time", null=True)),
('active_end', models.DateTimeField(blank=True, help_text='[Optional] alert will stop showing at this time. Expired alerts will auto-delete eventually.', null=True)),
('overlay', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='overlay.overlay')),
],
),
]
# Generated by Django 3.1 on 2021-07-09 20:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('overlay', '0003_alertmessage'),
]
operations = [
migrations.RenameModel(
old_name='AlertMessage',
new_name='Notice',
),
]
# Generated by Django 3.1 on 2021-07-09 20:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('overlay', '0004_auto_20210709_1608'),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('highlight', models.CharField(choices=[('default (no color)', 'default'), ('success (green)', 'success'), ('info (blue)', 'info'), ('warning (yellow/orange)', 'warning'), ('danger (red)', 'danger')], default='default', max_length=64)),
('title', models.CharField(blank=True, max_length=256)),
('content', models.TextField()),
('active_start', models.DateTimeField(blank=True, help_text="[Optional] alert won't show until this time", null=True)),
('active_end', models.DateTimeField(blank=True, help_text='[Optional] alert will stop showing at this time. Expired alerts will auto-delete eventually.', null=True)),
('overlay', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='overlay.overlay')),
],
),
migrations.DeleteModel(
name='Notice',
),
]
# Generated by Django 3.1 on 2021-07-09 20:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('overlay', '0005_auto_20210709_1611'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='highlight',
field=models.CharField(choices=[('default', 'default (no color)'), ('success', 'success (green)'), ('info', 'info (blue)'), ('warning', 'warning (yellow/orange)'), ('danger', 'danger (red)')], default='default', max_length=64),
),
]
from django.db import models
from django.utils import timezone
class Overlay(models.Model):
project = models.OneToOneField("projector.Project", on_delete=models.CASCADE)
title_override = models.CharField(
help_text="Force a different title in the 'Get Help for X' part of the modal",
blank=True,
max_length=256,
)
show_notifications = models.BooleanField(
help_text="If true, notifications set here will pop up in your overlay",
default=True,
)
show_contacts = models.BooleanField(
help_text="Email addresses and names of contacts will show up on the overlay modal",
default=True,
)
show_documentation_link = models.BooleanField(
help_text="If the documentation url is set in your project, it will show up on the overlay modal",
default=True,
)
show_ticket_link = models.BooleanField(
help_text="If the ticketing system url is set in your project, it will show up on the overlay modal",
default=True,
)
jira_issue_collector_url = models.TextField(
help_text="If you have a Jira Issue Collector configured, paste the URL for it here.",
blank=True,
)
@property
def active_notifications(self):
return self.notifications.filter(
models.Q(
models.Q(active_start__lte=timezone.now()) |
models.Q(active_start=None)
) &
models.Q(
models.Q(active_end__gte=timezone.now()) |
models.Q(active_end=None)
)
)
class Notification(models.Model):
overlay = models.ForeignKey(
'overlay.Overlay',
related_name='notifications',
on_delete=models.CASCADE
)
highlight = models.CharField(
choices=(
( 'default','default (no color)' ),
('success', 'success (green)'),
('info', 'info (blue)'),
('warning', 'warning (yellow/orange)'),
('danger', 'danger (red)'),
),
max_length=64,
default='default'
)
title = models.CharField(
blank=True, max_length=256
)
content = models.TextField()
active_start = models.DateTimeField(
help_text="[Optional] alert won't show until this time",
blank=True,
null=True
)
active_end = models.DateTimeField(
help_text="[Optional] alert will stop showing at this time. Expired alerts will auto-delete eventually.",
blank=True,
null=True
)
def __str__(self):
return self.title
var t={compress:function(t,n,i){!function(t,n,i){var e=new Date;if(void 0!==i){e.setTime(e.getTime()+60*i*1e3);var o="expires="+e.toUTCString()+";"}else o="";document.cookie=t+"="+n+";"+o+"path=/"}(t,btoa(JSON.stringify(n)),i)},decompress:function(t){let n=function(t){for(var n=t+"=",i=decodeURIComponent(document.cookie).split(";"),e=0;e<i.length;e++){for(var o=i[e];" "==o.charAt(0);)o=o.substring(1);if(0==o.indexOf(n))return o.substring(n.length,o.length)}return""}(t);return""!==n?JSON.parse(atob(n)):null}};const n=document.createElement("template"),i='\n<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">\n <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>\n</svg>\n';n.innerHTML=`\n<style>\n #wrap{\n font-family: Arial,sans-serif;\n\tfont-size: 12pt;\n font-weight: 400;\n line-height: 1.2;\n color: #212529;\n }\n ul>li{\n\t margin: 0;\n\t margin-left: 0;\n }\n h1, h2, h3, h4, h5{\n\t font-weight: 500;\n\t margin-bottom: 0.4em;\n }\n #modal{\n box-sizing: border-box;\n background-color: white;\n width: 98%;\n max-width: 660px;\n padding: 18px 24px 0px 24px;\n border-radius: 8px;\n\tposition: relative;\n\tborder: 1px solid rgba(0,0,0,.2);\n }\n #overlay{\n width: 100vw;\n height: 100vh;\n position: fixed;\n top: 0;\n left: 0;\n background-color: rgba(0,0,0,0.6);\n opacity: 0;\n align-items: center;\n justify-content: center;\n z-index: 99999;\n }\n #trigger-button-position{\n position: fixed;\n right: 10px;\n bottom: 10px;\n opacity: 0;\n }\n #title-wrapper>h2{\n\t margin: 0;\n\t flex-grow: 1;\n }\n\n .slight-indent{\n\t margin-bottom: 2em;\n }\n #title-wrapper{\n\t display: flex;\n\t margin-bottom: 0.7em;\n\t align-items: flex-start;\n\t margin: 0 -24px;\n\t padding: 0 12px 8px 24px;\n\t\tborder-bottom: 1px solid rgba(0,0,0,.2);\n }\n\n #closebtn{\n\t cursor: pointer;\n\t outline: none;\n\t border: none;\n\t background: transparent;\n }\n\n .show{\n animation: fadein 0.2s;\n animation-fill-mode: forwards;\n }\n\n .hide{\n animation: fadeout 0.2s !important;\n animation-fill-mode: forwards !important;\n }\n\n #notifications{\n\t position: fixed;\n\t top: 24px;\n\t left: 50%;\n\t transform: translateX(-50%);\n\t width: 90%;\n\t max-width: 800px;\n }\n\n\n .default-help-button{\n width: 40px;\n height: 40px;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n border-radius: 4px;\n\tborder: 1px solid;\n cursor: pointer;\n\tcolor: white;\n\ttransform: scale(1) translateY(0);\n\ttransition: all 50ms ease-in;\n\tbackground-color: #3f51b5;\n\tborder-color: #606fc7;\n\tbox-shadow: 0 6px 10px 0 rgb(0 0 0 / 30%);\n }\n .default-help-button:hover{\n\ttransform: scale(1.1) translateY(-4px);\n\tbox-shadow: 0 8px 15px 0 rgb(0 0 0 / 30%);\n\tbackground-color: #606fc7;\n }\n\n .notification{\n box-sizing: border-box;\n\t padding: 18px 24px;\n\t border-bottom-right-radius: 4px;\n\t border-bottom-left-radius: 4px;\n border-top: 2px solid;\n\t position: relative;\n\t animation: fadein 1s;\n\t opacity: 0;\n\t animation-fill-mode: forwards;\n box-shadow: 0px 5px 5px -1px rgba(0,0,0,0.24);\n\n\n color: #636464;\n border-color: #636464;\n background-color: #fefefe;\n }\n\n .notification-title{\n\t position: absolute;\n\t padding: 4px 16px 4px 8px;\n\t top: 0;\n\t left: 0;\n\t background-color: #333;\n\t color: white;\n\t border-bottom-right-radius: 4px;\n\t font-weight: 600;\n font-size: 80%;\n }\n\n .notification.success{\n color: #0f5132;\n border-color: #0f5132;\n background-color: #d1e7dd;\n }\n .notification.success>.notification-title{\n background-color: #0f5132;\n }\n .notification.info{\n color: #055160;\n border-color: #055160;\n background-color: #cff4fc;\n}\n }\n .notification.info>.notification-title{\n background-color: #055160;\n }\n .notification.warning{\n color: #664d03;\n border-color: #664d03;\n background-color: #fff3cd;\n }\n .notification.warning>.notification-title{\n background-color: #664d03;\n }\n .notification.danger{\n color: #842029;\n border-color: #842029;\n background-color: #f8d7da;\n }\n .notification.danger>.notification-title{\n background-color: #842029;\n }\n\n .notification + .notification{\n\t margin-top: 16px;\n }\n\n .notification > p {\n\t margin-top: 16px;\n\t margin-bottom: 0;\n }\n\n .close-notification{\n\t position: absolute;\n\t top: 4px;\n\t right: 0px;\n cursor: pointer;\n border: none;\n background-color: transparent;\n }\n\n\n\n @keyframes fadein {\n 0% { opacity: 0; }\n 100% { opacity: 1; }\n }\n\n @keyframes fadeout {\n 0% { opacity: 1; }\n 100% { opacity: 0; }\n }\n</style>\n<div id="wrap">\n <div id="notifications"></div>\n <div id="trigger-button-position" part="trigger-button-position" style="display: none;">\n <slot name="trigger-button">\n <button class="default-help-button" title="Get Help">\n\t\t<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-question-circle" viewBox="0 0 16 16">\n\t\t\t<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>\n\t\t\t<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>\n\t\t</svg>\n </button>\n </slot>\n </div>\n <div id="overlay" style="display: none;">\n <div id="modal" part="modal">\n <div id="title-wrapper">\n\t\t<h2>Get Help For <span id="project-title"></span></h2>\n\t\t<button id="closebtn">\n ${i}\n\t\t</button>\n\t </div>\n <div id="help-preface">\n <slot name="help-preface"></slot>\n </div>\n <div id="documentation"></div>\n <div id="contacts"></div>\n <div id="tickets"></div>\n <div id="help-footer">\n <slot name="help-footer"></slot>\n </div>\n </div>\n </div>\n</div>\n`;const e=(t,n)=>`<h4><strong>${t}</strong></h4><div class="slight-indent">${n}</div>`,o='\n <button class="open-jira-button" onclick="window.FASTOverlayJiraDialog()">\n Open a Ticket\n </button>\n';class a extends HTMLElement{static get observedAttributes(){return["project-slug","api-root","no-button"]}get project_slug(){return this.getAttribute("project-slug")}set project_slug(t){this.setAttribute("project-slug",t)}get cached_data_key(){return`fast-overlay-cache-${this.project_slug}`}get api_root(){return this.getAttribute("api-root")||"https://fast.uwaterloo.ca/overlay"}get no_button(){return this.hasAttribute("no-button")}get documentation_url(){return this._state.fast_data.documentation_url}get ticket_url(){return this._state.fast_data.ticket_url}get contacts(){return this._state.fast_data.contacts.filter((t=>""!==t.email))}get jira_enabled(){return""!==this._state.fast_data.jira}get notifications(){return this._state.fast_data.notifications.filter((t=>!this._state.seen_notifications.includes(t.id)))}get project_title(){return this._state.fast_data.title}constructor(){if(super(),null===this.project_slug)throw"<fast-overlay> component requires 'project-slug' attribute!";this._shadow=this.attachShadow({mode:"open"}),this._shadow.appendChild(n.content.cloneNode(!0)),this._state={show:!1,seen_notifications:[],jira_mounted:!1,fast_data:{title:null,notifications:[],contacts:[],documentation_url:"",ticket_url:"",jira:"",initialized:!1}},this.$overlay=this._shadow.getElementById("overlay"),this.$notifications=this._shadow.getElementById("notifications"),this.$closebtn=this._shadow.getElementById("closebtn"),this.$trigger_holder=this._shadow.getElementById("trigger-button-position"),this.worldListener={handleEvent:t=>{switch(t.type){case"keydown":"Escape"===t.key&&this.closeHelpModal();break;case"click":(t.target==this.$overlay||this.$closebtn.contains(t.target))&&this.closeHelpModal()}}}}connectedCallback(){this._state.fast_data=Object.assign(this._state.fast_data,t.decompress(this.cached_data_key)||{}),this._state.seen_notifications=t.decompress("fast-overlay-seen-notifications")||[],this._state.fast_data.initialized?this.finalizeConnectedCallback():this.retrieveData()}retrieveData(){fetch(`${this.api_root}/${this.project_slug}`).then((t=>{if(!t.ok)throw"Failed to get overlay endpoint";return t.json()})).then((n=>{this._state.fast_data=Object.assign(this._state.fast_data,n),this._state.fast_data.initialized=!0,t.compress(this.cached_data_key,this._state.fast_data,5),this.finalizeConnectedCallback()})).catch((t=>{console.error("Failed to retrieve FAST project data; aborting overlay init")}))}finalizeConnectedCallback(){var t,n,i;this.no_button?this.$trigger_holder.remove():(this.$trigger_holder.style.display="block",this.$trigger_holder.classList.add("show"),this.$trigger_holder.addEventListener("click",(()=>{this.triggerHelpModal()}))),this.setHtml("project-title",this.project_title),""!==this.documentation_url&&this.setHtml("documentation",(t=this.documentation_url,e("Documentation",`If you haven't already, take a look at this service's <a target="blank" href="${t}">Documentation</a>`))),this.contacts.length>0&&this.setHtml("contacts",(t=>{let n=t.map((t=>`\n <li class="contact">\n ${t.fullname} (<a href="mailto:${t.email}">${t.email}</a>)<br>\n ${t.contact_for}\n </li>\n `)).join("");return e("Email",`You can reach out directly to the following individuals for assistance:\n <ul class="contacts">\n ${n}\n </ul>\n `)})(this.contacts)),(""!==this.ticket_url||this.jira_enabled)&&this.setHtml("tickets",(n=this.ticket_url,i=this.jira_enabled,e("Open a Ticket",""===n&&i?"Use the following button to open a new ticket: "+o:""!==n&&i?`${o} or head to the <a target="_blank" href="${n}">ticket submission portal</a>`:`Use the <a target="_blank" href="${n}">ticket submission portal</a> to open a new ticket`))),this.showNotifications()}makeNotification(n){let e=document.createElement("div");e.classList.add("notification"),e.classList.add(n.highlight),e.setAttribute("aria-label","dismiss notification");let o=""!==n.title?`<div class="notification-title">${n.title}</div>`:"";o+=`<button class="close-notification">${i}</button>`,o+=`<p>${n.content.replace(/(<([^>]+)>)/gi,"").split("\n").join("<br/>")}</p>`,e.innerHTML=o,this.$notifications.appendChild(e),e.getElementsByClassName("close-notification")[0].addEventListener("click",(i=>{e.classList.add("hide"),e.addEventListener("animationend",(()=>{this._state.seen_notifications.push(n.id),t.compress("fast-overlay-seen-notifications",this._state.seen_notifications),e.remove()}))}))}showNotifications(){this.notifications.forEach(((t,n)=>{setTimeout((()=>this.makeNotification(t)),200*n)}))}setHtml(t,n){this._shadow.getElementById(t).innerHTML=n}triggerHelpModal(){this.$overlay.style.display="flex",this.$overlay.classList.add("show"),this.$overlay.addEventListener("animationend",(()=>{this._state.show=!0,this.$overlay.addEventListener("click",this.worldListener),document.addEventListener("keydown",this.worldListener)}),{once:!0}),this._state.fast_data.jira&&!this._state.jira_mounted&&(!function(t){window.ATL_JQ_PAGE_PROPS={triggerFunction:function(t){window.FASTOverlayJiraDialog=function(){t()}}};let n=document.createElement("script");n.setAttribute("src",t),document.head.appendChild(n)}(this._state.fast_data.jira),this._state.jira_mounted=!0)}closeHelpModal(){this.$overlay.classList.add("hide"),this.$overlay.addEventListener("animationend",(()=>{this.$overlay.classList.remove("show"),this.$overlay.classList.remove("hide"),this._state.show=!1,this.$overlay.style.display="none",this.$overlay.removeEventListener("click",this.worldListener),document.removeEventListener("keydown",this.worldListener)}),{once:!0})}}customElements.get("fast-overlay")||window.customElements.define("fast-overlay",a);
......@@ -4,9 +4,10 @@ from django.views.decorators.csrf import csrf_exempt
from django.core.mail import EmailMessage
from django.template import *
from django.template import loader
from django.http import JsonResponse
from django.http import JsonResponse, Http404
from projector.models import Project
from urllib.parse import urlparse
from overlay.models import Notification
import json
import re
......@@ -82,7 +83,7 @@ def send_message(request, pk):
def project_json_view(request, pk):
project = get_object_or_404(Project, pk=pk)
notices = project.active_notices
notices = project.overlay.active_notifications if hasattr(project, 'overlay') else []
contacts = project.contact_set.all()
output = {
......@@ -104,3 +105,34 @@ def project_json_view(request, pk):
}
return JsonResponse(output)
def overlay_api(request, slug):
project = Project.objects.filter(slug=slug).select_related('overlay').first()
if project is None:
raise Http404
ol = getattr(project, 'overlay', None)
if ol is None:
raise Http404
output = {
"title": ol.title_override if ol.title_override else project.title,
"contacts": [
{
'fullname': '{} {}'.format(x.first_name, x.last_name),
'email': x.email,
'contact_for': x.contact_for
} for x in project.contact_set.all()
] if ol.show_contacts else [],
"notifications": [
{
'title': x.title,
'content': x.content,
'id': x.id,
'highlight': x.highlight
} for x in ol.active_notifications
] if ol.show_notifications else [],
"documentation_url": project.documentation_url if ol.show_documentation_link else '',
"ticket_url": project.ticket_url if ol.show_ticket_link else '',
"jira": ol.jira_issue_collector_url
}
return JsonResponse(output)
\ No newline at end of file
from django.contrib import admin
from django.contrib.admin.filters import ListFilter
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Group as UselessGroup
from projector.models import *
from overlay.models import Overlay, Notification
from markdownx.admin import MarkdownxModelAdmin
......@@ -41,10 +43,11 @@ class ContactInline(UserLinkInline):
(None, dict(fields=('contact_for',)))
]
class NoticeInline(admin.StackedInline):
model = Notice
extra = 1
class FallBackInline(UserLinkInline):
model = FallBack
extra_fieldsets = [
(None, dict(fields=('notes',)))
]
class OwnProjectFilter(admin.SimpleListFilter):
......@@ -57,15 +60,36 @@ class OwnProjectFilter(admin.SimpleListFilter):
#.filter(project__contact__user=request.user)\
def queryset(self, request, queryset):
print(self.value())
if self.value():
return queryset.filter(project=self.value())
class LimitProjectDropdownMixin:
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "project":
kwargs["queryset"] = Project.objects.filter(contact__user=request.user)
kwargs["queryset"] = Project.objects.all().by_user(request.user)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class NotificationInline(admin.StackedInline):
model = Notification
extra = 0
class OverlayAdmin(LimitProjectDropdownMixin, admin.ModelAdmin):
list_filter = [OwnProjectFilter]
list_display = ['project_title']
inlines = [NotificationInline]
def project_title(self, obj):
return obj.project.title
def get_queryset(self, request):
qs = super().get_queryset(request)
#if request.user.is_superuser:
# return qs
return qs.filter(project__in=Project.objects.all().by_user(request.user))
class TrackedDocumentAdmin(LimitProjectDropdownMixin, MarkdownxModelAdmin):
list_display = ['title', 'project']
list_filter = [OwnProjectFilter]
......@@ -81,7 +105,9 @@ class TrackedDocumentAdmin(LimitProjectDropdownMixin, MarkdownxModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.filter(project__contact__user=request.user)
#if request.user.is_superuser:
# return qs
return qs.filter(project__in=Project.objects.all().by_user(request.user))
def save_model(self, request, obj, form, change):
......@@ -107,7 +133,7 @@ class ProjectAdmin(MarkdownxModelAdmin):
list_display = ['title', 'url', 'get_groups']
fieldsets = [
('Project Information', {
'fields': ['title', 'slug', 'icon', 'tagline', 'description', 'url', 'project_created', 'visible', 'api_key']
'fields': ['title', 'slug', 'icon', 'tagline', 'description', 'url', 'documentation_url', 'ticket_url', 'project_created', 'visible', 'api_key']
}),
('Project Details', {'fields': ['scope', 'technologies', 'groups']}),
('Gitlab Integration', {
......@@ -120,11 +146,20 @@ class ProjectAdmin(MarkdownxModelAdmin):
prepopulated_fields = {'slug': ("title",)}
filter_horizontal = ['technologies', 'groups']
inlines = [ContributorInline, ContactInline, NoticeInline]
inlines = [ContactInline, ContributorInline, FallBackInline]
def get_groups(self, obj):
return ", ".join(obj.groups.order_by('name').values_list('name', flat=True))
def get_queryset(self, request):
qs = super().get_queryset(request)
#if request.user.is_superuser:
# return qs
return qs.by_user(request.user)
class CustomUserAdmin(UserAdmin):
def has_change_permission(self, request, obj=None):
return obj == request.user
fieldsets = (
(None, {'fields': ('avatar', 'username', 'password')}),
('Personal info', {'fields': ('first_name', 'last_name', 'email')}),
......@@ -138,7 +173,16 @@ class TechnologyAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ('name',)}
class GroupAdmin(admin.ModelAdmin):
def has_change_permission(self, request, obj=None):
if obj is not None:
return obj.users.filter(id=request.user.id).exists()
return super().has_change_permission(self, request, obj)
prepopulated_fields = {"slug": ('name',)}
def get_queryset(self, request):
qs = super().get_queryset(request)
#if request.user.is_superuser:
# return qs
return qs.filter(users=request.user)
admin.site.register(User, CustomUserAdmin)
......@@ -148,7 +192,9 @@ admin.site.register(Technology, TechnologyAdmin)
# hide these because they are more usable from the project model view.
#admin.site.register(Contributor)
#admin.site.register(Contact)
admin.site.register(Notice)
admin.site.register(Group, GroupAdmin)
admin.site.unregister(UselessGroup)
admin.site.register(Overlay, OverlayAdmin)
# Register your models here.
# Generated by Django 3.1 on 2021-07-08 15:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('projector', '0042_auto_20201216_1604'),
]
operations = [
migrations.AddField(
model_name='project',
name='documentation_url',
field=models.CharField(blank=True, help_text='A link to your USER-FACING documentation', max_length=256),
),
migrations.AddField(
model_name='project',
name='ticket_url',
field=models.CharField(blank=True, help_text='A link, if one exists to the page for submitting tickets to this project', max_length=256),
),
]
# Generated by Django 3.1 on 2021-07-12 14:40
# Moves notices into overlay notifications
from django.db import migrations
from django.utils import timezone
def move_to_notices(apps, schema):
Project = apps.get_model('projector', 'Project')
Overlay = apps.get_model('overlay', 'Overlay')
Notification = apps.get_model('overlay', 'Notification')
for p in Project.objects.filter(notice__isnull=False).prefetch_related('notice_set'):
overlay, isnew = Overlay.objects.get_or_create(project=p)
for notice in p.notice_set.all():
Notification.objects.create(
overlay=overlay,
title=notice.title,
content=notice.content,
active_end=notice.expires_on
)
notice.delete()
def undo_move_to_notices(apps, schema):
Notification = apps.get_model('overlay', 'Notification')
Notice = apps.get_model('projector', 'Notice')
for note in Notification.objects.all():
Notice.objects.create(
project=note.overlay.project,
title=note.title,
content=note.content,
expires_on=note.active_end if note.active_end else timezone.now()
)
class Migration(migrations.Migration):
dependencies = [
('projector', '0043_auto_20210708_1136'),
('overlay', '0006_auto_20210709_1656'),
]
operations = [
migrations.RunPython(move_to_notices, reverse_code=undo_move_to_notices)
]
# Generated by Django 3.1 on 2021-07-12 16:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('projector', '0044_auto_20210712_1040'),
]
operations = [
migrations.CreateModel(
name='FallBack',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('override_first_name', models.CharField(blank=True, max_length=250)),
('override_last_name', models.CharField(blank=True, max_length=250)),
('override_email', models.EmailField(blank=True, max_length=254)),
('notes', models.TextField(help_text='additional notes reguarding fallback capabilities for this user')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='project',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='contact',
name='contact_for',
field=models.TextField(help_text='eg. general support / user support / server configuration. Will show up in overlays'),
),
migrations.AlterField(
model_name='contributor',
name='contributed_to',
field=models.TextField(help_text='Quick summary of contributions, ex: co-op winter 2021, UX design, etc.'),
),
migrations.DeleteModel(
name='Notice',
),
migrations.AddField(
model_name='fallback',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projector.project'),
),
migrations.AddField(
model_name='fallback',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]
# Generated by Django 3.1 on 2021-07-12 18:27
from django.db import migrations
def set_author(apps, schema_editor):
Project = apps.get_model('projector', 'Project')
LogEntry = apps.get_model('admin', 'LogEntry')
ContentType = apps.get_model('contenttypes', 'ContentType')
project_contype = ContentType.objects.get(app_label='projector', model='project')
for project in Project.objects.filter(author=None):
creation_entry = LogEntry.objects.filter(
action_flag=1,