Compare commits

...

146 commits
v2.0 ... master

Author SHA1 Message Date
D4rk4
7df1fd995f 16G 2020-08-02 04:11:31 +02:00
D4rk4
9c3f7d5948 Update 2020-08-02 04:11:00 +02:00
D4rk4
97ec2309f0 +registry 2020-08-02 02:21:54 +02:00
D4rk4
0875c84796 fix 2020-08-02 01:08:39 +02:00
D4rk4
990aa1df61 Update 2020-08-02 01:06:44 +02:00
D4rk4
2b43278546 Update 2020-08-02 01:05:38 +02:00
D4rk4
e8284a695a Update 2020-08-02 01:04:33 +02:00
D4rk4
4d2cc36338 fix 2020-08-01 23:39:49 +02:00
D4rk4
e9bbe2a143 Merge branch 'master' of git.ix.gs:public/coins-demo 2020-08-01 23:38:27 +02:00
D4rk4
76170f973d CleanUp+Werf 2020-08-01 23:38:16 +02:00
ad7f63a5b9 Update 'README.md' 2020-08-01 20:28:12 +00:00
D4rk4
7c8239b942 Update of README 2020-08-01 22:21:40 +02:00
D4rk4
6b52a69f23 Werf is still laggy for deploy 3-party charts, skip it 2020-08-01 22:20:09 +02:00
D4rk4
ef1a224546 werf fix? 2020-08-01 22:08:41 +02:00
D4rk4
33caa62205 Downgrade werf 2020-08-01 21:48:06 +02:00
D4rk4
834095e10a +htpasswd 2020-08-01 21:45:23 +02:00
D4rk4
903a1fa4d5 Disable persistance 2020-08-01 21:17:37 +02:00
D4rk4
fb59e92386 fix 2020-08-01 21:09:32 +02:00
D4rk4
1b9a650a19 fix 2020-08-01 20:59:27 +02:00
D4rk4
7e4935174a Fix annotations and more 2020-08-01 20:57:53 +02:00
D4rk4
07c19e1541 fix 2020-08-01 20:47:21 +02:00
D4rk4
533d5021a7 Introduce registry 2020-08-01 20:45:08 +02:00
D4rk4
73c78ae875 +Postgress 2020-08-01 18:13:45 +02:00
639278299b Update 'README.md' 2020-08-01 16:03:00 +00:00
D4rk4
3e7cbffbbd +CSI 2020-08-01 17:51:41 +02:00
D4rk4
f80637fbfc Merge branch 'master' of git.ix.gs:public/coins-demo 2020-08-01 17:04:07 +02:00
D4rk4
8fac1e2a46 +ingress fix 2020-08-01 17:03:57 +02:00
c2e5429c4d Update 'README.md' 2020-08-01 14:37:04 +00:00
D4rk4
8be1061c52 -timeout 2020-08-01 16:35:32 +02:00
D4rk4
bdf505b79c +Ingress 2020-08-01 16:33:34 +02:00
D4rk4
db67dde361 fix 2020-07-31 17:04:12 +02:00
D4rk4
abc8d04f2f fix 2020-07-31 16:53:50 +02:00
D4rk4
bcca957c21 fix 2020-07-31 16:44:43 +02:00
D4rk4
f16161568e fix 2020-07-31 16:33:43 +02:00
D4rk4
9ba9a4f70c +idle 2020-07-31 16:30:17 +02:00
D4rk4
e3d346cf6a +KillMode=none 2020-07-31 15:52:25 +02:00
D4rk4
6d6b6d0de0 fix 2020-07-31 15:49:11 +02:00
D4rk4
6b089ea9e8 Merge branch 'master' of git.ix.gs:public/coins-demo 2020-07-31 15:32:58 +02:00
D4rk4
7e9d5f0586 Introducing systemd mfg.service 2020-07-31 15:32:43 +02:00
75de0d5cdc Update 'README.md' 2020-07-31 13:12:06 +00:00
D4rk4
5adc1b90ef fix 2020-07-31 15:07:36 +02:00
D4rk4
d07d61f7bf fix 2020-07-31 15:00:11 +02:00
D4rk4
6caa6e01af Introducing manufacturing 2020-07-31 14:58:53 +02:00
D4rk4
ae8ed85e43 Ignore condition 2020-07-31 14:53:37 +02:00
D4rk4
a22215fa08 fix network 2020-07-31 14:36:44 +02:00
D4rk4
c294cf1694 +more RAM 2020-07-31 14:00:40 +02:00
D4rk4
bed32dcf23 +vCPU count detection 2020-07-31 13:58:45 +02:00
D4rk4
3c882f8913 Cleanup 2020-07-31 13:35:15 +02:00
D4rk4
424bd0a051 Small ansible impruvments 2020-07-31 13:33:22 +02:00
343ea0c339 Update 'README.md' 2020-07-31 10:23:31 +00:00
D4rk4
65b2720f70 fix 2020-07-31 12:19:33 +02:00
D4rk4
aac1d41217 First boot handling (not activated yet) 2020-07-31 12:11:20 +02:00
D4rk4
5bc450dadf clone repo inside VM at bootstrap 2020-07-31 12:02:59 +02:00
6531e03b1a Update 'README.md' 2020-07-31 09:58:12 +00:00
D4rk4
1bb80be1c9 +helm +werf in VM 2020-07-31 11:43:54 +02:00
c3d51a59bc Update 'README.md' 2020-07-31 09:33:56 +00:00
f2d213c284 Add 'README.md' 2020-07-31 09:31:36 +00:00
D4rk4
a6706498b6 Import Ansible playbook for bootstrap k8s cluster 2020-07-31 03:39:43 +02:00
D4rk4
5068274017 Remove k8s from seed 2020-07-31 03:30:03 +02:00
D4rk4
f13f2502a7 8Gb disk 2020-07-31 03:20:43 +02:00
D4rk4
ef13cb283e Merge branch 'master' of git.ix.gs:public/coins-demo 2020-07-31 03:20:19 +02:00
D4rk4
8622493dd2 8Gb disk 2020-07-31 03:19:39 +02:00
D4rk4
9f799832a9 NO SWAP 2020-07-31 03:07:48 +02:00
D4rk4
92bf4fecdf Remove LVM 2020-07-31 02:53:24 +02:00
D4rk4
16fa9ed24f Fix postinstall script 2020-07-31 02:47:35 +02:00
D4rk4
c68e399eac Small URL fix 2020-07-31 02:24:05 +02:00
D4rk4
c4807b7d37 initial VM config 2020-07-31 02:20:36 +02:00
dependabot[bot]
7efa164eb8
Bump django from 2.2.10 to 2.2.13 (#105)
Bumps [django](https://github.com/django/django) from 2.2.10 to 2.2.13.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.10...2.2.13)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-05 23:13:38 -07:00
dependabot[bot]
2d86a51177
Bump bleach from 3.1.2 to 3.1.4 (#101)
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.2 to 3.1.4.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.2...v3.1.4)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-03-30 22:38:10 -07:00
dependabot[bot]
c5740b9a84
Bump bleach from 3.1.1 to 3.1.2 (#100)
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.1...v3.1.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-03-24 23:05:34 -07:00
dependabot[bot]
a5c83dad83
Bump bleach from 3.1.0 to 3.1.1 (#99)
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.0...v3.1.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-24 22:43:39 -08:00
dependabot[bot]
988609d265
Bump django from 2.1.11 to 2.2.10 (#98)
Bumps [django](https://github.com/django/django) from 2.1.11 to 2.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.1.11...2.2.10)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-11 22:32:23 -08:00
Scot Hacker
58d7bdfc30 Bleach task title on edit 2020-01-11 15:23:47 -08:00
dependabot[bot]
6c31d4446d Bump django from 2.1.7 to 2.1.11 (#90)
Bumps [django](https://github.com/django/django) from 2.1.7 to 2.1.11.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.1.7...2.1.11)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-02 16:39:17 -07:00
Scot Hacker
ce3fd8c66e Allow unicode chars as the only chars in a list title 2019-09-19 22:58:52 -07:00
Scot Hacker
2b722afb8a Slightly DRYer list sorting 2019-09-19 22:41:45 -07:00
james1293
d07cb30fce Sort list of lists by group name instead of group id (#86) 2019-09-19 22:37:38 -07:00
Scot Hacker
4f9f379543 Editing task should not change its completed status 2019-09-18 23:18:01 -07:00
james1293
2d40ef471e Only set creator when creating task (#80)
* Only set creator when creating task

* allow blank form input, use clean func to keep old val

* remove unneeded created_date assignment

* add test
2019-07-29 22:53:33 -07:00
james1293
7f576c9bc8 Delete perms: must be staff and in group (#82)
* Delete perms: must be staff and in group

* separate group check and staff check

* test_del_list => test_del_list_not_in_list_group
2019-07-23 22:30:07 -07:00
Scot Hacker
21e0c6d656 Fix bug when retrieving/setting default settings values 2019-06-04 00:40:31 -07:00
Dave St.Germain
caed3b384d Support custom User model in external_add (#74) 2019-05-11 23:51:28 -07:00
Scot Hacker
befc7ad2cd Formatting 2019-04-12 00:09:01 -07:00
Scot Hacker
4a385bde6b Bump version to 2.4.6 2019-04-11 23:51:38 -07:00
Scot Hacker
1cd9700366 Get settings from defaults everywhere, update tests and perms 2019-04-11 23:44:28 -07:00
Scot Hacker
7a4984dc35 Get current version for setup.py from __init__ 2019-04-11 23:42:53 -07:00
Scot Hacker
f42d34205c Document TODO_MAXIMUM_ATTACHMENT_SIZE 2019-04-11 23:42:26 -07:00
Scot Hacker
44de86f1ca Use hash/lookup in defaults.py 2019-04-11 23:42:11 -07:00
Scot Hacker
7fe0728716 Fix file extension limiter issue, and establish defaults system 2019-04-11 22:09:41 -07:00
Scot Hacker
edff438623 Bump version after merging "notify" in task edit 2019-04-11 00:09:36 -07:00
james1293
8a20998f8c Re-enable name="notify" to task editor to enable notifications (#67) 2019-04-11 00:05:44 -07:00
Scot Hacker
602cf247e2 Resolve setup.py package installation issues, bump version 2019-04-10 00:28:00 -07:00
Scot Hacker
fb94fdb130 Bump version, README for 2.4.0 2019-04-09 00:01:27 -07:00
Scot Hacker
e7655ccfe8 Implement attachment removal 2019-04-08 23:46:34 -07:00
Scot Hacker
2e02163701 readme tweak 2019-04-07 23:57:48 -07:00
Scot Hacker
ab929b07e1 Limit attachments to specified file types 2019-04-07 23:55:31 -07:00
Scot Hacker
8cd169e502 MIsc tweaks 2019-04-07 16:11:28 -07:00
Scot Hacker
b6c2227417 Working file upload dialog and receive in view 2019-04-07 16:11:19 -07:00
Scot Hacker
9a5c794c41 Formatting 2019-04-06 16:39:34 -07:00
Scot Hacker
276ead54e7 Modeling and admin for attachment support 2019-04-06 16:30:01 -07:00
Scot Hacker
cdacc5fed5 Remove redundant 2.0 upgrade notes 2019-04-02 00:26:28 -07:00
Scot Hacker
8b448e88a5 Update setup.py, bump to 2.3.2 2019-04-02 00:19:46 -07:00
Scot Hacker
388fb40c00 Bump version to 2.3.1 2019-04-01 23:48:54 -07:00
Scot Hacker
e9a7bbe48c Improve error handling for web upload of bad CSV 2019-04-01 23:41:04 -07:00
Scot Hacker
ad0a1aa44a Don't crash if CSV web importer does not receive a file 2019-04-01 23:17:57 -07:00
Scot Hacker
b3d94ab608 Update readme for 2.3 release 2019-03-25 23:43:34 -07:00
Scot Hacker
6996c25842 Make comment field required 2019-03-25 23:42:59 -07:00
Scot Hacker
4a99d90d1e
Import tasks via CSV (#51)
* Bare start on CSV support

* Move core of CSV importer to operations

* More validations, break out validation function

* Validate dates and TaskList; convert errors to list of dictionaries

* Finish upsert code, and documentation

* Print msgs from the mgmt command, not the operations module

* Handle BOM marks

* Handle both in-memory and local file objects

* Update readme

* Working browser-upload view

* Bail on incorrect headers

* Fix default values and finish example spreadsheet

* Change column order, update docs

* Update index.md for RTD

* First round of responses to PR feedback

* Restore independent summaries/errors/upserts properties

* PR responses

* Split off reusable date validator into separate function

* Fix URLs append

* General test suite for CSV importer
2019-03-25 23:19:11 -07:00
Scot Hacker
184084c6a8
Prevent occasional crash during task re-ordering
Prevent occasional crash during task re-ordering
2019-03-25 07:45:26 -07:00
Scot Hacker
f6d79879ae
Fix MySQL migration (#57)
* Use CharField, not TextField for MySQL compat.

* black formatting
2019-03-25 07:43:53 -07:00
multun
70cac8b4e9 Fix IMAP mail deletion (#54)
Fixes #53
2019-03-11 23:28:40 -07:00
Scot Hacker
3b9c47cd7c Bump init version to 2.3.0 2019-03-11 00:39:01 -07:00
Scot Hacker
575e2649ac Fix dal_check integrity error when autocomplete not enabled 2019-03-11 00:34:55 -07:00
Scot Hacker
6f47f9d388 README tweaks 2019-03-11 00:34:24 -07:00
multun
c7ad961ef3 Implement mail tracker system
* Implement mail tracking

Signed-off-by: Victor "multun" Collod <victor.collod@prologin.org>

* Implement task merging

* Add a mail tracker title format pattern

* Autocomplete task names

* Fix comment display

* Track notification answers

* Add a socket timeout for the mail worker

A mail worker is a long running application. And sometimes, the IMAP server
just hangs for hours for no apparent reason. imaplib doesn't enable setting
a timeout, and setting it globally seems fine.

* Only validate the merge form when submitted

* Redirect to the new form when merging

* Prettier task edit UI

* Make task merging optional

* Test mail tracking

* Update documentation for mail tracking

* Update dependencies

* Add the TODO_COMMENT_CLASSES setting

* Fix dependencies install order

* Remove debug leftovers, improve documentation

* Fail on missing from_address
2019-03-11 00:04:19 -07:00
Scot Hacker
d0212b8a55 Update dependencies 2019-03-03 18:39:35 -08:00
Scot Hacker
84c441cf39 Update pipfile.lock 2019-02-10 23:26:39 -08:00
Scot Hacker
01cab7a82f Convert task_delete and task_done views from GET to POST 2019-02-10 11:06:36 -08:00
Scot Hacker
891148e496 Supported RTD theme 2019-01-10 23:48:23 -08:00
Scot Hacker
0abc9bf16c Move conf.py to mkdocs.yml 2019-01-10 23:46:24 -08:00
Scot Hacker
3816abd123 Tweak RTD theme invocation 2019-01-10 23:42:48 -08:00
Scot Hacker
7b58a12d97 Change RTD theme 2019-01-10 23:39:52 -08:00
Scot Hacker
fdd14392fe Add conf.py for readthedocs 2019-01-10 23:37:04 -08:00
Scot Hacker
8e52aad828 Move index.md to docs dir 2019-01-10 23:18:09 -08:00
Scot Hacker
dc3d4b647e Update index.me for readthedocs 2019-01-10 00:55:48 -08:00
Scot Hacker
513ef59d4a Add comment 2019-01-10 00:54:14 -08:00
Scot Hacker
91b9a099a3 Enforce and test TODO_STAFF_ONLY setting 2019-01-10 00:39:21 -08:00
Scot Hacker
6953085285 Correctly specify minimum requirement of Python 3.6 2019-01-08 23:11:13 -08:00
Scot Hacker
78e9c510bc Split up views into separate modules 2018-12-21 02:00:36 -08:00
Scot Hacker
21ec87cee4 Black formatting 2018-12-21 00:38:44 -08:00
Scot Hacker
f526ed5166 Strip unwanted tags from task note and comments 2018-12-20 17:08:09 -08:00
Scot Hacker
c6bd3bcdb0 Replace staticfiles in templates 2018-12-20 16:41:34 -08:00
Scot Hacker
8e16de5d8f Update .gitignore 2018-12-20 12:10:18 -08:00
Scot Hacker
8fe6e4856e Gitignore mypy cache 2018-12-20 11:43:28 -08:00
Scot Hacker
064e1d2659
Merge pull request #40 from madscientistproductions/patch-1
Update views.py
2018-12-11 09:33:53 -08:00
madscientistproductions
c41d15344b
Update views.py
Fix MultipleObjects error that happens if you have multiple lists of the same name and you delete one.
2018-12-11 17:16:13 +01:00
Scot Hacker
cb7b4e4c1f Copy index.md to readme.md to make both RTD and github happy 2018-04-10 08:56:27 -07:00
Scot Hacker
c18080c55d Bump release version for pypi 2018-04-10 08:23:44 -07:00
Scot Hacker
fe03d3f5b5 Bump production version in setup.py 2018-04-10 08:21:06 -07:00
Scot Hacker
3991a2c82c Bump version 2018-04-08 23:33:52 -07:00
Scot Hacker
782950d904 Fix / improve notification email subjects and bodies 2018-04-08 23:32:52 -07:00
Scot Hacker
5805cf52ad Increment version in readme 2018-04-08 00:58:32 -07:00
Scot Hacker
9098e3f0d4 Refactor Done and Delete actions in list and detail views 2018-04-08 00:49:01 -07:00
Scot Hacker
4fe3829b98 Fix silent crasher when reordering table rows 2018-04-07 23:31:24 -07:00
Scot Hacker
d169f131a2 Documentation tweak 2018-04-07 13:50:53 -07:00
Scot Hacker
50d182103c Rename readme to index for readthedocs to find 2018-04-07 13:42:34 -07:00
88 changed files with 839 additions and 2870 deletions

13
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -1,27 +0,0 @@
sudo: true
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq build-essential gettext python-dev zlib1g-dev libpq-dev xvfb libjpeg8-dev
- sudo apt-get install -qq python-setuptools python3-dev python-virtualenv python-pip
addons:
postgresql: "9.6"
install:
- "pip3 install -e . --upgrade"
- "pip3 install git+https://github.com/pypa/pipenv.git"
- "pipenv install --dev"
language: python
python:
- "3.6"
# Attempt to use cached versions of python deps
cache: pip
cache:
directories:
- $HOME/.cache/pip
script: pipenv run pytest -x -v

15
Dockerfile Normal file
View file

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

27
LICENSE
View file

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

View file

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

19
Pipfile
View file

@ -1,19 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
django-extensions = "*"
"psycopg2-binary" = "*"
pytest = "*"
pytest-django = "*"
"flake8" = "*"
factory-boy = "*"
titlecase = "*"
[dev-packages]
[requires]
python_version = "3.6"

201
Pipfile.lock generated
View file

@ -1,201 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "8aa62fe5923a75a6df757c643dbd98177e66bd0bc9e429d65006f042f73f5a32"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"attrs": {
"hashes": [
"sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9",
"sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"
],
"version": "==17.4.0"
},
"django": {
"hashes": [
"sha256:2d8b9eed8815f172a8e898678ae4289a5e9176bc08295676eff4228dd638ea61",
"sha256:d81a1652963c81488e709729a80b510394050e312f386037f26b54912a3a10d0"
],
"index": "pypi",
"version": "==2.0.4"
},
"django-extensions": {
"hashes": [
"sha256:37a543af370ee3b0721ff50442d33c357dd083e6ea06c5b94a199283b6f9e361",
"sha256:bc9f2946c117bb2f49e5e0633eba783787790ae810ea112fe7fd82fa64de2ff1"
],
"index": "pypi",
"version": "==2.0.6"
},
"factory-boy": {
"hashes": [
"sha256:bd5a096d0f102d79b6c78cef1c8c0b650f2e1a3ecba351c735c6d2df8dabd29c",
"sha256:be2abc8092294e4097935a29b4e37f5b9ed3e4205e2e32df215c0315b625995e"
],
"index": "pypi",
"version": "==2.10.0"
},
"faker": {
"hashes": [
"sha256:9cc12b821f32ff45f6edfdc1ab7be3893b60b1224e952d68322a57e5b26a4a15",
"sha256:b06d0dc0166618298e668ced513ced7b10df34f3ad2045f22f1d7d88704e8e9c"
],
"version": "==0.8.12"
},
"flake8": {
"hashes": [
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
],
"index": "pypi",
"version": "==3.5.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"more-itertools": {
"hashes": [
"sha256:0dd8f72eeab0d2c3bd489025bb2f6a1b8342f9b198f6fc37b52d15cfa4531fea",
"sha256:11a625025954c20145b37ff6309cd54e39ca94f72f6bb9576d1195db6fa2442e",
"sha256:c9ce7eccdcb901a2c75d326ea134e0886abfbea5f93e91cc95de9507c0816c44"
],
"version": "==4.1.0"
},
"pluggy": {
"hashes": [
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
],
"version": "==0.6.0"
},
"psycopg2-binary": {
"hashes": [
"sha256:02eb674e3d5810e19b4d5d00720b17130e182da1ba259dda608aaf33d787347d",
"sha256:3a14baeabcebd4662f12f4bff03e0574a2369a2e41baf829e6fb4a24c95cf88b",
"sha256:436a503eda41f6adb08f292f40a3784fce0a5f351b6ae7b19a911904db53af93",
"sha256:465ff1d427ed42c31e456dbbd9edab3552be18a0edaef7450c5b3e6fee745052",
"sha256:4a1a5ea2fa4b53191637b162873a82822d92a85a08beefe28296b8eb5cf2fea5",
"sha256:4a4f23a08fbccbe40ecdb5384d807bcb469ea71dd87e6be2e80b036b8e6d47df",
"sha256:77a2fc622a1f2d08a707673c9be5769d521f03d867d305f172bb417fa7882754",
"sha256:8014c06a9ed7b78ba81beff3ae71acd78c212390f8ed839e9ce22735880bd5b4",
"sha256:83af04029bcb4b56c852e5876fef71340dcb465fa44fc99f80bac72e10fb0b74",
"sha256:86c0d2587f56776f25d52cca8e275adf495c8e01933fbfc2ca23b124610ab761",
"sha256:9305d7cbc802aaefac5c75a3df725f2654797369f32b18d4d0adb382dfab6c09",
"sha256:9b5ddbed85ec73293695d7116589d956ef0dd3fcf7bf3b2a3bc1e8e54c1d543a",
"sha256:a3d2cc0cb0b988dbfd0d11f7fac34058b25a6ce533ed5b8e88d6cb315e77d54a",
"sha256:ab1db8f3e96570d9f7ebc45133ce2574804b2280499baade178e163d022107b5",
"sha256:b039f51bca1ddd70234cc3f84f94f42ad43861b931bdfb497f887c60c39a6565",
"sha256:b287ddf4cafcfb632974907d1e7862119e36bb758228bdb07dd247553e4cdfc0",
"sha256:b6b2b26590304d97ef2af28d153ee99ace6fe0806934f4618edfc87216c77f91",
"sha256:c4c6004d410c77bfa5389ae9485498ce32805447a67afbfe8db0d247a5c88fa1",
"sha256:c606bff0978ee4858d86d40f6b6ab0c4cac4474f627bd054683dc03a4fc1a366",
"sha256:c8220c521a408b41c4f14036004a621ed0d965941286b978cd2ea2623fabd755",
"sha256:cb07184a4bfad304831f0a88b1c13fbd8cf9fcdf1f11e71c477dd6d7b1b078a0",
"sha256:cf3911fba0c47fc1313b5783183cda301032b14637a0b7a336766ae46998c7ee",
"sha256:d0972f062c73956332e9681dfdb133168618f0abfecc96e89f0205ac89cd454b",
"sha256:d1dd3eb8edd354083f5d27b968c5a17854c41347ba5a480b520be85ec1a8495c",
"sha256:d51c7ed810fce1e50464088c37cc8da05534de8afb12a732500827ebcc480081",
"sha256:d8940b5104588d6313315e037f0f5ed68d2e5f62ccc1c429d3cff11d2ba6de3f",
"sha256:de4f88f823037a71ea5ef3c1041d96b8a68d73343133edda684fd42f575bd9d7"
],
"index": "pypi",
"version": "==2.7.4"
},
"py": {
"hashes": [
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
],
"version": "==1.5.3"
},
"pycodestyle": {
"hashes": [
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
],
"version": "==2.3.1"
},
"pyflakes": {
"hashes": [
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
],
"version": "==1.6.0"
},
"pytest": {
"hashes": [
"sha256:6266f87ab64692112e5477eba395cfedda53b1933ccd29478e671e73b420c19c",
"sha256:fae491d1874f199537fd5872b5e1f0e74a009b979df9d53d1553fd03da1703e1"
],
"index": "pypi",
"version": "==3.5.0"
},
"pytest-django": {
"hashes": [
"sha256:00995c2999b884a38ae9cd30a8c00ed32b3d38c1041250ea84caf18085589662",
"sha256:038ccc5a9daa1b1b0eb739ab7dce54e495811eca5ea3af4815a2a3ac45152309"
],
"index": "pypi",
"version": "==3.1.2"
},
"python-dateutil": {
"hashes": [
"sha256:3220490fb9741e2342e1cf29a503394fdac874bc39568288717ee67047ff29df",
"sha256:9d8074be4c993fbe4947878ce593052f71dac82932a677d49194d8ce9778002e"
],
"version": "==2.7.2"
},
"pytz": {
"hashes": [
"sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd",
"sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5",
"sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0",
"sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d",
"sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9",
"sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef",
"sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f",
"sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe",
"sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda"
],
"version": "==2018.3"
},
"six": {
"hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
],
"version": "==1.11.0"
},
"text-unidecode": {
"hashes": [
"sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d",
"sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"
],
"version": "==1.2"
},
"titlecase": {
"hashes": [
"sha256:84de7a97fb702c400e5ba11c6b30849944b39db12e20fbf4515a23c7538a0611",
"sha256:95d643a0c08097c02933aced707adfe1c275c335019e8e514dea782a465c5b84"
],
"index": "pypi",
"version": "==0.12.0"
}
},
"develop": {}
}

241
README.md
View file

@ -1,213 +1,28 @@
# django-todo
django-todo is a pluggable, multi-user, multi-group task management and
assignment application for Django, designed to be dropped into an existing site as a reusable app. django-todo can be used as a personal to-do tracker, or a group task management system, or a ticketing system for organizations (or all of these at once!)
## Features
* Drag and drop task prioritization
* Email task notification
* Search
* Comments on tasks
* Public-facing submission form for tickets
* Mobile-friendly (work in progress)
* Separate view for My Tasks (across lists)
## Requirements
* Django 2.0+
* Python 3.3+
* jQuery (full version, not "slim", for drag/drop prioritization)
* Bootstrap (to work with provided templates, though you can override them)
## Overview
**The best way to learn how django-todo works is to visit the live demo site at [django-todo.org](http://django-todo.org)!**
The assumption is that your organization/publication/company has multiple groups of employees, each with multiple users (where actual users and groups map to Django Users and Groups). Users may belong to multiple groups, and each group can have multiple todo lists.
You must have at least one Group set up in Django admin, and that group must have at least one User as a member. This is true even if you're the sole user of django-todo.
Users can view and modify all to-do lists belonging to their group(s). Only users with `is_staff()` can add or delete lists.
Identical list names can exist in different groups, but not in the same group.
Emails are generated to the assigned-to person when new tasks are created.
Comment threads can be added to tasks. Each participant in a thread receives email when new comments are added.
django-todo is auth-only. You must set up a login system and at least one group before deploying.
All tasks are "created by" the current user and can optionally be "assigned to" a specific user. Unassigned tickets appear as belonging to "anyone" in the UI.
django-todo v2 makes use of features only available in Django 2.0. It will not work in previous versions. v2 is only tested against Python 3.x -- no guarantees if running it against older versions.
# Installation
django-todo is a Django app, not a project site. It needs a site to live in. You can either install it into an existing Django project site, or clone the django-todo [demo site (GTD)](https://github.com/shacker/gtd).
If using your own site, be sure you have jQuery and Bootstrap wired up and working.
django-todo pages that require it will insert additional CSS/JavaScript into page heads,
so your project's base templates must include:
```
{% block extrahead %}{% endblock extrahead %}
{% block extra_js %}{% endblock extra_js %}
```
django-todo comes with its own `todo/base.html`, which extends your master `base.html`. All content lives inside of:
`{% block content %}{% endblock %}`
If you use some other name for your main content area, you'll need to override and alter the provided templates.
All views are login-required. Therefore, you must have a working user authentication system.
For email notifications to work, make sure your site/project is [set up to send email](https://docs.djangoproject.com/en/2.0/topics/email/).
Make sure you've installed the Django "sites" framework and have specified the default site in settings, e.g. `SITE_ID = 1`
Put django-todo/todo somewhere on your Python path, or install via pip:
pip install django-todo
Add to your settings:
INSTALLED_APPS = (
...
'todo',
)
Create database tables:
python manage.py migrate todo
Add to your URL conf:
path('todo/', include('todo.urls', namespace="todo")),
Add links to your site's navigation system:
<a href="{% url 'todo:lists' %}">Todo Lists</a>
<a href="{% url 'todo:mine' %}">My Tasks</a>
django-todo makes use of the Django `messages` system. Make sure you have something like [this](https://docs.djangoproject.com/en/2.0/ref/contrib/messages/#displaying-messages) in your `base.html`.
Log in and access `/todo`!
The provided templates are fairly bare-bones, and are meant as starting points only. Unlike previous versions of django-todo, they now ship as Bootstrap examples, but feel free to override them - there is no hard dependency on Bootstrap. To override a template, create a `todo` folder in your project's `templates` dir, then copy the template you want to override from django-todo source and into that dir.
If you wish to use the public ticket-filing system, first create the list into which those tickets should be filed, then add its slug to `TODO_DEFAULT_LIST_SLUG` in settings (more on settings below).
## Settings
Optional configuration options:
```
# Restrict access to todo lists/views to `is_staff()` users.
# False here falls back to `is_authenticated()` users.
TODO_STAFF_ONLY = True
# If you use the "public" ticket filing option, to whom should these tickets be assigned?
# Must be a valid username in your system. If unset, unassigned tickets go to "Anyone."
TODO_DEFAULT_ASSIGNEE = 'johndoe'
# If you use the "public" ticket filing option, to which list should these tickets be saved?
# Defaults to first list found, which is probably not what you want!
TODO_DEFAULT_LIST_SLUG = 'tickets'
# If you use the "public" ticket filing option, to which *named URL* should the user be
# redirected after submitting? (since they can't see the rest of the ticket system).
# Defaults to "/"
TODO_PUBLIC_SUBMIT_REDIRECT = 'dashboard'
```
The current django-todo version number is available from the [todo package](https://github.com/shacker/django-todo/blob/master/todo/__init__.py):
python -c "import todo; print(todo.__version__)"
## Upgrade Notes
django-todo 2.0 was rebuilt almost from the ground up, and included some radical changes, including model name changes. As a result, it is *not compatible* with data from django-todo 1.x. If you would like to upgrade an existing installation, try this:
* Use `./manage.py dumpdata todo --indent 4 > todo.json` to export your old todo data
* Edit the dump file, replacing the old model names `Item` and `List` with the new model names (`Task` and `TaskList`)
* Delete your existing todo data
* Uninstall the old todo app and reinstall
* Migrate, then use `./manage.py loaddata todo.json` to import the edited data
### Why not provide migrations?
That was the plan, but unfortunately, `makemigrations` created new tables and dropped the old ones, making this a destructive update. Renaming models is unfortunately not something `makemigrations` can do, and I really didn't want to keep the badly named original models. Sorry!
### Datepicker
django-todo no longer references a jQuery datepicker, but defaults to native html5 browser datepicker (not supported by Safari, unforunately). Feel free to implement one of your choosing.
### URLs
Some views and URLs were renamed for logical consistency. If this affects you, see source code and the demo GTD site for reference to the new URL names.
## Running Tests
django-todo uses pytest exclusively for testing. The best way to run the suite is to clone django-todo into its own directory, install pytest, then:
pip install pytest pytest-django
pip install --editable .
pytest -x -v
The previous `tox` system was removed with the v2 release, since we no longer aim to support older Python or Django versions.
# Version History
**2.0** April 2018: Major project refactor, with almost completely rewritten views, templates, and todo's first real test suite.
**1.6.2** Added support for unicode characters in list name/slugs.
**1.6.1** Minor bug fixes.
**1.6** Allow unassigned ("Anyone") tasks. Clean-up / modernize templates and views. Testing infrastructure in place.
**1.5** flake8 support, Item note no longer a required field, fix warnings for Django 1.8, Python 2/3-compatible unicode strings, simple search for tasks, get_absolute_url() for items.
**1.4** - Removed styling from default templates. Added excludes fields from Form definitions to prevent warnings. Removed deprecated 'cycle' tags from templates. Added settings for various elements for public ticket submissions.
**1.3** - Removed stray direct_to_template reference. Quoted all named URL references for Django 1.5 compatibility.
**1.2** - Added CSRF protection to all sample templates. Added integrated search function. Now showing the ratio of completed/total items for each
list. Better separation of media and templates. Cleaned up Item editing form (removed extraneous fields). Re-assigning tasks now properly limits
the list of assignees. Moved project to github.
**1.1** - Completion date was set properly when checking items off a list, but not when saving from an Item detail page. Added a save method on Item to
fix. Fixed documentation bug re: context_processors. Newly added comments are now emailed to everyone who has participated in a thread on a task.
**1.0.1** - When viewing a single task that you want to close, it's useful to be able to comment on and close a task at the same time. We were using
django-comments so these were different models in different views. Solution was to stop using django-comments and roll our own, then rewire the
view. Apologies if you were using a previous version - you may need to port over your comments to the new system.
**1.0.0** - Major upgrade to release version. Drag and drop task prioritization. E-mail notifications (now works more like a ticket system). More
attractive date picker. Bug fixes.
**0.9.5** - Fixed jquery bug when editing existing events - datepicker now shows correct date. Removed that damned Django pony from base template.
**0.9.4** - Replaced str with unicode in models. Fixed links back to lists in "My Tasks" view.
**0.9.3** - Missing link to the individual task editing view
**0.9.2** - Now fails gracefully when trying to add a 2nd list with the same name to the same group. - Due dates for tasks are now truly optional. -
Corrected datetime editing conflict when editing tasks - Max length of a task name has been raised from 60 to 140 chars. If upgrading, please
modify your database accordingly (field todo_item.name = maxlength 140). - Security: Users supplied with direct task URLs can no longer view/edit
tasks outside their group scope Same for list views - authorized views only. - Correct item and group counts on homepage (note - admin users see
ALL groups, not just the groups they "belong" to)
**0.9.1** - Removed context_processors.py - leftover turdlet
**0.9** - First release
# [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~~

View file

@ -1,15 +0,0 @@
from django.urls import include, path
"""
This urlconf exists so we can run tests without an actual Django project
(Django expects ROOT_URLCONF to exist.) This helps the tests remain isolated.
For your project, ignore this file and add
`path('lists/', include('todo.urls')),`
to your site's urlconf.
"""
urlpatterns = [
path('lists/', include('todo.urls')),
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
---

View file

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

View file

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

View file

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

77
contrib/firstboot.sh Executable file
View file

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

112
contrib/k8s-seed.txt Normal file
View file

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

18
contrib/mfg.service Normal file
View file

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

BIN
contrib/qemu-system-x86_64 Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

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

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

3
entrypoint.sh Normal file
View file

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

26
local.py Normal file
View file

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

View file

@ -1,4 +0,0 @@
[pytest]
DJANGO_SETTINGS_MODULE = test_settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py

34
runme.sh Executable file
View file

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

View file

@ -1,31 +0,0 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import todo as package
setup(
name='django-todo',
version=package.__version__,
description=package.__doc__.strip(),
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 :: 3',
'Topic :: Office/Business :: Groupware',
'Topic :: Software Development :: Bug Tracking',
],
include_package_data=True,
zip_safe=False,
install_requires=['unidecode', ],
)

View file

@ -1,64 +0,0 @@
import os
DEBUG = True,
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
print("bd ", BASE_DIR)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
}
}
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Document
TODO_STAFF_ONLY = False
TODO_DEFAULT_LIST_SLUG = 'tickets'
TODO_DEFAULT_ASSIGNEE = None
TODO_PUBLIC_SUBMIT_REDIRECT = '/'
SECRET_KEY = "LKFSD8sdl.,8&sdf--"
SITE_ID = 1
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'todo',
)
ROOT_URLCONF = 'base_urls'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'todo', 'templates'), ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.contrib.messages.context_processors.messages',
# Your stuff: custom template context processors go here
],
},
},
]

View file

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

View file

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

View file

@ -1,77 +0,0 @@
from django import forms
from django.contrib.auth.models import Group
from django.forms import ModelForm
from todo.models import Task, TaskList
class AddTaskListForm(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(AddTaskListForm, self).__init__(*args, **kwargs)
self.fields['group'].queryset = Group.objects.filter(user=user)
self.fields['group'].widget.attrs = {
'id': 'id_group', 'class': "custom-select mb-3", 'name': 'group'}
class Meta:
model = TaskList
exclude = ['created_date', 'slug', ]
class AddEditTaskForm(ModelForm):
"""The picklist showing the users to which a new task can be assigned
must find other members of the group this TaskList is attached to."""
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
task_list = kwargs.get('initial').get('task_list')
members = task_list.group.user_set.all()
self.fields['assigned_to'].queryset = members
self.fields['assigned_to'].label_from_instance = lambda obj: "%s (%s)" % (obj.get_full_name(), obj.username)
self.fields['assigned_to'].widget.attrs = {
'id': 'id_assigned_to', 'class': "custom-select mb-3", 'name': 'assigned_to'}
self.fields['task_list'].value = kwargs['initial']['task_list'].id
due_date = forms.DateField(
widget=forms.DateInput(attrs={'type': 'date'}), required=False)
title = forms.CharField(
widget=forms.widgets.TextInput())
note = forms.CharField(
widget=forms.Textarea(), required=False)
class Meta:
model = Task
exclude = []
class AddExternalTaskForm(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}),
label="Summary"
)
note = forms.CharField(
widget=forms.widgets.Textarea(),
label='Problem Description',
)
priority = forms.IntegerField(
widget=forms.HiddenInput(),
)
class Meta:
model = Task
exclude = (
'task_list', 'created_date', 'due_date', 'created_by', 'assigned_to', 'completed', 'completed_date', )
class SearchForm(forms.Form):
"""Search."""
q = forms.CharField(
widget=forms.widgets.TextInput(attrs={'size': 35})
)

View file

@ -1,146 +0,0 @@
import factory
from faker import Faker
from titlecase import titlecase
import random
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
from django.utils.text import slugify
from todo.models import Task, TaskList
num_lists = 5
def gen_title(tc=True):
# faker doesn't provide a way to generate headlines in Title Case, without periods, so make our own.
# With arg `tc=True`, Title Cases The Generated Text
fake = Faker()
thestr = fake.text(max_nb_chars=32).rstrip('.')
if tc:
thestr = titlecase(thestr)
return thestr
def gen_content():
# faker provides paragraphs as a list; convert with linebreaks
fake = Faker()
grafs = fake.paragraphs()
thestr = ''
for g in grafs:
thestr += "{}\n\n".format(g)
return thestr
class Command(BaseCommand):
help = """Create random list and task data for a few fake users."""
def add_arguments(self, parser):
parser.add_argument(
"-d",
"--delete",
help="Wipe out existing content before generating new.",
action="store_true")
def handle(self, *args, **options):
if options.get('delete'):
# Wipe out previous contents? Cascade deletes the Tasks from the TaskLists.
TaskList.objects.all().delete()
print("Content from previous run deleted.")
print("Working...")
fake = Faker() # Use to create user's names
# Create users and groups, add different users to different groups. Staff user is in both groups.
sd_group, created = Group.objects.get_or_create(name='Scuba Divers')
bw_group, created = Group.objects.get_or_create(name='Basket Weavers')
# Put user1 and user2 in one group, user3 and user4 in another
usernames = ['user1', 'user2', 'user3', 'user4', 'staffer']
for username in usernames:
if get_user_model().objects.filter(username=username).exists():
user = get_user_model().objects.get(username=username)
else:
user = get_user_model().objects.create_user(
username=username,
first_name=fake.first_name(),
last_name=fake.last_name(),
email="{}@example.com".format(username),
password="todo")
if username in ['user1', 'user2']:
user.groups.add(bw_group)
if username in ['user3', 'user4']:
user.groups.add(sd_group)
if username == 'staffer':
user.is_staff = True
user.first_name = fake.first_name()
user.last_name = fake.last_name()
user.save()
user.groups.add(bw_group)
user.groups.add(sd_group)
# Create lists with tasks, plus one with fixed name for externally added tasks
TaskListFactory.create_batch(5, group=bw_group)
TaskListFactory.create_batch(5, group=sd_group)
TaskListFactory.create(name="Public Tickets", slug="tickets", group=bw_group)
print("For each of two groups, created fake tasks in each of {} fake lists.".format(num_lists))
class TaskListFactory(factory.django.DjangoModelFactory):
"""Group not generated here - call with group as arg."""
class Meta:
model = TaskList
name = factory.LazyAttribute(lambda o: gen_title(tc=True))
slug = factory.LazyAttribute(lambda o: slugify(o.name))
group = None # Pass this in
@factory.post_generation
def add_tasks(self, build, extracted, **kwargs):
num = random.randint(5, 25)
TaskFactory.create_batch(num, task_list=self)
class TaskFactory(factory.django.DjangoModelFactory):
"""TaskList not generated here - call with TaskList as arg."""
class Meta:
model = Task
title = factory.LazyAttribute(lambda o: gen_title(tc=False))
task_list = None # Pass this in
note = factory.LazyAttribute(lambda o: gen_content())
priority = factory.LazyAttribute(lambda o: random.randint(1, 100))
completed = factory.Faker('boolean', chance_of_getting_true=30)
created_by = factory.LazyAttribute(lambda o: get_user_model().objects.get(username='staffer')) # Randomized in post
created_date = factory.Faker('date_this_year')
@factory.post_generation
def add_details(self, build, extracted, **kwargs):
fake = Faker() # Use to create user's names
taskgroup = self.task_list.group
self.created_by = taskgroup.user_set.all().order_by('?').first()
if self.completed:
self.completed_date = fake.date_this_year()
# 1/3 of generated tasks have a due_date
if random.randint(1, 3) == 1:
self.due_date = fake.date_this_year()
# 1/3 of generated tasks are assigned to someone in this tasks's group
if random.randint(1, 3) == 1:
self.assigned_to = taskgroup.user_set.all().order_by('?').first()
self.save()

View file

@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import datetime
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('auth', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('date', models.DateTimeField(default=datetime.datetime.now)),
('body', models.TextField(blank=True)),
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
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, on_delete=models.CASCADE)),
('created_by', models.ForeignKey(related_name='todo_created_by', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
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', on_delete=models.CASCADE)),
],
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', on_delete=models.CASCADE),
preserve_default=True,
),
migrations.AddField(
model_name='comment',
name='task',
field=models.ForeignKey(to='todo.Item', on_delete=models.CASCADE),
preserve_default=True,
),
]

View file

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

View file

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

View file

@ -1,52 +0,0 @@
# Generated by Django 2.0.2 on 2018-02-09 23:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
('todo', '0003_assignee_optional'),
]
operations = [
migrations.CreateModel(
name='TaskList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=60)),
('slug', models.SlugField(default='')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
],
options={
'verbose_name_plural': 'Lists',
'ordering': ['name'],
},
),
migrations.AlterUniqueTogether(
name='list',
unique_together=set(),
),
migrations.RemoveField(
model_name='list',
name='group',
),
migrations.RemoveField(
model_name='item',
name='list',
),
migrations.DeleteModel(
name='List',
),
migrations.AddField(
model_name='item',
name='task_list',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='todo.TaskList'),
),
migrations.AlterUniqueTogether(
name='tasklist',
unique_together={('group', 'slug')},
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 2.0.2 on 2018-02-12 23:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('todo', '0004_rename_list_tasklist'),
]
operations = [
migrations.AlterModelOptions(
name='tasklist',
options={'ordering': ['name'], 'verbose_name_plural': 'Task Lists'},
),
migrations.AlterField(
model_name='item',
name='completed',
field=models.BooleanField(default=False),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 2.0.3 on 2018-03-28 22:40
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
atomic = False
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('todo', '0005_auto_20180212_2325'),
]
operations = [
migrations.RenameModel(
old_name='Item',
new_name='Task',
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 2.0.4 on 2018-04-05 00:24
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('todo', '0006_rename_item_model'),
]
operations = [
migrations.AlterField(
model_name='task',
name='created_date',
field=models.DateField(blank=True, default=django.utils.timezone.now, null=True),
),
]

View file

@ -1,78 +0,0 @@
from __future__ import unicode_literals
import datetime
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import models
from django.urls import reverse
from django.utils import timezone
class TaskList(models.Model):
name = models.CharField(max_length=60)
slug = models.SlugField(default='',)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Meta:
ordering = ["name"]
verbose_name_plural = "Task Lists"
# Prevents (at the database level) creation of two lists with the same slug in the same group
unique_together = ("group", "slug")
class Task(models.Model):
title = models.CharField(max_length=140)
task_list = models.ForeignKey(TaskList, on_delete=models.CASCADE, null=True)
created_date = models.DateField(default=timezone.now, blank=True, null=True)
due_date = models.DateField(blank=True, null=True, )
completed = models.BooleanField(default=False)
completed_date = models.DateField(blank=True, null=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='todo_created_by', on_delete=models.CASCADE)
assigned_to = models.ForeignKey(
settings.AUTH_USER_MODEL, blank=True, null=True, related_name='todo_assigned_to', on_delete=models.CASCADE)
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 Tasks's due date has passed or not."
if self.due_date and datetime.date.today() > self.due_date:
return True
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('todo:task_detail', kwargs={'task_id': self.id, })
# Auto-set the Task creation / completed date
def save(self, **kwargs):
# If Task is being marked complete, set the completed_date
if self.completed:
self.completed_date = datetime.datetime.now()
super(Task, self).save()
class Meta:
ordering = ["priority"]
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, on_delete=models.CASCADE)
task = models.ForeignKey(Task, on_delete=models.CASCADE)
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

View file

@ -1,4 +0,0 @@
label {
display: block;
font-weight: bold;
}

View file

@ -1,382 +0,0 @@
/**
* TableDnD plug-in for JQuery, allows you to drag and drop table rows
* You can set up various options to control how the system will work
* Copyright (c) Denis Howlett <denish@isocra.com>
* Licensed like jQuery, see http://docs.jquery.com/License.
*
* Configuration options:
*
* onDragStyle
* This is the style that is assigned to the row during drag. There are limitations to the styles that can be
* associated with a row (such as you can't assign a border--well you can, but it won't be
* displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
* a map (as used in the jQuery css(...) function).
* onDropStyle
* This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
* to what you can do. Also this replaces the original style, so again consider using onDragClass which
* is simply added and then removed on drop.
* onDragClass
* This class is added for the duration of the drag and then removed when the row is dropped. It is more
* flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
* is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
* stylesheet.
* onDrop
* Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
* and the row that was dropped. You can work out the new order of the rows by using
* table.rows.
* onDragStart
* Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
* table and the row which the user has started to drag.
* onAllowDrop
* Pass a function that will be called as a row is over another row. If the function returns true, allow
* dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
* the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
* scrollAmount
* This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
* window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
* FF3 beta
* dragHandle
* This is the name of a class that you assign to one or more cells in each row that is draggable. If you
* specify this class, then you are responsible for setting cursor: move in the CSS and only these cells
* will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
* the whole row is draggable.
*
* Other ways to control behaviour:
*
* Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
* that you don't want to be draggable.
*
* Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
* <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
* an ID as must all the rows.
*
* Other methods:
*
* $("...").tableDnDUpdate()
* Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
* This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
* The table maintains the original configuration (so you don't have to specify it again).
*
* $("...").tableDnDSerialize()
* Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
* called from anywhere and isn't dependent on the currentTable being set up correctly before calling
*
* Known problems:
* - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
*
* Version 0.2: 2008-02-20 First public version
* Version 0.3: 2008-02-07 Added onDragStart option
* Made the scroll amount configurable (default is 5 as before)
* Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
* Added onAllowDrop to control dropping
* Fixed a bug which meant that you couldn't set the scroll amount in both directions
* Added serialize method
* Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
* draggable
* Improved the serialize method to use a default (and settable) regular expression.
* Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
*/
jQuery.tableDnD = {
/** Keep hold of the current table being dragged */
currentTable : null,
/** Keep hold of the current drag object if any */
dragObject: null,
/** The current mouse offset */
mouseOffset: null,
/** Remember the old value of Y so that we don't do too much processing */
oldY: 0,
/** Actually build the structure */
build: function(options) {
// Set up the defaults if any
this.each(function() {
// This is bound to each matching table, set up the defaults and override with user options
this.tableDnDConfig = jQuery.extend({
onDragStyle: null,
onDropStyle: null,
// Add in the default class for whileDragging
onDragClass: "tDnD_whileDrag",
onDrop: null,
onDragStart: null,
scrollAmount: 5,
serializeRegexp: /[^\-]*$/, // The regular expression to use to trim row IDs
serializeParamName: null, // If you want to specify another parameter name instead of the table ID
dragHandle: null // If you give the name of a class here, then only Cells with this class will be draggable
}, options || {});
// Now make the rows draggable
jQuery.tableDnD.makeDraggable(this);
});
// Now we need to capture the mouse up and mouse move event
// We can use bind so that we don't interfere with other event handlers
jQuery(document)
.bind('mousemove', jQuery.tableDnD.mousemove)
.bind('mouseup', jQuery.tableDnD.mouseup);
// Don't break the chain
return this;
},
/** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
makeDraggable: function(table) {
var config = table.tableDnDConfig;
if (table.tableDnDConfig.dragHandle) {
// We only need to add the event to the specified cells
var cells = jQuery("td."+table.tableDnDConfig.dragHandle, table);
cells.each(function() {
// The cell is bound to "this"
jQuery(this).mousedown(function(ev) {
jQuery.tableDnD.dragObject = this.parentNode;
jQuery.tableDnD.currentTable = table;
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
if (config.onDragStart) {
// Call the onDrop method if there is one
config.onDragStart(table, this);
}
return false;
});
})
} else {
// For backwards compatibility, we add the event to the whole row
var rows = jQuery("tr", table); // get all the rows as a wrapped set
rows.each(function() {
// Iterate through each row, the row is bound to "this"
var row = jQuery(this);
if (! row.hasClass("nodrag")) {
row.mousedown(function(ev) {
if (ev.target.tagName == "TD") {
jQuery.tableDnD.dragObject = this;
jQuery.tableDnD.currentTable = table;
jQuery.tableDnD.mouseOffset = jQuery.tableDnD.getMouseOffset(this, ev);
if (config.onDragStart) {
// Call the onDrop method if there is one
config.onDragStart(table, this);
}
return false;
}
}).css("cursor", "move"); // Store the tableDnD object
}
});
}
},
updateTables: function() {
this.each(function() {
// this is now bound to each matching table
if (this.tableDnDConfig) {
jQuery.tableDnD.makeDraggable(this);
}
})
},
/** Get the mouse coordinates from the event (allowing for browser differences) */
mouseCoords: function(ev){
if(ev.pageX || ev.pageY){
return {x:ev.pageX, y:ev.pageY};
}
return {
x:ev.clientX + document.body.scrollLeft - document.body.clientLeft,
y:ev.clientY + document.body.scrollTop - document.body.clientTop
};
},
/** Given a target element and a mouse event, get the mouse offset from that element.
To do this we need the element's position and the mouse position */
getMouseOffset: function(target, ev) {
ev = ev || window.event;
var docPos = this.getPosition(target);
var mousePos = this.mouseCoords(ev);
return {x:mousePos.x - docPos.x, y:mousePos.y - docPos.y};
},
/** Get the position of an element by going up the DOM tree and adding up all the offsets */
getPosition: function(e){
var left = 0;
var top = 0;
/** Safari fix -- thanks to Luis Chato for this! */
if (e.offsetHeight == 0) {
/** Safari 2 doesn't correctly grab the offsetTop of a table row
this is detailed here:
http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
note that firefox will return a text node as a first child, so designing a more thorough
solution may need to take that into account, for now this seems to work in firefox, safari, ie */
e = e.firstChild; // a table cell
}
while (e.offsetParent){
left += e.offsetLeft;
top += e.offsetTop;
e = e.offsetParent;
}
left += e.offsetLeft;
top += e.offsetTop;
return {x:left, y:top};
},
mousemove: function(ev) {
if (jQuery.tableDnD.dragObject == null) {
return;
}
var dragObj = jQuery(jQuery.tableDnD.dragObject);
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
var mousePos = jQuery.tableDnD.mouseCoords(ev);
var y = mousePos.y - jQuery.tableDnD.mouseOffset.y;
//auto scroll the window
var yOffset = window.pageYOffset;
if (document.all) {
// Windows version
//yOffset=document.body.scrollTop;
if (typeof document.compatMode != 'undefined' &&
document.compatMode != 'BackCompat') {
yOffset = document.documentElement.scrollTop;
}
else if (typeof document.body != 'undefined') {
yOffset=document.body.scrollTop;
}
}
if (mousePos.y-yOffset < config.scrollAmount) {
window.scrollBy(0, -config.scrollAmount);
} else {
var windowHeight = window.innerHeight ? window.innerHeight
: document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight;
if (windowHeight-(mousePos.y-yOffset) < config.scrollAmount) {
window.scrollBy(0, config.scrollAmount);
}
}
if (y != jQuery.tableDnD.oldY) {
// work out if we're going up or down...
var movingDown = y > jQuery.tableDnD.oldY;
// update the old value
jQuery.tableDnD.oldY = y;
// update the style to show we're dragging
if (config.onDragClass) {
dragObj.addClass(config.onDragClass);
} else {
dragObj.css(config.onDragStyle);
}
// If we're over a row then move the dragged row to there so that the user sees the
// effect dynamically
var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y);
if (currentRow) {
// TODO worry about what happens when there are multiple TBODIES
if (movingDown && jQuery.tableDnD.dragObject != currentRow) {
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling);
} else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) {
jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow);
}
}
}
return false;
},
/** We're only worried about the y position really, because we can only move rows up and down */
findDropTargetRow: function(draggedRow, y) {
var rows = jQuery.tableDnD.currentTable.rows;
for (var i=0; i<rows.length; i++) {
var row = rows[i];
var rowY = this.getPosition(row).y;
var rowHeight = parseInt(row.offsetHeight)/2;
if (row.offsetHeight == 0) {
rowY = this.getPosition(row.firstChild).y;
rowHeight = parseInt(row.firstChild.offsetHeight)/2;
}
// Because we always have to insert before, we need to offset the height a bit
if ((y > rowY - rowHeight) && (y < (rowY + rowHeight))) {
// that's the row we're over
// If it's the same as the current row, ignore it
if (row == draggedRow) {return null;}
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
if (config.onAllowDrop) {
if (config.onAllowDrop(draggedRow, row)) {
return row;
} else {
return null;
}
} else {
// If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
var nodrop = jQuery(row).hasClass("nodrop");
if (! nodrop) {
return row;
} else {
return null;
}
}
return row;
}
}
return null;
},
mouseup: function(e) {
if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) {
var droppedRow = jQuery.tableDnD.dragObject;
var config = jQuery.tableDnD.currentTable.tableDnDConfig;
// If we have a dragObject, then we need to release it,
// The row will already have been moved to the right place so we just reset stuff
if (config.onDragClass) {
jQuery(droppedRow).removeClass(config.onDragClass);
} else {
jQuery(droppedRow).css(config.onDropStyle);
}
jQuery.tableDnD.dragObject = null;
if (config.onDrop) {
// Call the onDrop method if there is one
config.onDrop(jQuery.tableDnD.currentTable, droppedRow);
}
jQuery.tableDnD.currentTable = null; // let go of the table too
}
},
serialize: function() {
if (jQuery.tableDnD.currentTable) {
return jQuery.tableDnD.serializeTable(jQuery.tableDnD.currentTable);
} else {
return "Error: No Table id set, you need to set an id on your table and every row";
}
},
serializeTable: function(table) {
var result = "";
var tableId = table.id;
var rows = table.rows;
for (var i=0; i<rows.length; i++) {
if (result.length > 0) result += "&";
var rowId = rows[i].id;
if (rowId && rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
}
result += tableId + '[]=' + rowId;
}
return result;
},
serializeTables: function() {
var result = "";
this.each(function() {
// this is now bound to each matching table
result += jQuery.tableDnD.serializeTable(this);
});
return result;
}
}
jQuery.fn.extend(
{
tableDnD : jQuery.tableDnD.build,
tableDnDUpdate : jQuery.tableDnD.updateTables,
tableDnDSerialize: jQuery.tableDnD.serializeTables
}
);

View file

@ -1 +0,0 @@
This file not actually used by django-todo - here to satisfy the test runner.

View file

@ -1,23 +0,0 @@
{% extends "todo/base.html" %}
{% block page_heading %}{% endblock %}
{% block title %}Add Todo List{% endblock %}
{% block content %}
<h2>Add a list:</h2>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="id_name">List Name</label>
<input type="text" class="form-control" id="id_name" name="name" aria-describedby="inputNameHelp" placeholder="">
<small id="inputNameHelp" class="form-text text-muted">The full display name for this list.</small>
</div>
<div class="form-group">
<label for="id_group">Group</label>
{{form.group}}
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}

View file

@ -1,47 +0,0 @@
{% extends "todo/base.html" %}
{% block page_heading %}{% endblock %}
{% block title %}File Ticket{% endblock %}
{% block content %}
<h2>{{ task }}</h2>
<form action="" method="POST">
{% csrf_token %}
<h3>File Trouble Ticket</h3>
<p>
Have a support issue? Use this form to report the difficulty - we'll get right back to you.
</p>
{% if form.errors %}
{% for error in form.errors %}
<ul class="errorlist">
<li>
<strong>The {{ error|escape }} field is required.</strong>
</li>
</ul>
{% endfor %}
{% endif %}
<form action="" name="add_task" method="post">
{% csrf_token %}
<div class="form-group">
<label for="id_title" name="title">Subject</label>
<input type="text" class="form-control" id="id_title" name="title" required placeholder="Our spaceship doesn't go"
value="{% if form.title.value %}{{ form.title.value }}{% endif %}">
</div>
<div class="form-group">
<label for="id_note">Description</label>
<textarea class="form-control" id="id_note" name="note" rows="5"
aria-describedby="inputNoteHelp">{% if form.note.value %}{{ form.note.value }}{% endif %}</textarea>
<small id="inputNoteHelp" class="form-text text-muted">
Describe the issue. Please include essential details.
</small>
</div>
<input type="hidden" id="id_priority" name="priority" value="50">
<p><input type="submit" class="btn btn-primary" name="add_task" value="Submit"></p>
</form>
{% endblock %}

View file

@ -1,7 +0,0 @@
{% extends "base.html" %}
{% load staticfiles %}
{% block extrahead %}
<!-- CSS and JavaScripts for django-todo -->
<link rel="stylesheet" type="text/css" href="{% static 'todo/css/styles.css' %}" />
{% endblock extrahead %}

View file

@ -1,33 +0,0 @@
{% extends "todo/base.html" %}
{% block title %}Delete list{% endblock %}
{% block content %}
{% if user.is_staff %}
<h1>Delete entire list: {{ task_list.name }} ?</h1>
<p>Category tally:</p>
<ul>
<li>Incomplete: {{ task_count_undone }} </li>
<li>Complete: {{ task_count_done }} </li>
<li>
<strong>Total: {{ task_count_total }}</strong>
</li>
</ul>
<p> ... all of which will be irretrievably
<strong>blown away</strong>. Are you sure you want to do that?</p>
<form action="" method="post" accept-charset="utf-8">
{% csrf_token %}
<input type="hidden" name="task_list" value="{{ task_list.id }}">
<p>
<a href="{% url 'todo:list_detail' task_list.id task_list.slug %}" class="btn btn-success">Return to list: {{ task_list.name }}</a>
<input type="submit" name="delete-confirm" value="Do it! &rarr;" class="btn btn-danger">
</p>
</form>
{% else %}
<p>Sorry, you don't have permission to delete lists. Please contact your group administrator.</p>
{% endif %} {% endblock %}

View file

@ -1,17 +0,0 @@
{{ task.assigned_to.first_name }} -
A new task on the list {{ task.task_list.name }} has been assigned to you by {{ task.created_by.get_full_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.task_list.name }}:
http://{{ site }}{% url 'todo:list_detail' task.task_list.id task.task_list.slug %}

View file

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

View file

@ -1,16 +0,0 @@
A new task comment has been added.
Task: {{ task.title }}
Commenter: {{ user.first_name }} {{ user.last_name }}
Comment:
{% autoescape off %}
{{ body }}
{% endautoescape %}
Task details/comments:
https://{{ site }}{% url 'todo:task_detail' task.id %}
List {{ task.task_list.name }}:
https://{{ site }}{% url 'todo:list_detail' task.task_list.id task.task_list.slug %}

View file

@ -1,54 +0,0 @@
{# Form used by both Add Task and Edit Task views #}
<form action="" name="add_task" method="post">
{% csrf_token %}
<div id="AddEditTask" class="collapse mt-3">
<div class="form-group">
<label for="id_title" name="title">Task</label>
<input type="text" class="form-control" id="id_title" name="title" required placeholder="Task title"
value="{% if form.title.value %}{{ form.title.value }}{% endif %}">
</div>
<div class="form-group">
<label for="id_note">Description</label>
<textarea class="form-control" id="id_note" name="note" rows="5"
aria-describedby="inputNoteHelp">{% if form.note.value %}{{ form.note.value }}{% endif %}</textarea>
<small id="inputNoteHelp" class="form-text text-muted">
Describe the task or bug. Provide steps to reproduce the issue.
</small>
</div>
<div class="form-group">
<label for="id_due_date">Due Date</label>
<input type="date" class="form-control" id="id_due_date" name="due_date"
value="{% if form.due_date.value %}{{ form.due_date.value|date:"Y-m-d" }}{% endif %}">
</div>
<div class="form-group">
<label for="id_assigned_to">Assigned To</label>
{# See todo.forms.AddEditTaskForm #}
{{form.assigned_to}}
</div>
<div class="form-group">
<label for="id_notify">Notify</label>
<input type="checkbox" checked="checked" class="form-control" id="id_notify" name="notify" aria-describedby="inputNotifyHelp"
value="{{ form.notify.text }}">
<small id="inputNotifyHelp" class="form-text text-muted">
Email notifications will only be sent if task is assigned to someone other than yourself.
</small>
</div>
<input type="hidden" name="priority"
value="{% if form.priority.value %}{{ form.priority.value }}{% else %}999{% endif %}" id="id_priority">
<input type="hidden" name="created_by" value="{{ request.user.id }}" id="id_created_by">
<input type="hidden" name="task_list" value="{{ form.task_list.value }}" id="id_task_list">
<p>
<input type="submit" name="add_edit_task" value="Submit" class="btn btn-primary">
</p>
</div>
</form>

View file

@ -1,12 +0,0 @@
{% if list_slug != "mine" %}
{% if view_completed %}
<a href="{% url 'todo:list_detail' list_id list_slug %}" class="btn btn-sm btn-warning">View incomplete tasks</a>
{% else %}
<a href="{% url 'todo:list_detail_completed' list_id list_slug %}" class="btn btn-sm btn-info">View completed tasks</a>
{% endif %}
<a href="{% url 'todo:del_list' list_id list_slug %}" class="btn btn-sm btn-danger">Delete this list</a>
{% endif %}

View file

@ -1,122 +0,0 @@
{% extends "todo/base.html" %}
{% load staticfiles %}
{% block title %}Todo List: {{ task_list.name }}{% endblock %}
{% block content %}
{% if list_slug != "mine" %}
<button class="btn btn-primary" id="AddTaskButton" type="button"
data-toggle="collapse" data-target="#AddEditTask">Add Task</button>
{# Task edit / new task form #}
{% include 'todo/include/task_edit.html' %}
<hr />
{% endif %}
{% if tasks %}
{% if list_slug == "mine" %}
<h1>Tasks assigned to me (in all groups)</h1>
{% else %}
<h1>{{ view_completed|yesno:"Completed tasks, Tasks" }} in "{{ task_list.name }}"</h1>
<p><small><i>In workgroup "{{ task_list.group }}" - drag rows to set priorities.</i></small></p>
{% endif %}
<form action="" name="show_tasks" method="post">
{% csrf_token %}
<table class="table" id="tasktable">
<tr>
<th>Done</th>
<th>Task</th>
<th>Created</th>
<th>Due on</th>
<th>Owner</th>
<th>Assigned</th>
<th>Note</th>
<th>Comm</th>
<th>Del</th>
</tr>
{% for task in tasks %}
<tr id="{{ task.id }}">
<td>
<input type="checkbox" name="toggle_done_tasks"
value="{{ task.id }}" id="{{ task.id }}"
{% if task.completed %}checked{% endif %}>
</td>
<td>
<a href="{% url 'todo:task_detail' task.id %}">{{ task.title|truncatewords:10 }}</a>
</td>
<td>
{{ task.created_date|date:"m/d/Y" }}
</td>
<td>
<span {% if task.overdue_status %}class="overdue"{% endif %}>
{{ task.due_date|date:"m/d/Y" }}
</span>
</td>
<td>
{{ task.created_by }}
</td>
<td>
{% if task.assigned_to %}{{ task.assigned_to }}{% else %}Anyone{% endif %}
</td>
<td>
{% if task.note %}&asymp;{% endif %}
</td>
<td>
{% if task.comment_set.all.count > 0 %}{{ task.comment_set.all.count }}{% endif %}
</td>
<td>
<input type="checkbox" name="toggle_deleted_tasks" value="{{ task.id }}" id="{{ task.id }}">
</td>
</tr>
{% endfor %}
</table>
<input type="submit" name="process_tasks" value="Process selection" class="btn btn-sm btn-success">
{% include 'todo/include/toggle_delete.html' %}
</form>
{% else %}
<h4>No tasks on this list yet (add one!)</h4>
{% include 'todo/include/toggle_delete.html' %}
{% endif %}
{% endblock %}
{% block extra_js %}
<script src="{% static 'todo/js/jquery.tablednd_0_5.js' %}" type="text/javascript"></script>
<script type="text/javascript">
function order_tasks(data) {
// The JQuery plugin tableDnD provides a serialize() function which provides the re-ordered
// data in a list. We pass that list as an object called "data" to a Django view
// to save the re-ordered data into the database.
$.post("{% url 'todo:reorder_tasks' %}", data, "json");
return false;
};
$(document).ready(function() {
// Initialise the task table for drag/drop re-ordering
$("#tasktable").tableDnD();
$('#tasktable').tableDnD({
onDrop: function(table, row) {
order_tasks($.tableDnD.serialize());
}
});
});
// When adding a task, change the text of the Add Task button
function handleClick()
{
console.log(this.innerHTML);
this.innerHTML = (this.innerHTML == 'Add Task' ? 'Cancel' : 'Add Task');
}
document.getElementById('AddTaskButton').onclick=handleClick;
</script>
{% endblock extra_js %}

View file

@ -1,33 +0,0 @@
{% extends "todo/base.html" %}
{% block title %}{{ list_title }} Todo Lists{% endblock %}
{% block content %}
<h1>Todo Lists</h1>
<p>{{ task_count }} tasks in {{ list_count }} list{{ list_count|pluralize }}</p>
{% regroup lists by group as section_list %}
{% for group in section_list %}
<h3>Group: {{ group.grouper }}</h3>
<ul class="list-group mb-4">
{% for task in group.list %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{% url 'todo:list_detail' task.id task.slug %}">{{ task.name }}</a>
<span class="badge badge-primary badge-pill">{{ task.task_set.count }}</span>
</li>
{% endfor %}
</ul>
{% endfor %}
<div class="mt-3">
{% if user.is_staff %}
<a href="{% url 'todo:add_list' %}" class="btn btn-primary">Create new todo list</a>
{% else %}
<a href="" class="btn btn-primary disabled">If you were staff, you could create a new list</a>
{% endif %}
</div>
{% endblock %}

View file

@ -1,30 +0,0 @@
{% extends "todo/base.html" %}
{% block title %}Search results{% endblock %}
{% block content_title %}<h2 class="page_title">Search</h2>{% endblock %}
{% block content %}
{% if found_tasks %}
<h2>{{found_tasks.count}} search results for term: "{{ query_string }}"</h2>
<div class="post_list">
{% for f in found_tasks %}
<p>
<strong>
<a href="{% url 'todo:task_detail' f.id %}">{{ f.title }}</a>
</strong>
<br />
<span class="minor">
In list:
<a href="{% url 'todo:list_detail' f.task_list.id f.task_list.slug %}">
{{ f.task_list.name }}
</a>
<br /> Assigned to: {% if f.assigned_to %}{{ f.assigned_to }}{% else %}Anyone{% endif %}
<br /> Complete: {{ f.completed|yesno:"Yes,No" }}
</span>
</p>
{% endfor %}
</div>
{% else %}
<h2> No results to show, sorry.</h2>
{% endif %}
{% endblock %}

View file

@ -1,89 +0,0 @@
{% extends "todo/base.html" %}
{% block title %}Task:{{ task.title }}{% endblock %}
{% block content %}
<div class="row">
<div class="col-sm-8">
<h3 class="card-title">{{ task.title }}</h3>
{% if task.note %}
<p class="card-text">{{ task.note|safe|urlize|linebreaks }}</p>
{% endif %}
</div>
<div class="col-sm-4">
<ul class="list-group">
<li class="list-group-item">
<form action="" method="post">
{% csrf_token %}
<button class="btn btn-sm btn-primary" id="EditTaskButton" type="button"
data-toggle="collapse" data-target="#AddEditTask">Edit Task</button>
<input class="btn btn-sm btn-info" id="CompleteTaskButton" type="submit" name="toggle_done"
value="{% if task.completed %}Mark Incomplete{% else %}Mark Done{% endif %}">
</form>
</li>
<li class="list-group-item">
<strong>Assigned to:</strong>
{% if task.assigned_to %} {{ task.assigned_to.get_full_name }} {% else %} Anyone {% endif %}
</li>
<li class="list-group-item">
<strong>Reported by:</strong> {{ task.created_by.get_full_name }}
</li>
<li class="list-group-item">
<strong>Due date:</strong> {{ task.due_date }}
</li>
{% if task.completed %}
<li class="list-group-item">
<strong>Completed on:</strong> {{ task.completed_date}}
</li>
{% else %}
<li class="list-group-item">
<strong>Completed:</strong> {{ task.completed|yesno:"Yes,No" }}
</li>
{% endif %}
<li class="list-group-item">
<strong>In list:</strong>
<a href="{% url 'todo:list_detail' task.task_list.id task.task_list.slug %}">
{{ task.task_list }}
</a>
</li>
</ul>
</div>
</div>
<div id="TaskEdit">
{# Task edit / new task form #}
{% include 'todo/include/task_edit.html' %}
</div>
<h5>Add comment</h5>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<textarea class="form-control" name="comment-body" rows="3"></textarea>
</div>
<input class="btn btn-sm btn-primary" type="submit" name="add_comment" value="Add Comment">
</form>
<div class="task_comments mt-4">
{% if comment_list %}
<h5>Comments on this task</h5>
{% for comment in comment_list %}
<p>
<strong>{{ comment.author.first_name }}
{{ comment.author.last_name }},
{{ comment.date|date:"F d Y P" }}
</strong>
</p>
{{ comment.body|safe|urlize|linebreaks }}
{% endfor %}
{% else %}
<h5>No comments (yet).</h5>
{% endif %}
</div>
{% endblock %}

View file

View file

@ -1,27 +0,0 @@
import pytest
from django.contrib.auth.models import Group
from todo.models import Task, TaskList
@pytest.fixture
def todo_setup(django_user_model):
# Two groups with different users, two sets of tasks.
g1 = Group.objects.create(name="Workgroup One")
u1 = django_user_model.objects.create_user(username="u1", password="password", email="u1@example.com")
u1.groups.add(g1)
tlist1 = TaskList.objects.create(group=g1, name="Zip", slug="zip")
Task.objects.create(created_by=u1, title="Task 1", task_list=tlist1, priority=1)
Task.objects.create(created_by=u1, title="Task 2", task_list=tlist1, priority=2, completed=True)
Task.objects.create(created_by=u1, title="Task 3", task_list=tlist1, priority=3)
g2 = Group.objects.create(name="Workgroup Two")
u2 = django_user_model.objects.create_user(username="u2", password="password", email="u2@example.com")
u2.groups.add(g2)
tlist2 = TaskList.objects.create(group=g2, name="Zap", slug="zap")
Task.objects.create(created_by=u2, title="Task 1", task_list=tlist2, priority=1)
Task.objects.create(created_by=u2, title="Task 2", task_list=tlist2, priority=2, completed=True)
Task.objects.create(created_by=u2, title="Task 3", task_list=tlist2, priority=3)

View file

@ -1,94 +0,0 @@
import pytest
from django.core import mail
from todo.models import Task, Comment
from todo.utils import toggle_done, toggle_deleted, send_notify_mail, send_email_to_thread_participants
@pytest.fixture()
# Set up an in-memory mail server to receive test emails
def email_backend_setup(settings):
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
def test_toggle_done(todo_setup):
"""Utility function takes an array of POSTed IDs and changes their `completed` status.
"""
u1_tasks = Task.objects.filter(created_by__username="u1")
completed = u1_tasks.filter(completed=True)
incomplete = u1_tasks.filter(completed=False)
# Expected counts in fixture data
assert u1_tasks.count() == 3
assert incomplete.count() == 2
assert completed.count() == 1
# Mark incomplete tasks completed and check again
toggle_done([t.id for t in incomplete])
now_completed = u1_tasks.filter(created_by__username="u1", completed=True)
assert now_completed.count() == 3
# Mark all incomplete and check again
toggle_done([t.id for t in now_completed])
now_incomplete = u1_tasks.filter(created_by__username="u1", completed=False)
assert now_incomplete.count() == 3
def test_toggle_deleted(todo_setup):
"""Unlike toggle_done, delete means delete, so it's not really a toggle.
"""
u1_tasks = Task.objects.filter(created_by__username="u1")
assert u1_tasks.count() == 3
t1 = u1_tasks.first()
t2 = u1_tasks.last()
toggle_deleted([t1.id, t2.id, ])
u1_tasks = Task.objects.filter(created_by__username="u1")
assert u1_tasks.count() == 1
def test_send_notify_mail_not_me(todo_setup, django_user_model, email_backend_setup):
"""Assign a task to someone else, mail should be sent.
TODO: Future tests could check for email contents.
"""
u1 = django_user_model.objects.get(username="u1")
u2 = django_user_model.objects.get(username="u2")
task = Task.objects.filter(created_by=u1).first()
task.assigned_to = u2
task.save()
send_notify_mail(task)
assert len(mail.outbox) == 1
def test_send_notify_mail_myself(todo_setup, django_user_model, email_backend_setup):
"""Assign a task to myself, no mail should be sent.
"""
u1 = django_user_model.objects.get(username="u1")
task = Task.objects.filter(created_by=u1).first()
task.assigned_to = u1
task.save()
send_notify_mail(task)
assert len(mail.outbox) == 0
def test_send_email_to_thread_participants(todo_setup, django_user_model, email_backend_setup):
"""For a given task authored by one user, add comments by two other users.
Notification email should be sent to all three users."""
u1 = django_user_model.objects.get(username="u1")
task = Task.objects.filter(created_by=u1).first()
u3 = django_user_model.objects.create_user(username="u3", password="zzz", email="u3@example.com")
u4 = django_user_model.objects.create_user(username="u4", password="zzz", email="u4@example.com")
Comment.objects.create(author=u3, task=task, body="Hello", )
Comment.objects.create(author=u4, task=task, body="Hello", )
send_email_to_thread_participants(task, "test body", u1)
assert len(mail.outbox) == 1 # One message to multiple recipients
assert 'u1@example.com' in mail.outbox[0].recipients()
assert 'u3@example.com' in mail.outbox[0].recipients()
assert 'u4@example.com' in mail.outbox[0].recipients()

View file

@ -1,164 +0,0 @@
import pytest
from django.contrib.auth.models import Group
from django.urls import reverse
from todo.models import Task, TaskList
"""
First the "smoketests" - do they respond at all for a logged in admin user?
Next permissions tests - some views should respond for staffers only.
After that, view contents and behaviors.
"""
# ### SMOKETESTS ###
@pytest.mark.django_db
def test_todo_setup(todo_setup):
assert Task.objects.all().count() == 6
def test_view_list_lists(todo_setup, admin_client):
url = reverse('todo:lists')
response = admin_client.get(url)
assert response.status_code == 200
def test_view_reorder(todo_setup, admin_client):
url = reverse('todo:reorder_tasks')
response = admin_client.get(url)
assert response.status_code == 201 # Special case return value expected
def test_view_external_add(todo_setup, admin_client, settings):
default_list = TaskList.objects.first()
settings.TODO_DEFAULT_LIST_SLUG = default_list.slug
assert settings.TODO_DEFAULT_LIST_SLUG == default_list.slug
url = reverse('todo:external_add')
response = admin_client.get(url)
assert response.status_code == 200
def test_view_mine(todo_setup, admin_client):
url = reverse('todo:mine')
response = admin_client.get(url)
assert response.status_code == 200
def test_view_list_completed(todo_setup, admin_client):
tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:list_detail_completed', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug})
response = admin_client.get(url)
assert response.status_code == 200
def test_view_list(todo_setup, admin_client):
tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:list_detail', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug})
response = admin_client.get(url)
assert response.status_code == 200
def test_del_list(todo_setup, admin_client):
tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:del_list', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug})
response = admin_client.get(url)
assert response.status_code == 200
def test_view_add_list(todo_setup, admin_client):
url = reverse('todo:add_list')
response = admin_client.get(url)
assert response.status_code == 200
def test_view_task_detail(todo_setup, admin_client):
task = Task.objects.first()
url = reverse('todo:task_detail', kwargs={'task_id': task.id})
response = admin_client.get(url)
assert response.status_code == 200
def test_view_search(todo_setup, admin_client):
url = reverse('todo:search')
response = admin_client.get(url)
assert response.status_code == 200
# ### PERMISSIONS ###
"""
Some views are for staff users only.
We've already smoke-tested with Admin user - try these with normal user.
These exercise our custom @staff_only decorator without calling that function explicitly.
"""
def test_view_add_list_nonadmin(todo_setup, client):
url = reverse('todo:add_list')
client.login(username="you", password="password")
response = client.get(url)
assert response.status_code == 403
def test_view_del_list_nonadmin(todo_setup, client):
tlist = TaskList.objects.get(slug="zip")
url = reverse('todo:del_list', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug})
client.login(username="you", password="password")
response = client.get(url)
assert response.status_code == 403
def test_view_list_mine(todo_setup, client):
"""View a list in a group I belong to.
"""
tlist = TaskList.objects.get(slug="zip") # User u1 is in this group's list
url = reverse('todo:list_detail', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug})
client.login(username="u1", password="password")
response = client.get(url)
assert response.status_code == 200
def test_view_list_not_mine(todo_setup, client):
"""View a list in a group I don't belong to.
"""
tlist = TaskList.objects.get(slug="zip") # User u1 is in this group, user u2 is not.
url = reverse('todo:list_detail', kwargs={'list_id': tlist.id, 'list_slug': tlist.slug})
client.login(username="u2", password="password")
response = client.get(url)
assert response.status_code == 403
def test_view_task_mine(todo_setup, client):
# Users can always view their own tasks
task = Task.objects.filter(created_by__username="u1").first()
client.login(username="u1", password="password")
url = reverse('todo:task_detail', kwargs={'task_id': task.id})
response = client.get(url)
assert response.status_code == 200
def test_view_task_my_group(todo_setup, client, django_user_model):
# User can always view tasks that are NOT theirs IF the task is in a shared group.
# u1 and u2 are in different groups in the fixture -
# Put them in the same group.
g1 = Group.objects.get(name="Workgroup One")
u2 = django_user_model.objects.get(username="u2")
u2.groups.add(g1)
# Now u2 should be able to view one of u1's tasks.
task = Task.objects.filter(created_by__username="u1").first()
url = reverse('todo:task_detail', kwargs={'task_id': task.id})
client.login(username="u2", password="password")
response = client.get(url)
assert response.status_code == 200
def test_view_task_not_in_my_group(todo_setup, client):
# User canNOT view a task that isn't theirs if the two users are not in a shared group.
# For this we can use the fixture data as-is.
task = Task.objects.filter(created_by__username="u1").first()
url = reverse('todo:task_detail', kwargs={'task_id': task.id})
client.login(username="u2", password="password")
response = client.get(url)
assert response.status_code == 403

View file

@ -1,63 +0,0 @@
from django.urls import path
from todo import views
app_name = 'todo'
urlpatterns = [
path(
'',
views.list_lists,
name="lists"),
# View reorder_tasks is only called by JQuery for drag/drop task ordering.
path(
'reorder_tasks/',
views.reorder_tasks,
name="reorder_tasks"),
# Allow users to post tasks from outside django-todo (e.g. for filing tickets - see docs)
path(
'ticket/add/',
views.external_add,
name="external_add"),
# Three paths into `list_detail` view
path(
'mine/',
views.list_detail,
{'list_slug': 'mine'},
name="mine"),
path(
'<int:list_id>/<str:list_slug>/completed/',
views.list_detail,
{'view_completed': True},
name='list_detail_completed'),
path(
'<int:list_id>/<str:list_slug>/',
views.list_detail,
name='list_detail'),
path(
'<int:list_id>/<str:list_slug>/delete/',
views.del_list,
name="del_list"),
path(
'add_list/',
views.add_list,
name="add_list"),
path(
'task/<int:task_id>/',
views.task_detail,
name='task_detail'),
path(
'search/',
views.search,
name="search"),
]

View file

@ -1,73 +0,0 @@
import datetime
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.template.loader import render_to_string
from todo.models import Task, Comment
def toggle_done(task_ids):
"""Check for tasks in the mark_done POST array. If present, change status to complete.
Takes a list of task IDs. Returns list of status change strings.
"""
_ret = []
for task_id in task_ids:
i = Task.objects.get(id=task_id)
old_state = "completed" if i.completed else "incomplete"
i.completed = not i.completed # Invert the done state, either way
new_state = "completed" if i.completed else "incomplete"
i.completed_date = datetime.datetime.now()
i.save()
_ret.append("Task \"{i}\" changed from {o} to {n}.".format(i=i.title, o=old_state, n=new_state))
return _ret
def toggle_deleted(deleted_task_ids):
"""Delete selected tasks. Returns list of status change strings.
"""
_ret = []
for task_id in deleted_task_ids:
i = Task.objects.get(id=task_id)
_ret.append("Task \"{i}\" deleted.".format(i=i.title))
i.delete()
return _ret
def send_notify_mail(new_task):
# Send email to assignee if task is assigned to someone other than submittor.
# Unassigned tasks should not try to notify.
if not new_task.assigned_to == new_task.created_by:
current_site = Site.objects.get_current()
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, })
send_mail(
email_subject, email_body, new_task.created_by.email,
[new_task.assigned_to.email], fail_silently=False)
def send_email_to_thread_participants(task, msg_body, user):
# Notify all previous commentors on a Task about a new comment.
current_site = Site.objects.get_current()
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': msg_body, 'site': current_site, 'user': user}
)
# Get list of all thread participants - everyone who has commented, plus task creator.
commenters = Comment.objects.filter(task=task)
recip_list = [ca.author.email for ca in commenters]
recip_list.append(task.created_by.email)
recip_list = list(set(recip_list)) # Eliminate duplicates
send_mail(email_subject, email_body, task.created_by.email, recip_list, fail_silently=False)

View file

@ -1,395 +0,0 @@
import datetime
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.db import IntegrityError
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.text import slugify
from django.views.decorators.csrf import csrf_exempt
from todo.forms import AddTaskListForm, AddEditTaskForm, AddExternalTaskForm, SearchForm
from todo.models import Task, TaskList, Comment
from todo.utils import (
toggle_done,
toggle_deleted,
send_notify_mail,
send_email_to_thread_participants,
)
def staff_only(function):
"""
Custom view decorator allows us to raise 403 on insufficient permissions,
rather than redirect user to login view.
"""
def wrap(request, *args, **kwargs):
if request.user.is_staff:
return function(request, *args, **kwargs)
else:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
@login_required
def list_lists(request) -> HttpResponse:
"""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.warning(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:
lists = TaskList.objects.all().order_by('group', 'name')
else:
lists = TaskList.objects.filter(group__in=request.user.groups.all()).order_by('group', 'name')
list_count = lists.count()
# superusers see all lists, so count shouldn't filter by just lists the admin belongs to
if request.user.is_superuser:
task_count = Task.objects.filter(completed=0).count()
else:
task_count = Task.objects.filter(completed=0).filter(task_list__group__in=request.user.groups.all()).count()
context = {
"lists": lists,
"thedate": thedate,
"searchform": searchform,
"list_count": list_count,
"task_count": task_count,
}
return render(request, 'todo/list_lists.html', context)
@staff_only
@login_required
def del_list(request, list_id: int, list_slug: str) -> HttpResponse:
"""Delete an entire list. Danger Will Robinson! Only staff members should be allowed to access this view.
"""
task_list = get_object_or_404(TaskList, slug=list_slug)
# Ensure user has permission to delete list. Admins can delete all lists.
# Get the group this list belongs to, and check whether current user is a member of that group.
# FIXME: This means any group member can delete lists, which is probably too permissive.
if task_list.group not in request.user.groups.all() and not request.user.is_staff:
raise PermissionDenied
if request.method == 'POST':
TaskList.objects.get(id=task_list.id).delete()
messages.success(request, "{list_name} is gone.".format(list_name=task_list.name))
return redirect('todo:lists')
else:
task_count_done = Task.objects.filter(task_list=task_list.id, completed=True).count()
task_count_undone = Task.objects.filter(task_list=task_list.id, completed=False).count()
task_count_total = Task.objects.filter(task_list=task_list.id).count()
context = {
"task_list": task_list,
"task_count_done": task_count_done,
"task_count_undone": task_count_undone,
"task_count_total": task_count_total,
}
return render(request, 'todo/del_list.html', context)
@login_required
def list_detail(request, list_id=None, list_slug=None, view_completed=False):
"""Display and manage tasks in a todo list.
"""
# Defaults
task_list = None
form = None
# Which tasks to show on this list view?
if list_slug == "mine":
tasks = Task.objects.filter(assigned_to=request.user)
else:
# Show a specific list, ensuring permissions.
task_list = get_object_or_404(TaskList, id=list_id)
if task_list.group not in request.user.groups.all() and not request.user.is_staff:
raise PermissionDenied
tasks = Task.objects.filter(task_list=task_list.id)
# Additional filtering
if view_completed:
tasks = tasks.filter(completed=True)
else:
tasks = tasks.filter(completed=False)
if request.POST:
# Process completed and deleted tasks on each POST
results_changed = toggle_done(request.POST.getlist('toggle_done_tasks'))
for res in results_changed:
messages.success(request, res)
results_changed = toggle_deleted(request.POST.getlist('toggle_deleted_tasks'))
for res in results_changed:
messages.success(request, res)
# ######################
# Add New Task Form
# ######################
if request.POST.getlist('add_edit_task'):
form = AddEditTaskForm(request.user, request.POST, initial={
'assigned_to': request.user.id,
'priority': 999,
'task_list': task_list
})
if form.is_valid():
new_task = form.save(commit=False)
new_task.created_date = timezone.now()
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 and new_task.assigned_to != request.user:
send_notify_mail(new_task)
messages.success(request, "New task \"{t}\" has been added.".format(t=new_task.title))
return redirect(request.path)
else:
# Don't allow adding new tasks on some views
if list_slug not in ["mine", "recent-add", "recent-complete", ]:
form = AddEditTaskForm(request.user, initial={
'assigned_to': request.user.id,
'priority': 999,
'task_list': task_list,
})
context = {
"list_id": list_id,
"list_slug": list_slug,
"task_list": task_list,
"form": form,
"tasks": tasks,
"view_completed": view_completed,
}
return render(request, 'todo/list_detail.html', context)
@login_required
def task_detail(request, task_id: int) -> HttpResponse:
"""View task details. Allow task details to be edited. Process new comments on task.
"""
task = get_object_or_404(Task, pk=task_id)
comment_list = Comment.objects.filter(task=task_id)
# Ensure user has permission to view task. Admins can view all tasks.
# Get the group this task belongs to, and check whether current user is a member of that group.
if task.task_list.group not in request.user.groups.all() and not request.user.is_staff:
raise PermissionDenied
# Save submitted comments
if request.POST.get('add_comment'):
Comment.objects.create(
author=request.user,
task=task,
body=request.POST['comment-body'],
)
send_email_to_thread_participants(task, request.POST['comment-body'], request.user)
messages.success(request, "Comment posted. Notification email sent to thread participants.")
# Save task edits
if request.POST.get('add_edit_task'):
form = AddEditTaskForm(request.user, request.POST, instance=task, initial={'task_list': task.task_list})
if form.is_valid():
form.save()
messages.success(request, "The task has been edited.")
return redirect('todo:list_detail', list_id=task.task_list.id, list_slug=task.task_list.slug)
else:
form = AddEditTaskForm(request.user, instance=task, initial={'task_list': task.task_list})
# Mark complete
if request.POST.get('toggle_done'):
results_changed = toggle_done([task.id, ])
for res in results_changed:
messages.success(request, res)
return redirect('todo:task_detail', task_id=task.id,)
if task.due_date:
thedate = task.due_date
else:
thedate = datetime.datetime.now()
context = {
"task": task,
"comment_list": comment_list,
"form": form,
"thedate": thedate,
}
return render(request, 'todo/task_detail.html', context)
@csrf_exempt
@login_required
def reorder_tasks(request) -> HttpResponse:
"""Handle task re-ordering (priorities) from JQuery drag/drop in list_detail.html
"""
newtasklist = request.POST.getlist('tasktable[]')
if newtasklist:
# First task in received list is always empty - remove it
del newtasklist[0]
# Re-prioritize each task in list
i = 1
for t in newtasklist:
newtask = Task.objects.get(pk=t)
newtask.priority = i
newtask.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)
@staff_only
@login_required
def add_list(request) -> HttpResponse:
"""Allow users to add a new todo list to the group they're in.
"""
if request.POST:
form = AddTaskListForm(request.user, request.POST)
if form.is_valid():
try:
newlist = form.save(commit=False)
newlist.slug = slugify(newlist.name)
newlist.save()
messages.success(request, "A new list has been added.")
return redirect('todo:lists')
except IntegrityError:
messages.warning(
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 = AddTaskListForm(request.user, initial={"group": request.user.groups.all()[0]})
else:
form = AddTaskListForm(request.user)
context = {
"form": form,
}
return render(request, 'todo/add_list.html', context)
@login_required
def search(request) -> HttpResponse:
"""Search for tasks user has permission to see.
"""
if request.GET:
query_string = ''
found_tasks = None
if ('q' in request.GET) and request.GET['q'].strip():
query_string = request.GET['q']
found_tasks = Task.objects.filter(
Q(title__icontains=query_string) |
Q(note__icontains=query_string)
)
else:
# What if they selected the "completed" toggle but didn't enter a query string?
# We still need found_tasks in a queryset so it can be "excluded" below.
found_tasks = Task.objects.all()
if 'inc_complete' in request.GET:
found_tasks = found_tasks.exclude(completed=True)
else:
query_string = None
found_tasks =None
# Only include tasks that are in groups of which this user is a member:
if not request.user.is_superuser:
found_tasks = found_tasks.filter(task_list__group__in=request.user.groups.all())
context = {
'query_string': query_string,
'found_tasks': found_tasks
}
return render(request, 'todo/search_results.html', context)
@login_required
def external_add(request) -> HttpResponse:
"""Allow authenticated users who don't have access to the rest of the ticket system to file a ticket
in the list specified in settings (e.g. django-todo can be used a ticket filing system for a school, where
students can file tickets without access to the rest of the todo system).
Publicly filed tickets are unassigned unless settings.DEFAULT_ASSIGNEE exists.
"""
if not settings.TODO_DEFAULT_LIST_SLUG:
raise RuntimeError("This feature requires TODO_DEFAULT_LIST_SLUG: in settings. See documentation.")
if not TaskList.objects.filter(slug=settings.TODO_DEFAULT_LIST_SLUG).exists():
raise RuntimeError("There is no TaskList with slug specified for TODO_DEFAULT_LIST_SLUG in settings.")
if request.POST:
form = AddExternalTaskForm(request.POST)
if form.is_valid():
current_site = Site.objects.get_current()
task = form.save(commit=False)
task.task_list = TaskList.objects.get(slug=settings.TODO_DEFAULT_LIST_SLUG)
task.created_by = request.user
if settings.TODO_DEFAULT_ASSIGNEE:
task.assigned_to = User.objects.get(username=settings.TODO_DEFAULT_ASSIGNEE)
task.save()
# Send email to assignee if we have one
if task.assigned_to:
email_subject = render_to_string("todo/email/assigned_subject.txt", {'task': task.title})
email_body = render_to_string("todo/email/assigned_body.txt", {'task': task, 'site': current_site, })
try:
send_mail(
email_subject, email_body, task.created_by.email,
[task.assigned_to.email, ], fail_silently=False)
except ConnectionRefusedError:
messages.warning(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 redirect(settings.TODO_PUBLIC_SUBMIT_REDIRECT)
else:
form = AddExternalTaskForm(initial={'priority': 999})
context = {
"form": form,
}
return render(request, 'todo/add_task_external.html', context)

5
werf.yml Normal file
View file

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