Moving from google code to github.
This commit is contained in:
commit
5be5ea023c
22 changed files with 1974 additions and 0 deletions
14
README
Normal file
14
README
Normal file
|
@ -0,0 +1,14 @@
|
|||
django-todo is a pluggable multi-user, multi-group task management and
|
||||
assignment application for Django. It can serve as anything from a
|
||||
personal to-do system to a complete, working ticketing system for organizations.
|
||||
|
||||
For documentation, see the django-todo wiki pages:
|
||||
|
||||
Overview and screenshots
|
||||
http://github.com/shacker/django-todo/wiki/Overview-and-screenshots
|
||||
|
||||
Requirements and installation
|
||||
http://github.com/shacker/django-todo/wiki/Requirements-and-Installation
|
||||
|
||||
Version history
|
||||
http://github.com/shacker/django-todo/wiki/Version-history
|
0
__init__.py
Normal file
0
__init__.py
Normal file
13
admin.py
Normal file
13
admin.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from django.contrib import admin
|
||||
from todo.models import Item, User, List, Comment
|
||||
|
||||
class ItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'list', 'priority', 'due_date')
|
||||
list_filter = ('list',)
|
||||
ordering = ('priority',)
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
admin.site.register(List)
|
||||
admin.site.register(Comment)
|
||||
admin.site.register(Item,ItemAdmin)
|
83
forms.py
Normal file
83
forms.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
from django.db import models
|
||||
from django import forms
|
||||
from django.forms import ModelForm
|
||||
from django.contrib.auth.models import User,Group
|
||||
from todo.models import Item, List
|
||||
import datetime
|
||||
|
||||
class AddListForm(ModelForm):
|
||||
# The picklist showing allowable groups to which a new list can be added
|
||||
# determines which groups the user belongs to. This queries the form object
|
||||
# to derive that list.
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(AddListForm, self).__init__(*args, **kwargs)
|
||||
self.fields['group'].queryset = Group.objects.filter(user=user)
|
||||
|
||||
class Meta:
|
||||
model = List
|
||||
|
||||
|
||||
|
||||
class AddItemForm(ModelForm):
|
||||
|
||||
# The picklist showing the users to which a new task can be assigned
|
||||
# must find other members of the groups the current list belongs to.
|
||||
def __init__(self, task_list, *args, **kwargs):
|
||||
super(AddItemForm, self).__init__(*args, **kwargs)
|
||||
# print dir(self.fields['list'])
|
||||
# print self.fields['list'].initial
|
||||
self.fields['assigned_to'].queryset = User.objects.filter(groups__in=[task_list.group])
|
||||
|
||||
due_date = forms.DateField(
|
||||
required=False,
|
||||
widget=forms.DateTimeInput(attrs={'class':'due_date_picker'})
|
||||
)
|
||||
|
||||
title = forms.CharField(
|
||||
widget=forms.widgets.TextInput(attrs={'size':35})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
|
||||
|
||||
|
||||
class EditItemForm(ModelForm):
|
||||
|
||||
# The picklist showing the users to which a new task can be assigned
|
||||
# must find other members of the groups the current list belongs to.
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditItemForm, self).__init__(*args, **kwargs)
|
||||
self.fields['assigned_to'].queryset = User.objects.filter(groups__in=[self.instance.list.group])
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
exclude = ('created_date','created_by',)
|
||||
|
||||
|
||||
|
||||
class AddExternalItemForm(ModelForm):
|
||||
"""Form to allow users who are not part of the GTD system to file a ticket."""
|
||||
|
||||
title = forms.CharField(
|
||||
widget=forms.widgets.TextInput(attrs={'size':35})
|
||||
)
|
||||
note = forms.CharField (
|
||||
widget=forms.widgets.Textarea(),
|
||||
help_text='Foo',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
exclude = ('list','created_date','due_date','created_by','assigned_to',)
|
||||
|
||||
|
||||
|
||||
class SearchForm(ModelForm):
|
||||
"""Search."""
|
||||
|
||||
q = forms.CharField(
|
||||
widget=forms.widgets.TextInput(attrs={'size':35})
|
||||
)
|
||||
|
||||
|
106
media/todo/css/styles.css
Normal file
106
media/todo/css/styles.css
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*Distributed*/
|
||||
|
||||
ul.messages li {
|
||||
color:green;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.overdue {
|
||||
color:#9A2441;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
/* Lighter font for completed items */
|
||||
#completed li {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
a.todo {
|
||||
text-decoration:none;
|
||||
color:#474747;
|
||||
}
|
||||
|
||||
a.showlink {
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
#tasktable a {
|
||||
font-weight:bold;
|
||||
font-family:"Arial";
|
||||
text-decoration:none;
|
||||
color:#474747;
|
||||
}
|
||||
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
color: #3A3A3A;
|
||||
font-family:Verdana;
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width:300px;
|
||||
}
|
||||
|
||||
input#id_priority {
|
||||
width:30px;
|
||||
}
|
||||
|
||||
.todo-button {
|
||||
border: 1px solid #9B9B9B;
|
||||
background: #E0E0E0;
|
||||
padding-bottom:2px;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
hr {
|
||||
color:#E5E5E5;
|
||||
}
|
||||
|
||||
#slideToggle {
|
||||
color:#4A8251;
|
||||
}
|
||||
|
||||
.todo-break {
|
||||
margin-top: 30px;
|
||||
border-top: 1px dotted gray;
|
||||
}
|
||||
|
||||
table.nocolor, table.nocolor tr, table.nocolor td {
|
||||
background-color: ;
|
||||
}
|
||||
|
||||
table#tasktable td, table#tasktable th {
|
||||
padding: 5px;
|
||||
/* font-size:0.9em;*/
|
||||
}
|
||||
|
||||
table#tasktable th {
|
||||
text-align: left;
|
||||
/* background-color: #9BCAE4; */
|
||||
background-color: #046380;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
table#tasktable tr.row1 {
|
||||
background-color: #AEF0BB;
|
||||
}
|
||||
|
||||
table#tasktable tr.row2 {
|
||||
background-color: #B6F3D5;
|
||||
}
|
||||
|
||||
.minor {
|
||||
font-style:italic;
|
||||
font-size:0.8em;
|
||||
}
|
||||
|
||||
.task_note, .task_comments {
|
||||
width: 70%;
|
||||
overflow: visible;
|
||||
}
|
213
media/todo/css/ui.datepicker.css
Normal file
213
media/todo/css/ui.datepicker.css
Normal file
|
@ -0,0 +1,213 @@
|
|||
/* Main Style Sheet for jQuery UI date picker */
|
||||
#ui-datepicker-div, .ui-datepicker-inline {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #ddd;
|
||||
width: 185px;
|
||||
}
|
||||
#ui-datepicker-div {
|
||||
display: none;
|
||||
border: 1px solid #777;
|
||||
z-index: 100; /*must have*/
|
||||
}
|
||||
.ui-datepicker-inline {
|
||||
float: left;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
.ui-datepicker-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
.ui-datepicker-dialog {
|
||||
padding: 5px !important;
|
||||
border: 4px ridge #ddd !important;
|
||||
}
|
||||
.ui-datepicker-disabled {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
background-color: white;
|
||||
opacity: 0.5;
|
||||
}
|
||||
button.ui-datepicker-trigger {
|
||||
width: 25px;
|
||||
}
|
||||
img.ui-datepicker-trigger {
|
||||
margin: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.ui-datepicker-prompt {
|
||||
float: left;
|
||||
padding: 2px;
|
||||
background: #ddd;
|
||||
color: #000;
|
||||
}
|
||||
* html .ui-datepicker-prompt {
|
||||
width: 185px;
|
||||
}
|
||||
.ui-datepicker-control, .ui-datepicker-links, .ui-datepicker-header, .ui-datepicker {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
}
|
||||
.ui-datepicker-control {
|
||||
background: #400;
|
||||
padding: 2px 0px;
|
||||
}
|
||||
.ui-datepicker-links {
|
||||
background: #000;
|
||||
padding: 2px 0px;
|
||||
}
|
||||
.ui-datepicker-control, .ui-datepicker-links {
|
||||
font-weight: bold;
|
||||
font-size: 80%;
|
||||
}
|
||||
.ui-datepicker-links label { /* disabled links */
|
||||
padding: 2px 5px;
|
||||
color: #888;
|
||||
}
|
||||
.ui-datepicker-clear, .ui-datepicker-prev {
|
||||
float: left;
|
||||
width: 34%;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-clear, .ui-datepicker-rtl .ui-datepicker-prev {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.ui-datepicker-current {
|
||||
float: left;
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
}
|
||||
.ui-datepicker-close, .ui-datepicker-next {
|
||||
float: right;
|
||||
width: 34%;
|
||||
text-align: right;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-close, .ui-datepicker-rtl .ui-datepicker-next {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
.ui-datepicker-header {
|
||||
padding: 1px 0 3px;
|
||||
background: #333;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
height: 1.3em;
|
||||
}
|
||||
.ui-datepicker-header select {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border: 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.ui-datepicker {
|
||||
background: #ccc;
|
||||
text-align: center;
|
||||
font-size: 100%;
|
||||
}
|
||||
.ui-datepicker a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.ui-datepicker-title-row {
|
||||
background: #777;
|
||||
}
|
||||
.ui-datepicker-days-row {
|
||||
background: #eee;
|
||||
color: #666;
|
||||
}
|
||||
.ui-datepicker-week-col {
|
||||
background: #777;
|
||||
color: #fff;
|
||||
}
|
||||
.ui-datepicker-days-cell {
|
||||
color: #000;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.ui-datepicker-days-cell a{
|
||||
display: block;
|
||||
}
|
||||
.ui-datepicker-week-end-cell {
|
||||
background: #ddd;
|
||||
}
|
||||
.ui-datepicker-title-row .ui-datepicker-week-end-cell {
|
||||
background: #777;
|
||||
}
|
||||
.ui-datepicker-days-cell-over {
|
||||
background: #fff;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
.ui-datepicker-unselectable {
|
||||
color: #888;
|
||||
}
|
||||
.ui-datepicker-today {
|
||||
background: #fcc !important;
|
||||
}
|
||||
.ui-datepicker-current-day {
|
||||
background: #999 !important;
|
||||
}
|
||||
.ui-datepicker-status {
|
||||
background: #ddd;
|
||||
width: 100%;
|
||||
font-size: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ________ Datepicker Links _______
|
||||
|
||||
** Reset link properties and then override them with !important */
|
||||
#ui-datepicker-div a, .ui-datepicker-inline a {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #000;
|
||||
}
|
||||
.ui-datepicker-inline .ui-datepicker-links a {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
.ui-datepicker-control a, .ui-datepicker-links a {
|
||||
padding: 2px 5px !important;
|
||||
color: #eee !important;
|
||||
}
|
||||
.ui-datepicker-title-row a {
|
||||
color: #eee !important;
|
||||
}
|
||||
.ui-datepicker-control a:hover {
|
||||
background: #fdd !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
.ui-datepicker-links a:hover, .ui-datepicker-title-row a:hover {
|
||||
background: #ddd !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* ___________ MULTIPLE MONTHS _________*/
|
||||
|
||||
.ui-datepicker-multi .ui-datepicker {
|
||||
border: 1px solid #777;
|
||||
}
|
||||
.ui-datepicker-one-month {
|
||||
float: left;
|
||||
width: 185px;
|
||||
}
|
||||
.ui-datepicker-new-row {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* ___________ IE6 IFRAME FIX ________ */
|
||||
|
||||
.ui-datepicker-cover {
|
||||
display: none; /*sorry for IE5*/
|
||||
display/**/: block; /*sorry for IE5*/
|
||||
position: absolute; /*must have*/
|
||||
z-index: -1; /*must have*/
|
||||
filter: mask(); /*must have*/
|
||||
top: -4px; /*must have*/
|
||||
left: -4px; /*must have*/
|
||||
width: 200px; /*must have*/
|
||||
height: 200px; /*must have*/
|
||||
}
|
382
media/todo/js/jquery.tablednd_0_5.js
Normal file
382
media/todo/js/jquery.tablednd_0_5.js
Normal file
|
@ -0,0 +1,382 @@
|
|||
/**
|
||||
* TableDnD plug-in for JQuery, allows you to drag and drop table rows
|
||||
* You can set up various options to control how the system will work
|
||||
* Copyright (c) Denis Howlett <denish@isocra.com>
|
||||
* Licensed like jQuery, see http://docs.jquery.com/License.
|
||||
*
|
||||
* Configuration options:
|
||||
*
|
||||
* onDragStyle
|
||||
* This is the style that is assigned to the row during drag. There are limitations to the styles that can be
|
||||
* associated with a row (such as you can't assign a border--well you can, but it won't be
|
||||
* displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
|
||||
* a map (as used in the jQuery css(...) function).
|
||||
* onDropStyle
|
||||
* This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
|
||||
* to what you can do. Also this replaces the original style, so again consider using onDragClass which
|
||||
* is simply added and then removed on drop.
|
||||
* onDragClass
|
||||
* This class is added for the duration of the drag and then removed when the row is dropped. It is more
|
||||
* flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
|
||||
* is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
|
||||
* stylesheet.
|
||||
* onDrop
|
||||
* Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
|
||||
* and the row that was dropped. You can work out the new order of the rows by using
|
||||
* table.rows.
|
||||
* onDragStart
|
||||
* Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
|
||||
* table and the row which the user has started to drag.
|
||||
* onAllowDrop
|
||||
* Pass a function that will be called as a row is over another row. If the function returns true, allow
|
||||
* dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
|
||||
* the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
|
||||
* scrollAmount
|
||||
* This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
|
||||
* window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
|
||||
* FF3 beta
|
||||
* dragHandle
|
||||
* This is the name of a class that you assign to one or more cells in each row that is draggable. If you
|
||||
* specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
|
||||
* will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
|
||||
* the whole row is draggable.
|
||||
*
|
||||
* Other ways to control behaviour:
|
||||
*
|
||||
* Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
|
||||
* that you don't want to be draggable.
|
||||
*
|
||||
* Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
|
||||
* <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
|
||||
* an ID as must all the rows.
|
||||
*
|
||||
* Other methods:
|
||||
*
|
||||
* $("...").tableDnDUpdate()
|
||||
* Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
|
||||
* This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
|
||||
* The table maintains the original configuration (so you don't have to specify it again).
|
||||
*
|
||||
* $("...").tableDnDSerialize()
|
||||
* Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
|
||||
* called from anywhere and isn't dependent on the currentTable being set up correctly before calling
|
||||
*
|
||||
* Known problems:
|
||||
* - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
|
||||
*
|
||||
* Version 0.2: 2008-02-20 First public version
|
||||
* Version 0.3: 2008-02-07 Added onDragStart option
|
||||
* Made the scroll amount configurable (default is 5 as before)
|
||||
* Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
|
||||
* Added onAllowDrop to control dropping
|
||||
* Fixed a bug which meant that you couldn't set the scroll amount in both directions
|
||||
* Added serialize method
|
||||
* Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
|
||||
* draggable
|
||||
* Improved the serialize method to use a default (and settable) regular expression.
|
||||
* Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
|
||||
*/
|
||||
jQuery.tableDnD = {
|
||||
/** Keep hold of the current table being dragged */
|
||||
currentTable : null,
|
||||
/** Keep hold of the current drag object if any */
|
||||
dragObject: null,
|
||||
/** The current mouse offset */
|
||||
mouseOffset: null,
|
||||
/** Remember the old value of Y so that we don't do too much processing */
|
||||
oldY: 0,
|
||||
|
||||
/** Actually build the structure */
|
||||
build: function(options) {
|
||||
// Set up the defaults if any
|
||||
|
||||
this.each(function() {
|
||||
// This is bound to each matching table, set up the defaults and override with user options
|
||||
this.tableDnDConfig = jQuery.extend({
|
||||
onDragStyle: null,
|
||||
onDropStyle: null,
|
||||
// Add in the default class for whileDragging
|
||||
onDragClass: "tDnD_whileDrag",
|
||||
onDrop: null,
|
||||
onDragStart: null,
|
||||
scrollAmount: 5,
|
||||
serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
|
||||
serializeParamName: null, // If you want to specify another parameter name instead of the table ID
|
||||
dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
|
||||
}, options || {});
|
||||
// Now make the rows draggable
|
||||
jQuery.tableDnD.makeDraggable(this);
|
||||
});
|
||||
|
||||
// Now we need to capture the mouse up and mouse move event
|
||||
// We can use bind so that we don't interfere with other event handlers
|
||||
jQuery(document)
|
||||
.bind('mousemove', jQuery.tableDnD.mousemove)
|
||||
.bind('mouseup', jQuery.tableDnD.mouseup);
|
||||
|
||||
// Don't break the chain
|
||||
return this;
|
||||
},
|
||||
|
||||
/** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
|
||||
makeDraggable: function(table) {
|
||||
var config = table.tableDnDConfig;
|
||||
if (table.tableDnDConfig.dragHandle) {
|
||||
// We only need to add the event to the specified cells
|
||||
var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
|
||||
cells.each(function() {
|
||||
// The cell is bound to "this"
|
||||
jQuery(this).mousedown(function(ev) {
|
||||
jQuery.tableDnD.dragObject = this.parentNode;
|
||||
jQuery.tableDnD.currentTable = table;
|
||||
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
|
||||
if (config.onDragStart) {
|
||||
// Call the onDrop method if there is one
|
||||
config.onDragStart(table, this);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
})
|
||||
} else {
|
||||
// For backwards compatibility, we add the event to the whole row
|
||||
var rows = jQuery("tr", table); // get all the rows as a wrapped set
|
||||
rows.each(function() {
|
||||
// Iterate through each row, the row is bound to "this"
|
||||
var row = jQuery(this);
|
||||
if (! row.hasClass("nodrag")) {
|
||||
row.mousedown(function(ev) {
|
||||
if (ev.target.tagName == "TD") {
|
||||
jQuery.tableDnD.dragObject = this;
|
||||
jQuery.tableDnD.currentTable = table;
|
||||
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
|
||||
if (config.onDragStart) {
|
||||
// Call the onDrop method if there is one
|
||||
config.onDragStart(table, this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}).css("cursor", "move"); // Store the tableDnD object
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTables: function() {
|
||||
this.each(function() {
|
||||
// this is now bound to each matching table
|
||||
if (this.tableDnDConfig) {
|
||||
jQuery.tableDnD.makeDraggable(this);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** Get the mouse coordinates from the event (allowing for browser differences) */
|
||||
mouseCoords: function(ev){
|
||||
if(ev.pageX || ev.pageY){
|
||||
return {x:ev.pageX, y:ev.pageY};
|
||||
}
|
||||
return {
|
||||
x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
|
||||
y:ev.clientY + document.body.scrollTop - document.body.clientTop
|
||||
};
|
||||
},
|
||||
|
||||
/** Given a target element and a mouse event, get the mouse offset from that element.
|
||||
To do this we need the element's position and the mouse position */
|
||||
getMouseOffset: function(target, ev) {
|
||||
ev = ev || window.event;
|
||||
|
||||
var docPos = this.getPosition(target);
|
||||
var mousePos = this.mouseCoords(ev);
|
||||
return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
|
||||
},
|
||||
|
||||
/** Get the position of an element by going up the DOM tree and adding up all the offsets */
|
||||
getPosition: function(e){
|
||||
var left = 0;
|
||||
var top = 0;
|
||||
/** Safari fix -- thanks to Luis Chato for this! */
|
||||
if (e.offsetHeight == 0) {
|
||||
/** Safari 2 doesn't correctly grab the offsetTop of a table row
|
||||
this is detailed here:
|
||||
http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
|
||||
the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
|
||||
note that firefox will return a text node as a first child, so designing a more thorough
|
||||
solution may need to take that into account, for now this seems to work in firefox, safari, ie */
|
||||
e = e.firstChild; // a table cell
|
||||
}
|
||||
|
||||
while (e.offsetParent){
|
||||
left += e.offsetLeft;
|
||||
top += e.offsetTop;
|
||||
e = e.offsetParent;
|
||||
}
|
||||
|
||||
left += e.offsetLeft;
|
||||
top += e.offsetTop;
|
||||
|
||||
return {x:left, y:top};
|
||||
},
|
||||
|
||||
mousemove: function(ev) {
|
||||
if (jQuery.tableDnD.dragObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dragObj = jQuery(jQuery.tableDnD.dragObject);
|
||||
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
|
||||
var mousePos = jQuery.tableDnD.mouseCoords(ev);
|
||||
var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
|
||||
//auto scroll the window
|
||||
var yOffset = window.pageYOffset;
|
||||
if (document.all) {
|
||||
// Windows version
|
||||
//yOffset=document.body.scrollTop;
|
||||
if (typeof document.compatMode != 'undefined' &&
|
||||
document.compatMode != 'BackCompat') {
|
||||
yOffset = document.documentElement.scrollTop;
|
||||
}
|
||||
else if (typeof document.body != 'undefined') {
|
||||
yOffset=document.body.scrollTop;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (mousePos.y-yOffset < config.scrollAmount) {
|
||||
window.scrollBy(0, -config.scrollAmount);
|
||||
} else {
|
||||
var windowHeight = window.innerHeight ? window.innerHeight
|
||||
: document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
|
||||
if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
|
||||
window.scrollBy(0, config.scrollAmount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (y != jQuery.tableDnD.oldY) {
|
||||
// work out if we're going up or down...
|
||||
var movingDown = y > jQuery.tableDnD.oldY;
|
||||
// update the old value
|
||||
jQuery.tableDnD.oldY = y;
|
||||
// update the style to show we're dragging
|
||||
if (config.onDragClass) {
|
||||
dragObj.addClass(config.onDragClass);
|
||||
} else {
|
||||
dragObj.css(config.onDragStyle);
|
||||
}
|
||||
// If we're over a row then move the dragged row to there so that the user sees the
|
||||
// effect dynamically
|
||||
var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
|
||||
if (currentRow) {
|
||||
// TODO worry about what happens when there are multiple TBODIES
|
||||
if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
|
||||
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
|
||||
} else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
|
||||
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** We're only worried about the y position really, because we can only move rows up and down */
|
||||
findDropTargetRow: function(draggedRow, y) {
|
||||
var rows = jQuery.tableDnD.currentTable.rows;
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
var row = rows[i];
|
||||
var rowY = this.getPosition(row).y;
|
||||
var rowHeight = parseInt(row.offsetHeight)/2;
|
||||
if (row.offsetHeight == 0) {
|
||||
rowY = this.getPosition(row.firstChild).y;
|
||||
rowHeight = parseInt(row.firstChild.offsetHeight)/2;
|
||||
}
|
||||
// Because we always have to insert before, we need to offset the height a bit
|
||||
if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
|
||||
// that's the row we're over
|
||||
// If it's the same as the current row, ignore it
|
||||
if (row == draggedRow) {return null;}
|
||||
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
|
||||
if (config.onAllowDrop) {
|
||||
if (config.onAllowDrop(draggedRow, row)) {
|
||||
return row;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
|
||||
var nodrop = jQuery(row).hasClass("nodrop");
|
||||
if (! nodrop) {
|
||||
return row;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
mouseup: function(e) {
|
||||
if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
|
||||
var droppedRow = jQuery.tableDnD.dragObject;
|
||||
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
|
||||
// If we have a dragObject, then we need to release it,
|
||||
// The row will already have been moved to the right place so we just reset stuff
|
||||
if (config.onDragClass) {
|
||||
jQuery(droppedRow).removeClass(config.onDragClass);
|
||||
} else {
|
||||
jQuery(droppedRow).css(config.onDropStyle);
|
||||
}
|
||||
jQuery.tableDnD.dragObject = null;
|
||||
if (config.onDrop) {
|
||||
// Call the onDrop method if there is one
|
||||
config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
|
||||
}
|
||||
jQuery.tableDnD.currentTable = null; // let go of the table too
|
||||
}
|
||||
},
|
||||
|
||||
serialize: function() {
|
||||
if (jQuery.tableDnD.currentTable) {
|
||||
return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
|
||||
} else {
|
||||
return "Error: No Table id set, you need to set an id on your table and every row";
|
||||
}
|
||||
},
|
||||
|
||||
serializeTable: function(table) {
|
||||
var result = "";
|
||||
var tableId = table.id;
|
||||
var rows = table.rows;
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
if (result.length > 0) result += "&";
|
||||
var rowId = rows[i].id;
|
||||
if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
|
||||
rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
|
||||
}
|
||||
|
||||
result += tableId + '[]=' + rowId;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
serializeTables: function() {
|
||||
var result = "";
|
||||
this.each(function() {
|
||||
// this is now bound to each matching table
|
||||
result += jQuery.tableDnD.serializeTable(this);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
jQuery.fn.extend(
|
||||
{
|
||||
tableDnD : jQuery.tableDnD.build,
|
||||
tableDnDUpdate : jQuery.tableDnD.updateTables,
|
||||
tableDnDSerialize: jQuery.tableDnD.serializeTables
|
||||
}
|
||||
);
|
96
models.py
Normal file
96
models.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
from django.db import models
|
||||
from django.forms.models import ModelForm
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User,Group
|
||||
import string, datetime
|
||||
from django.template.defaultfilters import slugify
|
||||
|
||||
|
||||
class List(models.Model):
|
||||
name = models.CharField(max_length=60)
|
||||
slug = models.SlugField(max_length=60,editable=False)
|
||||
# slug = models.SlugField(max_length=60)
|
||||
group = models.ForeignKey(Group)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.id:
|
||||
self.slug = slugify(self.name)
|
||||
|
||||
super(List, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
# Custom manager lets us do things like Item.completed_tasks.all()
|
||||
objects = models.Manager()
|
||||
|
||||
def incomplete_tasks(self):
|
||||
# Count all incomplete tasks on the current list instance
|
||||
return Item.objects.filter(list=self,completed=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
verbose_name_plural = "Lists"
|
||||
|
||||
# Prevents (at the database level) creation of two lists with the same name in the same group
|
||||
unique_together = ("group", "slug")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Item(models.Model):
|
||||
title = models.CharField(max_length=140)
|
||||
list = models.ForeignKey(List)
|
||||
created_date = models.DateField()
|
||||
due_date = models.DateField(blank=True,null=True,)
|
||||
completed = models.BooleanField()
|
||||
completed_date = models.DateField(blank=True,null=True)
|
||||
created_by = models.ForeignKey(User, related_name='created_by')
|
||||
assigned_to = models.ForeignKey(User, related_name='todo_assigned_to')
|
||||
note = models.TextField(blank=True,null=True)
|
||||
priority = models.PositiveIntegerField(max_length=3)
|
||||
|
||||
# Model method: Has due date for an instance of this object passed?
|
||||
def overdue_status(self):
|
||||
"Returns whether the item's due date has passed or not."
|
||||
if datetime.date.today() > self.due_date :
|
||||
return 1
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
# Auto-set the item creation / completed date
|
||||
def save(self):
|
||||
# Set datetime on initial item save
|
||||
if not self.id:
|
||||
self.created_date = datetime.datetime.now()
|
||||
|
||||
# If Item is being marked complete, set the completed_date
|
||||
if self.completed :
|
||||
self.completed_date = datetime.datetime.now()
|
||||
super(Item, self).save()
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ["priority"]
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
"""
|
||||
Not using Django's built-in comments becase we want to be able to save
|
||||
a comment and change task details at the same time. Rolling our own since it's easy.
|
||||
"""
|
||||
author = models.ForeignKey(User)
|
||||
task = models.ForeignKey(Item)
|
||||
date = models.DateTimeField(default=datetime.datetime.now)
|
||||
body = models.TextField(blank=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return '%s - %s' % (
|
||||
self.author,
|
||||
self.date,
|
||||
)
|
22
setup.py
Normal file
22
setup.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='django-todo',
|
||||
version='1.2',
|
||||
description='A multi-user, multi-group task management and assignment system for Django.',
|
||||
author='Scot Hacker',
|
||||
author_email='shacker@birdhouse.org',
|
||||
url='http://github.com/shacker/django-todo',
|
||||
packages=find_packages(),
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Framework :: Django',
|
||||
],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
)
|
83
templates/todo/add_external_task.html
Normal file
83
templates/todo/add_external_task.html
Normal file
|
@ -0,0 +1,83 @@
|
|||
{% extends "todo/base.html" %}
|
||||
{% block page_heading %}{% endblock %}
|
||||
{% block title %}File Ticket{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<h2>{{ task }}</h2>
|
||||
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
{% if task.note %}
|
||||
<div class="task_note"><strong>Note:</strong> {{ task.note|safe|urlize|linebreaks }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="TaskEdit">
|
||||
<h3>File Trouble Ticket</h3>
|
||||
<p>Trouble with a computer or other technical system at the J-School? <br />
|
||||
Use this form to report the difficulty - we'll get right back to you. </p>
|
||||
|
||||
{% if form.errors %}
|
||||
|
||||
{% for error in form.errors %}
|
||||
<ul class="errorlist">
|
||||
<li><strong>The {{ error|escape }} field is required.</strong></li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
<br />
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<table>
|
||||
{{ form.management_form }}
|
||||
{{ form.id }}
|
||||
|
||||
<tr>
|
||||
<td>Summary:</td>
|
||||
|
||||
<td>{{ form.title }} <br />
|
||||
Include the workstation number in your summary, e.g. <br />
|
||||
"Radio Lab # 4: Purple smoke pouring out the back."
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td valign="top">Note:</td>
|
||||
|
||||
<td valign="top">{{ form.note }}<br />
|
||||
Please describe the problem. </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Priority:</td>
|
||||
|
||||
<td>{{ form.priority }} <br />
|
||||
Enter a number between 1 and 5, <br />
|
||||
where 5 is highest ("Computer is on fire = True").
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<p><input type="submit" class="todo-button" name="add_task" value="Submit"></p>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
{% endblock %}
|
24
templates/todo/add_list.html
Normal file
24
templates/todo/add_list.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
{% extends "todo/base.html" %}
|
||||
{% block page_heading %}{% endblock %}
|
||||
{% block title %}Add Todo List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h2>Add a list:</h2>
|
||||
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>{{ form }}</table>
|
||||
<p><input type="submit" value="Submit" class="todo-button"></p>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
20
templates/todo/base.html
Normal file
20
templates/todo/base.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% block page_heading %}GTD (Getting Things Done){% endblock %}
|
||||
{% block extrahead %}
|
||||
<!-- CSS and JavaScript for django-todo -->
|
||||
<link rel="stylesheet" type="text/css" href="/media/todo/css/styles.css" />
|
||||
|
||||
<script src="{{MEDIA_URL}}js/ui.datepicker.js" type="text/javascript"></script>
|
||||
<script src="{{MEDIA_URL}}todo/js/jquery.tablednd_0_5.js" type="text/javascript"></script>
|
||||
<link type="text/css" href="{{MEDIA_URL}}js/jquery-ui-1.7.1.custom.css" rel="Stylesheet" />
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
// thedate.x comes from the edit_task view. If this is a new entry,
|
||||
// thedate won't be present and datepicker will fall back on the default (today).
|
||||
$(document).ready(function(){
|
||||
$('#id_due_date').datepicker({defaultDate: new Date({{thedate.year}}, {{thedate.month}} - 1, {{thedate.day}}),});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock extrahead %}
|
56
templates/todo/del_list.html
Normal file
56
templates/todo/del_list.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ list_title }} to-do items{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# Only admins can delete lists. #}
|
||||
{% ifequal can_del 1 %}
|
||||
|
||||
|
||||
{% if list_killed %}
|
||||
|
||||
<p> {{ list.name }} is gone.</p>
|
||||
|
||||
<a href="{% url todo-lists %}">Return to lists</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<h1>Delete entire list: {{ list.name }} ?</h1>
|
||||
|
||||
<p>Category tally:</p>
|
||||
|
||||
<ul>
|
||||
<li>Incomplete: {{ item_count_undone }} </li>
|
||||
<li>Complete: {{ item_count_done }} </li>
|
||||
<li><strong>Total: {{ item_count_total }}</strong> </li>
|
||||
</ul>
|
||||
|
||||
<p> ... all of which will be irretrievably <strong>blown away</strong>. Are you sure you want to do that?</p>
|
||||
|
||||
<form action="" method="post" accept-charset="utf-8">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="hidden" name="list" value="{{ list.id }}" id="some_name">
|
||||
<p><input type="submit" name="delete-confirm" value="Do it! →" class="todo-button"> </p>
|
||||
</form>
|
||||
|
||||
|
||||
<a href="{% url todo-incomplete_tasks list.id list_slug %}">Return to list: {{ list.name }}</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>Sorry, you don't have permission to delete lists. Please contact your group administrator.</p>
|
||||
|
||||
{% endifequal %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
20
templates/todo/email/assigned_body.txt
Normal file
20
templates/todo/email/assigned_body.txt
Normal file
|
@ -0,0 +1,20 @@
|
|||
Dear {{ task.assigned_to.first_name }} -
|
||||
|
||||
A new task on the list {{ task.list.name }} has been assigned to you by {{ task.created_by.first_name }} {{ task.created_by.last_name }}:
|
||||
|
||||
{{ task.title }}
|
||||
|
||||
{% if task.note %}
|
||||
{% autoescape off %}
|
||||
Note: {{ task.note }}
|
||||
{% endautoescape %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
Task details/comments:
|
||||
http://{{ site }}{% url todo-task_detail task.id %}
|
||||
|
||||
List {{ task.list.name }}:
|
||||
http://{{ site }}{% url todo-incomplete_tasks task.list.id task.list.slug %}
|
1
templates/todo/email/assigned_subject.txt
Normal file
1
templates/todo/email/assigned_subject.txt
Normal file
|
@ -0,0 +1 @@
|
|||
GTD: New task - {% autoescape off %}Note: {{ task.title }}{% endautoescape %}
|
16
templates/todo/email/newcomment_body.txt
Normal file
16
templates/todo/email/newcomment_body.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
A new task comment has been added.
|
||||
|
||||
Task: {{ task.title }}
|
||||
Commenter: {{ user.first_name }} {{ user.last_name }}
|
||||
|
||||
Comment:
|
||||
{% autoescape off %}
|
||||
{{ body }}
|
||||
{% endautoescape %}
|
||||
|
||||
Task details/comments:
|
||||
https://{{ site }}{% url todo-task_detail task.id %}
|
||||
|
||||
List {{ task.list.name }}:
|
||||
https://{{ site }}{% url todo-incomplete_tasks task.list.id task.list.slug %}
|
||||
|
40
templates/todo/list_lists.html
Normal file
40
templates/todo/list_lists.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "todo/base.html" %}
|
||||
|
||||
{% block title %}{{ list_title }} Todo Lists{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h1>To-do Lists</h1>
|
||||
|
||||
<p>{{ item_count }} items in {{ list_count }} lists</p>
|
||||
|
||||
{% regroup list_list by group as section_list %}
|
||||
|
||||
{% for group in section_list %}
|
||||
<h3>{{ group.grouper }}</h3>
|
||||
<ul>
|
||||
{% for item in group.list %}
|
||||
<li><a class="todo" href="{% url todo-incomplete_tasks item.id item.slug %}">{{ item.name }} </a> ({{ item.incomplete_tasks.count }}/{{ item.item_set.count }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
|
||||
<div class="todo-break">
|
||||
|
||||
</div>
|
||||
|
||||
<p><a href="{% url todo-add_list %}">Create new todo list</a></p>
|
||||
|
||||
{% endblock %}
|
37
templates/todo/search_results.html
Normal file
37
templates/todo/search_results.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
{% extends "todo/base.html" %}
|
||||
|
||||
{% block title %}Search results{% endblock %}
|
||||
{% block body_id %}post_search{% endblock %}
|
||||
|
||||
|
||||
{% block content_title %}
|
||||
<h2 class="page_title">Search</h2>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if message %}
|
||||
<p class="message">{{ message }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if found_items %}
|
||||
<h2>{{found_items.count}} search results for term: "{{ query_string }}"</h2>
|
||||
<div class="post_list">
|
||||
|
||||
{% for f in found_items %}
|
||||
<p><strong><a href="{% url todo-task_detail f.id %}">{{ f.title }}</a></strong><br />
|
||||
|
||||
<span class="minor">
|
||||
On list: <a href="{% url todo-incomplete_tasks f.list.id f.list.slug %}">{{ f.list }}</a><br />
|
||||
Assigned to: {{ f.assigned_to }} (created by: {{ f.created_by }})<br />
|
||||
Complete: {{ f.completed|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<h2> No results to show, sorry.</h2>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
191
templates/todo/view_list.html
Normal file
191
templates/todo/view_list.html
Normal file
|
@ -0,0 +1,191 @@
|
|||
{% extends "todo/base.html" %}
|
||||
|
||||
{% block title %}Todo List: {{ list.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function order_tasks(data) {
|
||||
// The JQuery plugin tableDnD provides a serialize() function which provides the re-ordered
|
||||
// data in a list. We pass that list as an object called "data" to a Django view
|
||||
// to save the re-ordered data into the database.
|
||||
|
||||
$.post("{% url todo-reorder_tasks %}", data, "json");
|
||||
return False;
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initialise the task table for drag/drop re-ordering
|
||||
$("#tasktable").tableDnD();
|
||||
|
||||
$('#tasktable').tableDnD({
|
||||
onDrop: function(table, row) {
|
||||
order_tasks($.tableDnD.serialize());
|
||||
}
|
||||
});
|
||||
|
||||
// Initially hide the Add Task form
|
||||
$('#AddTask').hide();
|
||||
|
||||
// toggle slide to show the Add Task form when link clicked
|
||||
$('#slideToggle').click(function(){
|
||||
$(this).siblings('#AddTask').slideToggle();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
{% ifequal list_slug "mine" %}
|
||||
<h1>Tasks assigned to {{ request.user }}</h1>
|
||||
{% else %}
|
||||
{% ifequal auth_ok 1 %}
|
||||
<h1>Tasks filed under "{{ list.name }}"</h1>
|
||||
<p>This list belongs to group {{ list.group }}</p>
|
||||
{% endifequal %}
|
||||
{% endifequal %}
|
||||
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% ifequal auth_ok 1 %}
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Only show task adder if viewing a proper list #}
|
||||
{% ifnotequal list_slug "mine" %}
|
||||
<h2 style="margin-bottom:0px;" id="slideToggle" >→ Click to add task ←</h2>
|
||||
|
||||
<div id="AddTask">
|
||||
<table class="nocolor" border="0" cellspacing="5" cellpadding="5">
|
||||
<tr>
|
||||
<td>{{ form.title.errors }}</td>
|
||||
<td>{{ form.due_date.errors }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="id_title">Task:</label> {{ form.title }}</td>
|
||||
<td><label for="id_due_date">Due date:</label> {{ form.due_date }}</td>
|
||||
<td><label for="id_assigned">Assign to:</label> {{ form.assigned_to }}</td>
|
||||
<td><label for="id_notify">Notify*:</label> <input type="checkbox" checked="checked" name="notify" value="1" id="notify"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5"><label for="id_note">Note:</label> {{ form.note }}
|
||||
<p class="minor">*Email notifications will only be sent if task is assigned to someone besides yourself.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<input type="hidden" name="priority" value="999" id="id_priority">
|
||||
<input type="hidden" name="created_by" value="{{ request.user.id }}" id="id_created_by">
|
||||
<input type="hidden" name="list" value="{{ listid }}" id="id_list">
|
||||
<input type="hidden" name="created_date" value="{{ created_date }}" id="id_created_date">
|
||||
|
||||
<p><input type="submit" name="add_task" value="Add task" class="todo-button"></p>
|
||||
</div>
|
||||
|
||||
{% endifnotequal %}
|
||||
|
||||
|
||||
{% ifequal view_completed 0 %}
|
||||
|
||||
<h3>Incomplete tasks :: Drag rows to set priorities</h3>
|
||||
|
||||
<table border="0" id="tasktable">
|
||||
<tr>
|
||||
<th>Done</th>
|
||||
<th>Task</th>
|
||||
<th>Created</th>
|
||||
<th>Due on</th>
|
||||
<th>Owner</th>
|
||||
<th>Assigned</th>
|
||||
<th>Note</th>
|
||||
<th>Comm</th>
|
||||
|
||||
{% ifequal list_slug "mine" %}
|
||||
<th>List</th>
|
||||
{% endifequal %}
|
||||
|
||||
<th>Del</th>
|
||||
</tr>
|
||||
{% for task in task_list %}
|
||||
<tr class="{% cycle 'row1' 'row2' %}" id="{{ task.id }}">
|
||||
<td><input type="checkbox" name="mark_done" value="{{ task.id }}" id="mark_done_{{ task.id }}"> </td>
|
||||
<td><a href="{% url todo-task_detail task.id %}">{{ task.title|truncatewords:20 }}</a></td>
|
||||
<td>{{ task.created_date|date:"m/d/Y" }}</td>
|
||||
<td>
|
||||
{% if task.overdue_status %}<span class="overdue">{% endif %}
|
||||
{{ task.due_date|date:"m/d/Y" }}
|
||||
{% if task.overdue_status %}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ task.created_by }}</td>
|
||||
<td>{{ task.assigned_to }}</td>
|
||||
<td style="text-align:center;">{% if task.note %}≈{% endif %} </td>
|
||||
<td style="text-align:center;">{% ifnotequal task.comment_set.all.count 0 %}{{ task.comment_set.all.count }}{% endifnotequal %}
|
||||
|
||||
</td>
|
||||
{% ifequal list_slug "mine" %}
|
||||
<td><a href="{% url todo-incomplete_tasks task.list.id task.list.slug %}">{{ task.list }}</a></td>
|
||||
{% endifequal %}
|
||||
|
||||
<td><input type="checkbox" name="del_task" value="{{ task.id }}" id="del_task_{{ task.id }}"> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p><input type="submit" name="mark_tasks_done" value="Continue..." class="todo-button"></p>
|
||||
|
||||
<p><a class="todo" href="{% url todo-completed_tasks list_id list_slug %}">View completed tasks</a></p>
|
||||
{% endifequal %}
|
||||
|
||||
{% ifequal view_completed 1 %}
|
||||
<h3>Completed tasks</h3>
|
||||
|
||||
<table border="0" id="tasktable">
|
||||
<tr>
|
||||
<th>Undo</th>
|
||||
<th>Task</th>
|
||||
<th>Created</th>
|
||||
<th>Completed on</th>
|
||||
<th>Note</th>
|
||||
<th>Comm</th>
|
||||
{% ifequal list_slug "mine" %}
|
||||
<th>List</th>
|
||||
{% endifequal %}
|
||||
<th>Del</th>
|
||||
</tr>
|
||||
|
||||
|
||||
{% for task in completed_list %}
|
||||
<tr class="{% cycle 'row1' 'row2' %}">
|
||||
<td><input type="checkbox" name="undo_completed_task" value="{{ task.id }}" id="id_undo_completed_task{{ task.id }}"> </td>
|
||||
<td><a href="{% url todo-task_detail task.id %}">{{ task.title|truncatewords:20 }}</a></td>
|
||||
<td>{{ task.created_date|date:"m/d/Y" }}</td>
|
||||
<td>{{ task.completed_date|date:"m/d/Y" }}</td>
|
||||
<td style="text-align:center;">{% if task.note %}≈{% endif %} </td>
|
||||
<td style="text-align:center;">{% ifnotequal task.comment_set.all.count 0 %}{{ task.comment_set.all.count }}{% endifnotequal %}
|
||||
|
||||
<td><input type="checkbox" name="del_completed_task" value="{{ task.id }}" id="del_completed_task_{{ task.id }}"> </td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
<p><input type="submit" name="deldonetasks" value="Continue..." class="todo-button"></p>
|
||||
</form>
|
||||
<p><a class="todo" href="{% url todo-incomplete_tasks list_id list_slug %}">View incomplete tasks</a></p>
|
||||
{% endifequal %}
|
||||
|
||||
{% ifequal can_del 1 %}
|
||||
{% ifnotequal list_slug "mine" %}
|
||||
<p><a class="todo" href="{% url todo-del_list list_id list_slug %}">Delete this list</a></p>
|
||||
{% endifnotequal %}
|
||||
{% endifequal %}
|
||||
|
||||
{% endifequal %}
|
||||
{% endblock %}
|
120
templates/todo/view_task.html
Normal file
120
templates/todo/view_task.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
{% extends "todo/base.html" %}
|
||||
|
||||
{% block title %}Task: {{ task.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initially hide the TaskEdit form
|
||||
$('#TaskEdit').hide();
|
||||
|
||||
// toggle slide to show the Add Task form when link clicked
|
||||
$('#slideToggle').click(function(){
|
||||
$(this).siblings('#TaskEdit').slideToggle();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% ifequal auth_ok 1 %}
|
||||
|
||||
<h2>{{ task }}</h2>
|
||||
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<p id="slideToggle" ><strong>→ Click to edit details ←</strong></p>
|
||||
|
||||
<p><strong>In list:</strong> <a href="{% url todo-incomplete_tasks task.list.id task.list.slug %}" class="showlink">{{ task.list }}</a><br />
|
||||
<strong>Assigned to:</strong> {{ task.assigned_to.first_name }} {{ task.assigned_to.last_name }}<br />
|
||||
<strong>Created by:</strong> {{ task.created_by.first_name }} {{ task.created_by.last_name }}<br />
|
||||
<strong>Due date:</strong> {{ task.due_date }}<br />
|
||||
<strong>Completed:</strong> {{ form.completed }}<br />
|
||||
</p>
|
||||
|
||||
|
||||
{% if task.note %}
|
||||
<div class="task_note"><strong>Note:</strong> {{ task.note|safe|urlize|linebreaks }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="TaskEdit">
|
||||
<h3>Edit Task</h3>
|
||||
|
||||
<table>
|
||||
{{ form.management_form }}
|
||||
|
||||
|
||||
{{ form.id }}
|
||||
<tr>
|
||||
<td>Task:</td>
|
||||
<td>{{ form.title }} </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>List:</td>
|
||||
<td>{{ form.list }} </td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>Due:</td>
|
||||
<td>{{ form.due_date }} </td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td>Assigned to:</td>
|
||||
<td>{{ form.assigned_to }} </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td valign="top">Note:</td>
|
||||
<td>{{ form.note }} </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Priority:</td>
|
||||
<td>{{ form.priority }} </td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<p><input type="submit" class="todo-button" name="edit_task" value="Edit task"></p>
|
||||
</div>
|
||||
|
||||
|
||||
<hr />
|
||||
|
||||
|
||||
<h3>Add comment</h3>
|
||||
|
||||
<textarea name="comment-body" rows="8" cols="40"></textarea>
|
||||
<p><input class="todo-button"type="submit" value="Submit"></p>
|
||||
|
||||
</form>
|
||||
|
||||
<h3>Comments on this task</h3>
|
||||
|
||||
<div class="task_comments">
|
||||
{% for comment in comment_list %}
|
||||
<p><strong>{{ comment.author.first_name }} {{ comment.author.last_name }}, {{ comment.date|date:"F d Y P" }}</strong> </p>
|
||||
{{ comment.body|safe|urlize|linebreaks }}
|
||||
{% empty %}
|
||||
<p>No Comments</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endifequal %}
|
||||
|
||||
{% endblock %}
|
22
urls.py
Normal file
22
urls.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from django.conf.urls.defaults import *
|
||||
from django.views.generic.simple import direct_to_template
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^mine/$', 'todo.views.view_list',{'list_slug':'mine'},name="todo-mine"),
|
||||
url(r'^(?P<list_id>\d{1,4})/(?P<list_slug>[\w-]+)/delete$', 'todo.views.del_list',name="todo-del_list"),
|
||||
url(r'^task/(?P<task_id>\d{1,6})$', 'todo.views.view_task', name='todo-task_detail'),
|
||||
url(r'^(?P<list_id>\d{1,4})/(?P<list_slug>[\w-]+)$', 'todo.views.view_list', name='todo-incomplete_tasks'),
|
||||
url(r'^(?P<list_id>\d{1,4})/(?P<list_slug>[\w-]+)/completed$', 'todo.views.view_list', {'view_completed':1},name='todo-completed_tasks'),
|
||||
url(r'^add_list/$', 'todo.views.add_list',name="todo-add_list"),
|
||||
url(r'^search/$', 'todo.views.search',name="todo-search"),
|
||||
url(r'^$', 'todo.views.list_lists',name="todo-lists"),
|
||||
|
||||
# View reorder_tasks is only called by JQuery for drag/drop task ordering
|
||||
url(r'^reorder_tasks/$', 'todo.views.reorder_tasks',name="todo-reorder_tasks"),
|
||||
|
||||
url(r'^ticket/add/$', 'todo.views.external_add',name="todo-external-add"),
|
||||
url(r'^recent/added/$', 'todo.views.view_list',{'list_slug':'recent-add'},name="todo-recently_added"),
|
||||
url(r'^recent/completed/$', 'todo.views.view_list',{'list_slug':'recent-complete'},name="todo-recently_completed"),
|
||||
)
|
||||
|
415
views.py
Normal file
415
views.py
Normal file
|
@ -0,0 +1,415 @@
|
|||
from django import forms
|
||||
from django.shortcuts import render_to_response
|
||||
from todo.models import Item, List, Comment
|
||||
from todo.forms import AddListForm, AddItemForm, EditItemForm, AddExternalItemForm, SearchForm
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib import auth
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.sites.models import Site
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import send_mail
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
|
||||
import datetime
|
||||
|
||||
# Need for links in email templates
|
||||
current_site = Site.objects.get_current()
|
||||
|
||||
|
||||
@login_required
|
||||
def list_lists(request):
|
||||
|
||||
"""
|
||||
Homepage view - list of lists a user can view, and ability to add a list.
|
||||
"""
|
||||
|
||||
# Make sure belongs to at least one group.
|
||||
group_count = request.user.groups.all().count()
|
||||
if group_count == 0:
|
||||
request.user.message_set.create(message="You do not yet belong to any groups. Ask your administrator to add you to one.")
|
||||
|
||||
|
||||
# Only show lists to the user that belong to groups they are members of.
|
||||
# Superusers see all lists
|
||||
if request.user.is_superuser:
|
||||
list_list = List.objects.all().order_by('group','name')
|
||||
else:
|
||||
list_list = List.objects.filter(group__in=request.user.groups.all).order_by('group','name')
|
||||
|
||||
# Count everything
|
||||
list_count = list_list.count()
|
||||
|
||||
# Note admin users see all lists, so count shouldn't filter by just lists the admin belongs to
|
||||
if request.user.is_superuser :
|
||||
item_count = Item.objects.filter(completed=0).count()
|
||||
else:
|
||||
item_count = Item.objects.filter(completed=0).filter(list__group__in=request.user.groups.all()).count()
|
||||
|
||||
return render_to_response('todo/list_lists.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@login_required
|
||||
def del_list(request,list_id,list_slug):
|
||||
|
||||
"""
|
||||
Delete an entire list. Danger Will Robinson! Only staff members should be allowed to access this view.
|
||||
"""
|
||||
|
||||
if request.user.is_staff:
|
||||
can_del = 1
|
||||
|
||||
# Get this list's object (to derive list.name, list.id, etc.)
|
||||
list = get_object_or_404(List, slug=list_slug)
|
||||
|
||||
# If delete confirmation is in the POST, delete all items in the list, then kill the list itself
|
||||
if request.method == 'POST':
|
||||
# Can the items
|
||||
del_items = Item.objects.filter(list=list.id)
|
||||
for del_item in del_items:
|
||||
del_item.delete()
|
||||
|
||||
# Kill the list
|
||||
del_list = List.objects.get(id=list.id)
|
||||
del_list.delete()
|
||||
|
||||
# A var to send to the template so we can show the right thing
|
||||
list_killed = 1
|
||||
|
||||
else:
|
||||
item_count_done = Item.objects.filter(list=list.id,completed=1).count()
|
||||
item_count_undone = Item.objects.filter(list=list.id,completed=0).count()
|
||||
item_count_total = Item.objects.filter(list=list.id).count()
|
||||
|
||||
return render_to_response('todo/del_list.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@login_required
|
||||
def view_list(request,list_id=0,list_slug=None,view_completed=0):
|
||||
|
||||
"""
|
||||
Display and manage items in a task list
|
||||
"""
|
||||
|
||||
# Make sure the accessing user has permission to view this list.
|
||||
# Always authorize the "mine" view. Admins can view/edit all lists.
|
||||
|
||||
if list_slug == "mine" or list_slug == "recent-add" or list_slug == "recent-complete" :
|
||||
auth_ok =1
|
||||
else:
|
||||
list = get_object_or_404(List, slug=list_slug)
|
||||
listid = list.id
|
||||
|
||||
# Check whether current user is a member of the group this list belongs to.
|
||||
if list.group in request.user.groups.all() or request.user.is_staff or list_slug == "mine" :
|
||||
auth_ok = 1 # User is authorized for this view
|
||||
else: # User does not belong to the group this list is attached to
|
||||
request.user.message_set.create(message="You do not have permission to view/edit this list.")
|
||||
|
||||
|
||||
# First check for items in the mark_done POST array. If present, change
|
||||
# their status to complete.
|
||||
if request.POST.getlist('mark_done'):
|
||||
done_items = request.POST.getlist('mark_done')
|
||||
# Iterate through array of done items and update its representation in the model
|
||||
for thisitem in done_items:
|
||||
p = Item.objects.get(id=thisitem)
|
||||
p.completed = 1
|
||||
p.completed_date = datetime.datetime.now()
|
||||
p.save()
|
||||
request.user.message_set.create(message="Item \"%s\" marked complete." % p.title )
|
||||
|
||||
|
||||
# Undo: Set completed items back to incomplete
|
||||
if request.POST.getlist('undo_completed_task'):
|
||||
undone_items = request.POST.getlist('undo_completed_task')
|
||||
for thisitem in undone_items:
|
||||
p = Item.objects.get(id=thisitem)
|
||||
p.completed = 0
|
||||
p.save()
|
||||
request.user.message_set.create(message="Previously completed task \"%s\" marked incomplete." % p.title)
|
||||
|
||||
|
||||
# And delete any requested items
|
||||
if request.POST.getlist('del_task'):
|
||||
deleted_items = request.POST.getlist('del_task')
|
||||
for thisitem in deleted_items:
|
||||
p = Item.objects.get(id=thisitem)
|
||||
p.delete()
|
||||
request.user.message_set.create(message="Item \"%s\" deleted." % p.title )
|
||||
|
||||
# And delete any *already completed* items
|
||||
if request.POST.getlist('del_completed_task'):
|
||||
deleted_items = request.POST.getlist('del_completed_task')
|
||||
for thisitem in deleted_items:
|
||||
p = Item.objects.get(id=thisitem)
|
||||
p.delete()
|
||||
request.user.message_set.create(message="Deleted previously completed item \"%s\"." % p.title)
|
||||
|
||||
|
||||
thedate = datetime.datetime.now()
|
||||
created_date = "%s-%s-%s" % (thedate.year, thedate.month, thedate.day)
|
||||
|
||||
|
||||
# Get list of items with this list ID, or filter on items assigned to me, or recently added/completed
|
||||
if list_slug == "mine":
|
||||
task_list = Item.objects.filter(assigned_to=request.user, completed=0)
|
||||
completed_list = Item.objects.filter(assigned_to=request.user, completed=1)
|
||||
|
||||
elif list_slug == "recent-add":
|
||||
# We'll assume this only includes uncompleted items to avoid confusion.
|
||||
# Only show items in lists that are in groups that the current user is also in.
|
||||
task_list = Item.objects.filter(list__group__in=(request.user.groups.all()),completed=0).order_by('-created_date')[:50]
|
||||
# completed_list = Item.objects.filter(assigned_to=request.user, completed=1)
|
||||
|
||||
elif list_slug == "recent-complete":
|
||||
# Only show items in lists that are in groups that the current user is also in.
|
||||
task_list = Item.objects.filter(list__group__in=request.user.groups.all(),completed=1).order_by('-completed_date')[:50]
|
||||
# completed_list = Item.objects.filter(assigned_to=request.user, completed=1)
|
||||
|
||||
|
||||
else:
|
||||
task_list = Item.objects.filter(list=list.id, completed=0)
|
||||
completed_list = Item.objects.filter(list=list.id, completed=1)
|
||||
|
||||
|
||||
if request.POST.getlist('add_task') :
|
||||
form = AddItemForm(list, request.POST,initial={
|
||||
'assigned_to':request.user.id,
|
||||
'priority':999,
|
||||
})
|
||||
|
||||
if form.is_valid():
|
||||
# Save task first so we have a db object to play with
|
||||
new_task = form.save()
|
||||
|
||||
# Send email alert only if the Notify checkbox is checked AND the assignee is not the same as the submittor
|
||||
# Email subect and body format are handled by templates
|
||||
if "notify" in request.POST :
|
||||
if new_task.assigned_to != request.user :
|
||||
|
||||
# Send email
|
||||
email_subject = render_to_string("todo/email/assigned_subject.txt", { 'task': new_task })
|
||||
email_body = render_to_string("todo/email/assigned_body.txt", { 'task': new_task, 'site': current_site, })
|
||||
try:
|
||||
send_mail(email_subject, email_body, new_task.created_by.email, [new_task.assigned_to.email], fail_silently=False)
|
||||
except:
|
||||
request.user.message_set.create(message="Task saved but mail not sent. Contact your administrator." )
|
||||
|
||||
request.user.message_set.create(message="New task \"%s\" has been added." % new_task.title )
|
||||
return HttpResponseRedirect(request.path)
|
||||
|
||||
else:
|
||||
if list_slug != "mine" and list_slug != "recent-add" and list_slug != "recent-complete" : # We don't allow adding a task on the "mine" view
|
||||
form = AddItemForm(list, initial={
|
||||
'assigned_to':request.user.id,
|
||||
'priority':999,
|
||||
} )
|
||||
|
||||
if request.user.is_staff:
|
||||
can_del = 1
|
||||
|
||||
return render_to_response('todo/view_list.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@login_required
|
||||
def view_task(request,task_id):
|
||||
|
||||
"""
|
||||
View task details. Allow task details to be edited.
|
||||
"""
|
||||
|
||||
task = get_object_or_404(Item, pk=task_id)
|
||||
comment_list = Comment.objects.filter(task=task_id)
|
||||
|
||||
# Before doing anything, make sure the accessing user has permission to view this item.
|
||||
# Determine the group this task belongs to, and check whether current user is a member of that group.
|
||||
# Admins can edit all tasks.
|
||||
|
||||
if task.list.group in request.user.groups.all() or request.user.is_staff:
|
||||
|
||||
auth_ok = 1
|
||||
if request.POST:
|
||||
form = EditItemForm(request.POST,instance=task)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
# Also save submitted comment, if non-empty
|
||||
if request.POST['comment-body']:
|
||||
c = Comment(
|
||||
author=request.user,
|
||||
task=task,
|
||||
body=request.POST['comment-body'],
|
||||
)
|
||||
c.save()
|
||||
|
||||
# And email comment to all people who have participated in this thread.
|
||||
email_subject = render_to_string("todo/email/assigned_subject.txt", { 'task': task })
|
||||
email_body = render_to_string("todo/email/newcomment_body.txt", { 'task': task, 'body':request.POST['comment-body'], 'site': current_site, 'user':request.user })
|
||||
|
||||
# Get list of all thread participants - task creator plus everyone who has commented on it.
|
||||
recip_list = []
|
||||
recip_list.append(task.created_by.email)
|
||||
commenters = Comment.objects.filter(task=task)
|
||||
for c in commenters:
|
||||
recip_list.append(c.author.email)
|
||||
# Eliminate duplicate emails with the Python set() function
|
||||
recip_list = set(recip_list)
|
||||
|
||||
# Send message
|
||||
try:
|
||||
send_mail(email_subject, email_body, task.created_by.email, recip_list, fail_silently=False)
|
||||
request.user.message_set.create(message="Comment sent to thread participants.")
|
||||
except:
|
||||
request.user.message_set.create(message="Comment saved but mail not sent. Contact your administrator." )
|
||||
|
||||
|
||||
request.user.message_set.create(message="The task has been edited.")
|
||||
return HttpResponseRedirect(reverse('todo-incomplete_tasks', args=[task.list.id, task.list.slug]))
|
||||
|
||||
else:
|
||||
form = EditItemForm(instance=task)
|
||||
if task.due_date:
|
||||
thedate = task.due_date
|
||||
else:
|
||||
thedate = datetime.datetime.now()
|
||||
|
||||
|
||||
else:
|
||||
request.user.message_set.create(message="You do not have permission to view/edit this task.")
|
||||
|
||||
return render_to_response('todo/view_task.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def reorder_tasks(request):
|
||||
"""
|
||||
Handle task re-ordering (priorities) from JQuery drag/drop in view_list.html
|
||||
"""
|
||||
|
||||
newtasklist = request.POST.getlist('tasktable[]')
|
||||
# First item in received list is always empty - remove it
|
||||
del newtasklist[0]
|
||||
|
||||
# Items arrive in order, so all we need to do is increment up from one, saving
|
||||
# "i" as the new priority for the current object.
|
||||
i = 1
|
||||
for t in newtasklist:
|
||||
newitem = Item.objects.get(pk=t)
|
||||
newitem.priority = i
|
||||
newitem.save()
|
||||
i = i + 1
|
||||
|
||||
# All views must return an httpresponse of some kind ... without this we get
|
||||
# error 500s in the log even though things look peachy in the browser.
|
||||
return HttpResponse(status=201)
|
||||
|
||||
|
||||
@login_required
|
||||
def external_add(request):
|
||||
"""
|
||||
Allow users who don't have access to the rest of the ticket system to file a ticket in a specific list.
|
||||
This is useful if, for example, a core web team are in a group that can file todos for each other,
|
||||
but you also want students to be able to post trouble tickets to a list just for the sysadmin. This
|
||||
way we don't have to put all students into a group that gives them access to the whole ticket system.
|
||||
"""
|
||||
if request.POST:
|
||||
form = AddExternalItemForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
# Don't commit the save until we've added in the fields we need to set
|
||||
item = form.save(commit=False)
|
||||
item.list_id = 20 # Hate hard-coding in IDs like this.
|
||||
item.created_by = request.user
|
||||
item.assigned_to = User.objects.get(username='roy_baril')
|
||||
item.save()
|
||||
|
||||
# Send email
|
||||
email_subject = render_to_string("todo/email/assigned_subject.txt", { 'task': item.title })
|
||||
email_body = render_to_string("todo/email/assigned_body.txt", { 'task': item, 'site': current_site, })
|
||||
try:
|
||||
send_mail(email_subject, email_body, item.created_by.email, [item.assigned_to.email], fail_silently=False)
|
||||
except:
|
||||
request.user.message_set.create(message="Task saved but mail not sent. Contact your administrator." )
|
||||
|
||||
request.user.message_set.create(message="Your trouble ticket has been submitted. We'll get back to you soon.")
|
||||
return HttpResponseRedirect(reverse('intranet_home'))
|
||||
|
||||
|
||||
else:
|
||||
form = AddExternalItemForm()
|
||||
|
||||
return render_to_response('todo/add_external_task.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def add_list(request):
|
||||
"""
|
||||
Allow users to add a new todo list to the group they're in.
|
||||
"""
|
||||
|
||||
if request.POST:
|
||||
form = AddListForm(request.user,request.POST)
|
||||
if form.is_valid():
|
||||
try:
|
||||
form.save()
|
||||
request.user.message_set.create(message="A new list has been added.")
|
||||
return HttpResponseRedirect(request.path)
|
||||
except IntegrityError:
|
||||
request.user.message_set.create(message="There was a problem saving the new list. Most likely a list with the same name in the same group already exists.")
|
||||
|
||||
else:
|
||||
form = AddListForm(request.user)
|
||||
|
||||
return render_to_response('todo/add_list.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
def search(request):
|
||||
"""
|
||||
Search for tasks
|
||||
"""
|
||||
|
||||
if request.GET:
|
||||
|
||||
query_string = ''
|
||||
found_items = None
|
||||
if ('q' in request.GET) and request.GET['q'].strip():
|
||||
query_string = request.GET['q']
|
||||
|
||||
found_items = Item.objects.filter(
|
||||
Q(title__icontains=query_string) |
|
||||
Q(note__icontains=query_string)
|
||||
)
|
||||
else:
|
||||
|
||||
# What if they selected the "completed" toggle but didn't type in a query string?
|
||||
# In that case we still need found_items in a queryset so it can be "excluded" below.
|
||||
found_items = Item.objects.all()
|
||||
|
||||
if request.GET['inc_complete'] == "0" :
|
||||
found_items = found_items.exclude(completed=True)
|
||||
|
||||
else :
|
||||
query_string = None
|
||||
found_items = None
|
||||
|
||||
return render_to_response('todo/search_results.html',
|
||||
{ 'query_string': query_string, 'found_items': found_items },
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue