diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ae827f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# tools, IDEs, build folders +/.coverage/ +/.eggs/ +/.idea/ +/.tox/ +/build/ +/dist/ +/docs/build/ +/*.egg-info/ + +# Django and Python +*.py[cod] diff --git a/.helm/templates/02-deployment.yaml b/.helm/templates/02-deployment.yaml deleted file mode 100644 index c8b1de5..0000000 --- a/.helm/templates/02-deployment.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Chart.Name }} - namespace: {{ .Chart.Name }}-{{ .Values.global.env }} - -spec: - replicas: 1 - selector: - matchLabels: - app: {{ .Chart.Name }} - template: - metadata: - labels: - app: {{ .Chart.Name }} - spec: - containers: - - name: {{ .Chart.Name }} - image: {{ .Values.DockerImage }} - ports: - - containerPort: 8888 - env: - - name: DATABASE_HOST - value: "db-postgresql" - - name: DATABASE_USER - value: "postgres" - - name: DATABASE_PASSWORD - value: {{.Values.DBPwd | required "DBPwd is required" }} - resources: - limits: - cpu: 100m - memory: 150Mi - requests: - cpu: 100m - memory: 150Mi - diff --git a/.helm/templates/03-service.yaml b/.helm/templates/03-service.yaml deleted file mode 100644 index 18c0cb1..0000000 --- a/.helm/templates/03-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ .Chart.Name }}-service - namespace: {{ .Chart.Name }}-{{ .Values.global.env }} - -spec: - type: ClusterIP - ports: - - name: http - port: 8888 - targetPort: 8888 - selector: - app: {{ .Chart.Name }} diff --git a/.helm/templates/04-ingress.yaml b/.helm/templates/04-ingress.yaml deleted file mode 100644 index 77e28ac..0000000 --- a/.helm/templates/04-ingress.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: networking.k8s.io/v1beta1 -kind: Ingress -metadata: - name: ingress-{{ .Chart.Name }} - namespace: {{ .Chart.Name }}-{{ .Values.global.env }} - - annotations: - kubernetes.io/ingress.class: "nginx" -spec: - rules: - - http: - paths: - - path: / - backend: - serviceName: {{ .Chart.Name }}-service - servicePort: 8888 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0e6a36b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: python +install: + - pip install virtualenv +script: + - python setup.py test diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index adc4433..0000000 --- a/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.6-alpine - -WORKDIR /app - -RUN apk add git gcc musl-dev postgresql-dev && \ - pip install pipenv && \ - git clone https://github.com/shacker/gtd.git /app && \ - pipenv --python 3.6 && \ - pipenv install --dev - -ADD local.py project/. -ADD entrypoint.sh . -RUN chmod +x entrypoint.sh - -CMD [ "./entrypoint.sh" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68eef42 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2010, Scot Hacker, Birdhouse Arts and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Birdhouse Arts nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a13f7f3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include README.rst +recursive-include todo/static * +recursive-include todo/templates * diff --git a/README.md b/README.md deleted file mode 100644 index 4d85b72..0000000 --- a/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# [WiP] PoC of DevOps magic - -### Pre requires: -1. Any x86_64 Linux distribution. Tested on Debian 10, but must work on any modern Linux system -2. Internet access -3. wget -4. bash/dash shell -5. GNU awk -5. KVM - -### Known bugs and limitation: -1. Automated Keymap selection not working in the bootstrap process. You must hit Enter key. -2. Swap automatically created in the bootstrap process, I can't switch it off now. As a result, the minimum image size must be 12Gb or more. -3. It's still dirty, most exceptions during bootstrap still not handled - -### For start adventures: -1. just clone this repo -2. go to the repo directory -3. and type `./runme.sh` in your shell - -### What happened after you run this script: -1. We bootup in-userspace VM and starting the automated installation of Debian 9 with docker and common automation tools (git, ansible, helm, werf). -2. Bootstrapping one node Kubernates cluster with Amsible (LOL we invented minicube without minicube limitations) -3. Installing Nginx.org's Ingress with Helm -4. Installing docker-registry in our shiny Kube (we are lazy of course we use Helm for it) -5. Installing Postgress into Kube 0_o Also with Helm -6. ~~Deploying test service with Helm/Werf~~ -256. ~~PROFIT!!!1~~ diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..138b5e8 --- /dev/null +++ b/README.rst @@ -0,0 +1,55 @@ +============================ +django todo |latest-version| +============================ + +|build-status| |health| |downloads| |license| + +django-todo is a pluggable multi-user, multi-group task management and +assignment application for Django. It can serve as anything from a personal +to-do system to a complete, working ticketing system for organizations. + +Documentation +============= + +For documentation, see the django-todo wiki pages: + +- `Overview and screenshots + `_ + +- `Requirements and installation + `_ + +- `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 diff --git a/contrib/ansible/ansible.cfg b/contrib/ansible/ansible.cfg deleted file mode 100644 index b21d257..0000000 --- a/contrib/ansible/ansible.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[defaults] -allow_world_readable_tmpfiles=True -pipelining=True -retry_files_enabled = False -inventory = inventory -roles_path = roles -library = library -remote_tmp = /root/.ansible/tmp -[connection] -pipelining=True diff --git a/contrib/ansible/bootstrap-node.yml b/contrib/ansible/bootstrap-node.yml deleted file mode 100644 index 1928037..0000000 --- a/contrib/ansible/bootstrap-node.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: bootstrap playbook for any k8s machine - hosts: k8s - become: yes - roles: - - bootstrap diff --git a/contrib/ansible/group_vars/all/main.yml b/contrib/ansible/group_vars/all/main.yml deleted file mode 100644 index 113cfae..0000000 --- a/contrib/ansible/group_vars/all/main.yml +++ /dev/null @@ -1,11 +0,0 @@ -k8s_version: '1.17.5' -k8s_first_master_node: 'k8s-demo' -k8s_domain: coins.k8s.demo.ix.gs -k8s_pod_network: '192.168.0.0/16' -k8s_service_network: '10.254.0.0/24' -k8s_controlplane_vip: '100.100.100.15' -k8s_controlplane_address: '{{ k8s_controlplane_vip }}:6443' -k8s_cluster_name: k8s-demo - -cloud_provider: baremetal -ha_enabled: false diff --git a/contrib/ansible/host_vars/k8s-demo.yml b/contrib/ansible/host_vars/k8s-demo.yml deleted file mode 100644 index 4e7d5ba..0000000 --- a/contrib/ansible/host_vars/k8s-demo.yml +++ /dev/null @@ -1 +0,0 @@ -k8s_node_role: 'master' diff --git a/contrib/ansible/init-cluster.yml b/contrib/ansible/init-cluster.yml deleted file mode 100644 index f2e57d6..0000000 --- a/contrib/ansible/init-cluster.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -- name: Init k8s cluster - hosts: 'k8s-demo' - become: yes - max_fail_percentage: 0 - roles: - - init-cluster diff --git a/contrib/ansible/inventory/hosts b/contrib/ansible/inventory/hosts deleted file mode 100644 index e91cbbe..0000000 --- a/contrib/ansible/inventory/hosts +++ /dev/null @@ -1,2 +0,0 @@ -[k8s] -k8s-demo ansible_connection=local diff --git a/contrib/ansible/keepalived.yml b/contrib/ansible/keepalived.yml deleted file mode 100644 index 634312f..0000000 --- a/contrib/ansible/keepalived.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: bootstrap playbook for any k8s machine - hosts: k8s-masters - become: yes - roles: - - keepalived diff --git a/contrib/ansible/roles/bootstrap/defaults/main.yml b/contrib/ansible/roles/bootstrap/defaults/main.yml deleted file mode 100644 index 4f288b5..0000000 --- a/contrib/ansible/roles/bootstrap/defaults/main.yml +++ /dev/null @@ -1,6 +0,0 @@ -kubernetes_apt_release_channel: main -# Note that xenial repo is used for all Debian derivatives at this time. -kubernetes_apt_repository: "deb http://apt.kubernetes.io/ kubernetes-xenial {{ kubernetes_apt_release_channel }}" -## Calico config files -kubernetes_calico_manifest_file: https://docs.projectcalico.org/v3.10/manifests/calico.yaml - diff --git a/contrib/ansible/roles/bootstrap/handlers/main.yml b/contrib/ansible/roles/bootstrap/handlers/main.yml deleted file mode 100644 index 4a99fa9..0000000 --- a/contrib/ansible/roles/bootstrap/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: restart kubelet - service: name=kubelet state=restarted - -- name: restart docker daemon - service: name=docker state=restarted diff --git a/contrib/ansible/roles/bootstrap/tasks/main.yml b/contrib/ansible/roles/bootstrap/tasks/main.yml deleted file mode 100644 index ed0ec24..0000000 --- a/contrib/ansible/roles/bootstrap/tasks/main.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -- name: Ensure dependencies are installed. - apt: - name: - - apt-transport-https - - ca-certificates - state: present - -- name: Add Kubernetes apt key. - apt_key: - url: https://packages.cloud.google.com/apt/doc/apt-key.gpg - state: present - -- name: Add Kubernetes repository. - apt_repository: - repo: "{{ kubernetes_apt_repository }}" - state: present - update_cache: true - -- name: Install kubeadm kubelet kubectl - apt: - pkg: - - kubeadm={{ k8s_version }}-00 - - kubelet={{ k8s_version }}-00 - - kubectl={{ k8s_version }}-00 - - kubernetes-cni=0.7.5-00 - update_cache: yes - notify: restart kubelet - -- name: Add Kubernetes apt preferences file to pin a version. - template: - src: apt-preferences-kubernetes.j2 - dest: /etc/apt/preferences.d/kubernetes - -- name: - template: - src: daemon.json - dest: /etc/docker/daemon.json - notify: restart docker daemon diff --git a/contrib/ansible/roles/bootstrap/templates/apt-preferences-kubernetes.j2 b/contrib/ansible/roles/bootstrap/templates/apt-preferences-kubernetes.j2 deleted file mode 100644 index 7709524..0000000 --- a/contrib/ansible/roles/bootstrap/templates/apt-preferences-kubernetes.j2 +++ /dev/null @@ -1,11 +0,0 @@ -Package: kubectl -Pin: version {{ k8s_version }}.* -Pin-Priority: 1000 - -Package: kubeadm -Pin: version {{ k8s_version }}.* -Pin-Priority: 1000 - -Package: kubelet -Pin: version {{ k8s_version }}.* -Pin-Priority: 1000 diff --git a/contrib/ansible/roles/bootstrap/templates/daemon.json b/contrib/ansible/roles/bootstrap/templates/daemon.json deleted file mode 100644 index 5d18abc..0000000 --- a/contrib/ansible/roles/bootstrap/templates/daemon.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "exec-opts": ["native.cgroupdriver=systemd"], - "log-driver": "json-file", - "log-opts": { - "max-size": "100m" - }, - "storage-driver": "overlay2" -} diff --git a/contrib/ansible/roles/init-cluster/tasks/main.yml b/contrib/ansible/roles/init-cluster/tasks/main.yml deleted file mode 100644 index 8a7db14..0000000 --- a/contrib/ansible/roles/init-cluster/tasks/main.yml +++ /dev/null @@ -1,52 +0,0 @@ -- name: Add Kubeadm config file - template: - src: kubeadm.conf.j2 - dest: /etc/kubeadm.conf - when: k8s_node_role == 'master' - -- name: Init cluster - command: kubeadm init --config /etc/kubeadm.conf --upload-certs --ignore-preflight-errors serviceSubnet - when: ansible_hostname == k8s_first_master_node - -- name: Create kube config directory for root - file: path=/root/.kube state=directory - when: k8s_node_role == 'master' - -- name: Copy Kubernetes admin config to home directory - copy: - src: "/etc/kubernetes/admin.conf" - dest: "/root/.kube/config" -# remote_src: yes -# when: and ansible_hostname == k8s_first_master_node - -- name: Install Calico CNI - command: kubectl apply -f "https://docs.projectcalico.org/v3.13/manifests/calico.yaml" - when: ansible_hostname == k8s_first_master_node - - -- name: Generate join token - command: kubeadm token create --print-join-command - register: join_cmd - delegate_to: '{{ k8s_first_master_node }}' - -# Эта часть не работает -# Правильная команда выглядит так: -# kubeadm join 10.129.64.60:6443 --token --discovery-token-ca-cert-hash --control-plane --certificate-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' - diff --git a/contrib/ansible/roles/init-cluster/templates/kubeadm.conf.j2 b/contrib/ansible/roles/init-cluster/templates/kubeadm.conf.j2 deleted file mode 100644 index 84e3ddd..0000000 --- a/contrib/ansible/roles/init-cluster/templates/kubeadm.conf.j2 +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: kubelet.config.k8s.io/v1beta1 -kind: KubeletConfiguration -cgroupDriver: systemd ---- -apiVersion: kubeadm.k8s.io/v1beta2 -kind: InitConfiguration -nodeRegistration: ---- -apiVersion: kubeadm.k8s.io/v1beta2 -kind: ClusterConfiguration -kubernetesVersion: {{ k8s_version }} -certificatesDir: /etc/kubernetes/pki -clusterName: {{ k8s_cluster_name }} -controlPlaneEndpoint: {{ k8s_controlplane_address }} -dns: - type: CoreDNS -etcd: - local: - dataDir: /var/lib/etcd -imageRepository: k8s.gcr.io -networking: - dnsDomain: {{ k8s_domain }} - podSubnet: {{ k8s_pod_network }} - serviceSubnet: {{ k8s_service_network }} -scheduler: {} diff --git a/contrib/ansible/roles/keepalived/defaults/main.yml b/contrib/ansible/roles/keepalived/defaults/main.yml deleted file mode 100644 index ed97d53..0000000 --- a/contrib/ansible/roles/keepalived/defaults/main.yml +++ /dev/null @@ -1 +0,0 @@ ---- diff --git a/contrib/ansible/roles/keepalived/handlers/main.yml b/contrib/ansible/roles/keepalived/handlers/main.yml deleted file mode 100644 index 2ac9fe3..0000000 --- a/contrib/ansible/roles/keepalived/handlers/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -- name: restart keepalived - service: name=keepalived state=restarted diff --git a/contrib/ansible/roles/keepalived/tasks/main.yml b/contrib/ansible/roles/keepalived/tasks/main.yml deleted file mode 100644 index 4998ba8..0000000 --- a/contrib/ansible/roles/keepalived/tasks/main.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: Install keepalived - apt: - pkg: - - keepalived - state: latest - -- name: Configure keepalived - template: src=keepalived.conf.j2 dest=/etc/keepalived/keepalived.conf - tags: keepalived - notify: restart keepalived - -- name: Start keepalived - service: name=keepalived state=started diff --git a/contrib/ansible/roles/keepalived/templates/keepalived.conf.j2 b/contrib/ansible/roles/keepalived/templates/keepalived.conf.j2 deleted file mode 100644 index dd127ec..0000000 --- a/contrib/ansible/roles/keepalived/templates/keepalived.conf.j2 +++ /dev/null @@ -1,31 +0,0 @@ - ! Configuration File for keepalived -global_defs { -} - -{#vrrp_script haproxy-check { - script "killall -0 haproxy" - interval 2 - weight 20 -}#} - -vrrp_instance VI_1 { - state {{ keepalived_role }} - interface {{ keepalived_shared_iface }} - virtual_router_id {{ keepalived_router_id }} - {% if keepalived_role.lower() == "master" %} - priority {{ keepalived_priority }} - {% else %} - priority {{ keepalived_backup_priority }} - {% endif %} - advert_int 1 - authentication { - auth_type PASS - auth_pass {{ keepalived_auth_pass }} - } - virtual_ipaddress { - {{ keepalived_shared_ip }} dev {{ keepalived_shared_iface }} label {{ keepalived_shared_iface }}:0 - } -{# track_script { - haproxy-check weight 20 - }#} -} diff --git a/contrib/firstboot.sh b/contrib/firstboot.sh deleted file mode 100755 index cf20fe7..0000000 --- a/contrib/firstboot.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash -bootflag='/.manufactured' -groot='/opt/coins-demo' - -k8sDemoWA () { - printf "\033c" - echo 'Disabling swap space...' - sync && swapoff -a && sed -i '/ swap / s/^/#/' /etc/fstab - export KUBECONFIG=/root/.kube/config -} -k8sDeploy () { - echo 'Installing K8s...' - cd ${groot}/contrib/ansible && \ - ansible-playbook bootstrap-node.yml && \ - ansible-playbook init-cluster.yml - # Sometimes it's still not ready on this stage, let's check it just to be sure - while true ; do - echo "Waiting for node up..." - result=$(kubectl get nodes|awk '{print $2}'| tail -1| grep -nE '^Ready') - if [ -z "$result" ] ; then - break - fi - sleep 10 - done -} -InstallCSI () { - helm repo add rimusz https://charts.rimusz.net - helm install rimusz/hostpath-provisioner --generate-name -} -InstallRegistry () { - helm repo add harbor https://helm.goharbor.io - helm install registry harbor/harbor \ - --set expose.tls.enabled=false \ - --set expose.ingress.hosts.core="registry.k8s-demo.ix.gs" \ - --set expose.ingress.hosts.notary="notary.k8s-demo.ix.gs" \ - --set externalURL="http://registry.k8s-demo.ix.gs" \ - --set persistence.storageClass=hostpath \ - --set registry.credentials.username=k8s \ - --set registry.credentials.password=k8s \ - --set notary.enabled=false \ - --set trivy.enabled=false \ - --set clair.enabled=false \ - --set chartmuseum.enabled=false -} -InstallPGSQL () { - helm repo add bitnami https://charts.bitnami.com/bitnami - helm install db bitnami/postgresql \ - --set persistence.storageClass=hostpath \ - --set persistence.size=1Gi -} -InstallIngress () { - # Allow scheduling on our master node - kubectl taint nodes k8s-demo node-role.kubernetes.io/master- - # Installing Ingress - helm repo add nginx-stable https://helm.nginx.com/stable && \ - helm install nginx-stable/nginx-ingress --namespace kube-system --generate-name --set rbac.create=true - # Fix external IP for LB... - kubectl patch svc $(kubectl get svc -n kube-system|grep nginx-ingress|awk '{print $1}') -n kube-system --patch "$(cat ${groot}/contrib/ymls/ingress.fix.yaml)" -} -InstallApp () { - cd ${groot} - export WERF_INSECURE_REGISTRY=true - export WERF_IMAGES_REPO='http://registry.k8s-demo.ix.gs/todo' - werf build --stages-storage :local && \ - werf publish --stages-storage :local --tag-custom stable - werf deploy --stages-storage :local --tag-custom latest --env production --set 'DBPwd=$(kubectl get secret db-postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)' -} - -if [ ! -f ${bootflag} ]; then - touch ${bootflag} - k8sDemoWA; - k8sDeploy; - InstallCSI; - InstallIngress; - InstallRegistry; - InstallPGSQL; -fi diff --git a/contrib/k8s-seed.txt b/contrib/k8s-seed.txt deleted file mode 100644 index fb89498..0000000 --- a/contrib/k8s-seed.txt +++ /dev/null @@ -1,112 +0,0 @@ -### Keyboard config -d-i debian-installer/locale string en_US.UTF-8 -d-i keyboard-configuration/variant select American English -d-i keyboard-configuration/xkb-keymap select us -d-i keyboard-configuration/toggle select No toggling - -### Network configuration -d-i netcfg/choose_interface select auto -d-i netcfg/get_hostname string unassigned-hostname -d-i netcfg/get_domain string unassigned-domain -d-i netcfg/hostname string k8s-demo -d-i netcfg/wireless_wep string -d-i netcfg/dhcp_hostname string k8s-demo -d-i hw-detect/load_firmware boolean true - -### Mirror settings -d-i mirror/country string manual -d-i mirror/http/hostname string cdn.debian.net -d-i mirror/http/directory string /debian -d-i mirror/http/proxy string - -### Account setup -d-i passwd/root-login boolean false - -# To create a normal user account. -d-i passwd/user-fullname string K8S Admin -d-i passwd/username string k8s -d-i passwd/user-password password K8Sdemo -d-i passwd/user-password-again password K8Sdemo - -# The user account will be added to some standard initial groups. To -# override that, use this. -#d-i passwd/user-default-groups string docker - -### Clock and time zone setup -d-i clock-setup/utc boolean true -d-i time/zone string UTC -d-i clock-setup/ntp boolean true -d-i clock-setup/ntp-server string clock.ix.gs - -### Partitioning -d-i partman-auto/method string regular -d-i partman-lvm/device_remove_lvm boolean true -d-i partman-md/device_remove_md boolean true -d-i partman-lvm/confirm boolean true -d-i partman-lvm/confirm_nooverwrite boolean true -d-i partman-auto/choose_recipe select atomic -d-i partman-basicfilesystems/no_swap boolean true -d-i partman-partitioning/confirm_write_new_label boolean true -d-i partman/choose_partition select finish -d-i partman/confirm boolean true -d-i partman/confirm_nooverwrite boolean true -d-i partman/default_filesystem string xfs - -### Apt setup -d-i apt-setup/non-free boolean true -d-i apt-setup/contrib boolean true -d-i debian-installer/allow_unauthenticated boolean true - - -### Package selection -tasksel tasksel/first multiselect none, ssh-server, standard - -# Individual additional packages to install -d-i pkgsel/include string \ - apt-transport-https gnupg2 ca-certificates curl \ - iptables linux-headers-amd64 git ansible wget \ - gdebi-core - -# Whether to upgrade packages after debootstrap. -# Allowed values: none, safe-upgrade, full-upgrade -d-i pkgsel/upgrade select full-upgrade -popularity-contest popularity-contest/participate boolean true - -### Boot loader installation -d-i grub-installer/only_debian boolean true -d-i grub-installer/with_other_os boolean true -d-i grub-installer/bootdev string default - -### Finishing up the installation -d-i finish-install/reboot_in_progress note -d-i debian-installer/exit/poweroff boolean true - -#### Advanced options -### Running custom commands during the installation -d-i preseed/late_command string in-target /bin/sh -c " \ - echo k8s-demo > /etc/hostname ; \ - sed -i 's/debian/k8s-demo/' /etc/hosts ; \ - update-alternatives --set iptables /usr/sbin/iptables-legacy ; \ - update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy ; \ - update-alternatives --set arptables /usr/sbin/arptables-legacy ; \ - update-alternatives --set ebtables /usr/sbin/ebtables-legacy ; \ - ( systemctl disable nftables && systemctl mask nftables ) ; \ - curl -s https://download.docker.com/linux/debian/gpg | sudo apt-key add - ; \ - curl https://helm.baltorepo.com/organization/signing.asc | sudo apt-key add - ; \ - echo 'deb [arch=amd64] https://download.docker.com/linux/debian/ stretch stable' >/etc/apt/sources.list.d/docker.list ; \ - echo 'deb https://baltocdn.com/helm/stable/debian/ all main' > /etc/apt/sources.list.d/helm-stable-debian.list ; \ - apt-get update ; \ - apt-get install -y \ - docker-ce docker-ce-cli containerd.io helm ; \ - apt-get clean ; \ - curl -L https://dl.bintray.com/flant/werf/v1.1.19+fix10/werf-linux-amd64-v1.1.19+fix10 -o /usr/bin/werf ; \ - chmod +x /usr/bin/werf ; \ - git clone https://git.ix.gs/public/coins-demo.git /opt/coins-demo ; \ - cp /opt/coins-demo/contrib/mfg.service /etc/systemd/system/ ; \ - mkdir -p /lib/systemd/system/docker.service.d ; \ - sed -i 's/sock$/sock --insecure-registry="registry.k8s-demo.ix.gs"/' /lib/systemd/system/docker.service ; \ - systemctl daemon-reload ; \ - systemctl enable mfg.service ; \ - systemctl enable serial-getty@ttyS0.service ; \ - systemctl enable docker.service \ - || true" diff --git a/contrib/mfg.service b/contrib/mfg.service deleted file mode 100644 index 4aa829a..0000000 --- a/contrib/mfg.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Manufacturing service -After=serial-getty@ttyS0.service - -[Service] -Type=idle -ExecStart=/opt/coins-demo/contrib/firstboot.sh -StandardInput=tty -StandardOutput=tty -TTYPath=/dev/ttyS0 -TTYReset=yes -#TTYVHangup=yes -KillMode=none -TimeoutSec=0 -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target diff --git a/contrib/qemu-system-x86_64 b/contrib/qemu-system-x86_64 deleted file mode 100755 index 57598d3..0000000 Binary files a/contrib/qemu-system-x86_64 and /dev/null differ diff --git a/contrib/share/qemu/bios-256k.bin b/contrib/share/qemu/bios-256k.bin deleted file mode 100644 index fab9da2..0000000 Binary files a/contrib/share/qemu/bios-256k.bin and /dev/null differ diff --git a/contrib/share/qemu/efi-e1000.rom b/contrib/share/qemu/efi-e1000.rom deleted file mode 100644 index 776e217..0000000 Binary files a/contrib/share/qemu/efi-e1000.rom and /dev/null differ diff --git a/contrib/share/qemu/keymaps/common b/contrib/share/qemu/keymaps/common deleted file mode 100644 index adc56c7..0000000 --- a/contrib/share/qemu/keymaps/common +++ /dev/null @@ -1,157 +0,0 @@ -include modifiers - -# -# Top row -# -1 0x2 -2 0x3 -3 0x4 -4 0x5 -5 0x6 -6 0x7 -7 0x8 -8 0x9 -9 0xa -0 0xb -BackSpace 0xe - -# -# QWERTY first row -# -Tab 0xf localstate -ISO_Left_Tab 0xf shift -q 0x10 addupper -w 0x11 addupper -e 0x12 addupper -r 0x13 addupper -t 0x14 addupper -y 0x15 addupper -u 0x16 addupper -i 0x17 addupper -o 0x18 addupper -p 0x19 addupper - -# -# QWERTY second row -# -a 0x1e addupper -s 0x1f addupper -d 0x20 addupper -f 0x21 addupper -g 0x22 addupper -h 0x23 addupper -j 0x24 addupper -k 0x25 addupper -l 0x26 addupper -Return 0x1c localstate - -# -# QWERTY third row -# -z 0x2c addupper -x 0x2d addupper -c 0x2e addupper -v 0x2f addupper -b 0x30 addupper -n 0x31 addupper -m 0x32 addupper - -space 0x39 localstate - -less 0x56 -greater 0x56 shift -bar 0x56 altgr -brokenbar 0x56 shift altgr - -# -# Esc and Function keys -# -Escape 0x1 localstate -F1 0x3b localstate -F2 0x3c localstate -F3 0x3d localstate -F4 0x3e localstate -F5 0x3f localstate -F6 0x40 localstate -F7 0x41 localstate -F8 0x42 localstate -F9 0x43 localstate -F10 0x44 localstate -F11 0x57 localstate -F12 0x58 localstate - -# Printscreen, Scrollock and Pause -# Printscreen really requires four scancodes (0xe0, 0x2a, 0xe0, 0x37), -# but (0xe0, 0x37) seems to work. -Print 0xb7 localstate -Sys_Req 0xb7 localstate -Execute 0xb7 localstate -Scroll_Lock 0x46 - -# -# Insert - PgDown -# -Insert 0xd2 localstate -Delete 0xd3 localstate -Home 0xc7 localstate -End 0xcf localstate -Page_Up 0xc9 localstate -Page_Down 0xd1 localstate - -# -# Arrow keys -# -Left 0xcb localstate -Up 0xc8 localstate -Down 0xd0 localstate -Right 0xcd localstate - -# -# Numpad -# -Num_Lock 0x45 -KP_Divide 0xb5 -KP_Multiply 0x37 -KP_Subtract 0x4a -KP_Add 0x4e -KP_Enter 0x9c - -KP_Decimal 0x53 numlock -KP_Separator 0x53 numlock -KP_Delete 0x53 - -KP_0 0x52 numlock -KP_Insert 0x52 - -KP_1 0x4f numlock -KP_End 0x4f - -KP_2 0x50 numlock -KP_Down 0x50 - -KP_3 0x51 numlock -KP_Next 0x51 - -KP_4 0x4b numlock -KP_Left 0x4b - -KP_5 0x4c numlock -KP_Begin 0x4c - -KP_6 0x4d numlock -KP_Right 0x4d - -KP_7 0x47 numlock -KP_Home 0x47 - -KP_8 0x48 numlock -KP_Up 0x48 - -KP_9 0x49 numlock -KP_Prior 0x49 - -Caps_Lock 0x3a -# -# Inhibited keys -# -Multi_key 0x0 inhibit diff --git a/contrib/share/qemu/keymaps/en-us b/contrib/share/qemu/keymaps/en-us deleted file mode 100644 index f5784bb..0000000 --- a/contrib/share/qemu/keymaps/en-us +++ /dev/null @@ -1,35 +0,0 @@ -# generated from XKB map us -include common -map 0x409 -exclam 0x02 shift -at 0x03 shift -numbersign 0x04 shift -dollar 0x05 shift -percent 0x06 shift -asciicircum 0x07 shift -ampersand 0x08 shift -asterisk 0x09 shift -parenleft 0x0a shift -parenright 0x0b shift -minus 0x0c -underscore 0x0c shift -equal 0x0d -plus 0x0d shift -bracketleft 0x1a -braceleft 0x1a shift -bracketright 0x1b -braceright 0x1b shift -semicolon 0x27 -colon 0x27 shift -apostrophe 0x28 -quotedbl 0x28 shift -grave 0x29 -asciitilde 0x29 shift -backslash 0x2b -bar 0x2b shift -comma 0x33 -less 0x33 shift -period 0x34 -greater 0x34 shift -slash 0x35 -question 0x35 shift diff --git a/contrib/share/qemu/keymaps/modifiers b/contrib/share/qemu/keymaps/modifiers deleted file mode 100644 index d73b7a6..0000000 --- a/contrib/share/qemu/keymaps/modifiers +++ /dev/null @@ -1,18 +0,0 @@ -Shift_R 0x36 -Shift_L 0x2a - -Alt_R 0xb8 -Mode_switch 0xb8 -ISO_Level3_Shift 0xb8 -Alt_L 0x38 - -Control_R 0x9d -Control_L 0x1d - -# Translate Super to Windows keys. -# This is hardcoded. See documentation for details. -Super_R 0xdc -Super_L 0xdb - -# Translate Menu to the Windows Application key. -Menu 0xdd diff --git a/contrib/share/qemu/kvmvapic.bin b/contrib/share/qemu/kvmvapic.bin deleted file mode 100644 index 045f5c2..0000000 Binary files a/contrib/share/qemu/kvmvapic.bin and /dev/null differ diff --git a/contrib/share/qemu/linuxboot.bin b/contrib/share/qemu/linuxboot.bin deleted file mode 100644 index 130103f..0000000 Binary files a/contrib/share/qemu/linuxboot.bin and /dev/null differ diff --git a/contrib/share/qemu/vgabios-stdvga.bin b/contrib/share/qemu/vgabios-stdvga.bin deleted file mode 100644 index e5e5b14..0000000 Binary files a/contrib/share/qemu/vgabios-stdvga.bin and /dev/null differ diff --git a/contrib/ymls/ingress.fix.yaml b/contrib/ymls/ingress.fix.yaml deleted file mode 100644 index 691c3b1..0000000 --- a/contrib/ymls/ingress.fix.yaml +++ /dev/null @@ -1,4 +0,0 @@ -spec: - externalIPs: - - 100.100.100.15 - loadBalancerIP: 100.100.100.15 diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 64aef39..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -pipenv run python manage.py migrate todo -pipenv run python manage.py runserver 0.0.0.0:8888 diff --git a/local.py b/local.py deleted file mode 100644 index 7ef03cf..0000000 --- a/local.py +++ /dev/null @@ -1,26 +0,0 @@ -from .settings import * -import os - -DEBUG = True - -ALLOWED_HOSTS = ['*'] - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.environ['DATABASE_NAME'], - 'HOST': os.environ['DATABASE_HOST'], - 'USER': os.environ['DATABASE_USER'], - 'PASSWORD': os.environ['DATABASE_PASSWORD'], - 'PORT': '', - }, -} - -SECRET_KEY = os.environ['SECRET_KEY'] -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - -# TODO-specific settings -TODO_STAFF_ONLY = False -TODO_DEFAULT_LIST_SLUG = 'tickets' -TODO_DEFAULT_ASSIGNEE = None -TODO_PUBLIC_SUBMIT_REDIRECT = '/' diff --git a/runme.sh b/runme.sh deleted file mode 100755 index aa128fe..0000000 --- a/runme.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -appdir=$(cd `dirname $0` && pwd) -apptmp='/tmp' -vmsize='16G' -vmmem='4G' -vcpu=`cat /proc/cpuinfo|grep proc|tail -1|awk '{print $NF}'` -k8sdsk="${apptmp}/k8s-demo.raw" -httpboot='http://ftp.debian.org/debian/dists/stretch/main/installer-amd64/current/images/netboot/debian-installer/amd64' -seed='https://git.ix.gs/public/coins-demo/raw/branch/master/contrib/k8s-seed.txt' -qemu="${appdir}/contrib/qemu-system-x86_64 -L ${appdir}/contrib/share/qemu -net nic -net user,net=100.100.100.0/24,hostfwd=tcp::8888-:80 -m ${vmmem} -smp ${vcpu} -localtime -enable-kvm -cpu host,+nx -M pc -vga std -usbdevice tablet -k en-us -hda ${k8sdsk} -boot once=d -nographic" -wget='wget -cq4O' -### -installvm() { - echo "Allocating ${vmsize} disk..." && \ - fallocate -l ${vmsize} ${k8sdsk} && \ - echo "Downloading Linux kernel..." && \ - ${wget} ${apptmp}/kernel ${httpboot}/linux && \ - echo "Downloading Initial ramdisk..." && \ - ${wget} ${apptmp}/initrd ${httpboot}/initrd.gz && \ - echo "Preparing K8S VM..." && \ - ${qemu} -kernel ${apptmp}/kernel -initrd ${apptmp}/initrd -append "console=ttyS0,115200n8 apt-setup/proposed=true nomodeset fb=false priority=critical locale=en_US url=${seed}" && \ - echo "Housekeeping..." && \ - rm -f ${apptmp}/kernel ${apptmp}/initrd -} -runvm() { - echo "Look like we already has VM, let's go" - ${qemu} -} -### -if [ ! -f "${k8sdsk}" ]; then - installvm && runvm; -else - runvm; -fi diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..a708efe --- /dev/null +++ b/setup.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python + +from glob import glob +from os import remove +from os.path import abspath, dirname, join +from setuptools import setup, find_packages +from setuptools.command.test import test as TestCommand +from shlex import split +from shutil import rmtree +from sys import exit + +import todo as package + + +class Tox(TestCommand): + user_options = [('tox-args=', 'a', "Arguments to pass to tox")] + + def initialize_options(self): + TestCommand.initialize_options(self) + self.tox_args = None + + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + import tox + args = self.tox_args + if args: + args = split(self.tox_args) + errno = tox.cmdline(args=args) + exit(errno) + + +class Clean(TestCommand): + def run(self): + delete_in_root = [ + 'build', + 'dist', + '.eggs', + '*.egg-info', + '.tox', + ] + delete_everywhere = [ + '__pycache__', + '*.pyc', + ] + for candidate in delete_in_root: + rmtree_glob(candidate) + for visible_dir in glob('[A-Za-z0-9]*'): + for candidate in delete_everywhere: + rmtree_glob(join(visible_dir, candidate)) + rmtree_glob(join(visible_dir, '*', candidate)) + rmtree_glob(join(visible_dir, '*', '*', candidate)) + + +def rmtree_glob(file_glob): + for fobj in glob(file_glob): + try: + rmtree(fobj) + print('%s/ removed ...' % fobj) + except OSError: + try: + remove(fobj) + print('%s removed ...' % fobj) + except OSError: + pass + + +def read_file(*pathname): + with open(join(dirname(abspath(__file__)), *pathname)) as f: + return f.read() + + +setup( + name='django-todo', + version=package.__version__, + description=package.__doc__.strip(), + long_description=read_file('README.rst'), + author=package.__author__, + author_email=package.__email__, + url=package.__url__, + license=package.__license__, + packages=find_packages(), + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 3', + 'Topic :: Office/Business :: Groupware', + 'Topic :: Software Development :: Bug Tracking', + ], + include_package_data=True, + zip_safe=False, + tests_require=['tox'], + install_requires=['django-autoslug', 'unidecode', ], + cmdclass={ + 'clean': Clean, + 'test': Tox, + }, +) diff --git a/todo/__init__.py b/todo/__init__.py new file mode 100644 index 0000000..dd209ab --- /dev/null +++ b/todo/__init__.py @@ -0,0 +1,10 @@ +""" +A multi-user, multi-group task management and assignment system for Django. +""" +__version__ = '1.6.2' + +__author__ = 'Scot Hacker' +__email__ = 'shacker@birdhouse.org' + +__url__ = 'https://github.com/shacker/django-todo' +__license__ = 'BSD License' diff --git a/todo/admin.py b/todo/admin.py new file mode 100644 index 0000000..9e28467 --- /dev/null +++ b/todo/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from todo.models import Item, List, Comment + + +class ItemAdmin(admin.ModelAdmin): + list_display = ('title', 'list', 'priority', 'due_date') + list_filter = ('list',) + ordering = ('priority',) + search_fields = ('name',) + + +class CommentAdmin(admin.ModelAdmin): + list_display = ('author', 'date', 'snippet') + + +admin.site.register(List) +admin.site.register(Comment, CommentAdmin) +admin.site.register(Item, ItemAdmin) diff --git a/todo/forms.py b/todo/forms.py new file mode 100644 index 0000000..f7a015d --- /dev/null +++ b/todo/forms.py @@ -0,0 +1,81 @@ +from django import forms +from django.forms import ModelForm +from django.contrib.auth.models import Group +from todo.models import Item, List +from django.contrib.auth import get_user_model + + +class AddListForm(ModelForm): + # The picklist showing allowable groups to which a new list can be added + # determines which groups the user belongs to. This queries the form object + # to derive that list. + def __init__(self, user, *args, **kwargs): + super(AddListForm, self).__init__(*args, **kwargs) + self.fields['group'].queryset = Group.objects.filter(user=user) + + class Meta: + model = List + exclude = [] + + +class AddItemForm(ModelForm): + # The picklist showing the users to which a new task can be assigned + # must find other members of the groups the current list belongs to. + def __init__(self, task_list, *args, **kwargs): + super(AddItemForm, self).__init__(*args, **kwargs) + # print dir(self.fields['list']) + # print self.fields['list'].initial + self.fields['assigned_to'].queryset = get_user_model().objects.filter(groups__in=[task_list.group]) + self.fields['assigned_to'].label_from_instance = \ + lambda obj: "%s (%s)" % (obj.get_full_name(), obj.username) + + due_date = forms.DateField( + required=False, + widget=forms.DateTimeInput(attrs={'class': 'due_date_picker'}) + ) + + title = forms.CharField( + widget=forms.widgets.TextInput(attrs={'size': 35}) + ) + + note = forms.CharField(widget=forms.Textarea(), required=False) + + class Meta: + model = Item + exclude = [] + + +class EditItemForm(ModelForm): + # The picklist showing the users to which a new task can be assigned + # must find other members of the groups the current list belongs to. + def __init__(self, *args, **kwargs): + super(EditItemForm, self).__init__(*args, **kwargs) + self.fields['assigned_to'].queryset = get_user_model().objects.filter(groups__in=[self.instance.list.group]) + + class Meta: + model = Item + exclude = ('created_date', 'created_by',) + + +class AddExternalItemForm(ModelForm): + """Form to allow users who are not part of the GTD system to file a ticket.""" + + title = forms.CharField( + widget=forms.widgets.TextInput(attrs={'size': 35}) + ) + note = forms.CharField( + widget=forms.widgets.Textarea(), + help_text='Foo', + ) + + class Meta: + model = Item + exclude = ('list', 'created_date', 'due_date', 'created_by', 'assigned_to',) + + +class SearchForm(forms.Form): + """Search.""" + + q = forms.CharField( + widget=forms.widgets.TextInput(attrs={'size': 35}) + ) diff --git a/todo/migrations/0001_initial.py b/todo/migrations/0001_initial.py new file mode 100644 index 0000000..c0140e6 --- /dev/null +++ b/todo/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('date', models.DateTimeField(default=datetime.datetime.now)), + ('body', models.TextField(blank=True)), + ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=140)), + ('created_date', models.DateField(auto_now=True, auto_now_add=True)), + ('due_date', models.DateField(null=True, blank=True)), + ('completed', models.BooleanField(default=None)), + ('completed_date', models.DateField(null=True, blank=True)), + ('note', models.TextField(null=True, blank=True)), + ('priority', models.PositiveIntegerField(max_length=3)), + ('assigned_to', models.ForeignKey(related_name='todo_assigned_to', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(related_name='todo_created_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['priority'], + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='List', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=60)), + ('slug', models.SlugField(max_length=60, editable=False)), + ('group', models.ForeignKey(to='auth.Group')), + ], + options={ + 'ordering': ['name'], + 'verbose_name_plural': 'Lists', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='list', + unique_together=set([('group', 'slug')]), + ), + migrations.AddField( + model_name='item', + name='list', + field=models.ForeignKey(to='todo.List'), + preserve_default=True, + ), + migrations.AddField( + model_name='comment', + name='task', + field=models.ForeignKey(to='todo.Item'), + preserve_default=True, + ), + ] diff --git a/todo/migrations/0002_auto_20150614_2339.py b/todo/migrations/0002_auto_20150614_2339.py new file mode 100644 index 0000000..a346e88 --- /dev/null +++ b/todo/migrations/0002_auto_20150614_2339.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('todo', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='created_date', + field=models.DateField(auto_now=True), + ), + migrations.AlterField( + model_name='item', + name='priority', + field=models.PositiveIntegerField(), + ), + ] diff --git a/todo/migrations/0003_assignee_optional.py b/todo/migrations/0003_assignee_optional.py new file mode 100644 index 0000000..f5f2625 --- /dev/null +++ b/todo/migrations/0003_assignee_optional.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-04-09 11:11 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('todo', '0002_auto_20150614_2339'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='assigned_to', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='todo_assigned_to', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/todo/migrations/__init__.py b/todo/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/todo/models.py b/todo/models.py new file mode 100644 index 0000000..b7b67db --- /dev/null +++ b/todo/models.py @@ -0,0 +1,85 @@ +from __future__ import unicode_literals +import datetime + +from django.db import models +from django.contrib.auth.models import Group +from django.core.urlresolvers import reverse +from django.utils.encoding import python_2_unicode_compatible +from django.conf import settings +from autoslug import AutoSlugField + + +@python_2_unicode_compatible +class List(models.Model): + name = models.CharField(max_length=60) + slug = AutoSlugField(populate_from='name', editable=False, always_update=True) + group = models.ForeignKey(Group) + + def __str__(self): + return self.name + + def incomplete_tasks(self): + # Count all incomplete tasks on the current list instance + return Item.objects.filter(list=self, completed=0) + + class Meta: + ordering = ["name"] + verbose_name_plural = "Lists" + + # Prevents (at the database level) creation of two lists with the same name in the same group + unique_together = ("group", "slug") + + +@python_2_unicode_compatible +class Item(models.Model): + title = models.CharField(max_length=140) + list = models.ForeignKey(List) + created_date = models.DateField(auto_now=True) + due_date = models.DateField(blank=True, null=True, ) + completed = models.BooleanField(default=None) + completed_date = models.DateField(blank=True, null=True) + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='todo_created_by') + assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='todo_assigned_to') + note = models.TextField(blank=True, null=True) + priority = models.PositiveIntegerField() + + # Has due date for an instance of this object passed? + def overdue_status(self): + "Returns whether the item's due date has passed or not." + if self.due_date and datetime.date.today() > self.due_date: + return 1 + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse('todo-task_detail', kwargs={'task_id': self.id, }) + + # Auto-set the item creation / completed date + def save(self): + # If Item is being marked complete, set the completed_date + if self.completed: + self.completed_date = datetime.datetime.now() + super(Item, self).save() + + class Meta: + ordering = ["priority"] + + +@python_2_unicode_compatible +class Comment(models.Model): + """ + Not using Django's built-in comments because we want to be able to save + a comment and change task details at the same time. Rolling our own since it's easy. + """ + author = models.ForeignKey(settings.AUTH_USER_MODEL) + task = models.ForeignKey(Item) + date = models.DateTimeField(default=datetime.datetime.now) + body = models.TextField(blank=True) + + def snippet(self): + # Define here rather than in __str__ so we can use it in the admin list_display + return "{author} - {snippet}...".format(author=self.author, snippet=self.body[:35]) + + def __str__(self): + return self.snippet diff --git a/todo/settings.py b/todo/settings.py new file mode 100644 index 0000000..e9231b5 --- /dev/null +++ b/todo/settings.py @@ -0,0 +1,7 @@ +from django.conf import settings + + +STAFF_ONLY = getattr(settings, 'TODO_STAFF_ONLY', False) +DEFAULT_LIST_ID = getattr(settings, 'TODO_DEFAULT_LIST_ID', 1) +DEFAULT_ASSIGNEE = getattr(settings, 'TODO_DEFAULT_ASSIGNEE', None) +PUBLIC_SUBMIT_REDIRECT = getattr(settings, 'TODO_PUBLIC_SUBMIT_REDIRECT', '/') diff --git a/todo/static/todo/css/styles.css b/todo/static/todo/css/styles.css new file mode 100644 index 0000000..a20c4b8 --- /dev/null +++ b/todo/static/todo/css/styles.css @@ -0,0 +1,66 @@ +/*Distributed*/ + +ul.messages li { + color: green; + font-weight: bold; +} + +.overdue { + color: #9A2441; + font-weight: bold; +} + +/* Lighter font for completed items */ +#completed li { + color: gray; +} + +a.todo { + text-decoration: none; + color: #474747; +} + +a.showlink { + text-decoration: underline; +} + + +label { + display: block; + font-weight: bold; +} + +input { + color: #3A3A3A; + font-family: Verdana; + font-size: 14px; +} + +input[type='text'] { + width: 300px; +} + +input#id_priority { + width: 30px; +} + + +.todo-break { + margin-top: 30px; + border-top: 1px dotted gray; +} + +table.nocolor, table.nocolor tr, table.nocolor td { + background-color: transparent; +} + + +.minor { + font-style: italic; + font-size: 0.8em; +} + +.task_note, .task_comments { + width: 70%; + overflow: visible; +} diff --git a/todo/static/todo/css/ui.datepicker.css b/todo/static/todo/css/ui.datepicker.css new file mode 100644 index 0000000..49b00c0 --- /dev/null +++ b/todo/static/todo/css/ui.datepicker.css @@ -0,0 +1,213 @@ +/* Main Style Sheet for jQuery UI date picker */ +#ui-datepicker-div, .ui-datepicker-inline { + font-family: Arial, Helvetica, sans-serif; + font-size: 14px; + padding: 0; + margin: 0; + background: #ddd; + width: 185px; +} +#ui-datepicker-div { + display: none; + border: 1px solid #777; + z-index: 100; /*must have*/ +} +.ui-datepicker-inline { + float: left; + display: block; + border: 0; +} +.ui-datepicker-rtl { + direction: rtl; +} +.ui-datepicker-dialog { + padding: 5px !important; + border: 4px ridge #ddd !important; +} +.ui-datepicker-disabled { + position: absolute; + z-index: 100; + background-color: white; + opacity: 0.5; +} +button.ui-datepicker-trigger { + width: 25px; +} +img.ui-datepicker-trigger { + margin: 2px; + vertical-align: middle; +} +.ui-datepicker-prompt { + float: left; + padding: 2px; + background: #ddd; + color: #000; +} +* html .ui-datepicker-prompt { + width: 185px; +} +.ui-datepicker-control, .ui-datepicker-links, .ui-datepicker-header, .ui-datepicker { + clear: both; + float: left; + width: 100%; + color: #fff; +} +.ui-datepicker-control { + background: #400; + padding: 2px 0px; +} +.ui-datepicker-links { + background: #000; + padding: 2px 0px; +} +.ui-datepicker-control, .ui-datepicker-links { + font-weight: bold; + font-size: 80%; +} +.ui-datepicker-links label { /* disabled links */ + padding: 2px 5px; + color: #888; +} +.ui-datepicker-clear, .ui-datepicker-prev { + float: left; + width: 34%; +} +.ui-datepicker-rtl .ui-datepicker-clear, .ui-datepicker-rtl .ui-datepicker-prev { + float: right; + text-align: right; +} +.ui-datepicker-current { + float: left; + width: 30%; + text-align: center; +} +.ui-datepicker-close, .ui-datepicker-next { + float: right; + width: 34%; + text-align: right; +} +.ui-datepicker-rtl .ui-datepicker-close, .ui-datepicker-rtl .ui-datepicker-next { + float: left; + text-align: left; +} +.ui-datepicker-header { + padding: 1px 0 3px; + background: #333; + text-align: center; + font-weight: bold; + height: 1.3em; +} +.ui-datepicker-header select { + background: #333; + color: #fff; + border: 0px; + font-weight: bold; +} +.ui-datepicker { + background: #ccc; + text-align: center; + font-size: 100%; +} +.ui-datepicker a { + display: block; + width: 100%; +} +.ui-datepicker-title-row { + background: #777; +} +.ui-datepicker-days-row { + background: #eee; + color: #666; +} +.ui-datepicker-week-col { + background: #777; + color: #fff; +} +.ui-datepicker-days-cell { + color: #000; + border: 1px solid #ddd; +} +.ui-datepicker-days-cell a{ + display: block; +} +.ui-datepicker-week-end-cell { + background: #ddd; +} +.ui-datepicker-title-row .ui-datepicker-week-end-cell { + background: #777; +} +.ui-datepicker-days-cell-over { + background: #fff; + border: 1px solid #777; +} +.ui-datepicker-unselectable { + color: #888; +} +.ui-datepicker-today { + background: #fcc !important; +} +.ui-datepicker-current-day { + background: #999 !important; +} +.ui-datepicker-status { + background: #ddd; + width: 100%; + font-size: 80%; + text-align: center; +} + +/* ________ Datepicker Links _______ + +** Reset link properties and then override them with !important */ +#ui-datepicker-div a, .ui-datepicker-inline a { + cursor: pointer; + margin: 0; + padding: 0; + background: none; + color: #000; +} +.ui-datepicker-inline .ui-datepicker-links a { + padding: 0 5px !important; +} +.ui-datepicker-control a, .ui-datepicker-links a { + padding: 2px 5px !important; + color: #eee !important; +} +.ui-datepicker-title-row a { + color: #eee !important; +} +.ui-datepicker-control a:hover { + background: #fdd !important; + color: #333 !important; +} +.ui-datepicker-links a:hover, .ui-datepicker-title-row a:hover { + background: #ddd !important; + color: #333 !important; +} + +/* ___________ MULTIPLE MONTHS _________*/ + +.ui-datepicker-multi .ui-datepicker { + border: 1px solid #777; +} +.ui-datepicker-one-month { + float: left; + width: 185px; +} +.ui-datepicker-new-row { + clear: left; +} + +/* ___________ IE6 IFRAME FIX ________ */ + +.ui-datepicker-cover { + display: none; /*sorry for IE5*/ + display/**/: block; /*sorry for IE5*/ + position: absolute; /*must have*/ + z-index: -1; /*must have*/ + filter: mask(); /*must have*/ + top: -4px; /*must have*/ + left: -4px; /*must have*/ + width: 200px; /*must have*/ + height: 200px; /*must have*/ +} diff --git a/todo/static/todo/js/jquery.tablednd_0_5.js b/todo/static/todo/js/jquery.tablednd_0_5.js new file mode 100644 index 0000000..d4c9fec --- /dev/null +++ b/todo/static/todo/js/jquery.tablednd_0_5.js @@ -0,0 +1,382 @@ +/** + * TableDnD plug-in for JQuery, allows you to drag and drop table rows + * You can set up various options to control how the system will work + * Copyright (c) Denis Howlett + * 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 + * []=&[]= 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 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 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 + } +); \ No newline at end of file diff --git a/todo/templates/todo/add_external_task.html b/todo/templates/todo/add_external_task.html new file mode 100644 index 0000000..a18d56f --- /dev/null +++ b/todo/templates/todo/add_external_task.html @@ -0,0 +1,59 @@ +{% extends "todo/base.html" %} +{% block page_heading %}{% endblock %} +{% block title %}File Ticket{% endblock %} + +{% block content %} + +

