Compare commits

..

No commits in common. "master" and "v1.6.2" have entirely different histories.

73 changed files with 2122 additions and 839 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
# tools, IDEs, build folders
/.coverage/
/.eggs/
/.idea/
/.tox/
/build/
/dist/
/docs/build/
/*.egg-info/
# Django and Python
*.py[cod]

View file

@ -1,37 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}
namespace: {{ .Chart.Name }}-{{ .Values.global.env }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: {{ .Values.DockerImage }}
ports:
- containerPort: 8888
env:
- name: DATABASE_HOST
value: "db-postgresql"
- name: DATABASE_USER
value: "postgres"
- name: DATABASE_PASSWORD
value: {{.Values.DBPwd | required "DBPwd is required" }}
resources:
limits:
cpu: 100m
memory: 150Mi
requests:
cpu: 100m
memory: 150Mi

View file

@ -1,15 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}-service
namespace: {{ .Chart.Name }}-{{ .Values.global.env }}
spec:
type: ClusterIP
ports:
- name: http
port: 8888
targetPort: 8888
selector:
app: {{ .Chart.Name }}

View file

@ -1,16 +0,0 @@
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: ingress-{{ .Chart.Name }}
namespace: {{ .Chart.Name }}-{{ .Values.global.env }}
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: {{ .Chart.Name }}-service
servicePort: 8888

5
.travis.yml Normal file
View file

@ -0,0 +1,5 @@
language: python
install:
- pip install virtualenv
script:
- python setup.py test

View file

@ -1,15 +0,0 @@
FROM python:3.6-alpine
WORKDIR /app
RUN apk add git gcc musl-dev postgresql-dev && \
pip install pipenv && \
git clone https://github.com/shacker/gtd.git /app && \
pipenv --python 3.6 && \
pipenv install --dev
ADD local.py project/.
ADD entrypoint.sh .
RUN chmod +x entrypoint.sh
CMD [ "./entrypoint.sh" ]

27
LICENSE Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2010, Scot Hacker, Birdhouse Arts and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of Birdhouse Arts nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

4
MANIFEST.in Normal file
View file

@ -0,0 +1,4 @@
include LICENSE
include README.rst
recursive-include todo/static *
recursive-include todo/templates *

View file

@ -1,28 +0,0 @@
# [WiP] PoC of DevOps magic
### Pre requires:
1. Any x86_64 Linux distribution. Tested on Debian 10, but must work on any modern Linux system
2. Internet access
3. wget
4. bash/dash shell
5. GNU awk
5. KVM
### Known bugs and limitation:
1. Automated Keymap selection not working in the bootstrap process. You must hit Enter key.
2. Swap automatically created in the bootstrap process, I can't switch it off now. As a result, the minimum image size must be 12Gb or more.
3. It's still dirty, most exceptions during bootstrap still not handled
### For start adventures:
1. just clone this repo
2. go to the repo directory
3. and type `./runme.sh` in your shell
### What happened after you run this script:
1. We bootup in-userspace VM and starting the automated installation of Debian 9 with docker and common automation tools (git, ansible, helm, werf).
2. Bootstrapping one node Kubernates cluster with Amsible (LOL we invented minicube without minicube limitations)
3. Installing Nginx.org's Ingress with Helm
4. Installing docker-registry in our shiny Kube (we are lazy of course we use Helm for it)
5. Installing Postgress into Kube 0_o Also with Helm
6. ~~Deploying test service with Helm/Werf~~
256. ~~PROFIT!!!1~~

55
README.rst Normal file
View file

@ -0,0 +1,55 @@
============================
django todo |latest-version|
============================
|build-status| |health| |downloads| |license|
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.
Documentation
=============
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>`_
Tests
=====
Serious tests are missing, but we're checking PEP8 conformity of our syntax on
both Python 2 and 3 using ``tox``. You can run the tests locally via::
$ python setup.py test
No prerequisites are required, all test dependencies will be installed
automatically by ``tox`` in virtual environments created on the fly.
Unfortunately, you'll have to install ``virtualenv`` for this to work, though.
To remove all build files and folders including Python byte code you can run::
$ python setup.py clean
.. |latest-version| image:: https://img.shields.io/pypi/v/django-todo.svg
:alt: Latest version on PyPI
:target: https://pypi.python.org/pypi/django-todo
.. |build-status| image:: https://travis-ci.org/shacker/django-todo.svg
:alt: Build status
:target: https://travis-ci.org/shacker/django-todo
.. |health| image:: https://landscape.io/github/shacker/django-todo/master/landscape.svg?style=flat
:target: https://landscape.io/github/shacker/django-todo/master
:alt: Code health
.. |downloads| image:: https://img.shields.io/pypi/dm/django-todo.svg
:alt: Monthly downloads from PyPI
:target: https://pypi.python.org/pypi/django-todo
.. |license| image:: https://img.shields.io/pypi/l/django-todo.svg
:alt: Software license
:target: https://github.com/shacker/django-todo/blob/master/LICENSE

View file

@ -1,10 +0,0 @@
[defaults]
allow_world_readable_tmpfiles=True
pipelining=True
retry_files_enabled = False
inventory = inventory
roles_path = roles
library = library
remote_tmp = /root/.ansible/tmp
[connection]
pipelining=True

View file

@ -1,6 +0,0 @@
---
- name: bootstrap playbook for any k8s machine
hosts: k8s
become: yes
roles:
- bootstrap

View file

@ -1,11 +0,0 @@
k8s_version: '1.17.5'
k8s_first_master_node: 'k8s-demo'
k8s_domain: coins.k8s.demo.ix.gs
k8s_pod_network: '192.168.0.0/16'
k8s_service_network: '10.254.0.0/24'
k8s_controlplane_vip: '100.100.100.15'
k8s_controlplane_address: '{{ k8s_controlplane_vip }}:6443'
k8s_cluster_name: k8s-demo
cloud_provider: baremetal
ha_enabled: false

View file

@ -1 +0,0 @@
k8s_node_role: 'master'

View file

@ -1,7 +0,0 @@
---
- name: Init k8s cluster
hosts: 'k8s-demo'
become: yes
max_fail_percentage: 0
roles:
- init-cluster

View file

@ -1,2 +0,0 @@
[k8s]
k8s-demo ansible_connection=local

View file

@ -1,6 +0,0 @@
---
- name: bootstrap playbook for any k8s machine
hosts: k8s-masters
become: yes
roles:
- keepalived

View file

@ -1,6 +0,0 @@
kubernetes_apt_release_channel: main
# Note that xenial repo is used for all Debian derivatives at this time.
kubernetes_apt_repository: "deb http://apt.kubernetes.io/ kubernetes-xenial {{ kubernetes_apt_release_channel }}"
## Calico config files
kubernetes_calico_manifest_file: https://docs.projectcalico.org/v3.10/manifests/calico.yaml

View file

@ -1,6 +0,0 @@
---
- name: restart kubelet
service: name=kubelet state=restarted
- name: restart docker daemon
service: name=docker state=restarted

View file

@ -1,39 +0,0 @@
---
- name: Ensure dependencies are installed.
apt:
name:
- apt-transport-https
- ca-certificates
state: present
- name: Add Kubernetes apt key.
apt_key:
url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
state: present
- name: Add Kubernetes repository.
apt_repository:
repo: "{{ kubernetes_apt_repository }}"
state: present
update_cache: true
- name: Install kubeadm kubelet kubectl
apt:
pkg:
- kubeadm={{ k8s_version }}-00
- kubelet={{ k8s_version }}-00
- kubectl={{ k8s_version }}-00
- kubernetes-cni=0.7.5-00
update_cache: yes
notify: restart kubelet
- name: Add Kubernetes apt preferences file to pin a version.
template:
src: apt-preferences-kubernetes.j2
dest: /etc/apt/preferences.d/kubernetes
- name:
template:
src: daemon.json
dest: /etc/docker/daemon.json
notify: restart docker daemon

View file

@ -1,11 +0,0 @@
Package: kubectl
Pin: version {{ k8s_version }}.*
Pin-Priority: 1000
Package: kubeadm
Pin: version {{ k8s_version }}.*
Pin-Priority: 1000
Package: kubelet
Pin: version {{ k8s_version }}.*
Pin-Priority: 1000

View file

@ -1,8 +0,0 @@
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}

View file

@ -1,52 +0,0 @@
- name: Add Kubeadm config file
template:
src: kubeadm.conf.j2
dest: /etc/kubeadm.conf
when: k8s_node_role == 'master'
- name: Init cluster
command: kubeadm init --config /etc/kubeadm.conf --upload-certs --ignore-preflight-errors serviceSubnet
when: ansible_hostname == k8s_first_master_node
- name: Create kube config directory for root
file: path=/root/.kube state=directory
when: k8s_node_role == 'master'
- name: Copy Kubernetes admin config to home directory
copy:
src: "/etc/kubernetes/admin.conf"
dest: "/root/.kube/config"
# remote_src: yes
# when: and ansible_hostname == k8s_first_master_node
- name: Install Calico CNI
command: kubectl apply -f "https://docs.projectcalico.org/v3.13/manifests/calico.yaml"
when: ansible_hostname == k8s_first_master_node
- name: Generate join token
command: kubeadm token create --print-join-command
register: join_cmd
delegate_to: '{{ k8s_first_master_node }}'
# Эта часть не работает
# Правильная команда выглядит так:
# kubeadm join 10.129.64.60:6443 --token <token> --discovery-token-ca-cert-hash <ca-cert-hash> --control-plane --certificate-key <key>
# Предыдущий блок генерит сертификат и токен без указания ключа
# Надо пофиксить как будет время
- name: Join rest of master nodes
command: "{{ join_cmd.stdout }} --control-plane"
when: k8s_node_role == 'master' and ha_enabled and ansible_hostname != k8s_first_master_node
ignore_errors: yes
#- name: Copy Kubernetes admin config to home directory
# copy:
# src: "/etc/kubernetes/admin.conf"
# dest: "/root/.kube/config"
# remote_src: yes
# when: k8s_node_role == 'master'
- name: Join worker nodes
command: "{{ join_cmd.stdout }}"
when: k8s_node_role == 'worker'

View file

@ -1,25 +0,0 @@
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
nodeRegistration:
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
kubernetesVersion: {{ k8s_version }}
certificatesDir: /etc/kubernetes/pki
clusterName: {{ k8s_cluster_name }}
controlPlaneEndpoint: {{ k8s_controlplane_address }}
dns:
type: CoreDNS
etcd:
local:
dataDir: /var/lib/etcd
imageRepository: k8s.gcr.io
networking:
dnsDomain: {{ k8s_domain }}
podSubnet: {{ k8s_pod_network }}
serviceSubnet: {{ k8s_service_network }}
scheduler: {}

View file

@ -1,3 +0,0 @@
---
- name: restart keepalived
service: name=keepalived state=restarted

View file

@ -1,14 +0,0 @@
---
- name: Install keepalived
apt:
pkg:
- keepalived
state: latest
- name: Configure keepalived
template: src=keepalived.conf.j2 dest=/etc/keepalived/keepalived.conf
tags: keepalived
notify: restart keepalived
- name: Start keepalived
service: name=keepalived state=started

View file

@ -1,31 +0,0 @@
! Configuration File for keepalived
global_defs {
}
{#vrrp_script haproxy-check {
script "killall -0 haproxy"
interval 2
weight 20
}#}
vrrp_instance VI_1 {
state {{ keepalived_role }}
interface {{ keepalived_shared_iface }}
virtual_router_id {{ keepalived_router_id }}
{% if keepalived_role.lower() == "master" %}
priority {{ keepalived_priority }}
{% else %}
priority {{ keepalived_backup_priority }}
{% endif %}
advert_int 1
authentication {
auth_type PASS
auth_pass {{ keepalived_auth_pass }}
}
virtual_ipaddress {
{{ keepalived_shared_ip }} dev {{ keepalived_shared_iface }} label {{ keepalived_shared_iface }}:0
}
{# track_script {
haproxy-check weight 20
}#}
}

View file

@ -1,77 +0,0 @@
#!/bin/bash
bootflag='/.manufactured'
groot='/opt/coins-demo'
k8sDemoWA () {
printf "\033c"
echo 'Disabling swap space...'
sync && swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab
export KUBECONFIG=/root/.kube/config
}
k8sDeploy () {
echo 'Installing K8s...'
cd ${groot}/contrib/ansible && \
ansible-playbook bootstrap-node.yml && \
ansible-playbook init-cluster.yml
# Sometimes it's still not ready on this stage, let's check it just to be sure
while true ; do
echo "Waiting for node up..."
result=$(kubectl get nodes|awk '{print $2}'| tail -1| grep -nE '^Ready')
if [ -z "$result" ] ; then
break
fi
sleep 10
done
}
InstallCSI () {
helm repo add rimusz https://charts.rimusz.net
helm install rimusz/hostpath-provisioner --generate-name
}
InstallRegistry () {
helm repo add harbor https://helm.goharbor.io
helm install registry harbor/harbor \
--set expose.tls.enabled=false \
--set expose.ingress.hosts.core="registry.k8s-demo.ix.gs" \
--set expose.ingress.hosts.notary="notary.k8s-demo.ix.gs" \
--set externalURL="http://registry.k8s-demo.ix.gs" \
--set persistence.storageClass=hostpath \
--set registry.credentials.username=k8s \
--set registry.credentials.password=k8s \
--set notary.enabled=false \
--set trivy.enabled=false \
--set clair.enabled=false \
--set chartmuseum.enabled=false
}
InstallPGSQL () {
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install db bitnami/postgresql \
--set persistence.storageClass=hostpath \
--set persistence.size=1Gi
}
InstallIngress () {
# Allow scheduling on our master node
kubectl taint nodes k8s-demo node-role.kubernetes.io/master-
# Installing Ingress
helm repo add nginx-stable https://helm.nginx.com/stable && \
helm install nginx-stable/nginx-ingress --namespace kube-system --generate-name --set rbac.create=true
# Fix external IP for LB...
kubectl patch svc $(kubectl get svc -n kube-system|grep nginx-ingress|awk '{print $1}') -n kube-system --patch "$(cat ${groot}/contrib/ymls/ingress.fix.yaml)"
}
InstallApp () {
cd ${groot}
export WERF_INSECURE_REGISTRY=true
export WERF_IMAGES_REPO='http://registry.k8s-demo.ix.gs/todo'
werf build --stages-storage :local && \
werf publish --stages-storage :local --tag-custom stable
werf deploy --stages-storage :local --tag-custom latest --env production --set 'DBPwd=$(kubectl get secret db-postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)'
}
if [ ! -f ${bootflag} ]; then
touch ${bootflag}
k8sDemoWA;
k8sDeploy;
InstallCSI;
InstallIngress;
InstallRegistry;
InstallPGSQL;
fi

View file

@ -1,112 +0,0 @@
### Keyboard config
d-i debian-installer/locale string en_US.UTF-8
d-i keyboard-configuration/variant select American English
d-i keyboard-configuration/xkb-keymap select us
d-i keyboard-configuration/toggle select No toggling
### Network configuration
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string unassigned-hostname
d-i netcfg/get_domain string unassigned-domain
d-i netcfg/hostname string k8s-demo
d-i netcfg/wireless_wep string
d-i netcfg/dhcp_hostname string k8s-demo
d-i hw-detect/load_firmware boolean true
### Mirror settings
d-i mirror/country string manual
d-i mirror/http/hostname string cdn.debian.net
d-i mirror/http/directory string /debian
d-i mirror/http/proxy string
### Account setup
d-i passwd/root-login boolean false
# To create a normal user account.
d-i passwd/user-fullname string K8S Admin
d-i passwd/username string k8s
d-i passwd/user-password password K8Sdemo
d-i passwd/user-password-again password K8Sdemo
# The user account will be added to some standard initial groups. To
# override that, use this.
#d-i passwd/user-default-groups string docker
### Clock and time zone setup
d-i clock-setup/utc boolean true
d-i time/zone string UTC
d-i clock-setup/ntp boolean true
d-i clock-setup/ntp-server string clock.ix.gs
### Partitioning
d-i partman-auto/method string regular
d-i partman-lvm/device_remove_lvm boolean true
d-i partman-md/device_remove_md boolean true
d-i partman-lvm/confirm boolean true
d-i partman-lvm/confirm_nooverwrite boolean true
d-i partman-auto/choose_recipe select atomic
d-i partman-basicfilesystems/no_swap boolean true
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
d-i partman/default_filesystem string xfs
### Apt setup
d-i apt-setup/non-free boolean true
d-i apt-setup/contrib boolean true
d-i debian-installer/allow_unauthenticated boolean true
### Package selection
tasksel tasksel/first multiselect none, ssh-server, standard
# Individual additional packages to install
d-i pkgsel/include string \
apt-transport-https gnupg2 ca-certificates curl \
iptables linux-headers-amd64 git ansible wget \
gdebi-core
# Whether to upgrade packages after debootstrap.
# Allowed values: none, safe-upgrade, full-upgrade
d-i pkgsel/upgrade select full-upgrade
popularity-contest popularity-contest/participate boolean true
### Boot loader installation
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true
d-i grub-installer/bootdev string default
### Finishing up the installation
d-i finish-install/reboot_in_progress note
d-i debian-installer/exit/poweroff boolean true
#### Advanced options
### Running custom commands during the installation
d-i preseed/late_command string in-target /bin/sh -c " \
echo k8s-demo > /etc/hostname ; \
sed -i 's/debian/k8s-demo/' /etc/hosts ; \
update-alternatives --set iptables /usr/sbin/iptables-legacy ; \
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy ; \
update-alternatives --set arptables /usr/sbin/arptables-legacy ; \
update-alternatives --set ebtables /usr/sbin/ebtables-legacy ; \
( systemctl disable nftables && systemctl mask nftables ) ; \
curl -s https://download.docker.com/linux/debian/gpg | sudo apt-key add - ; \
curl https://helm.baltorepo.com/organization/signing.asc | sudo apt-key add - ; \
echo 'deb [arch=amd64] https://download.docker.com/linux/debian/ stretch stable' >/etc/apt/sources.list.d/docker.list ; \
echo 'deb https://baltocdn.com/helm/stable/debian/ all main' > /etc/apt/sources.list.d/helm-stable-debian.list ; \
apt-get update ; \
apt-get install -y \
docker-ce docker-ce-cli containerd.io helm ; \
apt-get clean ; \
curl -L https://dl.bintray.com/flant/werf/v1.1.19+fix10/werf-linux-amd64-v1.1.19+fix10 -o /usr/bin/werf ; \
chmod +x /usr/bin/werf ; \
git clone https://git.ix.gs/public/coins-demo.git /opt/coins-demo ; \
cp /opt/coins-demo/contrib/mfg.service /etc/systemd/system/ ; \
mkdir -p /lib/systemd/system/docker.service.d ; \
sed -i 's/sock$/sock --insecure-registry="registry.k8s-demo.ix.gs"/' /lib/systemd/system/docker.service ; \
systemctl daemon-reload ; \
systemctl enable mfg.service ; \
systemctl enable serial-getty@ttyS0.service ; \
systemctl enable docker.service \
|| true"

View file

@ -1,18 +0,0 @@
[Unit]
Description=Manufacturing service
After=serial-getty@ttyS0.service
[Service]
Type=idle
ExecStart=/opt/coins-demo/contrib/firstboot.sh
StandardInput=tty
StandardOutput=tty
TTYPath=/dev/ttyS0
TTYReset=yes
#TTYVHangup=yes
KillMode=none
TimeoutSec=0
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,157 +0,0 @@
include modifiers
#
# Top row
#
1 0x2
2 0x3
3 0x4
4 0x5
5 0x6
6 0x7
7 0x8
8 0x9
9 0xa
0 0xb
BackSpace 0xe
#
# QWERTY first row
#
Tab 0xf localstate
ISO_Left_Tab 0xf shift
q 0x10 addupper
w 0x11 addupper
e 0x12 addupper
r 0x13 addupper
t 0x14 addupper
y 0x15 addupper
u 0x16 addupper
i 0x17 addupper
o 0x18 addupper
p 0x19 addupper
#
# QWERTY second row
#
a 0x1e addupper
s 0x1f addupper
d 0x20 addupper
f 0x21 addupper
g 0x22 addupper
h 0x23 addupper
j 0x24 addupper
k 0x25 addupper
l 0x26 addupper
Return 0x1c localstate
#
# QWERTY third row
#
z 0x2c addupper
x 0x2d addupper
c 0x2e addupper
v 0x2f addupper
b 0x30 addupper
n 0x31 addupper
m 0x32 addupper
space 0x39 localstate
less 0x56
greater 0x56 shift
bar 0x56 altgr
brokenbar 0x56 shift altgr
#
# Esc and Function keys
#
Escape 0x1 localstate
F1 0x3b localstate
F2 0x3c localstate
F3 0x3d localstate
F4 0x3e localstate
F5 0x3f localstate
F6 0x40 localstate
F7 0x41 localstate
F8 0x42 localstate
F9 0x43 localstate
F10 0x44 localstate
F11 0x57 localstate
F12 0x58 localstate
# Printscreen, Scrollock and Pause
# Printscreen really requires four scancodes (0xe0, 0x2a, 0xe0, 0x37),
# but (0xe0, 0x37) seems to work.
Print 0xb7 localstate
Sys_Req 0xb7 localstate
Execute 0xb7 localstate
Scroll_Lock 0x46
#
# Insert - PgDown
#
Insert 0xd2 localstate
Delete 0xd3 localstate
Home 0xc7 localstate
End 0xcf localstate
Page_Up 0xc9 localstate
Page_Down 0xd1 localstate
#
# Arrow keys
#
Left 0xcb localstate
Up 0xc8 localstate
Down 0xd0 localstate
Right 0xcd localstate
#
# Numpad
#
Num_Lock 0x45
KP_Divide 0xb5
KP_Multiply 0x37
KP_Subtract 0x4a
KP_Add 0x4e
KP_Enter 0x9c
KP_Decimal 0x53 numlock
KP_Separator 0x53 numlock
KP_Delete 0x53
KP_0 0x52 numlock
KP_Insert 0x52
KP_1 0x4f numlock
KP_End 0x4f
KP_2 0x50 numlock
KP_Down 0x50
KP_3 0x51 numlock
KP_Next 0x51
KP_4 0x4b numlock
KP_Left 0x4b
KP_5 0x4c numlock
KP_Begin 0x4c
KP_6 0x4d numlock
KP_Right 0x4d
KP_7 0x47 numlock
KP_Home 0x47
KP_8 0x48 numlock
KP_Up 0x48
KP_9 0x49 numlock
KP_Prior 0x49
Caps_Lock 0x3a
#
# Inhibited keys
#
Multi_key 0x0 inhibit

View file

@ -1,35 +0,0 @@
# generated from XKB map us
include common
map 0x409
exclam 0x02 shift
at 0x03 shift
numbersign 0x04 shift
dollar 0x05 shift
percent 0x06 shift
asciicircum 0x07 shift
ampersand 0x08 shift
asterisk 0x09 shift
parenleft 0x0a shift
parenright 0x0b shift
minus 0x0c
underscore 0x0c shift
equal 0x0d
plus 0x0d shift
bracketleft 0x1a
braceleft 0x1a shift
bracketright 0x1b
braceright 0x1b shift
semicolon 0x27
colon 0x27 shift
apostrophe 0x28
quotedbl 0x28 shift
grave 0x29
asciitilde 0x29 shift
backslash 0x2b
bar 0x2b shift
comma 0x33
less 0x33 shift
period 0x34
greater 0x34 shift
slash 0x35
question 0x35 shift

View file

@ -1,18 +0,0 @@
Shift_R 0x36
Shift_L 0x2a
Alt_R 0xb8
Mode_switch 0xb8
ISO_Level3_Shift 0xb8
Alt_L 0x38
Control_R 0x9d
Control_L 0x1d
# Translate Super to Windows keys.
# This is hardcoded. See documentation for details.
Super_R 0xdc
Super_L 0xdb
# Translate Menu to the Windows Application key.
Menu 0xdd

Binary file not shown.

Binary file not shown.

View file

@ -1,4 +0,0 @@
spec:
externalIPs:
- 100.100.100.15
loadBalancerIP: 100.100.100.15

View file

@ -1,3 +0,0 @@
#!/bin/sh
pipenv run python manage.py migrate todo
pipenv run python manage.py runserver 0.0.0.0:8888

View file

@ -1,26 +0,0 @@
from .settings import *
import os
DEBUG = True
ALLOWED_HOSTS = ['*']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ['DATABASE_NAME'],
'HOST': os.environ['DATABASE_HOST'],
'USER': os.environ['DATABASE_USER'],
'PASSWORD': os.environ['DATABASE_PASSWORD'],
'PORT': '',
},
}
SECRET_KEY = os.environ['SECRET_KEY']
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# TODO-specific settings
TODO_STAFF_ONLY = False
TODO_DEFAULT_LIST_SLUG = 'tickets'
TODO_DEFAULT_ASSIGNEE = None
TODO_PUBLIC_SUBMIT_REDIRECT = '/'

View file

@ -1,34 +0,0 @@
#!/bin/bash
appdir=$(cd `dirname $0` && pwd)
apptmp='/tmp'
vmsize='16G'
vmmem='4G'
vcpu=`cat /proc/cpuinfo|grep proc|tail -1|awk '{print $NF}'`
k8sdsk="${apptmp}/k8s-demo.raw"
httpboot='http://ftp.debian.org/debian/dists/stretch/main/installer-amd64/current/images/netboot/debian-installer/amd64'
seed='https://git.ix.gs/public/coins-demo/raw/branch/master/contrib/k8s-seed.txt'
qemu="${appdir}/contrib/qemu-system-x86_64 -L ${appdir}/contrib/share/qemu -net nic -net user,net=100.100.100.0/24,hostfwd=tcp::8888-:80 -m ${vmmem} -smp ${vcpu} -localtime -enable-kvm -cpu host,+nx -M pc -vga std -usbdevice tablet -k en-us -hda ${k8sdsk} -boot once=d -nographic"
wget='wget -cq4O'
###
installvm() {
echo "Allocating ${vmsize} disk..." && \
fallocate -l ${vmsize} ${k8sdsk} && \
echo "Downloading Linux kernel..." && \
${wget} ${apptmp}/kernel ${httpboot}/linux && \
echo "Downloading Initial ramdisk..." && \
${wget} ${apptmp}/initrd ${httpboot}/initrd.gz && \
echo "Preparing K8S VM..." && \
${qemu} -kernel ${apptmp}/kernel -initrd ${apptmp}/initrd -append "console=ttyS0,115200n8 apt-setup/proposed=true nomodeset fb=false priority=critical locale=en_US url=${seed}" && \
echo "Housekeeping..." && \
rm -f ${apptmp}/kernel ${apptmp}/initrd
}
runvm() {
echo "Look like we already has VM, let's go"
${qemu}
}
###
if [ ! -f "${k8sdsk}" ]; then
installvm && runvm;
else
runvm;
fi

107
setup.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python
from glob import glob
from os import remove
from os.path import abspath, dirname, join
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
from shlex import split
from shutil import rmtree
from sys import exit
import todo as package
class Tox(TestCommand):
user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.tox_args = None
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
self.test_suite = True
def run_tests(self):
import tox
args = self.tox_args
if args:
args = split(self.tox_args)
errno = tox.cmdline(args=args)
exit(errno)
class Clean(TestCommand):
def run(self):
delete_in_root = [
'build',
'dist',
'.eggs',
'*.egg-info',
'.tox',
]
delete_everywhere = [
'__pycache__',
'*.pyc',
]
for candidate in delete_in_root:
rmtree_glob(candidate)
for visible_dir in glob('[A-Za-z0-9]*'):
for candidate in delete_everywhere:
rmtree_glob(join(visible_dir, candidate))
rmtree_glob(join(visible_dir, '*', candidate))
rmtree_glob(join(visible_dir, '*', '*', candidate))
def rmtree_glob(file_glob):
for fobj in glob(file_glob):
try:
rmtree(fobj)
print('%s/ removed ...' % fobj)
except OSError:
try:
remove(fobj)
print('%s removed ...' % fobj)
except OSError:
pass
def read_file(*pathname):
with open(join(dirname(abspath(__file__)), *pathname)) as f:
return f.read()
setup(
name='django-todo',
version=package.__version__,
description=package.__doc__.strip(),
long_description=read_file('README.rst'),
author=package.__author__,
author_email=package.__email__,
url=package.__url__,
license=package.__license__,
packages=find_packages(),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Topic :: Office/Business :: Groupware',
'Topic :: Software Development :: Bug Tracking',
],
include_package_data=True,
zip_safe=False,
tests_require=['tox'],
install_requires=['django-autoslug', 'unidecode', ],
cmdclass={
'clean': Clean,
'test': Tox,
},
)

10
todo/__init__.py Normal file
View file

@ -0,0 +1,10 @@
"""
A multi-user, multi-group task management and assignment system for Django.
"""
__version__ = '1.6.2'
__author__ = 'Scot Hacker'
__email__ = 'shacker@birdhouse.org'
__url__ = 'https://github.com/shacker/django-todo'
__license__ = 'BSD License'

18
todo/admin.py Normal file
View file

@ -0,0 +1,18 @@
from django.contrib import admin
from todo.models import Item, List, Comment
class ItemAdmin(admin.ModelAdmin):
list_display = ('title', 'list', 'priority', 'due_date')
list_filter = ('list',)
ordering = ('priority',)
search_fields = ('name',)
class CommentAdmin(admin.ModelAdmin):
list_display = ('author', 'date', 'snippet')
admin.site.register(List)
admin.site.register(Comment, CommentAdmin)
admin.site.register(Item, ItemAdmin)

81
todo/forms.py Normal file
View file

@ -0,0 +1,81 @@
from django import forms
from django.forms import ModelForm
from django.contrib.auth.models import Group
from todo.models import Item, List
from django.contrib.auth import get_user_model
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
exclude = []
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 = get_user_model().objects.filter(groups__in=[task_list.group])
self.fields['assigned_to'].label_from_instance = \
lambda obj: "%s (%s)" % (obj.get_full_name(), obj.username)
due_date = forms.DateField(
required=False,
widget=forms.DateTimeInput(attrs={'class': 'due_date_picker'})
)
title = forms.CharField(
widget=forms.widgets.TextInput(attrs={'size': 35})
)
note = forms.CharField(widget=forms.Textarea(), required=False)
class Meta:
model = Item
exclude = []
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 = get_user_model().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(forms.Form):
"""Search."""
q = forms.CharField(
widget=forms.widgets.TextInput(attrs={'size': 35})
)

View file

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import datetime
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('date', models.DateTimeField(default=datetime.datetime.now)),
('body', models.TextField(blank=True)),
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Item',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=140)),
('created_date', models.DateField(auto_now=True, auto_now_add=True)),
('due_date', models.DateField(null=True, blank=True)),
('completed', models.BooleanField(default=None)),
('completed_date', models.DateField(null=True, blank=True)),
('note', models.TextField(null=True, blank=True)),
('priority', models.PositiveIntegerField(max_length=3)),
('assigned_to', models.ForeignKey(related_name='todo_assigned_to', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(related_name='todo_created_by', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['priority'],
},
bases=(models.Model,),
),
migrations.CreateModel(
name='List',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=60)),
('slug', models.SlugField(max_length=60, editable=False)),
('group', models.ForeignKey(to='auth.Group')),
],
options={
'ordering': ['name'],
'verbose_name_plural': 'Lists',
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='list',
unique_together=set([('group', 'slug')]),
),
migrations.AddField(
model_name='item',
name='list',
field=models.ForeignKey(to='todo.List'),
preserve_default=True,
),
migrations.AddField(
model_name='comment',
name='task',
field=models.ForeignKey(to='todo.Item'),
preserve_default=True,
),
]

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('todo', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='item',
name='created_date',
field=models.DateField(auto_now=True),
),
migrations.AlterField(
model_name='item',
name='priority',
field=models.PositiveIntegerField(),
),
]

View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-04-09 11:11
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('todo', '0002_auto_20150614_2339'),
]
operations = [
migrations.AlterField(
model_name='item',
name='assigned_to',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='todo_assigned_to', to=settings.AUTH_USER_MODEL),
),
]

View file

85
todo/models.py Normal file
View file

@ -0,0 +1,85 @@
from __future__ import unicode_literals
import datetime
from django.db import models
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
from django.utils.encoding import python_2_unicode_compatible
from django.conf import settings
from autoslug import AutoSlugField
@python_2_unicode_compatible
class List(models.Model):
name = models.CharField(max_length=60)
slug = AutoSlugField(populate_from='name', editable=False, always_update=True)
group = models.ForeignKey(Group)
def __str__(self):
return self.name
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")
@python_2_unicode_compatible
class Item(models.Model):
title = models.CharField(max_length=140)
list = models.ForeignKey(List)
created_date = models.DateField(auto_now=True)
due_date = models.DateField(blank=True, null=True, )
completed = models.BooleanField(default=None)
completed_date = models.DateField(blank=True, null=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='todo_created_by')
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='todo_assigned_to')
note = models.TextField(blank=True, null=True)
priority = models.PositiveIntegerField()
# 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 self.due_date and datetime.date.today() > self.due_date:
return 1
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('todo-task_detail', kwargs={'task_id': self.id, })
# Auto-set the item creation / completed date
def save(self):
# 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"]
@python_2_unicode_compatible
class Comment(models.Model):
"""
Not using Django's built-in comments because 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(settings.AUTH_USER_MODEL)
task = models.ForeignKey(Item)
date = models.DateTimeField(default=datetime.datetime.now)
body = models.TextField(blank=True)
def snippet(self):
# Define here rather than in __str__ so we can use it in the admin list_display
return "{author} - {snippet}...".format(author=self.author, snippet=self.body[:35])
def __str__(self):
return self.snippet

7
todo/settings.py Normal file
View file

@ -0,0 +1,7 @@
from django.conf import settings
STAFF_ONLY = getattr(settings, 'TODO_STAFF_ONLY', False)
DEFAULT_LIST_ID = getattr(settings, 'TODO_DEFAULT_LIST_ID', 1)
DEFAULT_ASSIGNEE = getattr(settings, 'TODO_DEFAULT_ASSIGNEE', None)
PUBLIC_SUBMIT_REDIRECT = getattr(settings, 'TODO_PUBLIC_SUBMIT_REDIRECT', '/')

View file

@ -0,0 +1,66 @@
/*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;
}
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-break {
margin-top: 30px;
border-top: 1px dotted gray;
}
table.nocolor, table.nocolor tr, table.nocolor td {
background-color: transparent;
}
.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
}
);

View file

@ -0,0 +1,59 @@
{% extends "todo/base.html" %}
{% block page_heading %}{% endblock %}
{% block title %}File Ticket{% endblock %}
{% block content %}
<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>
<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,16 @@
{% extends "todo/base.html" %}
{% block page_heading %}{% endblock %}
{% block title %}Add Todo List{% endblock %}
{% block content %}
<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 %}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block extrahead %}
<!-- CSS and JavaScript for django-todo -->
<link rel="stylesheet" type="text/css" href="{% static 'todo/css/styles.css' %}" />
<script src="{% static 'todo/js/jquery.tablednd_0_5.js' %}" type="text/javascript"></script>
<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,32 @@
{% extends "todo/base.html" %}
{% block title %}{{ list_title }} to-do items{% endblock %}
{% block content %}
{% if user.is_staff %}
<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>
{% else %}
<p>Sorry, you don't have permission to delete lists. Please contact your group administrator.</p>
{% endif %}
{% 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,24 @@
{% extends "todo/base.html" %}
{% block title %}{{ list_title }} Todo Lists{% endblock %}
{% block content %}
<h1>Todo 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 %}
<p><a href="{% url 'todo-add_list' %}">Create new todo list</a></p>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends "todo/base.html" %}
{% block title %}Search results{% endblock %}
{% block content_title %}
<h2 class="page_title">Search</h2>
{% endblock %}
{% block content %}
{% 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.name }}</a><br />
Assigned to: {% if f.assigned_to %}{{ f.assigned_to }}{% else %}Anyone{% endif %} (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,166 @@
{% 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>
{% if list_slug == "mine" %}
<h1>Tasks assigned to {{ request.user }}</h1>
{% elif auth_ok %}
<h1>Tasks filed under "{{ list.name }}"</h1>
<p>This list belongs to group {{ list.group }}</p>
{% endif %}
{% if auth_ok %}
<form action="" method="POST">
{% csrf_token %}
{# Only show task adder if viewing a proper list #}
{% if 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><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="{{ list.id }}" 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>
{% endif %}
{% if not view_completed %}
<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>
{% if list_slug == "mine" %}
<th>List</th>
{% endif %}
<th>Del</th>
</tr>
{% for task in task_list %}
<tr 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>{% if task.assigned_to %}{{ task.assigned_to }}{% else %}Anyone{% endif %}</td>
<td style="text-align:center;">{% if task.note %}&asymp;{% endif %} </td>
<td style="text-align:center;">{% if task.comment_set.all.count != 0 %}{{ task.comment_set.all.count }}{% endif %}</td>
{% if list_slug == "mine" %}
<td><a href="{% url 'todo-incomplete_tasks' task.list.id task.list.slug %}">{{ task.list }}</a></td>
{% endif %}
<td><input type="checkbox" name="del_tasks" 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>
{% else %}
<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>
{% if list_slug == "mine" %}
<th>List</th>
{% endif %}
<th>Del</th>
</tr>
{% for task in completed_list %}
<tr>
<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;">{% if task.comment_set.all.count != 0 %}{{ task.comment_set.all.count }}{% endif %}
<td><input type="checkbox" name="del_tasks" value="{{ task.id }}" id="del_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>
{% endif %}
{% if user.is_staff %}
{% if list_slug != "mine" %}
<p><a class="todo" href="{% url 'todo-del_list' list.id list_slug %}">Delete this list</a></p>
{% endif %}
{% endif %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,103 @@
{% 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 auth_ok %}
<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> {% if task.assigned_to %}{{ task.assigned_to.get_full_name }}{% else %}Anyone{% endif %}<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>
<tr>
<td>Title:</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"></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>
{% endif %}
{% endblock %}

23
todo/urls.py Normal file
View file

@ -0,0 +1,23 @@
from django.conf.urls import url
from todo import views
urlpatterns = [
url(r'^$', views.list_lists, name="todo-lists"),
url(r'^mine/$', views.view_list, {'list_slug': 'mine'}, name="todo-mine"),
url(r'^(?P<list_id>\d{1,4})/(?P<list_slug>[\w-]+)/delete$', views.del_list, name="todo-del_list"),
url(r'^task/(?P<task_id>\d{1,6})$', views.view_task, name='todo-task_detail'),
url(r'^(?P<list_id>\d{1,4})/(?P<list_slug>[\w-]+)$', views.view_list, name='todo-incomplete_tasks'),
url(r'^(?P<list_id>\d{1,4})/(?P<list_slug>[\w-]+)/completed$', views.view_list, {'view_completed': True},
name='todo-completed_tasks'),
url(r'^add_list/$', views.add_list, name="todo-add_list"),
url(r'^search-post/$', views.search_post, name="todo-search-post"),
url(r'^search/$', views.search, name="todo-search"),
# View reorder_tasks is only called by JQuery for drag/drop task ordering
url(r'^reorder_tasks/$', views.reorder_tasks, name="todo-reorder_tasks"),
url(r'^ticket/add/$', views.external_add, name="todo-external-add"),
url(r'^recent/added/$', views.view_list, {'list_slug': 'recent-add'}, name="todo-recently_added"),
url(r'^recent/completed/$', views.view_list, {'list_slug': 'recent-complete'},
name="todo-recently_completed"),
]

51
todo/utils.py Normal file
View file

@ -0,0 +1,51 @@
import datetime
from django.contrib import messages
from django.template.loader import render_to_string
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from todo.models import Item
# Need for links in email templates
current_site = Site.objects.get_current()
def mark_done(request, done_items):
# Check for items in the mark_done POST array. If present, change status to complete.
for item in done_items:
i = Item.objects.get(id=item)
i.completed = True
i.completed_date = datetime.datetime.now()
i.save()
messages.success(request, "Item \"{i}\" marked complete.".format(i=i.title))
def undo_completed_task(request, undone_items):
# Undo: Set completed items back to incomplete
for item in undone_items:
i = Item.objects.get(id=item)
i.completed = False
i.save()
messages.success(request, "Previously completed task \"{i}\" marked incomplete.".format(i=i.title))
def del_tasks(request, deleted_items):
# Delete selected items
for item_id in deleted_items:
i = Item.objects.get(id=item_id)
messages.success(request, "Item \"{i}\" deleted.".format(i=i.title))
i.delete()
def send_notify_mail(request, new_task):
# 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:
messages.error(request, "Task saved but mail not sent. Contact your administrator.")

351
todo/views.py Normal file
View file

@ -0,0 +1,351 @@
import datetime
from django.contrib.auth.decorators import user_passes_test, login_required
from django.contrib.auth.models import User
from django.contrib import messages
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db import IntegrityError
from django.db.models import Q
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.template import RequestContext
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt
from django.contrib.sites.models import Site
from todo import settings
from todo.forms import AddListForm, AddItemForm, EditItemForm, AddExternalItemForm, SearchForm
from todo.models import Item, List, Comment
from todo.utils import mark_done, undo_completed_task, del_tasks, send_notify_mail
# Need for links in email templates
current_site = Site.objects.get_current()
def check_user_allowed(user):
"""
Conditions for user_passes_test decorator.
"""
if settings.STAFF_ONLY:
return user.is_authenticated() and user.is_staff
else:
return user.is_authenticated()
@user_passes_test(check_user_allowed)
def list_lists(request):
"""
Homepage view - list of lists a user can view, and ability to add a list.
"""
thedate = datetime.datetime.now()
searchform = SearchForm(auto_id=False)
# Make sure user belongs to at least one group.
if request.user.groups.all().count() == 0:
messages.error(request, "You do not yet belong to any groups. Ask your administrator to add you to one.")
# 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')
list_count = list_list.count()
# superusers 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(request, 'todo/list_lists.html', locals())
@user_passes_test(check_user_allowed)
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.
"""
list = get_object_or_404(List, slug=list_slug)
if request.method == 'POST':
List.objects.get(id=list.id).delete()
messages.success(request, "{list_name} is gone.".format(list_name=list.name))
return HttpResponseRedirect(reverse('todo-lists'))
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(request, 'todo/del_list.html', locals())
@user_passes_test(check_user_allowed)
def view_list(request, list_id=0, list_slug=None, view_completed=False):
"""
Display and manage items in a 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 = True
else:
list = get_object_or_404(List, id=list_id)
if list.group in request.user.groups.all() or request.user.is_staff or list_slug == "mine":
auth_ok = True
else: # User does not belong to the group this list is attached to
messages.error(request, "You do not have permission to view/edit this list.")
# Process all possible list interactions on each submit
mark_done(request, request.POST.getlist('mark_done'))
del_tasks(request, request.POST.getlist('del_tasks'))
undo_completed_task(request, request.POST.getlist('undo_completed_task'))
thedate = datetime.datetime.now()
created_date = "%s-%s-%s" % (thedate.year, thedate.month, thedate.day)
# Get set 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=False)
completed_list = Item.objects.filter(assigned_to=request.user, completed=True)
elif list_slug == "recent-add":
# Only show items in lists that are in groups that the current user is also in.
# Assume this only includes uncompleted items.
task_list = Item.objects.filter(
list__group__in=(request.user.groups.all()),
completed=False).order_by('-created_date')[:50]
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=True).order_by('-completed_date')[:50]
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():
new_task = form.save()
# Send email alert only if Notify checkbox is checked AND assignee is not same as the submitter
if "notify" in request.POST and new_task.assigned_to != request.user:
send_notify_mail(request, new_task)
messages.success(request, "New task \"{t}\" has been added.".format(t=new_task.title))
return HttpResponseRedirect(request.path)
else:
# Don't allow adding new tasks on some views
if list_slug != "mine" and list_slug != "recent-add" and list_slug != "recent-complete":
form = AddItemForm(list, initial={
'assigned_to': request.user.id,
'priority': 999,
})
return render(request, 'todo/view_list.html', locals())
@user_passes_test(check_user_allowed)
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)
# Ensure user has permission to view item.
# Get 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 = True
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)
recip_list = set(recip_list) # Eliminate duplicates
try:
send_mail(email_subject, email_body, task.created_by.email, recip_list, fail_silently=False)
messages.success(request, "Comment sent to thread participants.")
except:
messages.error(request, "Comment saved but mail not sent. Contact your administrator.")
messages.success(request, "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:
messages.info(request, "You do not have permission to view/edit this task.")
return render(request, 'todo/view_task.html', locals())
@csrf_exempt
@user_passes_test(check_user_allowed)
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]
# Re-prioritize each item in list
i = 1
for t in newtasklist:
newitem = Item.objects.get(pk=t)
newitem.priority = i
newitem.save()
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.
Public tickets are unassigned unless settings.DEFAULT_ASSIGNEE exists.
"""
if request.POST:
form = AddExternalItemForm(request.POST)
if form.is_valid():
item = form.save(commit=False)
item.list_id = settings.DEFAULT_LIST_ID
item.created_by = request.user
if settings.DEFAULT_ASSIGNEE:
item.assigned_to = User.objects.get(username=settings.DEFAULT_ASSIGNEE)
item.save()
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:
messages.error(request, "Task saved but mail not sent. Contact your administrator.")
messages.success(request, "Your trouble ticket has been submitted. We'll get back to you soon.")
return HttpResponseRedirect(settings.PUBLIC_SUBMIT_REDIRECT)
else:
form = AddExternalItemForm()
return render(request, 'todo/add_external_task.html', locals())
@user_passes_test(check_user_allowed)
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()
messages.success(request, "A new list has been added.")
return HttpResponseRedirect(request.path)
except IntegrityError:
messages.error(
request,
"There was a problem saving the new list. "
"Most likely a list with the same name in the same group already exists.")
else:
if request.user.groups.all().count() == 1:
form = AddListForm(request.user, initial={"group": request.user.groups.all()[0]})
else:
form = AddListForm(request.user)
return render(request, 'todo/add_list.html', locals())
@user_passes_test(check_user_allowed)
def search_post(request):
"""
Redirect POST'd search param to query GET string
"""
if request.POST:
q = request.POST.get('q')
url = reverse('todo-search') + "?q=" + q
return HttpResponseRedirect(url)
@user_passes_test(check_user_allowed)
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?
# We still need found_items in a queryset so it can be "excluded" below.
found_items = Item.objects.all()
if 'inc_complete' in request.GET:
found_items = found_items.exclude(completed=True)
else:
query_string = None
found_items = None
return render(
request,
'todo/search_results.html', {
'query_string': query_string,
'found_items': found_items
}, context_instance=RequestContext(request))

23
tox.ini Normal file
View file

@ -0,0 +1,23 @@
[tox]
envlist =
flake8py{2,3}
[testenv]
# deps = pytest
commands =
# py.test
[testenv:flake8py2]
basepython = python2.7
deps = flake8
commands = flake8 .
[testenv:flake8py3]
basepython = python3.5
deps = flake8
commands = flake8 .
[flake8]
max-line-length = 120
max-complexity = 10
exclude = [build, lib, bin, dist, docs/conf.py, */migrations, .eggs, *.egg-info]

View file

@ -1,5 +0,0 @@
project: coins-demo
configVersion: 1
---
image: ~
dockerfile: Dockerfile