├── .gitignore ├── README.md ├── README_en.md ├── parser.py ├── sample ├── test.markdown └── test_en.markdown ├── scheduler.py └── spec.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyscheduler 2 | =========== 3 | 4 | For english introduction check out:[README_en](README_en.md) 5 | 6 | 做项目的时候大家都需要的做的一个事情是排计划安排,一般有两种做法:一是用类`Microsoft Project`的软件,这种软件的优点是功能很强大,缺点是有点重,非文本的,想把它嵌入到比如 markdown 文档里面很难;二是自己用表格的语法直接写在一个 markdown 文件里面。我个人比较喜欢第二种做法,这种做法好处是它是纯文本的,不需要借助任务特殊软件就可以展示。它的缺点是功能比较弱: 7 | 8 | * 你如果想要对中间某个任务的时间安排做出调整,那么这个任务后面的所有任务你需要重新排一次。 9 | * 你无法像`Microsoft Project`软件那样给你展示整个项目总的资源情况。 10 | * 你无法对整个任务安排进行筛选,让它只显示某个人的工作 11 | 12 | 这个小项目的作用就是为了解决前面提到的几个问题。 13 | 14 | ## 目前我们支持如下几个特性: 15 | 16 | * [我们现在有可视化UI了](#scheduler-ui) 17 | * [对列出来的任务自动进行排期,并以 Markdown Table 的语法进行输出](#usage) 18 | * [跟踪任务进度](#track-progress) 19 | * [根据任务负责人对任务进行过滤](#filter-by-people) 20 | * [支持对人员请假情况进行记录](#track-leave) 21 | * [支持对一个大任务进行拆解](#task-break) 22 | * [支持显示每个人任务完成情况](#show-all-progress) 23 | * [显示没有开始的任务](#show-undone-task) 24 | 25 | ## Scheduler 命令行用法 26 | 27 | ### 基本用法 28 | 29 | ```bash 30 | scheduler.py [-m ] /path/to/work-breakdown-file.markdown 31 | ``` 32 | 33 | 这个软件的输入是一个`markdown` 文件(已提供,见test.markdown), 你在这个 markdown 文件里面定义你的项目的任务细分,比如下面这段: 34 | 35 | ```bash 36 | # 项目基本信息 37 | * 项目开始时间: 2014-08-21 38 | 39 | # 任务细分 40 | * 任务一 -- 2[James] 41 | * 任务二 -- 1[Lucy] 42 | * 任务三 -- 1[James] 43 | * 任务四 -- 2[Lucy] 44 | ``` 45 | 46 | 上面定义了我们这个项目是从`2014-08-21`开始做,并且定义了我们这个项目的主要工作安排,那么运行以下命令,我们可以得到如下的自动排期表(如果你的 Terminal 使用的是等宽字体的话,你会发现下面这个表格是工整对齐的): 47 | 48 | ```bash 49 | > ./scheduler.py /tmp/test.markdown 50 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 51 | ------ | ------ | -------- | ---------- | ---------- | ---- 52 | 任务一 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 53 | 任务二 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 0% 54 | 任务三 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 55 | 任务四 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 56 | 57 | >>> 总人日: 6.0, 已经完成的人日: 0.0, 完成度: 0.00% 58 | ``` 59 | 60 | 大家也许注意到了,你如果把这段输出保存成一个`markdown`文件,它其实就形成了一个表格。也就是说你只需要维护上面提到的任务基本信息,利用这个小工具可以自动给你生成排期表。 61 | 62 | 63 | ## track-progress 64 | 65 | ### 跟踪任务进度 66 | 67 | 当然,随着时间的推移你可以对你的任务的进度进行更新, 我们支持在 breakdown 文件里面对任务进度进行跟踪,生成的排期里面会自动把进度带过去: 68 | 69 | ```bash 70 | # 项目基本信息 71 | * 项目开始时间: 2014-08-21 72 | 73 | # 任务细分 74 | * 任务一 -- 2[James][100%] 75 | * 任务二 -- 1[Lucy][80%] 76 | * 任务三 -- 1[James] 77 | * 任务四 -- 2[Lucy] 78 | ``` 79 | 80 | 再次运行你会得到这样的表格: 81 | 82 | ```bash 83 | > ./scheduler.py /tmp/test.markdown 84 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 85 | ------ | ------ | -------- | ---------- | ---------- | ---- 86 | 任务一 | James | 2.0 | 2014-08-21 | 2014-08-22 | 100% 87 | 任务二 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 80% 88 | 任务三 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 89 | 任务四 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 90 | 91 | >>> 总人日: 6.0, 已经完成的人日: 2.8, 完成度: 46.67% 92 | ``` 93 | 94 | ## filter-by-people 95 | 96 | ### 根据任务负责人对任务进行过滤 97 | 98 | 如果你想只看看所有分配给`James`的任务,那么加个参数即可: 99 | 100 | ```bash 101 | > ./scheduler.py -m James /tmp/test.markdown 102 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 103 | ------ | ------ | -------- | ---------- | ---------- | ---- 104 | 任务一 | James | 2.0 | 2014-08-21 | 2014-08-22 | 100% 105 | 任务三 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 106 | 107 | >>> 总人日: 3.0, 已经完成的人日: 2.0, 完成度: 66.67% 108 | ``` 109 | 110 | ## track-leave 111 | 112 | ### 支持对人员请假情况进行记录 113 | 114 | 做项目的过程中,人员难免请假,对于已经排好的计划怎么办? 手工调整?不用,你只需要记录谁在哪天请假就好: 115 | 116 | ```bash 117 | # 项目基本信息 118 | * 项目开始时间: 2014-08-21 119 | 120 | # 任务细分 121 | * 任务一 -- 2[James][100%] 122 | * 任务二 -- 1[Lucy][80%] 123 | * 任务三 -- 1[James] 124 | * 任务四 -- 2[Lucy] 125 | 126 | # 请假情况 127 | * James -- 2014-08-22 128 | ``` 129 | 130 | 再次运行相同命令, 我们会得到如下排期: 131 | 132 | ```bash 133 | > ./scheduler.py -m James /tmp/test.markdown 134 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 135 | ------ | ------ | -------- | ---------- | ---------- | ---- 136 | 任务一 | James | 2.0 | 2014-08-21 | 2014-08-25 | 100% 137 | 任务三 | James | 1.0 | 2014-08-26 | 2014-08-26 | 0% 138 | 139 | >>> 总人日: 3.0, 已经完成的人日: 2.0, 完成度: 66.67% 140 | ``` 141 | 142 | ## task-break 143 | 144 | ### 支持对一个大任务进行拆解 145 | 146 | 我们有时候会碰到这种问题: 一个任务由很多小任务组成。比如我们做一个网上购物网站,我们可能有几个任务是关于商品的增,删,改,查,很自然的,我们会把描述成这样: 147 | 148 | ```bash 149 | # 项目基本信息 150 | * 项目开始时间: 2014-08-21 151 | 152 | # 商品相关 153 | * 增 -- 2[James] 154 | * 删 -- 1[Lucy] 155 | * 改 -- 1[James] 156 | * 查 -- 2[Lucy] 157 | ``` 158 | 159 | 运行命令我们会得到如下的排期: 160 | 161 | ```bash 162 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 163 | -- | ------ | -------- | ---------- | ---------- | ---- 164 | 增 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 165 | 删 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 0% 166 | 改 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 167 | 查 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 168 | 169 | >> 总人日: 6.0, 已经完成的人日: 0.00, 完成度: 0.00% 170 | ``` 171 | 172 | 任务的名字直接显示成`增`,`删`,`改`,`查`, 不知道背景的同学完全不知道这个排期在说什么。 173 | 174 | 因此我们支持把每个大任务的标题自动添加到每个子任务的任务名字里面去,只要添加`-t`参数: 175 | 176 | ```bash 177 | > ./scheduler.py -t /tmp/test.md 178 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 179 | ------------- | ------ | -------- | ---------- | ---------- | ---- 180 | 商品相关: 增 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 181 | 商品相关: 删 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 0% 182 | 商品相关: 改 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 183 | 商品相关: 查 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 184 | 185 | >> 总人日: 6.0, 已经完成的人日: 0.00, 完成度: 0.00% 186 | ``` 187 | 188 | 我们甚至支持一个超大任务的多级细分, 比如下面的这个输入文件: 189 | 190 | ```bash 191 | # 项目基本信息 192 | * 项目开始时间: 2014-08-21 193 | 194 | # 交易 195 | 196 | ## 商品 197 | * 增 -- 2[James] 198 | * 删 -- 1[Lucy] 199 | * 改 -- 1[James] 200 | * 查 -- 2[Lucy] 201 | 202 | ## 会员 203 | * 增 -- 1[James] 204 | 205 | # 娱乐 206 | ## KTV 207 | * 唱 -- 2[Lucy] 208 | ``` 209 | 210 | 排期结果是这样的, 两级细分的标题在最终的排期结果里面都有体现: 211 | 212 | ```bash 213 | > ./scheduler.py -t /tmp/test.md 214 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 215 | ------------ | ------ | -------- | ---------- | ---------- | ---- 216 | 交易-商品-增 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 217 | 交易-商品-删 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 0% 218 | 交易-商品-改 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 219 | 交易-商品-查 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 220 | 交易-会员-增 | James | 1.0 | 2014-08-26 | 2014-08-26 | 0% 221 | 娱乐-KTV-唱 | Lucy | 2.0 | 2014-08-26 | 2014-08-27 | 0% 222 | 223 | >> 总人日: 9.0, 已经完成的人日: 0.00, 完成度: 0.00% 224 | ``` 225 | 226 | ## show-all-progress 227 | 228 | ### 支持显示每个人任务完成情况 229 | 230 | 在排期的时候我们往往希望每个人分配到的任务相对均匀,这时候我们就希望可以看到所有人被分配的工时的列表,这个只需要指定`-s`参数即可,比如对于上面的那个输入,输出会是下面这样的: 231 | 232 | ```bash 233 | > ./scheduler.py -s /tmp/test.md 234 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 235 | -- | ------ | -------- | ---------- | ---------- | ---- 236 | 增 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 237 | 删 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 0% 238 | 改 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 239 | 查 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 240 | 241 | >> 总人日: 6.0, 已经完成的人日: 0.00, 完成度: 0.00% 242 | Lucy: 3.0 243 | James: 3.0 244 | ``` 245 | 246 | ## show-undone-task 247 | 248 | ### 显示没有开始的任务 249 | 有时候我们想看看有哪些任务还没有开始做,这个我们也是支持的,比如下面的 breakdown 文件: 250 | 251 | ```bash 252 | # 项目基本信息 253 | * 项目开始时间: 2014-08-21 254 | 255 | # 任务细分 256 | * 任务一 -- 2[James][100%] 257 | * 任务二 -- 1[Lucy][80%] 258 | * 任务三 -- 1[James] 259 | * 任务四 -- 2[Lucy] 260 | 261 | # 请假情况 262 | * James -- 2014-08-22 263 | ``` 264 | 265 | 如下执行即可显示所有开始的任务: 266 | 267 | ```bash 268 | > ./scheduler.py -n /tmp/test.md 269 | 任务 | 责任人 | 所需人日 | 开始时间 | 结束时间 | 进度 270 | ------ | ------ | -------- | ---------- | ---------- | ---- 271 | 任务三 | James | 1.0 | 2014-08-26 | 2014-08-26 | 0% 272 | 任务四 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 273 | 274 | >> 总人日: 3.0, 已经完成的人日: 0.00, 完成度: 0.00% 275 | ``` 276 | 277 | 更多功能?Try it yourself! 278 | 279 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | pyscheduler 2 | =========== 3 | 4 | When we are in a project, we need to make plans, there are usually two ways to do it: The first is to use a software like `Microsoft Project`, this kind of software have rich features, but there is one downside which I don't like: they are too heavy, it is different to embbed the project plan into a text file like `Markdown` file; The second is to utilize the table syntax to write the plan directly in a markdown file. Personally I prefer the second approach: it is pure text, you do not rely on any special app to display it; It also has its downside, it is hard to maintain: 5 | 6 | * If you want to change the scheduled time for on of the tasks, you need to re-schedule all the tasks after it. 7 | * You can not see all the resource usage for the whole project like what `Microsoft Project` did. 8 | * You cann't filter the tasks according to some perticually critera. 9 | 10 | This tiny software is to solve the above issues. 11 | 12 | ## Currently we support the following features 13 | 14 | * [Auto-Schedule](#usage) 15 | * [Track task progress](#track-task-progress) 16 | * [Filter by prople](#filter-by-people) 17 | * [Trackvacation](#track-vacation) 18 | * [Break big tasks](#break-big-tasks) 19 | * [Scheduler UI](#scheduler-ui) 20 | 21 | 22 | ## Scheduler Command Line 23 | 24 | ### Basic Usage 25 | 26 | ```bash 27 | scheduler.py [-m ] -e /path/to/work-breakdown-file.markdown 28 | ``` 29 | 30 | The input file for the command line tool is a `markdown` file(see test_en.markdown for an example), you can define the tasks of your project in a file, e.g. 31 | 32 | ```bash 33 | # Task basic info 34 | * ProjectStartDate: 2014-08-21 35 | 36 | # task breakdown 37 | * task1 -- 2[James] 38 | * task2 -- 1[Lucy] 39 | * task3 -- 1[James] 40 | * task4 -- 2[Lucy] 41 | ``` 42 | 43 | The above file defines that the project starts from `2014-08-21`, and defines the tasks of the project, now run the following command, we get an auto-scheduled project plan(If you use a Fixed-Width Font you will find that the table generated is well aligned) 44 | 45 | ```bash 46 | > ./scheduler.py -e /tmp/test.markdown 47 | Task | Developer | Man-days | Start Date | End Date | Progress 48 | ----- | ---------- | -------- | ---------- | ---------- | ---- 49 | task1 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 50 | task2 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 0% 51 | task3 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 52 | task4 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 53 | 54 | >> Total mandays: 6.0, Finished mandays: 0.00, Progress: 0.00% 55 | ``` 56 | 57 | You may have already noticed that if you save the output into a `markdown` file, the content is actually a table. i.e. **You only need to maintain the task list and how much time every task takes, this software will auto-schedule the plan for you. 58 | 59 | ## Track task progress 60 | 61 | As time flies, you need to update the status of each task, we support to update the status in the task `breakdown` file, e.g. 62 | 63 | ```bash 64 | # Task basic info 65 | * ProjectStartDate: 2014-08-21 66 | 67 | # task breakdown 68 | * task1 -- 2[James][100%] 69 | * task2 -- 1[Lucy][80%] 70 | * task3 -- 1[James] 71 | * task4 -- 2[Lucy] 72 | ``` 73 | 74 | re-run the command, you get a new **updated** plan: 75 | 76 | ```bash 77 | > ./scheduler.py -e /tmp/test.markdown 78 | Task | Developer | Man-days | Start Date | End Date | Progress 79 | ----- | ---------- | -------- | ---------- | ---------- | ---- 80 | task1 | James | 2.0 | 2014-08-21 | 2014-08-22 | 100% 81 | task2 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 80% 82 | task3 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 83 | task4 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 84 | 85 | >> Total mandays: 6.0, Finished mandays: 0.00, Progress: 46.67% 86 | ``` 87 | 88 | ## Filter tasks by people 89 | 90 | If you want to check out all the tasks assigned to `James`, add a `-m` param: 91 | 92 | ```bash 93 | > ./scheduler.py -m James /tmp/test.markdown 94 | Task | Developer | Man-days | Start Date | End Date | Progress 95 | ----- | ---------- | -------- | ---------- | ---------- | ---- 96 | task1 | James | 2.0 | 2014-08-21 | 2014-08-22 | 0% 97 | task3 | James | 1.0 | 2014-08-25 | 2014-08-25 | 0% 98 | 99 | >> Total mandays: 6.0, Finished mandays: 0.00, Progress: 0.00% 100 | ``` 101 | 102 | ### Track Vacation 103 | 104 | During a project, it is almost inevitable that someone will ask for leave for some time, but the plan is already scheduled? how? re-schedule the plan manually? No! Just record the vacation record(take a look at the `vacation` section): 105 | 106 | ```bash 107 | # Task basic info 108 | * ProjectStartDate: 2014-08-21 109 | 110 | # task breakdown 111 | * task1 -- 2[James][100%] 112 | * task2 -- 1[Lucy][80%] 113 | * task3 -- 1[James] 114 | * task4 -- 2[Lucy] 115 | 116 | # vacations 117 | * James -- 2014-08-22 118 | ``` 119 | 120 | re-run the command again, we get the following plan: 121 | 122 | ```bash 123 | > ./scheduler.py -e -m James /tmp/test.markdown 124 | Task | Developer | Man-days | Start Date | End Date | Progress 125 | ----- | ---------- | -------- | ---------- | ---------- | ---- 126 | task1 | James | 2.0 | 2014-08-21 | 2014-08-25 | 100% 127 | task3 | James | 1.0 | 2014-08-26 | 2014-08-26 | 0% 128 | 129 | >> Total mandays: 6.0, Finished mandays: 0.00, Progress: 46.67% 130 | ``` 131 | 132 | ## Break big tasks 133 | 134 | Sometimes we may encounter this kind of issue: A task is made up of several small tasks. e.g. If we are developing an online shopping website, we might have several tasks related with product: Create, Update, Delete, Retrieve, and we want to group these related task together: 135 | 136 | ```bash 137 | # Task basic info 138 | * ProjectStartDate: 2014-08-21 139 | 140 | # Basic 141 | * task1 -- 2[James][100%] 142 | * task2 -- 1[Lucy][80%] 143 | * task3 -- 1[James] 144 | * task4 -- 2[Lucy] 145 | 146 | ## Product 147 | * Create -- 2[James] 148 | * Delete -- 1[Lucy] 149 | * Update -- 1[James] 150 | * Retrieve -- 2[Lucy] 151 | 152 | # vacations 153 | * James -- 2014-08-22 154 | ``` 155 | 156 | re-run the command(with a new option: `-t`) we get: 157 | 158 | ```bash 159 | > ./scheduler.py -e -t /tmp/test.md 160 | Task | Developer | Man-days | Start Date | End Date | Progress 161 | ---------------------- | ---------- | -------- | ---------- | ---------- | ---- 162 | Basic-task1 | James | 2.0 | 2014-08-21 | 2014-08-25 | 100% 163 | Basic-task2 | Lucy | 1.0 | 2014-08-21 | 2014-08-21 | 80% 164 | Basic-task3 | James | 1.0 | 2014-08-26 | 2014-08-26 | 0% 165 | Basic-task4 | Lucy | 2.0 | 2014-08-22 | 2014-08-25 | 0% 166 | Basic-Product-Create | James | 2.0 | 2014-08-27 | 2014-08-28 | 0% 167 | Basic-Product-Delete | Lucy | 1.0 | 2014-08-26 | 2014-08-26 | 0% 168 | Basic-Product-Update | James | 1.0 | 2014-08-29 | 2014-08-29 | 0% 169 | Basic-Product-Retrieve | Lucy | 2.0 | 2014-08-27 | 2014-08-28 | 0% 170 | 171 | >> Total mandays: 12.0, Finished mandays: 0.00, Progress: 23.33% 172 | ``` 173 | 174 | There is more features, try it yourself! 175 | 176 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | #-*-encoding: utf-8 -*- 2 | import sys 3 | import re 4 | import datetime 5 | from math import ceil 6 | 7 | TASK_LINE_PATTERN = "\*(.+)\-\-\s*([0-9]+\.?[0-9]?)\s*(\[(.+?)\])?(\[([0-9]+)%\s*\])?\s*$" 8 | HEADER_PATTERN = "^(#{2,})(.*)" 9 | VACATION_PATTERN = "\*(.+)\-\-\s*([0-9]{4}\-[0-9]{2}\-[0-9]{2})(\s*\-\s*([0-9]{4}\-[0-9]{2}\-[0-9]{2}))?\s*$" 10 | PROJECT_START_DATE_PATTERN = 'ProjectStartDate\:\s*([0-9]{4}\-[0-9]{2}\-[0-9]{2})' 11 | 12 | class Options: 13 | def __init__(self): 14 | self.print_man_stats = False 15 | self.only_nonstarted = False 16 | self.english = False 17 | self.man = None 18 | 19 | class Project: 20 | def __init__(self, project_start_date, tasks, vacations): 21 | self.project_start_date = project_start_date 22 | self.tasks = tasks 23 | self.vacations = vacations 24 | 25 | self.mans = [] 26 | self.status = 0 27 | self.total_man_days = 0 28 | self.cost_man_days = 0 29 | self.init_status() 30 | 31 | def task_start_date(self, task): 32 | return add_days(task.man, self.project_start_date, self.vacations, task.start_point) 33 | 34 | def task_end_date(self, task): 35 | return add_days(task.man, self.project_start_date, self.vacations, task.start_point + task.man_day, False) 36 | 37 | def init_status(self): 38 | total_man_days = 0 39 | cost_man_days = 0 40 | for task in self.tasks: 41 | total_man_days += task.man_day 42 | cost_man_days += task.man_day * task.status / 100 43 | if not task.man in self.mans: 44 | self.mans.append(task.man) 45 | 46 | task.start_date = self.task_start_date(task) 47 | task.end_date = self.task_end_date(task) 48 | 49 | project_status = 0 50 | if total_man_days > 0: 51 | project_status = cost_man_days / total_man_days 52 | 53 | self.total_man_days = total_man_days 54 | self.cost_man_days = cost_man_days 55 | self.status = project_status 56 | 57 | class Task: 58 | def __init__(self, name, man_day, man, status=0): 59 | """ 60 | Arguments: 61 | - `self`: 62 | - `name`: 63 | - `man_day` 64 | """ 65 | self.name = name 66 | self.man_day = man_day 67 | self.man = man 68 | self.status = int(status) 69 | self.start_point = None 70 | self.start_date = None 71 | self.end_date = None 72 | 73 | def skip_weekend(date1): 74 | weekday = date1.isoweekday() 75 | if weekday > 5: 76 | padding_days = (7 - weekday) + 1 77 | date1 = date1 + datetime.timedelta(days=padding_days) 78 | return True, date1 79 | else: 80 | return False, date1 81 | 82 | 83 | def skip_vacation(man, date1, vacations): 84 | if vacations.get(man) and vacations.get(man).count(str(date1)) > 0: 85 | date1 = date1 + datetime.timedelta(days=1) 86 | return True, date1 87 | else: 88 | return False, date1 89 | 90 | def skip_weekend_or_vacation(man, date1, vacations): 91 | while True: 92 | skipped, date1 = skip_weekend(date1) 93 | skipped, date1 = skip_vacation(man, date1, vacations) 94 | 95 | if not skipped: 96 | break 97 | 98 | return date1 99 | 100 | def add_days(man, curr_day, vacations, days, is_start_date = True): 101 | idx = int(ceil(days)) 102 | if idx > days: 103 | idx -= 1 104 | else: 105 | if not is_start_date: 106 | idx -= 1 107 | 108 | ret = curr_day 109 | # current day may be a weekend day, so we skip the weekend first 110 | ret = skip_weekend_or_vacation(man, ret, vacations) 111 | 112 | while idx > 0: 113 | ret = ret + datetime.timedelta(days=1) 114 | 115 | # skip the weekend and vacations 116 | ret = skip_weekend_or_vacation(man, ret, vacations) 117 | 118 | idx -= 1 119 | 120 | return ret 121 | 122 | def schedule(tasks): 123 | curr_days = {} 124 | id_to_start_point = {} 125 | for task in tasks: 126 | if not curr_days.get(task.man): 127 | curr_days[task.man] = 0 128 | task.start_point = curr_days[task.man] 129 | curr_days[task.man] += task.man_day 130 | 131 | 132 | def parse_date(input): 133 | return datetime.datetime.strptime(input, '%Y-%m-%d').date() 134 | 135 | def get_headers_as_str(headers): 136 | return "-".join([header for [_, header] in headers]) 137 | 138 | def parse_header_line(curr_headers, m): 139 | new_header_level = len(m.group(1).strip()) 140 | new_header = m.group(2).strip() 141 | for i in range(len(curr_headers)): 142 | header_level, header = curr_headers[len(curr_headers) - 1 - i] 143 | if new_header_level <= header_level: 144 | curr_headers.pop() 145 | 146 | curr_headers.append([new_header_level, new_header]) 147 | 148 | def parse_task_line(tasks, curr_headers, m): 149 | task_name = m.group(1).strip() 150 | if len(curr_headers) > 0: 151 | task_name = get_headers_as_str(curr_headers) + "-" + task_name 152 | 153 | man_day = m.group(2).strip() 154 | man_day = float(man_day) 155 | man = m.group(4) 156 | if man: 157 | man = man.strip() 158 | else: 159 | man = "TODO" 160 | 161 | status = 0 162 | if m.group(6): 163 | status = m.group(6).strip() 164 | 165 | task = Task(task_name, man_day, man, status) 166 | tasks.append(task) 167 | 168 | def parse_vacation_line(vacations, m): 169 | man = m.group(1).strip() 170 | vacation_date = parse_date(m.group(2).strip()) 171 | vacation_date_end = vacation_date 172 | if m.group(4): 173 | vacation_date_end = parse_date(m.group(4).strip()) 174 | 175 | if not vacations.get(man): 176 | vacations[man] = [] 177 | 178 | xdate = vacation_date 179 | while xdate <= vacation_date_end: 180 | vacations[man].append(str(xdate)) 181 | xdate += datetime.timedelta(days=1) 182 | 183 | def parse(content): 184 | lines = content.split('\n') 185 | tasks = [] 186 | vacations = {} 187 | 188 | project_start_date = None 189 | curr_headers = [] 190 | for line in lines: 191 | 192 | # parse task line 193 | m = re.search(TASK_LINE_PATTERN, line) 194 | if m: 195 | parse_task_line(tasks, curr_headers, m) 196 | continue 197 | 198 | # parse vacation line 199 | m = re.search(VACATION_PATTERN, line) 200 | if m: 201 | parse_vacation_line(vacations, m) 202 | continue 203 | 204 | # parse project_start_date line 205 | m = re.search(PROJECT_START_DATE_PATTERN, line) 206 | if m and m.group(1): 207 | project_start_date = parse_date(m.group(1).strip()) 208 | continue 209 | 210 | # parse header line 211 | m = re.search(HEADER_PATTERN, line) 212 | if m: 213 | parse_header_line(curr_headers, m) 214 | 215 | if not project_start_date: 216 | raise "Please specify the project start date!" 217 | exit(1) 218 | 219 | schedule(tasks) 220 | 221 | return Project(project_start_date, tasks, vacations) 222 | 223 | -------------------------------------------------------------------------------- /sample/test.markdown: -------------------------------------------------------------------------------- 1 | # 项目基本信息 2 | * ProjectStartDate: 2015-04-05 3 | 4 | # 任务细分 5 | * 任务一 -- 2[James][0%] 6 | * 任务二 -- 1[Lucy][0%] 7 | * 任务三 -- 1[James][100%] 8 | * 任务四 -- 2[Lucy][0%] 9 | -------------------------------------------------------------------------------- /sample/test_en.markdown: -------------------------------------------------------------------------------- 1 | # Task basic info 2 | * ProjectStartDate: 2014-08-21 3 | 4 | # task breakdown 5 | * task1 -- 2[James] 6 | * task2 -- 1[Lucy] 7 | * task3 -- 1[James] 8 | * task4 -- 2[Lucy] 9 | -------------------------------------------------------------------------------- /scheduler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*-coding: utf-8 -*- 3 | 4 | import sys 5 | import getopt 6 | import parser 7 | import codecs 8 | 9 | def find_max_length_of_tasks(tasks): 10 | ret = 0 11 | for task in tasks: 12 | if actual_width_str(task.name) > ret: 13 | ret = actual_width_str(task.name) 14 | 15 | return ret 16 | 17 | def actual_width(ch): 18 | if ord(ch) < 256: 19 | return 1 20 | 21 | return 2 22 | 23 | def actual_width_str(input): 24 | ret = 0 25 | for ch in input: 26 | ret += actual_width(ch) 27 | 28 | return ret 29 | 30 | def format_with_width(input, width): 31 | target = input 32 | actual_width = actual_width_str(input) 33 | 34 | delta = width - actual_width 35 | if delta > 0: 36 | for i in range(delta): 37 | target += ' ' 38 | 39 | return target 40 | 41 | def repeat(cnt): 42 | ret = '' 43 | for i in range(cnt): 44 | ret += '-' 45 | 46 | return ret 47 | 48 | MAN_LEN = 10 49 | MAN_DAY_LEN = 8 50 | START_DATE_LEN = 10 51 | END_DATE_LEN = 10 52 | STATUS_LEN = 4 53 | def pretty_print_second_line(task_name_len): 54 | pretty_print(repeat(task_name_len), repeat(MAN_LEN), repeat(MAN_DAY_LEN), 55 | repeat(START_DATE_LEN), repeat(END_DATE_LEN), repeat(STATUS_LEN), 56 | task_name_len) 57 | 58 | def pretty_print(task_name, man, man_day, start_date, end_date, status, task_name_len): 59 | actual_task_name = format_with_width(task_name, task_name_len) 60 | actual_man = format_with_width(man, MAN_LEN) 61 | actual_man_day = format_with_width(str(man_day), MAN_DAY_LEN) 62 | actual_start_date = format_with_width(str(start_date), START_DATE_LEN) 63 | actual_end_date = format_with_width(str(end_date), END_DATE_LEN) 64 | actual_status = format_with_width(str(status), STATUS_LEN) 65 | 66 | print("{} | {} | {} | {} | {} | {}".format(actual_task_name, actual_man, actual_man_day, 67 | actual_start_date, actual_end_date, actual_status)) 68 | 69 | def pretty_print_task(project, task): 70 | pretty_print(task.name, task.man, task.man_day, project.task_start_date(task), 71 | project.task_end_date(task), str(task.status) + "%", find_max_length_of_tasks(project.tasks)) 72 | 73 | def pretty_print_man_stats(tasks): 74 | man2days = {} 75 | for task in tasks: 76 | if not man2days.get(task.man): 77 | man2days[task.man] = [0,0] # finished_man_days, total_man_days 78 | 79 | task_status = task.status 80 | man_days = task.man_day 81 | 82 | finished_man_days = task_status * man_days / 100 83 | man2days[task.man][0] = man2days[task.man][0] + finished_man_days 84 | man2days[task.man][1] = man2days[task.man][1] + man_days 85 | 86 | for man in sorted(man2days): 87 | finished_man_days = man2days[man][0] 88 | total_man_days = man2days[man][1] 89 | total_status = (finished_man_days / total_man_days) * 100 90 | 91 | print("{}: {:.0f}/{} {:.0f}%".format(man, finished_man_days, total_man_days, total_status)) 92 | 93 | def pretty_print_scheduled_tasks(project, options): 94 | # pretty print the scheduler 95 | max_length_of_tasks = find_max_length_of_tasks(project.tasks) 96 | if options.english: 97 | pretty_print('Task', 'Developer', 'Man-days', 'Start Date', 'End Date', 'Progress', max_length_of_tasks) 98 | else: 99 | pretty_print('任务', '责任人', '所需人日', '开始时间', '结束时间', '进度', max_length_of_tasks) 100 | 101 | pretty_print_second_line(max_length_of_tasks) 102 | 103 | for task in project.tasks: 104 | if not options.man or task.man == options.man: 105 | pretty_print_task(project, task) 106 | 107 | print("") 108 | 109 | if options.english: 110 | print(">> Total mandays: {}, Finished mandays: {:.2f}, Progress: {:.2%}".format(project.total_man_days, 111 | project.cost_man_days, 112 | project.status)) 113 | else: 114 | print(">> 总人日: {}, 已经完成的人日: {:.2f}, 完成度: {:.2%}".format(project.total_man_days, 115 | project.cost_man_days, 116 | project.status)) 117 | 118 | 119 | def parse_and_print(filepath, options): 120 | f = codecs.open(filepath, 'r', 'utf-8') 121 | content = f.read() 122 | f.close() 123 | 124 | project = parser.parse(content) 125 | # filter the tasks 126 | if options.only_nonstarted: 127 | project.tasks = [task for task in project.tasks if task.status < 100] 128 | 129 | pretty_print_scheduled_tasks(project, options) 130 | if options.print_man_stats: 131 | pretty_print_man_stats(project.tasks) 132 | 133 | 134 | def help(): 135 | print("""用法: scheduler.py /path/to/work-breakdown-file.markdown 136 | 137 | Options: 138 | -m 只显示指定人的任务 139 | -t 把每个section的标题apppend到这个section下面所有任务名称前面去 140 | -s 显示每个人的任务数统计信息 141 | """) 142 | 143 | if __name__ == '__main__': 144 | opts, args = getopt.getopt(sys.argv[1:], 'm:tsne') 145 | if not args or len(args) != 1: 146 | help() 147 | exit(1) 148 | 149 | filepath = args[0] 150 | man = None 151 | 152 | options = parser.Options() 153 | for opt_name, opt_value in opts: 154 | opt_value = opt_value.strip() 155 | if opt_name == '-m': 156 | options.man = opt_value 157 | elif opt_name == '-s': 158 | options.print_man_stats = True 159 | elif opt_name == '-n': 160 | options.only_nonstarted = True 161 | elif opt_name == '-e': 162 | options.english = True 163 | 164 | parse_and_print(filepath, options) 165 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | # pyscheduler spec 2 | 3 | ## Define the `project start date` 4 | 5 | ```bash 6 | * ProjectStartDate: 2015-07-21 7 | ``` 8 | 9 | This project will start at `2015-07-21`. 10 | 11 | ## Define a task 12 | 13 | ### A task which is not assigned 14 | 15 | ```bash 16 | * task1 -- 2 17 | ``` 18 | 19 | This task named `task1` is evaluated to be 2 man-days. 20 | 21 | ### A task which is assigned 22 | 23 | ```bash 24 | * task1 -- 2[James] 25 | ``` 26 | 27 | This task is assigned to a guy named `James`. 28 | 29 | ### A task which has progress 30 | 31 | ```bash 32 | * task1 -- 2[James][90%] 33 | ``` 34 | 35 | This task's progress is about `90%`. 36 | 37 | ## Define a `time off` 38 | 39 | ```bash 40 | * James -- 2015-07-21 - 2015-07-25 41 | ``` 42 | 43 | James will be not in this project between `2015-07-21` and `2015-07-25`. 44 | --------------------------------------------------------------------------------