{{ task }}

+ +
+ {% csrf_token %} + + {% if task.note %} +
Note: {{ task.note|safe|urlize|linebreaks }}
+ {% endif %} + +
+

File Trouble Ticket

+

Trouble with a computer or other technical system at the J-School?
+ Use this form to report the difficulty - we'll get right back to you.

+ + {% if form.errors %} + + {% for error in form.errors %} +
    +
  • The {{ error|escape }} field is required.
  • +
+ {% endfor %} +
+ + {% endif %} + + + + + + + + + + + + + + + + +
Summary:{{ form.title }}
+ Include the workstation number in your summary, e.g.
+ "Radio Lab # 4: Purple smoke pouring out the back." +
Note:{{ form.note }}
+ Please describe the problem. +
Priority:{{ form.priority }}
+ Enter a number between 1 and 5,
+ where 5 is highest ("Computer is on fire = True"). +
+

+
+
+{% endblock %} diff --git a/todo/templates/todo/add_list.html b/todo/templates/todo/add_list.html new file mode 100644 index 0000000..6fcb22a --- /dev/null +++ b/todo/templates/todo/add_list.html @@ -0,0 +1,16 @@ +{% extends "todo/base.html" %} + +{% block page_heading %}{% endblock %} +{% block title %}Add Todo List{% endblock %} + +{% block content %} + +

