Moving from google code to github.

This commit is contained in:
Scot Hacker 2010-09-27 01:12:13 -07:00
commit 5be5ea023c
22 changed files with 1974 additions and 0 deletions

14
README Normal file
View 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
View file

13
admin.py Normal file
View 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
View 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
View 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;
}

View 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*/
}

View 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
View 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
View 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,
)

View 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 %}

View 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
View 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 %}

View 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! &rarr;" 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 %}

View 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 %}

View file

@ -0,0 +1 @@
GTD: New task - {% autoescape off %}Note: {{ task.title }}{% endautoescape %}

View 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 %}

View 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">
&nbsp;
</div>
<p><a href="{% url todo-add_list %}">Create new todo list</a></p>
{% endblock %}

View 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 %}

View 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" >&rarr; Click to add task &larr;</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 %}&asymp;{% 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 %}&asymp;{% 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 %}

View 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>&rarr; Click to edit details &larr;</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
View 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
View 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))