Compare commits
215 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7df1fd995f | ||
|
9c3f7d5948 | ||
|
97ec2309f0 | ||
|
0875c84796 | ||
|
990aa1df61 | ||
|
2b43278546 | ||
|
e8284a695a | ||
|
4d2cc36338 | ||
|
e9bbe2a143 | ||
|
76170f973d | ||
ad7f63a5b9 | |||
|
7c8239b942 | ||
|
6b52a69f23 | ||
|
ef1a224546 | ||
|
33caa62205 | ||
|
834095e10a | ||
|
903a1fa4d5 | ||
|
fb59e92386 | ||
|
1b9a650a19 | ||
|
7e4935174a | ||
|
07c19e1541 | ||
|
533d5021a7 | ||
|
73c78ae875 | ||
639278299b | |||
|
3e7cbffbbd | ||
|
f80637fbfc | ||
|
8fac1e2a46 | ||
c2e5429c4d | |||
|
8be1061c52 | ||
|
bdf505b79c | ||
|
db67dde361 | ||
|
abc8d04f2f | ||
|
bcca957c21 | ||
|
f16161568e | ||
|
9ba9a4f70c | ||
|
e3d346cf6a | ||
|
6d6b6d0de0 | ||
|
6b089ea9e8 | ||
|
7e9d5f0586 | ||
75de0d5cdc | |||
|
5adc1b90ef | ||
|
d07d61f7bf | ||
|
6caa6e01af | ||
|
ae8ed85e43 | ||
|
a22215fa08 | ||
|
c294cf1694 | ||
|
bed32dcf23 | ||
|
3c882f8913 | ||
|
424bd0a051 | ||
343ea0c339 | |||
|
65b2720f70 | ||
|
aac1d41217 | ||
|
5bc450dadf | ||
6531e03b1a | |||
|
1bb80be1c9 | ||
c3d51a59bc | |||
f2d213c284 | |||
|
a6706498b6 | ||
|
5068274017 | ||
|
f13f2502a7 | ||
|
ef13cb283e | ||
|
8622493dd2 | ||
|
9f799832a9 | ||
|
92bf4fecdf | ||
|
16fa9ed24f | ||
|
c68e399eac | ||
|
c4807b7d37 | ||
|
7efa164eb8 | ||
|
2d86a51177 | ||
|
c5740b9a84 | ||
|
a5c83dad83 | ||
|
988609d265 | ||
|
58d7bdfc30 | ||
|
6c31d4446d | ||
|
ce3fd8c66e | ||
|
2b722afb8a | ||
|
d07cb30fce | ||
|
4f9f379543 | ||
|
2d40ef471e | ||
|
7f576c9bc8 | ||
|
21e0c6d656 | ||
|
caed3b384d | ||
|
befc7ad2cd | ||
|
4a385bde6b | ||
|
1cd9700366 | ||
|
7a4984dc35 | ||
|
f42d34205c | ||
|
44de86f1ca | ||
|
7fe0728716 | ||
|
edff438623 | ||
|
8a20998f8c | ||
|
602cf247e2 | ||
|
fb94fdb130 | ||
|
e7655ccfe8 | ||
|
2e02163701 | ||
|
ab929b07e1 | ||
|
8cd169e502 | ||
|
b6c2227417 | ||
|
9a5c794c41 | ||
|
276ead54e7 | ||
|
cdacc5fed5 | ||
|
8b448e88a5 | ||
|
388fb40c00 | ||
|
e9a7bbe48c | ||
|
ad0a1aa44a | ||
|
b3d94ab608 | ||
|
6996c25842 | ||
|
4a99d90d1e | ||
|
184084c6a8 | ||
|
f6d79879ae | ||
|
70cac8b4e9 | ||
|
3b9c47cd7c | ||
|
575e2649ac | ||
|
6f47f9d388 | ||
|
c7ad961ef3 | ||
|
d0212b8a55 | ||
|
84c441cf39 | ||
|
01cab7a82f | ||
|
891148e496 | ||
|
0abc9bf16c | ||
|
3816abd123 | ||
|
7b58a12d97 | ||
|
fdd14392fe | ||
|
8e52aad828 | ||
|
dc3d4b647e | ||
|
513ef59d4a | ||
|
91b9a099a3 | ||
|
6953085285 | ||
|
78e9c510bc | ||
|
21ec87cee4 | ||
|
f526ed5166 | ||
|
c6bd3bcdb0 | ||
|
8e16de5d8f | ||
|
8fe6e4856e | ||
|
064e1d2659 | ||
|
c41d15344b | ||
|
cb7b4e4c1f | ||
|
c18080c55d | ||
|
fe03d3f5b5 | ||
|
3991a2c82c | ||
|
782950d904 | ||
|
5805cf52ad | ||
|
9098e3f0d4 | ||
|
4fe3829b98 | ||
|
d169f131a2 | ||
|
50d182103c | ||
|
416419301c | ||
|
b570d30f83 | ||
|
f0d2725ceb | ||
|
bb463f3974 | ||
|
4e8960c12f | ||
|
d9414f6de8 | ||
|
252153e16a | ||
|
d8b2600775 | ||
|
321c521fc3 | ||
|
3ecf4e58cb | ||
|
97bcfd96c2 | ||
|
1f27688aa4 | ||
|
403d1483f6 | ||
|
ff49d5cba2 | ||
|
d3d8d5e46c | ||
|
a2d02b0a8c | ||
|
97e596f463 | ||
|
5b2820df37 | ||
|
9d436674db | ||
|
dbc379da6a | ||
|
f4d1da0ab7 | ||
|
0d7a933d1c | ||
|
e4643eea26 | ||
|
b3194da825 | ||
|
d8cdd268d5 | ||
|
c03bc3579c | ||
|
0992c43d8b | ||
|
07e02b56f7 | ||
|
bc8adb63f5 | ||
|
998b53463d | ||
|
3be7ea7745 | ||
|
c470864c86 | ||
|
7e0435a948 | ||
|
439689b32e | ||
|
0daf9336c0 | ||
|
35cfd2eb39 | ||
|
f853296aa8 | ||
|
2bab3ec703 | ||
|
675210586c | ||
|
d9a0460942 | ||
|
fe7036dae3 | ||
|
982864e1ba | ||
|
b361abeaf6 | ||
|
cc1a48ee6f | ||
|
be9a87fc0f | ||
|
929f8df727 | ||
|
0f018b8572 | ||
|
91c0075ccf | ||
|
5cbfc554ee | ||
|
ad33d62024 | ||
|
5c1d98d952 | ||
|
1c2a247f83 | ||
|
3d93e176e8 | ||
|
4d0801313c | ||
|
8cba92b571 | ||
|
03c125ca50 | ||
|
0c8d31b6af | ||
|
fc4fa7b7f3 | ||
|
efc2dbe11a | ||
|
774cd3d057 | ||
|
1fff35c613 | ||
|
f2b87979f2 | ||
|
cd1678da47 | ||
|
0c42de2ffb | ||
|
c348ea1179 | ||
|
f0adb90224 | ||
|
58ac51a260 | ||
|
7ab41ed936 | ||
|
90c41d1f29 |
73 changed files with 839 additions and 2122 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -1,12 +0,0 @@
|
|||
# tools, IDEs, build folders
|
||||
/.coverage/
|
||||
/.eggs/
|
||||
/.idea/
|
||||
/.tox/
|
||||
/build/
|
||||
/dist/
|
||||
/docs/build/
|
||||
/*.egg-info/
|
||||
|
||||
# Django and Python
|
||||
*.py[cod]
|
37
.helm/templates/02-deployment.yaml
Normal file
37
.helm/templates/02-deployment.yaml
Normal file
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
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
|
||||
|
15
.helm/templates/03-service.yaml
Normal file
15
.helm/templates/03-service.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
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 }}
|
16
.helm/templates/04-ingress.yaml
Normal file
16
.helm/templates/04-ingress.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
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
|
|
@ -1,5 +0,0 @@
|
|||
language: python
|
||||
install:
|
||||
- pip install virtualenv
|
||||
script:
|
||||
- python setup.py test
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
27
LICENSE
|
@ -1,27 +0,0 @@
|
|||
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.
|
|
@ -1,4 +0,0 @@
|
|||
include LICENSE
|
||||
include README.rst
|
||||
recursive-include todo/static *
|
||||
recursive-include todo/templates *
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# [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
55
README.rst
|
@ -1,55 +0,0 @@
|
|||
============================
|
||||
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
|
10
contrib/ansible/ansible.cfg
Normal file
10
contrib/ansible/ansible.cfg
Normal file
|
@ -0,0 +1,10 @@
|
|||
[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
|
6
contrib/ansible/bootstrap-node.yml
Normal file
6
contrib/ansible/bootstrap-node.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: bootstrap playbook for any k8s machine
|
||||
hosts: k8s
|
||||
become: yes
|
||||
roles:
|
||||
- bootstrap
|
11
contrib/ansible/group_vars/all/main.yml
Normal file
11
contrib/ansible/group_vars/all/main.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
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
contrib/ansible/host_vars/k8s-demo.yml
Normal file
1
contrib/ansible/host_vars/k8s-demo.yml
Normal file
|
@ -0,0 +1 @@
|
|||
k8s_node_role: 'master'
|
7
contrib/ansible/init-cluster.yml
Normal file
7
contrib/ansible/init-cluster.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
- name: Init k8s cluster
|
||||
hosts: 'k8s-demo'
|
||||
become: yes
|
||||
max_fail_percentage: 0
|
||||
roles:
|
||||
- init-cluster
|
2
contrib/ansible/inventory/hosts
Normal file
2
contrib/ansible/inventory/hosts
Normal file
|
@ -0,0 +1,2 @@
|
|||
[k8s]
|
||||
k8s-demo ansible_connection=local
|
6
contrib/ansible/keepalived.yml
Normal file
6
contrib/ansible/keepalived.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: bootstrap playbook for any k8s machine
|
||||
hosts: k8s-masters
|
||||
become: yes
|
||||
roles:
|
||||
- keepalived
|
6
contrib/ansible/roles/bootstrap/defaults/main.yml
Normal file
6
contrib/ansible/roles/bootstrap/defaults/main.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
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
|
||||
|
6
contrib/ansible/roles/bootstrap/handlers/main.yml
Normal file
6
contrib/ansible/roles/bootstrap/handlers/main.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: restart kubelet
|
||||
service: name=kubelet state=restarted
|
||||
|
||||
- name: restart docker daemon
|
||||
service: name=docker state=restarted
|
39
contrib/ansible/roles/bootstrap/tasks/main.yml
Normal file
39
contrib/ansible/roles/bootstrap/tasks/main.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
- 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
|
|
@ -0,0 +1,11 @@
|
|||
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
|
8
contrib/ansible/roles/bootstrap/templates/daemon.json
Normal file
8
contrib/ansible/roles/bootstrap/templates/daemon.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"exec-opts": ["native.cgroupdriver=systemd"],
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "100m"
|
||||
},
|
||||
"storage-driver": "overlay2"
|
||||
}
|
52
contrib/ansible/roles/init-cluster/tasks/main.yml
Normal file
52
contrib/ansible/roles/init-cluster/tasks/main.yml
Normal file
|
@ -0,0 +1,52 @@
|
|||
- 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'
|
||||
|
25
contrib/ansible/roles/init-cluster/templates/kubeadm.conf.j2
Normal file
25
contrib/ansible/roles/init-cluster/templates/kubeadm.conf.j2
Normal file
|
@ -0,0 +1,25 @@
|
|||
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
contrib/ansible/roles/keepalived/defaults/main.yml
Normal file
1
contrib/ansible/roles/keepalived/defaults/main.yml
Normal file
|
@ -0,0 +1 @@
|
|||
---
|
3
contrib/ansible/roles/keepalived/handlers/main.yml
Normal file
3
contrib/ansible/roles/keepalived/handlers/main.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
- name: restart keepalived
|
||||
service: name=keepalived state=restarted
|
14
contrib/ansible/roles/keepalived/tasks/main.yml
Normal file
14
contrib/ansible/roles/keepalived/tasks/main.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
- 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
|
|
@ -0,0 +1,31 @@
|
|||
! 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
|
||||
}#}
|
||||
}
|
77
contrib/firstboot.sh
Executable file
77
contrib/firstboot.sh
Executable file
|
@ -0,0 +1,77 @@
|
|||
#!/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
|
112
contrib/k8s-seed.txt
Normal file
112
contrib/k8s-seed.txt
Normal file
|
@ -0,0 +1,112 @@
|
|||
### 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"
|
18
contrib/mfg.service
Normal file
18
contrib/mfg.service
Normal file
|
@ -0,0 +1,18 @@
|
|||
[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
|
BIN
contrib/qemu-system-x86_64
Executable file
BIN
contrib/qemu-system-x86_64
Executable file
Binary file not shown.
BIN
contrib/share/qemu/bios-256k.bin
Normal file
BIN
contrib/share/qemu/bios-256k.bin
Normal file
Binary file not shown.
BIN
contrib/share/qemu/efi-e1000.rom
Normal file
BIN
contrib/share/qemu/efi-e1000.rom
Normal file
Binary file not shown.
157
contrib/share/qemu/keymaps/common
Normal file
157
contrib/share/qemu/keymaps/common
Normal file
|
@ -0,0 +1,157 @@
|
|||
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
|
35
contrib/share/qemu/keymaps/en-us
Normal file
35
contrib/share/qemu/keymaps/en-us
Normal file
|
@ -0,0 +1,35 @@
|
|||
# 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
|
18
contrib/share/qemu/keymaps/modifiers
Normal file
18
contrib/share/qemu/keymaps/modifiers
Normal file
|
@ -0,0 +1,18 @@
|
|||
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
|
BIN
contrib/share/qemu/kvmvapic.bin
Normal file
BIN
contrib/share/qemu/kvmvapic.bin
Normal file
Binary file not shown.
BIN
contrib/share/qemu/linuxboot.bin
Normal file
BIN
contrib/share/qemu/linuxboot.bin
Normal file
Binary file not shown.
BIN
contrib/share/qemu/vgabios-stdvga.bin
Normal file
BIN
contrib/share/qemu/vgabios-stdvga.bin
Normal file
Binary file not shown.
4
contrib/ymls/ingress.fix.yaml
Normal file
4
contrib/ymls/ingress.fix.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
spec:
|
||||
externalIPs:
|
||||
- 100.100.100.15
|
||||
loadBalancerIP: 100.100.100.15
|
3
entrypoint.sh
Normal file
3
entrypoint.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
pipenv run python manage.py migrate todo
|
||||
pipenv run python manage.py runserver 0.0.0.0:8888
|
26
local.py
Normal file
26
local.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
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
Executable file
34
runme.sh
Executable file
|
@ -0,0 +1,34 @@
|
|||
#!/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
107
setup.py
|
@ -1,107 +0,0 @@
|
|||
#!/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,
|
||||
},
|
||||
)
|
|
@ -1,10 +0,0 @@
|
|||
"""
|
||||
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'
|
|
@ -1,18 +0,0 @@
|
|||
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)
|
|
@ -1,81 +0,0 @@
|
|||
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})
|
||||
)
|
|
@ -1,78 +0,0 @@
|
|||
# -*- 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,
|
||||
),
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
# -*- 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(),
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# -*- 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),
|
||||
),
|
||||
]
|
|
@ -1,85 +0,0 @@
|
|||
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
|
|
@ -1,7 +0,0 @@
|
|||
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', '/')
|
|
@ -1,66 +0,0 @@
|
|||
/*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;
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
/* 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*/
|
||||
}
|
|
@ -1,382 +0,0 @@
|
|||
/**
|
||||
* 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
|
||||
}
|
||||
);
|
|
@ -1,59 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,16 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,15 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,32 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,20 +0,0 @@
|
|||
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 +0,0 @@
|
|||
GTD: New task - {% autoescape off %}Note: {{ task.title }}{% endautoescape %}
|
|
@ -1,16 +0,0 @@
|
|||
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 %}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,26 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,166 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,103 +0,0 @@
|
|||
{% 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
23
todo/urls.py
|
@ -1,23 +0,0 @@
|
|||
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"),
|
||||
]
|
|
@ -1,51 +0,0 @@
|
|||
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
351
todo/views.py
|
@ -1,351 +0,0 @@
|
|||
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
23
tox.ini
|
@ -1,23 +0,0 @@
|
|||
[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
Normal file
5
werf.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
project: coins-demo
|
||||
configVersion: 1
|
||||
---
|
||||
image: ~
|
||||
dockerfile: Dockerfile
|
Loading…
Add table
Add a link
Reference in a new issue