Add a list:

+ +
+ {% csrf_token %} + {{ form }}
+

+
+ +{% endblock %} diff --git a/todo/templates/todo/base.html b/todo/templates/todo/base.html new file mode 100644 index 0000000..3e128f6 --- /dev/null +++ b/todo/templates/todo/base.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block extrahead %} + + + + +{% endblock extrahead %} diff --git a/todo/templates/todo/del_list.html b/todo/templates/todo/del_list.html new file mode 100644 index 0000000..78c429c --- /dev/null +++ b/todo/templates/todo/del_list.html @@ -0,0 +1,32 @@ +{% extends "todo/base.html" %} + +{% block title %}{{ list_title }} to-do items{% endblock %} + +{% block content %} + + {% if user.is_staff %} +

Delete entire list: {{ list.name }} ?

+ +

Category tally:

+ +
    +
  • Incomplete: {{ item_count_undone }}
  • +
  • Complete: {{ item_count_done }}
  • +
  • Total: {{ item_count_total }}
  • +
+ +

... all of which will be irretrievably blown away. Are you sure you want to do that?

+ +
+ {% csrf_token %} + +

+
+ + Return to list: {{ list.name }} + + {% else %} +

Sorry, you don't have permission to delete lists. Please contact your group administrator.

+ {% endif %} + +{% endblock %} diff --git a/todo/templates/todo/email/assigned_body.txt b/todo/templates/todo/email/assigned_body.txt new file mode 100644 index 0000000..51d143d --- /dev/null +++ b/todo/templates/todo/email/assigned_body.txt @@ -0,0 +1,20 @@ +Dear {{ task.assigned_to.first_name }} - + +A new task on the list {{ task.list.name }} has been assigned to you by {{ task.created_by.first_name }} {{ task.created_by.last_name }}: + +{{ task.title }} + +{% if task.note %} +{% autoescape off %} +Note: {{ task.note }} +{% endautoescape %} +{% endif %} + + + + +Task details/comments: +http://{{ site }}{% url 'todo-task_detail' task.id %} + +List {{ task.list.name }}: +http://{{ site }}{% url 'todo-incomplete_tasks' task.list.id task.list.slug %} diff --git a/todo/templates/todo/email/assigned_subject.txt b/todo/templates/todo/email/assigned_subject.txt new file mode 100644 index 0000000..b6cc9db --- /dev/null +++ b/todo/templates/todo/email/assigned_subject.txt @@ -0,0 +1 @@ +GTD: New task - {% autoescape off %}Note: {{ task.title }}{% endautoescape %} \ No newline at end of file diff --git a/todo/templates/todo/email/newcomment_body.txt b/todo/templates/todo/email/newcomment_body.txt new file mode 100644 index 0000000..2a1df3c --- /dev/null +++ b/todo/templates/todo/email/newcomment_body.txt @@ -0,0 +1,16 @@ +A new task comment has been added. + +Task: {{ task.title }} +Commenter: {{ user.first_name }} {{ user.last_name }} + +Comment: +{% autoescape off %} +{{ body }} +{% endautoescape %} + +Task details/comments: +https://{{ site }}{% url 'todo-task_detail' task.id %} + +List {{ task.list.name }}: +https://{{ site }}{% url 'todo-incomplete_tasks' task.list.id task.list.slug %} + diff --git a/todo/templates/todo/list_lists.html b/todo/templates/todo/list_lists.html new file mode 100644 index 0000000..459e842 --- /dev/null +++ b/todo/templates/todo/list_lists.html @@ -0,0 +1,24 @@ +{% extends "todo/base.html" %} + +{% block title %}{{ list_title }} Todo Lists{% endblock %} + +{% block content %} + +

