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
12
.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# tools, IDEs, build folders
|
||||||
|
/.coverage/
|
||||||
|
/.eggs/
|
||||||
|
/.idea/
|
||||||
|
/.tox/
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
/docs/build/
|
||||||
|
/*.egg-info/
|
||||||
|
|
||||||
|
# Django and Python
|
||||||
|
*.py[cod]
|
|
@ -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
|
|
||||||
|
|
|
@ -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 }}
|
|
|
@ -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
5
.travis.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
language: python
|
||||||
|
install:
|
||||||
|
- pip install virtualenv
|
||||||
|
script:
|
||||||
|
- python setup.py test
|
15
Dockerfile
15
Dockerfile
|
@ -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
27
LICENSE
Normal 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
4
MANIFEST.in
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
include LICENSE
|
||||||
|
include README.rst
|
||||||
|
recursive-include todo/static *
|
||||||
|
recursive-include todo/templates *
|
28
README.md
28
README.md
|
@ -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
55
README.rst
Normal 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
|
|
@ -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
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
- name: bootstrap playbook for any k8s machine
|
|
||||||
hosts: k8s
|
|
||||||
become: yes
|
|
||||||
roles:
|
|
||||||
- bootstrap
|
|
|
@ -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
|
|
|
@ -1 +0,0 @@
|
||||||
k8s_node_role: 'master'
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
- name: Init k8s cluster
|
|
||||||
hosts: 'k8s-demo'
|
|
||||||
become: yes
|
|
||||||
max_fail_percentage: 0
|
|
||||||
roles:
|
|
||||||
- init-cluster
|
|
|
@ -1,2 +0,0 @@
|
||||||
[k8s]
|
|
||||||
k8s-demo ansible_connection=local
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
- name: bootstrap playbook for any k8s machine
|
|
||||||
hosts: k8s-masters
|
|
||||||
become: yes
|
|
||||||
roles:
|
|
||||||
- keepalived
|
|
|
@ -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
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
- name: restart kubelet
|
|
||||||
service: name=kubelet state=restarted
|
|
||||||
|
|
||||||
- name: restart docker daemon
|
|
||||||
service: name=docker state=restarted
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"exec-opts": ["native.cgroupdriver=systemd"],
|
|
||||||
"log-driver": "json-file",
|
|
||||||
"log-opts": {
|
|
||||||
"max-size": "100m"
|
|
||||||
},
|
|
||||||
"storage-driver": "overlay2"
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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: {}
|
|
|
@ -1 +0,0 @@
|
||||||
---
|
|
|
@ -1,3 +0,0 @@
|
||||||
---
|
|
||||||
- name: restart keepalived
|
|
||||||
service: name=keepalived state=restarted
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
}#}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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"
|
|
|
@ -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.
|
@ -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
|
|
|
@ -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
|
|
|
@ -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.
Binary file not shown.
|
@ -1,4 +0,0 @@
|
||||||
spec:
|
|
||||||
externalIPs:
|
|
||||||
- 100.100.100.15
|
|
||||||
loadBalancerIP: 100.100.100.15
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
pipenv run python manage.py migrate todo
|
|
||||||
pipenv run python manage.py runserver 0.0.0.0:8888
|
|
26
local.py
26
local.py
|
@ -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 = '/'
|
|
34
runme.sh
34
runme.sh
|
@ -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
107
setup.py
Executable 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
10
todo/__init__.py
Normal 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
18
todo/admin.py
Normal 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
81
todo/forms.py
Normal 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})
|
||||||
|
)
|
78
todo/migrations/0001_initial.py
Normal file
78
todo/migrations/0001_initial.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
24
todo/migrations/0002_auto_20150614_2339.py
Normal file
24
todo/migrations/0002_auto_20150614_2339.py
Normal 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(),
|
||||||
|
),
|
||||||
|
]
|
22
todo/migrations/0003_assignee_optional.py
Normal file
22
todo/migrations/0003_assignee_optional.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
0
todo/migrations/__init__.py
Normal file
0
todo/migrations/__init__.py
Normal file
85
todo/models.py
Normal file
85
todo/models.py
Normal 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
7
todo/settings.py
Normal 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', '/')
|
66
todo/static/todo/css/styles.css
Normal file
66
todo/static/todo/css/styles.css
Normal 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;
|
||||||
|
}
|
213
todo/static/todo/css/ui.datepicker.css
Normal file
213
todo/static/todo/css/ui.datepicker.css
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
/* Main Style Sheet for jQuery UI date picker */
|
||||||
|
#ui-datepicker-div, .ui-datepicker-inline {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: #ddd;
|
||||||
|
width: 185px;
|
||||||
|
}
|
||||||
|
#ui-datepicker-div {
|
||||||
|
display: none;
|
||||||
|
border: 1px solid #777;
|
||||||
|
z-index: 100; /*must have*/
|
||||||
|
}
|
||||||
|
.ui-datepicker-inline {
|
||||||
|
float: left;
|
||||||
|
display: block;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.ui-datepicker-rtl {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
.ui-datepicker-dialog {
|
||||||
|
padding: 5px !important;
|
||||||
|
border: 4px ridge #ddd !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-disabled {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: white;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
button.ui-datepicker-trigger {
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
img.ui-datepicker-trigger {
|
||||||
|
margin: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.ui-datepicker-prompt {
|
||||||
|
float: left;
|
||||||
|
padding: 2px;
|
||||||
|
background: #ddd;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
* html .ui-datepicker-prompt {
|
||||||
|
width: 185px;
|
||||||
|
}
|
||||||
|
.ui-datepicker-control, .ui-datepicker-links, .ui-datepicker-header, .ui-datepicker {
|
||||||
|
clear: both;
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.ui-datepicker-control {
|
||||||
|
background: #400;
|
||||||
|
padding: 2px 0px;
|
||||||
|
}
|
||||||
|
.ui-datepicker-links {
|
||||||
|
background: #000;
|
||||||
|
padding: 2px 0px;
|
||||||
|
}
|
||||||
|
.ui-datepicker-control, .ui-datepicker-links {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
.ui-datepicker-links label { /* disabled links */
|
||||||
|
padding: 2px 5px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.ui-datepicker-clear, .ui-datepicker-prev {
|
||||||
|
float: left;
|
||||||
|
width: 34%;
|
||||||
|
}
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-clear, .ui-datepicker-rtl .ui-datepicker-prev {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.ui-datepicker-current {
|
||||||
|
float: left;
|
||||||
|
width: 30%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.ui-datepicker-close, .ui-datepicker-next {
|
||||||
|
float: right;
|
||||||
|
width: 34%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.ui-datepicker-rtl .ui-datepicker-close, .ui-datepicker-rtl .ui-datepicker-next {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.ui-datepicker-header {
|
||||||
|
padding: 1px 0 3px;
|
||||||
|
background: #333;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 1.3em;
|
||||||
|
}
|
||||||
|
.ui-datepicker-header select {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 0px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.ui-datepicker {
|
||||||
|
background: #ccc;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
.ui-datepicker a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.ui-datepicker-title-row {
|
||||||
|
background: #777;
|
||||||
|
}
|
||||||
|
.ui-datepicker-days-row {
|
||||||
|
background: #eee;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.ui-datepicker-week-col {
|
||||||
|
background: #777;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.ui-datepicker-days-cell {
|
||||||
|
color: #000;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.ui-datepicker-days-cell a{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ui-datepicker-week-end-cell {
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
.ui-datepicker-title-row .ui-datepicker-week-end-cell {
|
||||||
|
background: #777;
|
||||||
|
}
|
||||||
|
.ui-datepicker-days-cell-over {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #777;
|
||||||
|
}
|
||||||
|
.ui-datepicker-unselectable {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.ui-datepicker-today {
|
||||||
|
background: #fcc !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-current-day {
|
||||||
|
background: #999 !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-status {
|
||||||
|
background: #ddd;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 80%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ________ Datepicker Links _______
|
||||||
|
|
||||||
|
** Reset link properties and then override them with !important */
|
||||||
|
#ui-datepicker-div a, .ui-datepicker-inline a {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.ui-datepicker-inline .ui-datepicker-links a {
|
||||||
|
padding: 0 5px !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-control a, .ui-datepicker-links a {
|
||||||
|
padding: 2px 5px !important;
|
||||||
|
color: #eee !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-title-row a {
|
||||||
|
color: #eee !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-control a:hover {
|
||||||
|
background: #fdd !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
.ui-datepicker-links a:hover, .ui-datepicker-title-row a:hover {
|
||||||
|
background: #ddd !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ___________ MULTIPLE MONTHS _________*/
|
||||||
|
|
||||||
|
.ui-datepicker-multi .ui-datepicker {
|
||||||
|
border: 1px solid #777;
|
||||||
|
}
|
||||||
|
.ui-datepicker-one-month {
|
||||||
|
float: left;
|
||||||
|
width: 185px;
|
||||||
|
}
|
||||||
|
.ui-datepicker-new-row {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ___________ IE6 IFRAME FIX ________ */
|
||||||
|
|
||||||
|
.ui-datepicker-cover {
|
||||||
|
display: none; /*sorry for IE5*/
|
||||||
|
display/**/: block; /*sorry for IE5*/
|
||||||
|
position: absolute; /*must have*/
|
||||||
|
z-index: -1; /*must have*/
|
||||||
|
filter: mask(); /*must have*/
|
||||||
|
top: -4px; /*must have*/
|
||||||
|
left: -4px; /*must have*/
|
||||||
|
width: 200px; /*must have*/
|
||||||
|
height: 200px; /*must have*/
|
||||||
|
}
|
382
todo/static/todo/js/jquery.tablednd_0_5.js
Normal file
382
todo/static/todo/js/jquery.tablednd_0_5.js
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
/**
|
||||||
|
* TableDnD plug-in for JQuery, allows you to drag and drop table rows
|
||||||
|
* You can set up various options to control how the system will work
|
||||||
|
* Copyright (c) Denis Howlett <denish@isocra.com>
|
||||||
|
* Licensed like jQuery, see http://docs.jquery.com/License.
|
||||||
|
*
|
||||||
|
* Configuration options:
|
||||||
|
*
|
||||||
|
* onDragStyle
|
||||||
|
* This is the style that is assigned to the row during drag. There are limitations to the styles that can be
|
||||||
|
* associated with a row (such as you can't assign a border--well you can, but it won't be
|
||||||
|
* displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
|
||||||
|
* a map (as used in the jQuery css(...) function).
|
||||||
|
* onDropStyle
|
||||||
|
* This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
|
||||||
|
* to what you can do. Also this replaces the original style, so again consider using onDragClass which
|
||||||
|
* is simply added and then removed on drop.
|
||||||
|
* onDragClass
|
||||||
|
* This class is added for the duration of the drag and then removed when the row is dropped. It is more
|
||||||
|
* flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
|
||||||
|
* is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
|
||||||
|
* stylesheet.
|
||||||
|
* onDrop
|
||||||
|
* Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
|
||||||
|
* and the row that was dropped. You can work out the new order of the rows by using
|
||||||
|
* table.rows.
|
||||||
|
* onDragStart
|
||||||
|
* Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
|
||||||
|
* table and the row which the user has started to drag.
|
||||||
|
* onAllowDrop
|
||||||
|
* Pass a function that will be called as a row is over another row. If the function returns true, allow
|
||||||
|
* dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
|
||||||
|
* the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
|
||||||
|
* scrollAmount
|
||||||
|
* This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
|
||||||
|
* window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
|
||||||
|
* FF3 beta
|
||||||
|
* dragHandle
|
||||||
|
* This is the name of a class that you assign to one or more cells in each row that is draggable. If you
|
||||||
|
* specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
|
||||||
|
* will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
|
||||||
|
* the whole row is draggable.
|
||||||
|
*
|
||||||
|
* Other ways to control behaviour:
|
||||||
|
*
|
||||||
|
* Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
|
||||||
|
* that you don't want to be draggable.
|
||||||
|
*
|
||||||
|
* Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
|
||||||
|
* <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
|
||||||
|
* an ID as must all the rows.
|
||||||
|
*
|
||||||
|
* Other methods:
|
||||||
|
*
|
||||||
|
* $("...").tableDnDUpdate()
|
||||||
|
* Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
|
||||||
|
* This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
|
||||||
|
* The table maintains the original configuration (so you don't have to specify it again).
|
||||||
|
*
|
||||||
|
* $("...").tableDnDSerialize()
|
||||||
|
* Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
|
||||||
|
* called from anywhere and isn't dependent on the currentTable being set up correctly before calling
|
||||||
|
*
|
||||||
|
* Known problems:
|
||||||
|
* - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
|
||||||
|
*
|
||||||
|
* Version 0.2: 2008-02-20 First public version
|
||||||
|
* Version 0.3: 2008-02-07 Added onDragStart option
|
||||||
|
* Made the scroll amount configurable (default is 5 as before)
|
||||||
|
* Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
|
||||||
|
* Added onAllowDrop to control dropping
|
||||||
|
* Fixed a bug which meant that you couldn't set the scroll amount in both directions
|
||||||
|
* Added serialize method
|
||||||
|
* Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
|
||||||
|
* draggable
|
||||||
|
* Improved the serialize method to use a default (and settable) regular expression.
|
||||||
|
* Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
|
||||||
|
*/
|
||||||
|
jQuery.tableDnD = {
|
||||||
|
/** Keep hold of the current table being dragged */
|
||||||
|
currentTable : null,
|
||||||
|
/** Keep hold of the current drag object if any */
|
||||||
|
dragObject: null,
|
||||||
|
/** The current mouse offset */
|
||||||
|
mouseOffset: null,
|
||||||
|
/** Remember the old value of Y so that we don't do too much processing */
|
||||||
|
oldY: 0,
|
||||||
|
|
||||||
|
/** Actually build the structure */
|
||||||
|
build: function(options) {
|
||||||
|
// Set up the defaults if any
|
||||||
|
|
||||||
|
this.each(function() {
|
||||||
|
// This is bound to each matching table, set up the defaults and override with user options
|
||||||
|
this.tableDnDConfig = jQuery.extend({
|
||||||
|
onDragStyle: null,
|
||||||
|
onDropStyle: null,
|
||||||
|
// Add in the default class for whileDragging
|
||||||
|
onDragClass: "tDnD_whileDrag",
|
||||||
|
onDrop: null,
|
||||||
|
onDragStart: null,
|
||||||
|
scrollAmount: 5,
|
||||||
|
serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
|
||||||
|
serializeParamName: null, // If you want to specify another parameter name instead of the table ID
|
||||||
|
dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
|
||||||
|
}, options || {});
|
||||||
|
// Now make the rows draggable
|
||||||
|
jQuery.tableDnD.makeDraggable(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now we need to capture the mouse up and mouse move event
|
||||||
|
// We can use bind so that we don't interfere with other event handlers
|
||||||
|
jQuery(document)
|
||||||
|
.bind('mousemove', jQuery.tableDnD.mousemove)
|
||||||
|
.bind('mouseup', jQuery.tableDnD.mouseup);
|
||||||
|
|
||||||
|
// Don't break the chain
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
|
||||||
|
makeDraggable: function(table) {
|
||||||
|
var config = table.tableDnDConfig;
|
||||||
|
if (table.tableDnDConfig.dragHandle) {
|
||||||
|
// We only need to add the event to the specified cells
|
||||||
|
var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
|
||||||
|
cells.each(function() {
|
||||||
|
// The cell is bound to "this"
|
||||||
|
jQuery(this).mousedown(function(ev) {
|
||||||
|
jQuery.tableDnD.dragObject = this.parentNode;
|
||||||
|
jQuery.tableDnD.currentTable = table;
|
||||||
|
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
|
||||||
|
if (config.onDragStart) {
|
||||||
|
// Call the onDrop method if there is one
|
||||||
|
config.onDragStart(table, this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// For backwards compatibility, we add the event to the whole row
|
||||||
|
var rows = jQuery("tr", table); // get all the rows as a wrapped set
|
||||||
|
rows.each(function() {
|
||||||
|
// Iterate through each row, the row is bound to "this"
|
||||||
|
var row = jQuery(this);
|
||||||
|
if (! row.hasClass("nodrag")) {
|
||||||
|
row.mousedown(function(ev) {
|
||||||
|
if (ev.target.tagName == "TD") {
|
||||||
|
jQuery.tableDnD.dragObject = this;
|
||||||
|
jQuery.tableDnD.currentTable = table;
|
||||||
|
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
|
||||||
|
if (config.onDragStart) {
|
||||||
|
// Call the onDrop method if there is one
|
||||||
|
config.onDragStart(table, this);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).css("cursor", "move"); // Store the tableDnD object
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTables: function() {
|
||||||
|
this.each(function() {
|
||||||
|
// this is now bound to each matching table
|
||||||
|
if (this.tableDnDConfig) {
|
||||||
|
jQuery.tableDnD.makeDraggable(this);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Get the mouse coordinates from the event (allowing for browser differences) */
|
||||||
|
mouseCoords: function(ev){
|
||||||
|
if(ev.pageX || ev.pageY){
|
||||||
|
return {x:ev.pageX, y:ev.pageY};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
|
||||||
|
y:ev.clientY + document.body.scrollTop - document.body.clientTop
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Given a target element and a mouse event, get the mouse offset from that element.
|
||||||
|
To do this we need the element's position and the mouse position */
|
||||||
|
getMouseOffset: function(target, ev) {
|
||||||
|
ev = ev || window.event;
|
||||||
|
|
||||||
|
var docPos = this.getPosition(target);
|
||||||
|
var mousePos = this.mouseCoords(ev);
|
||||||
|
return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Get the position of an element by going up the DOM tree and adding up all the offsets */
|
||||||
|
getPosition: function(e){
|
||||||
|
var left = 0;
|
||||||
|
var top = 0;
|
||||||
|
/** Safari fix -- thanks to Luis Chato for this! */
|
||||||
|
if (e.offsetHeight == 0) {
|
||||||
|
/** Safari 2 doesn't correctly grab the offsetTop of a table row
|
||||||
|
this is detailed here:
|
||||||
|
http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
|
||||||
|
the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
|
||||||
|
note that firefox will return a text node as a first child, so designing a more thorough
|
||||||
|
solution may need to take that into account, for now this seems to work in firefox, safari, ie */
|
||||||
|
e = e.firstChild; // a table cell
|
||||||
|
}
|
||||||
|
|
||||||
|
while (e.offsetParent){
|
||||||
|
left += e.offsetLeft;
|
||||||
|
top += e.offsetTop;
|
||||||
|
e = e.offsetParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
left += e.offsetLeft;
|
||||||
|
top += e.offsetTop;
|
||||||
|
|
||||||
|
return {x:left, y:top};
|
||||||
|
},
|
||||||
|
|
||||||
|
mousemove: function(ev) {
|
||||||
|
if (jQuery.tableDnD.dragObject == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dragObj = jQuery(jQuery.tableDnD.dragObject);
|
||||||
|
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
|
||||||
|
var mousePos = jQuery.tableDnD.mouseCoords(ev);
|
||||||
|
var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
|
||||||
|
//auto scroll the window
|
||||||
|
var yOffset = window.pageYOffset;
|
||||||
|
if (document.all) {
|
||||||
|
// Windows version
|
||||||
|
//yOffset=document.body.scrollTop;
|
||||||
|
if (typeof document.compatMode != 'undefined' &&
|
||||||
|
document.compatMode != 'BackCompat') {
|
||||||
|
yOffset = document.documentElement.scrollTop;
|
||||||
|
}
|
||||||
|
else if (typeof document.body != 'undefined') {
|
||||||
|
yOffset=document.body.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mousePos.y-yOffset < config.scrollAmount) {
|
||||||
|
window.scrollBy(0, -config.scrollAmount);
|
||||||
|
} else {
|
||||||
|
var windowHeight = window.innerHeight ? window.innerHeight
|
||||||
|
: document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
|
||||||
|
if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
|
||||||
|
window.scrollBy(0, config.scrollAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (y != jQuery.tableDnD.oldY) {
|
||||||
|
// work out if we're going up or down...
|
||||||
|
var movingDown = y > jQuery.tableDnD.oldY;
|
||||||
|
// update the old value
|
||||||
|
jQuery.tableDnD.oldY = y;
|
||||||
|
// update the style to show we're dragging
|
||||||
|
if (config.onDragClass) {
|
||||||
|
dragObj.addClass(config.onDragClass);
|
||||||
|
} else {
|
||||||
|
dragObj.css(config.onDragStyle);
|
||||||
|
}
|
||||||
|
// If we're over a row then move the dragged row to there so that the user sees the
|
||||||
|
// effect dynamically
|
||||||
|
var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
|
||||||
|
if (currentRow) {
|
||||||
|
// TODO worry about what happens when there are multiple TBODIES
|
||||||
|
if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
|
||||||
|
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
|
||||||
|
} else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
|
||||||
|
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** We're only worried about the y position really, because we can only move rows up and down */
|
||||||
|
findDropTargetRow: function(draggedRow, y) {
|
||||||
|
var rows = jQuery.tableDnD.currentTable.rows;
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
var row = rows[i];
|
||||||
|
var rowY = this.getPosition(row).y;
|
||||||
|
var rowHeight = parseInt(row.offsetHeight)/2;
|
||||||
|
if (row.offsetHeight == 0) {
|
||||||
|
rowY = this.getPosition(row.firstChild).y;
|
||||||
|
rowHeight = parseInt(row.firstChild.offsetHeight)/2;
|
||||||
|
}
|
||||||
|
// Because we always have to insert before, we need to offset the height a bit
|
||||||
|
if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
|
||||||
|
// that's the row we're over
|
||||||
|
// If it's the same as the current row, ignore it
|
||||||
|
if (row == draggedRow) {return null;}
|
||||||
|
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
|
||||||
|
if (config.onAllowDrop) {
|
||||||
|
if (config.onAllowDrop(draggedRow, row)) {
|
||||||
|
return row;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
|
||||||
|
var nodrop = jQuery(row).hasClass("nodrop");
|
||||||
|
if (! nodrop) {
|
||||||
|
return row;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
mouseup: function(e) {
|
||||||
|
if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
|
||||||
|
var droppedRow = jQuery.tableDnD.dragObject;
|
||||||
|
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
|
||||||
|
// If we have a dragObject, then we need to release it,
|
||||||
|
// The row will already have been moved to the right place so we just reset stuff
|
||||||
|
if (config.onDragClass) {
|
||||||
|
jQuery(droppedRow).removeClass(config.onDragClass);
|
||||||
|
} else {
|
||||||
|
jQuery(droppedRow).css(config.onDropStyle);
|
||||||
|
}
|
||||||
|
jQuery.tableDnD.dragObject = null;
|
||||||
|
if (config.onDrop) {
|
||||||
|
// Call the onDrop method if there is one
|
||||||
|
config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
|
||||||
|
}
|
||||||
|
jQuery.tableDnD.currentTable = null; // let go of the table too
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
serialize: function() {
|
||||||
|
if (jQuery.tableDnD.currentTable) {
|
||||||
|
return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
|
||||||
|
} else {
|
||||||
|
return "Error: No Table id set, you need to set an id on your table and every row";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
serializeTable: function(table) {
|
||||||
|
var result = "";
|
||||||
|
var tableId = table.id;
|
||||||
|
var rows = table.rows;
|
||||||
|
for (var i=0; i<rows.length; i++) {
|
||||||
|
if (result.length > 0) result += "&";
|
||||||
|
var rowId = rows[i].id;
|
||||||
|
if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
|
||||||
|
rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
result += tableId + '[]=' + rowId;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
serializeTables: function() {
|
||||||
|
var result = "";
|
||||||
|
this.each(function() {
|
||||||
|
// this is now bound to each matching table
|
||||||
|
result += jQuery.tableDnD.serializeTable(this);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery.fn.extend(
|
||||||
|
{
|
||||||
|
tableDnD : jQuery.tableDnD.build,
|
||||||
|
tableDnDUpdate : jQuery.tableDnD.updateTables,
|
||||||
|
tableDnDSerialize: jQuery.tableDnD.serializeTables
|
||||||
|
}
|
||||||
|
);
|
59
todo/templates/todo/add_external_task.html
Normal file
59
todo/templates/todo/add_external_task.html
Normal 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 %}
|
16
todo/templates/todo/add_list.html
Normal file
16
todo/templates/todo/add_list.html
Normal 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 %}
|
15
todo/templates/todo/base.html
Normal file
15
todo/templates/todo/base.html
Normal 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 %}
|
32
todo/templates/todo/del_list.html
Normal file
32
todo/templates/todo/del_list.html
Normal 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! →" 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 %}
|
20
todo/templates/todo/email/assigned_body.txt
Normal file
20
todo/templates/todo/email/assigned_body.txt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Dear {{ task.assigned_to.first_name }} -
|
||||||
|
|
||||||
|
A new task on the list {{ task.list.name }} has been assigned to you by {{ task.created_by.first_name }} {{ task.created_by.last_name }}:
|
||||||
|
|
||||||
|
{{ task.title }}
|
||||||
|
|
||||||
|
{% if task.note %}
|
||||||
|
{% autoescape off %}
|
||||||
|
Note: {{ task.note }}
|
||||||
|
{% endautoescape %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Task details/comments:
|
||||||
|
http://{{ site }}{% url 'todo-task_detail' task.id %}
|
||||||
|
|
||||||
|
List {{ task.list.name }}:
|
||||||
|
http://{{ site }}{% url 'todo-incomplete_tasks' task.list.id task.list.slug %}
|
1
todo/templates/todo/email/assigned_subject.txt
Normal file
1
todo/templates/todo/email/assigned_subject.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
GTD: New task - {% autoescape off %}Note: {{ task.title }}{% endautoescape %}
|
16
todo/templates/todo/email/newcomment_body.txt
Normal file
16
todo/templates/todo/email/newcomment_body.txt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
A new task comment has been added.
|
||||||
|
|
||||||
|
Task: {{ task.title }}
|
||||||
|
Commenter: {{ user.first_name }} {{ user.last_name }}
|
||||||
|
|
||||||
|
Comment:
|
||||||
|
{% autoescape off %}
|
||||||
|
{{ body }}
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
Task details/comments:
|
||||||
|
https://{{ site }}{% url 'todo-task_detail' task.id %}
|
||||||
|
|
||||||
|
List {{ task.list.name }}:
|
||||||
|
https://{{ site }}{% url 'todo-incomplete_tasks' task.list.id task.list.slug %}
|
||||||
|
|
24
todo/templates/todo/list_lists.html
Normal file
24
todo/templates/todo/list_lists.html
Normal 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 %}
|
26
todo/templates/todo/search_results.html
Normal file
26
todo/templates/todo/search_results.html
Normal 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 %}
|
166
todo/templates/todo/view_list.html
Normal file
166
todo/templates/todo/view_list.html
Normal 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" >→ Click to add task ←</h2>
|
||||||
|
|
||||||
|
<div id="AddTask">
|
||||||
|
<table class="nocolor" border="0" cellspacing="5" cellpadding="5">
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.title.errors }}</td>
|
||||||
|
<td>{{ form.due_date.errors }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label for="id_title">Task:</label> {{ form.title }}</td>
|
||||||
|
<td><label for="id_due_date">Due date:</label> {{ form.due_date }}</td>
|
||||||
|
<td><label for="id_assigned">Assign to:</label> {{ form.assigned_to }}</td>
|
||||||
|
<td><label for="id_notify">Notify*:</label> <input type="checkbox" checked="checked" name="notify" value="1" id="notify"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><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 %}≈{% 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 %}≈{% 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 %}
|
103
todo/templates/todo/view_task.html
Normal file
103
todo/templates/todo/view_task.html
Normal 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>→ Click to edit details ←</strong></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>In list:</strong> <a href="{% url 'todo-incomplete_tasks' task.list.id task.list.slug %}" class="showlink">{{ task.list }}</a><br />
|
||||||
|
<strong>Assigned to:</strong> {% 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
23
todo/urls.py
Normal 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
51
todo/utils.py
Normal 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
351
todo/views.py
Normal 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
23
tox.ini
Normal 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]
|
5
werf.yml
5
werf.yml
|
@ -1,5 +0,0 @@
|
||||||
project: coins-demo
|
|
||||||
configVersion: 1
|
|
||||||
---
|
|
||||||
image: ~
|
|
||||||
dockerfile: Dockerfile
|
|
Loading…
Add table
Add a link
Reference in a new issue