Todo Lists

+ +

{{ item_count }} items in {{ list_count }} lists

+ + {% regroup list_list by group as section_list %} + + {% for group in section_list %} +

{{ group.grouper }}

+
    + {% for item in group.list %} +
  • {{ item.name }} ({{ item.incomplete_tasks.count }}/{{ item.item_set.count }})
  • + {% endfor %} +
+ {% endfor %} + +

Create new todo list

+ +{% endblock %} diff --git a/todo/templates/todo/search_results.html b/todo/templates/todo/search_results.html new file mode 100644 index 0000000..c1fdbe4 --- /dev/null +++ b/todo/templates/todo/search_results.html @@ -0,0 +1,26 @@ +{% extends "todo/base.html" %} + +{% block title %}Search results{% endblock %} + +{% block content_title %} +

Search

+{% endblock %} + +{% block content %} + {% if found_items %} +

{{found_items.count}} search results for term: "{{ query_string }}"

+
+ {% for f in found_items %} +

{{ f.title }}
+ + On list: {{ f.list.name }}
+ Assigned to: {% if f.assigned_to %}{{ f.assigned_to }}{% else %}Anyone{% endif %} (created by: {{ f.created_by }})
+ Complete: {{ f.completed|yesno:"Yes,No" }} +
+

+ {% endfor %} +
+ {% else %} +

No results to show, sorry.

+ {% endif %} +{% endblock %} diff --git a/todo/templates/todo/view_list.html b/todo/templates/todo/view_list.html new file mode 100644 index 0000000..1a27d38 --- /dev/null +++ b/todo/templates/todo/view_list.html @@ -0,0 +1,166 @@ +{% extends "todo/base.html" %} + +{% block title %}Todo List: {{ list.name }}{% endblock %} + +{% block content %} + + + {% if list_slug == "mine" %} +

Tasks assigned to {{ request.user }}

+ {% elif auth_ok %} +

Tasks filed under "{{ list.name }}"

+

This list belongs to group {{ list.group }}

+ {% endif %} + + {% if auth_ok %} +
+ {% csrf_token %} + + {# Only show task adder if viewing a proper list #} + {% if list_slug != "mine" %} +

→ Click to add task ←

+ +
+ + + + + + + + + + + + + + +
{{ form.title.errors }}{{ form.due_date.errors }}
{{ form.title }} {{ form.due_date }} {{ form.assigned_to }}
+ {{ form.note }} +

*Email notifications will only be sent if task is assigned to someone besides yourself.

+
+ + + + + +

+
+ {% endif %} + + {% if not view_completed %} + +

Incomplete tasks :: Drag rows to set priorities

+ + + + + + + + + + + + {% if list_slug == "mine" %} + + {% endif %} + + + {% for task in task_list %} + + + + + + + + + + {% if list_slug == "mine" %} + + {% endif %} + + + {% endfor %} +
DoneTaskCreatedDue onOwnerAssignedNoteCommListDel
{{ task.title|truncatewords:20 }}{{ task.created_date|date:"m/d/Y" }} + {% if task.overdue_status %}{% endif %} + {{ task.due_date|date:"m/d/Y" }} + {% if task.overdue_status %}{% endif %} + {{ task.created_by }}{% if task.assigned_to %}{{ task.assigned_to }}{% else %}Anyone{% endif %}{% if task.note %}≈{% endif %} {% if task.comment_set.all.count != 0 %}{{ task.comment_set.all.count }}{% endif %}{{ task.list }}
+ +

+

View completed tasks

+ + {% else %} + +

Completed tasks

+ + + + + + + + + + {% if list_slug == "mine" %} + + {% endif %} + + + + {% for task in completed_list %} + + + + + + + + + {% endfor %} + +
UndoTaskCreatedCompleted onNoteCommListDel
{{ task.title|truncatewords:20 }}{{ task.created_date|date:"m/d/Y" }}{{ task.completed_date|date:"m/d/Y" }}{% if task.note %}≈{% endif %} {% if task.comment_set.all.count != 0 %}{{ task.comment_set.all.count }}{% endif %} +
+

+
+

View incomplete tasks

+ {% endif %} + + {% if user.is_staff %} + {% if list_slug != "mine" %} +

Delete this list

+ {% endif %} + {% endif %} + + {% endif %} +{% endblock %} diff --git a/todo/templates/todo/view_task.html b/todo/templates/todo/view_task.html new file mode 100644 index 0000000..aec27ad --- /dev/null +++ b/todo/templates/todo/view_task.html @@ -0,0 +1,103 @@ +{% extends "todo/base.html" %} + +{% block title %}Task: {{ task.title }}{% endblock %} + +{% block content %} + + + + {% if auth_ok %} + +

{{ task }}

+ +
+ {% csrf_token %} + +

→ Click to edit details ←

+ +

+ In list: {{ task.list }}
+ Assigned to: {% if task.assigned_to %}{{ task.assigned_to.get_full_name }}{% else %}Anyone{% endif %}
+ Created by: {{ task.created_by.first_name }} {{ task.created_by.last_name }}
+ Due date: {{ task.due_date }}
+ Completed: {{ form.completed }}
+

+ + {% if task.note %} +
Note: {{ task.note|safe|urlize|linebreaks }}
+ {% endif %} + +
+

Edit Task

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Title:{{ form.title }}
List:{{ form.list }}
Due:{{ form.due_date }}
Assigned to:{{ form.assigned_to }}
Note:{{ form.note }}
Priority:{{ form.priority }}
+

+
+ +
+ +

Add comment

+ +

+ +
+ +

Comments on this task

+ +
+ {% for comment in comment_list %} +

+ {{ comment.author.first_name }} {{ comment.author.last_name }}, + {{ comment.date|date:"F d Y P" }} + +

+ {{ comment.body|safe|urlize|linebreaks }} + {% empty %} +

No Comments

+ {% endfor %} +
+ + {% endif %} + +{% endblock %} diff --git a/todo/urls.py b/todo/urls.py new file mode 100644 index 0000000..ff52363 --- /dev/null +++ b/todo/urls.py @@ -0,0 +1,23 @@ +from django.conf.urls import url +from todo import views + +urlpatterns = [ + url(r'^$', views.list_lists, name="todo-lists"), + url(r'^mine/$', views.view_list, {'list_slug': 'mine'}, name="todo-mine"), + url(r'^(?P\d{1,4})/(?P[\w-]+)/delete$', views.del_list, name="todo-del_list"), + url(r'^task/(?P\d{1,6})$', views.view_task, name='todo-task_detail'), + url(r'^(?P\d{1,4})/(?P[\w-]+)$', views.view_list, name='todo-incomplete_tasks'), + url(r'^(?P\d{1,4})/(?P[\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"), +] diff --git a/todo/utils.py b/todo/utils.py new file mode 100644 index 0000000..cb6db3a --- /dev/null +++ b/todo/utils.py @@ -0,0 +1,51 @@ +import datetime +from django.contrib import messages +from django.template.loader import render_to_string +from django.contrib.sites.models import Site +from django.core.mail import send_mail + +from todo.models import Item + +# Need for links in email templates +current_site = Site.objects.get_current() + + +def mark_done(request, done_items): + # Check for items in the mark_done POST array. If present, change status to complete. + for item in done_items: + i = Item.objects.get(id=item) + i.completed = True + i.completed_date = datetime.datetime.now() + i.save() + messages.success(request, "Item \"{i}\" marked complete.".format(i=i.title)) + + +def undo_completed_task(request, undone_items): + # Undo: Set completed items back to incomplete + for item in undone_items: + i = Item.objects.get(id=item) + i.completed = False + i.save() + messages.success(request, "Previously completed task \"{i}\" marked incomplete.".format(i=i.title)) + + +def del_tasks(request, deleted_items): + # Delete selected items + for item_id in deleted_items: + i = Item.objects.get(id=item_id) + messages.success(request, "Item \"{i}\" deleted.".format(i=i.title)) + i.delete() + + +def send_notify_mail(request, new_task): + # Send email + email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': new_task}) + email_body = render_to_string( + "todo/email/assigned_body.txt", + {'task': new_task, 'site': current_site, }) + try: + send_mail( + email_subject, email_body, new_task.created_by.email, + [new_task.assigned_to.email], fail_silently=False) + except: + messages.error(request, "Task saved but mail not sent. Contact your administrator.") diff --git a/todo/views.py b/todo/views.py new file mode 100644 index 0000000..c408ad8 --- /dev/null +++ b/todo/views.py @@ -0,0 +1,351 @@ +import datetime + +from django.contrib.auth.decorators import user_passes_test, login_required +from django.contrib.auth.models import User +from django.contrib import messages +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.db import IntegrityError +from django.db.models import Q +from django.http import HttpResponseRedirect, HttpResponse +from django.shortcuts import get_object_or_404, render +from django.template import RequestContext +from django.template.loader import render_to_string +from django.views.decorators.csrf import csrf_exempt +from django.contrib.sites.models import Site + +from todo import settings +from todo.forms import AddListForm, AddItemForm, EditItemForm, AddExternalItemForm, SearchForm +from todo.models import Item, List, Comment +from todo.utils import mark_done, undo_completed_task, del_tasks, send_notify_mail + +# Need for links in email templates +current_site = Site.objects.get_current() + + +def check_user_allowed(user): + """ + Conditions for user_passes_test decorator. + """ + if settings.STAFF_ONLY: + return user.is_authenticated() and user.is_staff + else: + return user.is_authenticated() + + +@user_passes_test(check_user_allowed) +def list_lists(request): + """ + Homepage view - list of lists a user can view, and ability to add a list. + """ + thedate = datetime.datetime.now() + searchform = SearchForm(auto_id=False) + + # Make sure user belongs to at least one group. + if request.user.groups.all().count() == 0: + messages.error(request, "You do not yet belong to any groups. Ask your administrator to add you to one.") + + # Superusers see all lists + if request.user.is_superuser: + list_list = List.objects.all().order_by('group', 'name') + else: + list_list = List.objects.filter(group__in=request.user.groups.all()).order_by('group', 'name') + + list_count = list_list.count() + + # superusers see all lists, so count shouldn't filter by just lists the admin belongs to + if request.user.is_superuser: + item_count = Item.objects.filter(completed=0).count() + else: + item_count = Item.objects.filter(completed=0).filter(list__group__in=request.user.groups.all()).count() + + return render(request, 'todo/list_lists.html', locals()) + + +@user_passes_test(check_user_allowed) +def del_list(request, list_id, list_slug): + """ + Delete an entire list. Danger Will Robinson! Only staff members should be allowed to access this view. + """ + list = get_object_or_404(List, slug=list_slug) + + if request.method == 'POST': + List.objects.get(id=list.id).delete() + messages.success(request, "{list_name} is gone.".format(list_name=list.name)) + return HttpResponseRedirect(reverse('todo-lists')) + else: + item_count_done = Item.objects.filter(list=list.id, completed=1).count() + item_count_undone = Item.objects.filter(list=list.id, completed=0).count() + item_count_total = Item.objects.filter(list=list.id).count() + + return render(request, 'todo/del_list.html', locals()) + + +@user_passes_test(check_user_allowed) +def view_list(request, list_id=0, list_slug=None, view_completed=False): + """ + Display and manage items in a list. + """ + + # Make sure the accessing user has permission to view this list. + # Always authorize the "mine" view. Admins can view/edit all lists. + if list_slug == "mine" or list_slug == "recent-add" or list_slug == "recent-complete": + auth_ok = True + else: + list = get_object_or_404(List, id=list_id) + if list.group in request.user.groups.all() or request.user.is_staff or list_slug == "mine": + auth_ok = True + else: # User does not belong to the group this list is attached to + messages.error(request, "You do not have permission to view/edit this list.") + + # Process all possible list interactions on each submit + mark_done(request, request.POST.getlist('mark_done')) + del_tasks(request, request.POST.getlist('del_tasks')) + undo_completed_task(request, request.POST.getlist('undo_completed_task')) + + thedate = datetime.datetime.now() + created_date = "%s-%s-%s" % (thedate.year, thedate.month, thedate.day) + + # Get set of items with this list ID, or filter on items assigned to me, or recently added/completed + if list_slug == "mine": + task_list = Item.objects.filter(assigned_to=request.user, completed=False) + completed_list = Item.objects.filter(assigned_to=request.user, completed=True) + + elif list_slug == "recent-add": + # Only show items in lists that are in groups that the current user is also in. + # Assume this only includes uncompleted items. + task_list = Item.objects.filter( + list__group__in=(request.user.groups.all()), + completed=False).order_by('-created_date')[:50] + + elif list_slug == "recent-complete": + # Only show items in lists that are in groups that the current user is also in. + task_list = Item.objects.filter( + list__group__in=request.user.groups.all(), + completed=True).order_by('-completed_date')[:50] + + else: + task_list = Item.objects.filter(list=list.id, completed=0) + completed_list = Item.objects.filter(list=list.id, completed=1) + + if request.POST.getlist('add_task'): + form = AddItemForm(list, request.POST, initial={ + 'assigned_to': request.user.id, + 'priority': 999, + }) + + if form.is_valid(): + new_task = form.save() + + # Send email alert only if Notify checkbox is checked AND assignee is not same as the submitter + if "notify" in request.POST and new_task.assigned_to != request.user: + send_notify_mail(request, new_task) + + messages.success(request, "New task \"{t}\" has been added.".format(t=new_task.title)) + return HttpResponseRedirect(request.path) + else: + # Don't allow adding new tasks on some views + if list_slug != "mine" and list_slug != "recent-add" and list_slug != "recent-complete": + form = AddItemForm(list, initial={ + 'assigned_to': request.user.id, + 'priority': 999, + }) + + return render(request, 'todo/view_list.html', locals()) + + +@user_passes_test(check_user_allowed) +def view_task(request, task_id): + """ + View task details. Allow task details to be edited. + """ + task = get_object_or_404(Item, pk=task_id) + comment_list = Comment.objects.filter(task=task_id) + + # Ensure user has permission to view item. + # Get the group this task belongs to, and check whether current user is a member of that group. + # Admins can edit all tasks. + + if task.list.group in request.user.groups.all() or request.user.is_staff: + auth_ok = True + + if request.POST: + form = EditItemForm(request.POST, instance=task) + + if form.is_valid(): + form.save() + + # Also save submitted comment, if non-empty + if request.POST['comment-body']: + c = Comment( + author=request.user, + task=task, + body=request.POST['comment-body'], + ) + c.save() + + # And email comment to all people who have participated in this thread. + email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': task}) + email_body = render_to_string( + "todo/email/newcomment_body.txt", + {'task': task, 'body': request.POST['comment-body'], 'site': current_site, 'user': request.user} + ) + + # Get list of all thread participants - task creator plus everyone who has commented on it. + recip_list = [] + recip_list.append(task.created_by.email) + commenters = Comment.objects.filter(task=task) + for c in commenters: + recip_list.append(c.author.email) + recip_list = set(recip_list) # Eliminate duplicates + + try: + send_mail(email_subject, email_body, task.created_by.email, recip_list, fail_silently=False) + messages.success(request, "Comment sent to thread participants.") + except: + messages.error(request, "Comment saved but mail not sent. Contact your administrator.") + + messages.success(request, "The task has been edited.") + + return HttpResponseRedirect(reverse('todo-incomplete_tasks', args=[task.list.id, task.list.slug])) + else: + form = EditItemForm(instance=task) + if task.due_date: + thedate = task.due_date + else: + thedate = datetime.datetime.now() + else: + messages.info(request, "You do not have permission to view/edit this task.") + + return render(request, 'todo/view_task.html', locals()) + + +@csrf_exempt +@user_passes_test(check_user_allowed) +def reorder_tasks(request): + """ + Handle task re-ordering (priorities) from JQuery drag/drop in view_list.html + """ + newtasklist = request.POST.getlist('tasktable[]') + # First item in received list is always empty - remove it + del newtasklist[0] + + # Re-prioritize each item in list + i = 1 + for t in newtasklist: + newitem = Item.objects.get(pk=t) + newitem.priority = i + newitem.save() + i += 1 + + # All views must return an httpresponse of some kind ... without this we get + # error 500s in the log even though things look peachy in the browser. + return HttpResponse(status=201) + + +@login_required +def external_add(request): + """ + Allow users who don't have access to the rest of the ticket system to file a ticket in a specific list. + Public tickets are unassigned unless settings.DEFAULT_ASSIGNEE exists. + """ + if request.POST: + form = AddExternalItemForm(request.POST) + + if form.is_valid(): + item = form.save(commit=False) + item.list_id = settings.DEFAULT_LIST_ID + item.created_by = request.user + if settings.DEFAULT_ASSIGNEE: + item.assigned_to = User.objects.get(username=settings.DEFAULT_ASSIGNEE) + item.save() + + email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': item.title}) + email_body = render_to_string("todo/email/assigned_body.txt", {'task': item, 'site': current_site, }) + try: + send_mail( + email_subject, email_body, item.created_by.email, [item.assigned_to.email, ], fail_silently=False) + except: + messages.error(request, "Task saved but mail not sent. Contact your administrator.") + + messages.success(request, "Your trouble ticket has been submitted. We'll get back to you soon.") + + return HttpResponseRedirect(settings.PUBLIC_SUBMIT_REDIRECT) + else: + form = AddExternalItemForm() + + return render(request, 'todo/add_external_task.html', locals()) + + +@user_passes_test(check_user_allowed) +def add_list(request): + """ + Allow users to add a new todo list to the group they're in. + """ + if request.POST: + form = AddListForm(request.user, request.POST) + if form.is_valid(): + try: + form.save() + messages.success(request, "A new list has been added.") + return HttpResponseRedirect(request.path) + except IntegrityError: + messages.error( + request, + "There was a problem saving the new list. " + "Most likely a list with the same name in the same group already exists.") + else: + if request.user.groups.all().count() == 1: + form = AddListForm(request.user, initial={"group": request.user.groups.all()[0]}) + else: + form = AddListForm(request.user) + + return render(request, 'todo/add_list.html', locals()) + + +@user_passes_test(check_user_allowed) +def search_post(request): + """ + Redirect POST'd search param to query GET string + """ + if request.POST: + q = request.POST.get('q') + url = reverse('todo-search') + "?q=" + q + return HttpResponseRedirect(url) + + +@user_passes_test(check_user_allowed) +def search(request): + """ + Search for tasks + """ + if request.GET: + + query_string = '' + found_items = None + if ('q' in request.GET) and request.GET['q'].strip(): + query_string = request.GET['q'] + + found_items = Item.objects.filter( + Q(title__icontains=query_string) | + Q(note__icontains=query_string) + ) + else: + + # What if they selected the "completed" toggle but didn't type in a query string? + # We still need found_items in a queryset so it can be "excluded" below. + found_items = Item.objects.all() + + if 'inc_complete' in request.GET: + found_items = found_items.exclude(completed=True) + + else: + query_string = None + found_items = None + + return render( + request, + 'todo/search_results.html', { + 'query_string': query_string, + 'found_items': found_items + }, context_instance=RequestContext(request)) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..090c1fa --- /dev/null +++ b/tox.ini @@ -0,0 +1,23 @@ +[tox] +envlist = + flake8py{2,3} + +[testenv] +# deps = pytest +commands = + # py.test + +[testenv:flake8py2] +basepython = python2.7 +deps = flake8 +commands = flake8 . + +[testenv:flake8py3] +basepython = python3.5 +deps = flake8 +commands = flake8 . + +[flake8] +max-line-length = 120 +max-complexity = 10 +exclude = [build, lib, bin, dist, docs/conf.py, */migrations, .eggs, *.egg-info] diff --git a/werf.yml b/werf.yml deleted file mode 100644 index f8622f4..0000000 --- a/werf.yml +++ /dev/null @@ -1,5 +0,0 @@ -project: coins-demo -configVersion: 1 ---- -image: ~ -dockerfile: Dockerfile