├── apps
├── github
│ ├── __init__.py
│ └── models.py
├── main
│ ├── __init__.py
│ ├── export
│ │ ├── __init__.py
│ │ └── csv_export.py
│ ├── tests
│ │ ├── mock_data.py
│ │ ├── __init__.py
│ │ └── base.py
│ ├── migrations
│ │ ├── 007_user_premium.py
│ │ ├── 001_url_and_external_url.py
│ │ ├── example.py.txt
│ │ ├── 004_event_description.py
│ │ ├── 006_featurerequests_implemented.py
│ │ ├── 005_settings_hash_tags.py
│ │ ├── 009_first_hour_settings.py
│ │ ├── 002_settings_disable_sound.py
│ │ ├── 003_offline_mode_user_settings.py
│ │ ├── 008_ampm_format_settings.py
│ │ ├── 012_usersettings_user.py
│ │ ├── 010_settings_newsletter_optout.py
│ │ ├── 011_0second_not_all_day_events.py
│ │ └── 013_share_featurerequest_featureeq_comment.py
│ ├── indexes.py
│ └── config.py
├── qunit
│ ├── __init__.py
│ └── handlers.py
├── smartphone
│ └── __init__.py
├── emailreminders
│ ├── __init__.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── 2011-09-19_090052_194569.email
│ │ ├── 2011-02-23_114902_621456.email
│ │ ├── 2011-05-17_220706_318577.email
│ │ ├── 2011-03-28_170726_751460.email
│ │ └── test_utils.py
│ ├── ui_modules.py
│ ├── migrations
│ │ ├── 002_emailreminders_user.py
│ │ └── 001_include_summary_and_instructions.py
│ ├── reminder_utils.py
│ └── models.py
├── __init__.py
├── templates
│ ├── emailreminders
│ │ ├── base.html
│ │ ├── show_weekday_reminders.html
│ │ └── send_reminder.txt
│ ├── user
│ │ ├── sharing.html
│ │ ├── reset_password.txt
│ │ ├── change-account.html
│ │ ├── recover_forgotten.html
│ │ ├── forgotten.html
│ │ ├── settings.html
│ │ └── account.html
│ ├── help
│ │ ├── see_also.html
│ │ ├── show_vimeo_video.html
│ │ ├── help_base.html
│ │ ├── bookmarklet.html
│ │ ├── internet-explorer.html
│ │ ├── feature-requests.html
│ │ ├── secure-passwords.html
│ │ ├── index.html
│ │ ├── google-calendar.html
│ │ ├── about.html
│ │ └── news.html
│ ├── sharing
│ │ ├── cant-share-yet.html
│ │ └── share.html
│ ├── qunit
│ │ ├── index.html
│ │ └── smartphone.html
│ ├── bookmarklet
│ │ ├── posted.html
│ │ └── index.html
│ ├── smartphone
│ │ ├── logged_in.html
│ │ └── base.html
│ ├── sound
│ │ └── test.html
│ ├── modules
│ │ ├── eventpreview.html
│ │ ├── footer.html
│ │ └── settings.html
│ ├── powerusers
│ │ ├── index.html
│ │ ├── users
│ │ │ ├── jerome-ferdinands.html
│ │ │ ├── power_user_base.html
│ │ │ ├── bill-mitchell.html
│ │ │ └── peter-bengtsson.html
│ │ └── top-10.html
│ ├── premium
│ │ ├── checkout.html
│ │ └── index.html
│ ├── testimonials.html
│ ├── eventlog
│ │ ├── activity.html
│ │ └── index.html
│ ├── event
│ │ └── edit.html
│ ├── splash.html
│ ├── featurerequests
│ │ ├── feature_request.html
│ │ └── index.html
│ ├── stats
│ │ └── index.html
│ └── report
│ │ └── index.html
└── eventlog
│ ├── tests
│ ├── __init__.py
│ ├── test_models.py
│ └── test_handlers.py
│ ├── indexes.py
│ ├── constants.py
│ ├── migrations
│ └── 001_user_reference.py
│ ├── __init__.py
│ ├── models.py
│ └── ui_modules.py
├── static
├── css
│ ├── bookmarklet.css
│ ├── ext
│ │ ├── indicator.gif
│ │ ├── qtip
│ │ │ ├── close.png
│ │ │ ├── close-red.png
│ │ │ ├── close-blue.png
│ │ │ ├── close-dark.png
│ │ │ ├── close-green.png
│ │ │ └── close-light.png
│ │ ├── fancybox
│ │ │ ├── blank.gif
│ │ │ ├── fancybox.png
│ │ │ ├── fancybox-x.png
│ │ │ ├── fancybox-y.png
│ │ │ ├── fancy_close.png
│ │ │ ├── fancy_loading.png
│ │ │ ├── fancy_nav_left.png
│ │ │ ├── fancy_nav_right.png
│ │ │ ├── fancy_shadow_e.png
│ │ │ ├── fancy_shadow_n.png
│ │ │ ├── fancy_shadow_ne.png
│ │ │ ├── fancy_shadow_nw.png
│ │ │ ├── fancy_shadow_s.png
│ │ │ ├── fancy_shadow_se.png
│ │ │ ├── fancy_shadow_sw.png
│ │ │ ├── fancy_shadow_w.png
│ │ │ ├── fancy_title_left.png
│ │ │ ├── fancy_title_main.png
│ │ │ ├── fancy_title_over.png
│ │ │ └── fancy_title_right.png
│ │ ├── jquery.mobile
│ │ │ └── images
│ │ │ │ ├── ajax-loader.png
│ │ │ │ ├── form-check-on.png
│ │ │ │ ├── form-radio-on.png
│ │ │ │ ├── form-check-off.png
│ │ │ │ ├── form-radio-off.png
│ │ │ │ ├── icons-18-black.png
│ │ │ │ ├── icons-18-white.png
│ │ │ │ ├── icons-36-black.png
│ │ │ │ ├── icons-36-white.png
│ │ │ │ └── icon-search-black.png
│ │ ├── jquery.autocomplete.css
│ │ ├── fullcalendar.print.css
│ │ └── jquery.jqplot.min.css
│ ├── icons
│ │ ├── text-csv.png
│ │ ├── application-pdf.png
│ │ └── x-office-spreadsheet.png
│ ├── smoothness
│ │ └── images
│ │ │ ├── ui-icons_222222_256x240.png
│ │ │ ├── ui-icons_2e83ff_256x240.png
│ │ │ ├── ui-icons_454545_256x240.png
│ │ │ ├── ui-icons_888888_256x240.png
│ │ │ ├── ui-icons_cd0a0a_256x240.png
│ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png
│ │ │ ├── ui-bg_flat_75_ffffff_40x100.png
│ │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png
│ │ │ ├── ui-bg_glass_65_ffffff_1x400.png
│ │ │ ├── ui-bg_glass_75_dadada_1x400.png
│ │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png
│ │ │ ├── ui-bg_glass_95_fef1ec_1x400.png
│ │ │ └── ui-bg_highlight-soft_75_cccccc_1x100.png
│ ├── stats.css
│ ├── logs.css
│ ├── calendar.css
│ ├── report.css
│ └── featurerequests.css
├── compiler.jar
├── images
│ ├── key.gif
│ ├── favicon.ico
│ ├── locked.gif
│ ├── premium.png
│ ├── tagging.png
│ ├── thumbup.png
│ ├── twitter.png
│ ├── tiny_close.png
│ ├── vimeovideo.png
│ ├── blank_button.png
│ ├── calendar_home.png
│ ├── google_openid.png
│ ├── splash
│ │ ├── take-1.png
│ │ ├── take-2.png
│ │ └── take-3.png
│ ├── bookmarklet_close.png
│ ├── smartphone
│ │ ├── icon_114x114.png
│ │ └── icon_57x57.png
│ ├── ui-bg_glass_100_fdf5ce_1x400.png
│ └── ui-bg_highlight-soft_100_eeeeee_1x100.png
├── sounds
│ ├── add.ogg
│ ├── delete.ogg
│ └── README.txt
├── ext
│ ├── jquery.jqplot.js
│ ├── jquery.cookie.min.js
│ ├── jquery.cookie.js
│ ├── jquery.lazy.js
│ └── head.load.min.js
├── yuicompressor-2.4.2.jar
├── recover.js
├── forgotten.js
├── play-sounds.js
├── activitylog.js
├── emailreminders.js
├── base.js
├── eventlog.js
├── share.js
├── ajaxproxy.js
└── account.js
├── utils
├── __init__.py
├── send_mail
│ ├── backends
│ │ ├── __init__.py
│ │ ├── locmem.py
│ │ ├── console.py
│ │ ├── base.py
│ │ └── smtp.py
│ ├── __init__.py
│ ├── config.py
│ ├── dns_name.py
│ └── importlib.py
├── decorators.py
├── truncate.py
├── git.py
└── datatoxml.py
├── bin
├── README
├── run_tests.sh
├── find_console.log.sh
├── cleanup_old_combined_files.sh
├── run_development_server.sh
├── ensure_indexes.py
├── run_coverage_tests.sh
├── here.py
├── _run_tests.py
├── _cleanup_old_combined_files.py
├── run_shell.py
├── _run_coverage_tests.py
├── run_migrations.py
├── download_static_urls.py
└── run_pyflakes.py
├── vendor
└── vendor.pth
├── COMPETITION.txt
├── requirements.txt
├── robots.txt
├── .gitmodules
├── INSTALL.txt
├── .gitignore
├── export_newsletter_subscribers.py
├── marketing
└── generate_emails_csv.py
├── settings.py
├── run_cleanup_deleted_events.py
├── sendmail.py
├── README.md
└── TODO.txt
/apps/github/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/main/__init__.py:
--------------------------------------------------------------------------------
1 | #
--------------------------------------------------------------------------------
/apps/qunit/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 |
--------------------------------------------------------------------------------
/apps/smartphone/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/css/bookmarklet.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/emailreminders/__init__.py:
--------------------------------------------------------------------------------
1 | #
--------------------------------------------------------------------------------
/apps/main/export/__init__.py:
--------------------------------------------------------------------------------
1 | #
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from utils import *
--------------------------------------------------------------------------------
/utils/send_mail/backends/__init__.py:
--------------------------------------------------------------------------------
1 | #
--------------------------------------------------------------------------------
/apps/__init__.py:
--------------------------------------------------------------------------------
1 | # perhaps more magic can be put here
--------------------------------------------------------------------------------
/utils/send_mail/__init__.py:
--------------------------------------------------------------------------------
1 | from send_email import send_email
--------------------------------------------------------------------------------
/apps/templates/emailreminders/base.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
--------------------------------------------------------------------------------
/apps/templates/user/sharing.html:
--------------------------------------------------------------------------------
1 |
Sharing your calendar
2 |
3 |
--------------------------------------------------------------------------------
/bin/README:
--------------------------------------------------------------------------------
1 | Put all files that are executable where except the main app.
--------------------------------------------------------------------------------
/vendor/vendor.pth:
--------------------------------------------------------------------------------
1 | src/tornado
2 | src/mongokit
3 | src/tornado-utils
4 |
--------------------------------------------------------------------------------
/bin/run_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | python bin/_run_tests.py --logging=error $@
4 |
--------------------------------------------------------------------------------
/apps/eventlog/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from test_models import *
2 | from test_handlers import *
3 |
--------------------------------------------------------------------------------
/bin/find_console.log.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | gg console.log | grep -v apply | grep -v '/ext/'
3 |
--------------------------------------------------------------------------------
/static/compiler.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/compiler.jar
--------------------------------------------------------------------------------
/static/images/key.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/key.gif
--------------------------------------------------------------------------------
/static/sounds/add.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/sounds/add.ogg
--------------------------------------------------------------------------------
/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/favicon.ico
--------------------------------------------------------------------------------
/static/images/locked.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/locked.gif
--------------------------------------------------------------------------------
/static/images/premium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/premium.png
--------------------------------------------------------------------------------
/static/images/tagging.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/tagging.png
--------------------------------------------------------------------------------
/static/images/thumbup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/thumbup.png
--------------------------------------------------------------------------------
/static/images/twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/twitter.png
--------------------------------------------------------------------------------
/static/sounds/delete.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/sounds/delete.ogg
--------------------------------------------------------------------------------
/bin/cleanup_old_combined_files.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | python bin/_cleanup_old_combined_files.py /tmp/combined -v
--------------------------------------------------------------------------------
/static/css/ext/indicator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/indicator.gif
--------------------------------------------------------------------------------
/static/ext/jquery.jqplot.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/ext/jquery.jqplot.js
--------------------------------------------------------------------------------
/static/images/tiny_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/tiny_close.png
--------------------------------------------------------------------------------
/static/images/vimeovideo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/vimeovideo.png
--------------------------------------------------------------------------------
/static/css/ext/qtip/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/qtip/close.png
--------------------------------------------------------------------------------
/static/css/icons/text-csv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/icons/text-csv.png
--------------------------------------------------------------------------------
/static/images/blank_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/blank_button.png
--------------------------------------------------------------------------------
/static/images/calendar_home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/calendar_home.png
--------------------------------------------------------------------------------
/static/images/google_openid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/google_openid.png
--------------------------------------------------------------------------------
/static/images/splash/take-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/splash/take-1.png
--------------------------------------------------------------------------------
/static/images/splash/take-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/splash/take-2.png
--------------------------------------------------------------------------------
/static/images/splash/take-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/splash/take-3.png
--------------------------------------------------------------------------------
/static/yuicompressor-2.4.2.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/yuicompressor-2.4.2.jar
--------------------------------------------------------------------------------
/static/css/ext/fancybox/blank.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/blank.gif
--------------------------------------------------------------------------------
/static/css/ext/qtip/close-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/qtip/close-red.png
--------------------------------------------------------------------------------
/apps/emailreminders/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from test_models import *
2 | from test_handlers import *
3 | from test_utils import *
--------------------------------------------------------------------------------
/bin/run_development_server.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ./app.py --debug --dont_combine --dont_embed_static_url --logging=debug
3 |
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancybox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancybox.png
--------------------------------------------------------------------------------
/static/css/ext/qtip/close-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/qtip/close-blue.png
--------------------------------------------------------------------------------
/static/css/ext/qtip/close-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/qtip/close-dark.png
--------------------------------------------------------------------------------
/static/css/ext/qtip/close-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/qtip/close-green.png
--------------------------------------------------------------------------------
/static/css/ext/qtip/close-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/qtip/close-light.png
--------------------------------------------------------------------------------
/static/css/icons/application-pdf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/icons/application-pdf.png
--------------------------------------------------------------------------------
/static/images/bookmarklet_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/bookmarklet_close.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancybox-x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancybox-x.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancybox-y.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancybox-y.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_close.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_loading.png
--------------------------------------------------------------------------------
/static/css/icons/x-office-spreadsheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/icons/x-office-spreadsheet.png
--------------------------------------------------------------------------------
/static/images/smartphone/icon_114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/smartphone/icon_114x114.png
--------------------------------------------------------------------------------
/static/images/smartphone/icon_57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/smartphone/icon_57x57.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_nav_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_nav_left.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_nav_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_nav_right.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_e.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_e.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_n.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_n.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_ne.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_ne.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_nw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_nw.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_s.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_se.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_se.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_sw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_sw.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_shadow_w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_shadow_w.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_title_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_title_left.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_title_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_title_main.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_title_over.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_title_over.png
--------------------------------------------------------------------------------
/static/css/ext/fancybox/fancy_title_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/fancybox/fancy_title_right.png
--------------------------------------------------------------------------------
/static/images/ui-bg_glass_100_fdf5ce_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/ui-bg_glass_100_fdf5ce_1x400.png
--------------------------------------------------------------------------------
/COMPETITION.txt:
--------------------------------------------------------------------------------
1 | http://www.getharvest.com/ $12/user/month and up
2 |
3 | http://www.clockspot.com/
4 | No free plan
5 | $9, $19 or $49 per month
--------------------------------------------------------------------------------
/bin/ensure_indexes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import here
3 |
4 | from apps.eventlog.indexes import run as eventlog_run
5 | eventlog_run()
6 |
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/ajax-loader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/ajax-loader.png
--------------------------------------------------------------------------------
/bin/run_coverage_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | python bin/_run_coverage_tests.py
4 | if [ "$?" == 0 ]; then
5 | open coverage_report/index.html
6 | fi
7 |
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/form-check-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/form-check-on.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/form-radio-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/form-radio-on.png
--------------------------------------------------------------------------------
/apps/emailreminders/tests/2011-09-19_090052_194569.email:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/apps/emailreminders/tests/2011-09-19_090052_194569.email
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/form-check-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/form-check-off.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/form-radio-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/form-radio-off.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/icons-18-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/icons-18-black.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/icons-18-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/icons-18-white.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/icons-36-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/icons-36-black.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/icons-36-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/icons-36-white.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-icons_222222_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-icons_222222_256x240.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-icons_2e83ff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-icons_2e83ff_256x240.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-icons_454545_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-icons_454545_256x240.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-icons_888888_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-icons_888888_256x240.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-icons_cd0a0a_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-icons_cd0a0a_256x240.png
--------------------------------------------------------------------------------
/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
--------------------------------------------------------------------------------
/static/css/ext/jquery.mobile/images/icon-search-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/ext/jquery.mobile/images/icon-search-black.png
--------------------------------------------------------------------------------
/static/sounds/README.txt:
--------------------------------------------------------------------------------
1 | WAV TO MP3
2 | lame -h -b 192 49__Anton__Glass_G_mf.wav "Glass_G.mp3"
3 |
4 | MP3 TO OGG
5 | mpg321 Glass_G.mp3 -w - | oggenc -o Glass_G.ogg -
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png
--------------------------------------------------------------------------------
/utils/send_mail/config.py:
--------------------------------------------------------------------------------
1 | EMAIL_HOST = 'mail.fry-it.com'
2 | EMAIL_PORT = 25
3 |
4 | EMAIL_HOST_USER = None
5 | EMAIL_HOST_PASSWORD = None
6 |
7 | EMAIL_USE_TLS = False
--------------------------------------------------------------------------------
/apps/templates/user/reset_password.txt:
--------------------------------------------------------------------------------
1 | {% if first_name %}Hi {{ first_name }}
2 |
3 | {% end %}To reset your password click on:
4 | {{ recover_url }}
5 |
6 | --
7 | {{ signature }}
--------------------------------------------------------------------------------
/static/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/peterbe/worklog/HEAD/static/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | py-bcrypt
2 | coverage
3 | mongokit
4 | pycurl
5 | simplejson
6 | xlwt
7 | lxml
8 | python-dateutil
9 | formatflowed
10 | markdown
11 | cssmin
12 | redis
13 | isodate
14 |
--------------------------------------------------------------------------------
/apps/main/tests/mock_data.py:
--------------------------------------------------------------------------------
1 | MOCK_GOOGLE_USER = {'email': u'peterbe@poomail.com',
2 | 'first_name': u'H\xc3\xbcseyin',
3 | 'last_name': u'Bengtsson',
4 | 'locale': u'en-gb',
5 | 'name': u'Peter Bengtsson'
6 | }
7 |
--------------------------------------------------------------------------------
/static/css/stats.css:
--------------------------------------------------------------------------------
1 | td.label {
2 | font-weight: bold;
3 | padding-right:10px;
4 | }
5 |
6 | td.number {
7 | font:16px Courier,monospace;
8 | }
9 |
10 |
11 | .plot {
12 | margin-bottom:50px
13 | }
--------------------------------------------------------------------------------
/bin/here.py:
--------------------------------------------------------------------------------
1 | import site
2 | import os
3 | ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
4 | path = lambda *a: os.path.join(ROOT,*a)
5 | site.addsitedir(path('.'))
6 | site.addsitedir(path('vendor'))
7 |
--------------------------------------------------------------------------------
/apps/main/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # These are here so you can do:
2 | # ./run_tests.sh --autoreload apps.main.tests
3 | from test_models import *
4 | from test_api import *
5 | from test_handlers import *
6 | from test_utils import *
7 |
--------------------------------------------------------------------------------
/apps/templates/help/see_also.html:
--------------------------------------------------------------------------------
1 |
2 | {% for each in links %}
3 | {% if each['is_on'] %}
4 | {{ each['label'] }}
5 | {% else %}
6 | {{ each['label'] }}
7 | {% end %}
8 | {% end %}
9 |
--------------------------------------------------------------------------------
/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Allow: /
3 | Disallow: /report/
4 | Disallow: /stats/
5 | Disallow: /share/
6 | Disallow: /user/account/
7 | Disallow: /user/logout/
8 | Disallow: /report/report.xls
9 | Disallow: /report/report.csv
10 | Disallow: /events.json
11 |
--------------------------------------------------------------------------------
/apps/templates/sharing/cant-share-yet.html:
--------------------------------------------------------------------------------
1 | Can't share yet
2 |
3 | You need to enter your name or email at least.
4 | Otherwise people won't know who's sharing what.
5 |
6 | Click here to fix that
7 |
--------------------------------------------------------------------------------
/static/css/logs.css:
--------------------------------------------------------------------------------
1 | #logs {
2 | width:100%;
3 | }
4 |
5 | #logs thead td {
6 | font-weight:bold;
7 | }
8 | #logs thead {
9 | background-color:#ccc;
10 | }
11 | #logs td {
12 | padding:1px 4px;
13 | }
14 |
15 | #actions-plot {
16 | margin-top:100px;
17 | }
--------------------------------------------------------------------------------
/apps/templates/qunit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | QUnit Test Suites
5 |
6 |
7 |
8 |
9 | {% for url in urls %}
10 | {{ url }}
11 | {% end %}
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/apps/templates/emailreminders/show_weekday_reminders.html:
--------------------------------------------------------------------------------
1 | {% for reminder in reminders %}
2 |
4 | {% end %}
5 |
--------------------------------------------------------------------------------
/apps/templates/bookmarklet/posted.html:
--------------------------------------------------------------------------------
1 | {% extends "bookmarklet_base.html" %}
2 | {% block content %}
3 | Thanks! Saved on DoneCal
4 |
5 | To edit the posted event click here:
6 | {{ event.title }}
7 |
8 | {% end %}
9 |
--------------------------------------------------------------------------------
/apps/emailreminders/ui_modules.py:
--------------------------------------------------------------------------------
1 | import tornado.web
2 |
3 | class ShowWeekdayReminders(tornado.web.UIModule):
4 | def render(self, weekday, weekday_reminders):
5 | reminders = weekday_reminders.get(weekday, [])
6 | return self.render_string("emailreminders/show_weekday_reminders.html", reminders=reminders)
7 |
8 |
9 |
--------------------------------------------------------------------------------
/apps/templates/help/show_vimeo_video.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Introduction to DoneCal.com
5 |
6 |
7 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/utils/decorators.py:
--------------------------------------------------------------------------------
1 | from tornado.web import HTTPError
2 |
3 | def login_required(func):
4 | def is_logged_in(self):
5 | guid = self.get_secure_cookie('user')
6 | if guid:
7 | if self.db.users.User(dict(guid=guid)):
8 | return func(self)
9 | raise HTTPError(403, "Must be logged in")
10 | return is_logged_in
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor/src/tornado-utils"]
2 | path = vendor/src/tornado-utils
3 | url = git://github.com/peterbe/tornado-utils.git
4 | [submodule "vendor/src/mongokit"]
5 | path = vendor/src/mongokit
6 | url = git://github.com/namlook/mongokit.git
7 | [submodule "vendor/src/tornado"]
8 | path = vendor/src/tornado
9 | url = git://github.com/facebook/tornado.git
10 |
--------------------------------------------------------------------------------
/INSTALL.txt:
--------------------------------------------------------------------------------
1 | INSTALLATION
2 | ============
3 |
4 | Tornado
5 | -------
6 |
7 | Install latest tornado from github.
8 |
9 |
10 | Python Dependencies
11 | -------------------
12 |
13 | All pip installable apps are mentioned in `external_apps.txt`
14 |
15 | $ pip install -r external_apps.txt
16 |
17 | Linux dependencies
18 | ------------------
19 |
20 | python-lxml
21 |
22 |
23 |
--------------------------------------------------------------------------------
/apps/main/migrations/007_user_premium.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import User
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([User])
5 |
6 |
7 | db = con.worklog
8 | print "Fixing", db.User.find({'premium':{'$exists': False}}).count(), "objects"
9 | for each in db.User.find({'premium':{'$exists': False}}):
10 | each['premium'] = False
11 | each.save()
12 |
--------------------------------------------------------------------------------
/apps/main/migrations/001_url_and_external_url.py:
--------------------------------------------------------------------------------
1 | from models import Event
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([Event])
5 |
6 | collection = con.worklog.events
7 | print "Fixing", collection.Event.find({'url':{'$exists': True}}).count(), "objects"
8 | for each in collection.Event.find({'url':{'$exists': True}}):
9 | del each['url']
10 | each.save()
--------------------------------------------------------------------------------
/apps/main/migrations/example.py.txt:
--------------------------------------------------------------------------------
1 | from models import File
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([File])
5 |
6 | collection = con.worklog.files
7 | print "Fixing", collection.File.find({'text_type':{'$exists': False}}).count(), "files"
8 | for each in collection.File.find({'text_type':{'$exists': False}}):
9 | each['text_type'] = u''
10 | each.save()
--------------------------------------------------------------------------------
/apps/main/migrations/004_event_description.py:
--------------------------------------------------------------------------------
1 | from models import Event
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([Event])
5 |
6 |
7 | db = con.worklog
8 | print "Fixing", db.Event.find({'description':{'$exists': False}}).count(), "objects"
9 | for each in db.Event.find({'description':{'$exists': False}}):
10 | each['description'] = u""
11 | each.save()
12 |
--------------------------------------------------------------------------------
/static/recover.js:
--------------------------------------------------------------------------------
1 | head.ready(function() {
2 | head.js(JS_URLS.validate, function() {
3 | var validated_emails = {};
4 |
5 | $('form.recover').validate({
6 | rules: {
7 | password: {
8 | required: true,
9 | minlength: 4
10 | }
11 | }
12 | });
13 | });
14 |
15 | $('input[name="password"]').focus();
16 | });
17 |
--------------------------------------------------------------------------------
/apps/templates/smartphone/logged_in.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Clock
5 |
6 |
7 |
8 |
Your tags:
9 |
10 | {% for tag in available_tags %}
11 | {{ tag }}
12 | {% end %}
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/static/forgotten.js:
--------------------------------------------------------------------------------
1 | head.ready(function() {
2 | head.js(JS_URLS.validate, function() {
3 | var validated_emails = {};
4 |
5 | $('form.forgotten').validate({
6 | rules: {
7 | email: {
8 | required: true,
9 | email: true
10 | }
11 | },
12 | onkeyup: false
13 | });
14 | });
15 |
16 | $('input[name="email"]').focus();
17 | });
18 |
--------------------------------------------------------------------------------
/apps/main/indexes.py:
--------------------------------------------------------------------------------
1 | # WORK IN PROGRESS
2 | #
3 | # Intention here is to write the indexes that need to be created and
4 | # make it possible to execute this script some how from the command
5 | # line.
6 | #
7 | # This file has been created as an intermediate of commenting out
8 | # indexes from the models.py
9 |
10 |
11 | import pymongo
12 |
13 | # SEE apps/eventlog/indexes.py for a good start
14 | # It's being called from ./bin/ensure_indexes.py
15 |
--------------------------------------------------------------------------------
/apps/main/migrations/006_featurerequests_implemented.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import FeatureRequest
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([FeatureRequest])
5 |
6 |
7 | db = con.worklog
8 | print "Fixing", db.FeatureRequest.find({'implemented':{'$exists': False}}).count(), "objects"
9 | for each in db.FeatureRequest.find({'implemented':{'$exists': False}}):
10 | each['implemented'] = False
11 | each.save()
12 |
--------------------------------------------------------------------------------
/apps/main/migrations/005_settings_hash_tags.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([UserSettings])
5 |
6 | collection = con.worklog.user_settings
7 | print "Fixing", collection.UserSettings.find({'hash_tags':{'$exists': False}}).count(), "objects"
8 | for each in collection.UserSettings.find({'hash_tags':{'$exists': False}}):
9 | each['hash_tags'] = False
10 | each.save()
11 |
--------------------------------------------------------------------------------
/apps/main/migrations/009_first_hour_settings.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([UserSettings])
5 |
6 |
7 | collection = con.worklog.user_settings
8 | print "Fixing", collection.UserSettings.find({'first_hour':{'$exists': False}}).count(), "objects"
9 | for each in collection.UserSettings.find({'first_hour':{'$exists': False}}):
10 | each['first_hour'] = 8
11 | each.save()
12 |
--------------------------------------------------------------------------------
/apps/main/migrations/002_settings_disable_sound.py:
--------------------------------------------------------------------------------
1 | from models import UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([UserSettings])
5 |
6 |
7 | collection = con.worklog.user_settings
8 | print "Fixing", collection.UserSettings.find({'disable_sound':{'$exists': False}}).count(), "objects"
9 | for each in collection.UserSettings.find({'disable_sound':{'$exists': False}}):
10 | each['disable_sound'] = False
11 | each.save()
12 |
--------------------------------------------------------------------------------
/apps/main/migrations/003_offline_mode_user_settings.py:
--------------------------------------------------------------------------------
1 | from models import UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([UserSettings])
5 |
6 |
7 | collection = con.worklog.user_settings
8 | print "Fixing", collection.UserSettings.find({'offline_mode':{'$exists': False}}).count(), "objects"
9 | for each in collection.UserSettings.find({'offline_mode':{'$exists': False}}):
10 | each['offline_mode'] = False
11 | each.save()
12 |
--------------------------------------------------------------------------------
/apps/main/migrations/008_ampm_format_settings.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([UserSettings])
5 |
6 |
7 | collection = con.worklog.user_settings
8 | print "Fixing", collection.UserSettings.find({'ampm_format':{'$exists': False}}).count(), "objects"
9 | for each in collection.UserSettings.find({'ampm_format':{'$exists': False}}):
10 | each['ampm_format'] = False
11 | each.save()
12 |
--------------------------------------------------------------------------------
/apps/eventlog/indexes.py:
--------------------------------------------------------------------------------
1 | from pymongo import ASCENDING, DESCENDING
2 | from models import EventLog
3 | from mongokit import Connection
4 | con = Connection()
5 | con.register([EventLog])
6 | db = con.worklog
7 |
8 | def run():
9 | collection = db.EventLog.collection
10 | collection.ensure_index([('add_date',DESCENDING)])
11 | test()
12 |
13 | def test():
14 | curs = db.EventLog.find().sort('add_date', DESCENDING).explain()['cursor']
15 | assert 'BtreeCursor' in curs
--------------------------------------------------------------------------------
/apps/main/migrations/012_usersettings_user.py:
--------------------------------------------------------------------------------
1 | from bson.objectid import ObjectId
2 | from apps.main.models import User, UserSettings, connection
3 | import settings
4 |
5 | db = connection[settings.DATABASE_NAME]
6 | collection = db.UserSettings.collection
7 | collection.drop_indexes()
8 |
9 | c = 0
10 | for msg in db.UserSettings.find():
11 | if type(msg['user']) is not ObjectId:
12 | msg['user'] = msg['user'].id
13 | msg.save()
14 | c += 1
15 |
16 | print "Fixed", c
17 |
--------------------------------------------------------------------------------
/utils/truncate.py:
--------------------------------------------------------------------------------
1 | def truncate_words(s, num, end_text='...'):
2 | """Truncates a string after a certain number of words. Takes an optional
3 | argument of what should be used to notify that the string has been
4 | truncated, defaults to ellipsis (...)"""
5 | length = int(num)
6 | words = s.split()
7 | if len(words) > length:
8 | words = words[:length]
9 | if not words[-1].endswith(end_text):
10 | words.append(end_text)
11 | return u' '.join(words)
--------------------------------------------------------------------------------
/apps/eventlog/constants.py:
--------------------------------------------------------------------------------
1 | ACTION_READ = 0
2 | ACTION_ADD = 1
3 | ACTION_EDIT = 2
4 | ACTION_DELETE = 3
5 | ACTION_RESTORE = 4
6 |
7 | ACTIONS_HUMAN_READABLE = {
8 | ACTION_READ: "Read",
9 | ACTION_ADD: "Add",
10 | ACTION_EDIT: "Edit",
11 | ACTION_DELETE: "Delete",
12 | ACTION_RESTORE: "Restore",
13 | }
14 |
15 | CONTEXT_CALENDAR = u'calendar'
16 | CONTEXT_API = u'api'
17 | CONTEXT_BOOKMARKLET = u'bookmarklet'
18 | CONTEXT_EMAILREMINDER = u'emailreminder'
19 | CONTEXT_SMARTPHONE = u'smartphone'
--------------------------------------------------------------------------------
/apps/main/migrations/010_settings_newsletter_optout.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([UserSettings])
5 |
6 |
7 | collection = con.worklog.user_settings
8 | print "Fixing", collection.UserSettings.find({'newsletter_opt_out':{'$exists': False}}).count(), "objects"
9 | for each in collection.UserSettings.find({'newsletter_opt_out':{'$exists': False}}):
10 | each['newsletter_opt_out'] = False
11 | each.save()
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage_report/
2 | *~
3 | *.pyc
4 | .venv
5 | run_mongodb.sh
6 | start_mongod.sh
7 | db/
8 | live-db/
9 | mongodb
10 | tags
11 | *.py.done
12 | build/
13 | pip-log.txt
14 | run_mongo_data_browser.sh
15 | mongo_data_browser/
16 | .base64-image-conversions.pickle
17 | marketing/emails.csv
18 | _run_all_static_modules2.py
19 | _run_all_static_modules.py
20 | _run_all_static_modules2.py
21 | .static_name_conversion
22 | mail.txt
23 | cdn_prefix.conf
24 | Timesheet.doc
25 | static_index.html
26 | cloudfronting/upload.py
27 |
--------------------------------------------------------------------------------
/apps/emailreminders/migrations/002_emailreminders_user.py:
--------------------------------------------------------------------------------
1 | from bson.objectid import ObjectId
2 | from apps.main.models import connection
3 | import apps.emailreminders.models
4 | import settings
5 |
6 | db = connection[settings.DATABASE_NAME]
7 | collection = db.EmailReminder.collection
8 | collection.drop_indexes()
9 |
10 | c = 0
11 | for msg in db.EmailReminder.find():
12 | if type(msg['user']) is not ObjectId:
13 | msg['user'] = msg['user'].id
14 | msg.save()
15 | c += 1
16 |
17 | print "Fixed", c
18 |
--------------------------------------------------------------------------------
/apps/templates/user/change-account.html:
--------------------------------------------------------------------------------
1 | Change your account
2 |
3 |
21 |
--------------------------------------------------------------------------------
/export_newsletter_subscribers.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import User, UserSettings
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([User, UserSettings])
5 | db = con.worklog
6 |
7 | for user_settings in db.UserSettings.find({'newsletter_opt_out':False}):
8 | user = user_settings.user
9 | if user.email is None:
10 | continue
11 | bits = [user.email]
12 | bits.append(user.first_name and user.first_name or u'')
13 | bits.append(user.last_name and user.last_name or u'')
14 | out = "\t".join(bits)
15 | print out.encode('utf8')
16 |
--------------------------------------------------------------------------------
/apps/emailreminders/migrations/001_include_summary_and_instructions.py:
--------------------------------------------------------------------------------
1 | from apps.emailreminders.models import EmailReminder
2 | from mongokit import Connection
3 | con = Connection()
4 | con.register([EmailReminder])
5 |
6 |
7 | collection = con.worklog[EmailReminder.__collection__]
8 | print "Fixing", collection.EmailReminder.find({'include_instructions':{'$exists': False}}).count(), "objects"
9 | for each in collection.EmailReminder.find({'include_instructions':{'$exists': False}}):
10 | each['include_instructions'] = True
11 | each['include_summary'] = False
12 | each.save()
13 |
14 |
--------------------------------------------------------------------------------
/apps/main/config.py:
--------------------------------------------------------------------------------
1 | MAX_TITLE_LENGTH = 500
2 | UNTAGGED_COLOR = "#4bb2c5"
3 | TAG_COLOR_SERIES = ("#EAA228", "#c5b47f", "#579575", "#839557", "#958c12",
4 | "#953579", "#4b5de4", "#d8b83f", "#ff5800", "#0085cc",
5 | "#c747a3", "#cddf54", "#FBD178", "#26B4E3", "#bd70c7")
6 |
7 | # This applies when it's not an all_day event and the date is the same
8 | MINIMUM_DAY_SECONDS = 60 * 30
9 |
10 | API_CHANGELOG = (
11 | ("1.1", "Validation in place to prevent end date less than start date"),
12 | ("1.0", "Initial API launched"),
13 | )
14 | API_VERSION = API_CHANGELOG[0][0]
15 |
--------------------------------------------------------------------------------
/apps/templates/user/recover_forgotten.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Recover forgotten password
6 |
21 |
22 | {% end %}
23 |
24 |
25 | {% block extrajs %}
26 | {% module Static("recover.js") %}
27 | {% end %}
28 |
--------------------------------------------------------------------------------
/utils/send_mail/dns_name.py:
--------------------------------------------------------------------------------
1 | """
2 | Email message and email sending related helper functions.
3 | """
4 |
5 | import socket
6 |
7 |
8 | # Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
9 | # seconds, which slows down the restart of the server.
10 | class CachedDnsName(object):
11 | def __str__(self):
12 | return self.get_fqdn()
13 |
14 | def get_fqdn(self):
15 | if not hasattr(self, '_fqdn'):
16 | self._fqdn = socket.getfqdn()
17 | return self._fqdn
18 |
19 | DNS_NAME = CachedDnsName()
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/apps/templates/sound/test.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block extra_head %}
4 | {% end %}
5 |
6 | {% block content_inner %}
7 |
8 | Test Sound
9 |
10 | Play delete
11 | Play add
12 |
13 | {% end %}
14 |
15 |
16 | {% block extrajs %}
17 | {% module Static("play-sounds.js") %}
18 |
29 | {% end %}
30 |
--------------------------------------------------------------------------------
/static/ext/jquery.cookie.min.js:
--------------------------------------------------------------------------------
1 | jQuery.cookie=function(e,b,a){if(arguments.length>1&&(b===null||typeof b!=="object")){a=jQuery.extend({},a);if(b===null)a.expires=-1;if(typeof a.expires==="number"){var d=a.expires,c=a.expires=new Date;c.setDate(c.getDate()+d)}return document.cookie=[encodeURIComponent(e),"=",a.raw?String(b):encodeURIComponent(String(b)),a.expires?"; expires="+a.expires.toUTCString():"",a.path?"; path="+a.path:"",a.domain?"; domain="+a.domain:"",a.secure?"; secure":""].join("")}a=b||{};c=a.raw?function(f){return f}:
2 | decodeURIComponent;return(d=(new RegExp("(?:^|; )"+encodeURIComponent(e)+"=([^;]*)")).exec(document.cookie))?c(d[1]):null};
3 |
--------------------------------------------------------------------------------
/apps/main/migrations/011_0second_not_all_day_events.py:
--------------------------------------------------------------------------------
1 | from apps.main.models import Event
2 | from apps.main.config import MINIMUM_DAY_SECONDS
3 | from mongokit import Connection
4 | con = Connection()
5 | con.register([Event])
6 |
7 |
8 | collection = con.worklog.events
9 | qs = collection.Event.find({'all_day':True, '$where':'this.start.getTime()==this.end.getTime()'})
10 | print "Faxing", qs.count(), "objects"
11 | for event in qs:
12 | assert event.start == event.end
13 | assert event.all_day
14 | # not all-day events with start==end *appear* like 2 hour events
15 | event.end += datetime.timedelta(hours=2)#seconds=MINIMUM_DAY_SECONDS)
16 | event.save()
17 |
--------------------------------------------------------------------------------
/apps/templates/help/help_base.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}{% module HelpPageTitle() %}{% end %}
4 |
5 | {% block sidebar %}
6 | See also
7 |
8 | {% module HelpSeeAlsoLinks() %}
9 |
10 |
11 |
20 | {% end %}
21 |
--------------------------------------------------------------------------------
/apps/templates/modules/eventpreview.html:
--------------------------------------------------------------------------------
1 | {% import tornado.escape %}
2 |
3 | {{ tornado.escape.linkify(event.title, extra_params='target="_blank"') }}
4 |
5 |
6 | {% if event.external_url %}
7 |
8 | {{ event.external_url }}
9 |
10 | {% end %}
11 |
12 | {% if event.description %}
13 |
14 | {{ event.description }}
15 |
16 | {% end %}
17 |
18 |
19 |
20 |
21 | Added {{ add_ago }} ago
22 | {% if user_name %}
23 | by {{ user_name }}
24 | {% end %}
25 |
26 |
--------------------------------------------------------------------------------
/marketing/generate_emails_csv.py:
--------------------------------------------------------------------------------
1 | from mongokit import *
2 | from models import *
3 | con.register([Event,User])
4 | users =db.User.find({'email':{'$ne':None}})
5 | emails=[]
6 | for user in users:
7 | c=db.Event.find({'user.$id':user._id}).count()
8 | emails.append((user.email, c, user.first_name, user.last_name, user.add_date))
9 |
10 | import csv
11 | out=open('emails.csv','w')
12 | writer=csv.writer(out)
13 | writer.writerow(['Email','# events', 'First', 'Last', 'Add date'])
14 | for email, count, first, last, date in emails:
15 | row=[email.encode('utf8'), count, first.encode('utf8'), last.encode('utf8'), date.isoformat()]
16 | writer.writerow(row)
17 | out.close()
18 |
19 |
--------------------------------------------------------------------------------
/apps/templates/help/bookmarklet.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Bookmarklet
6 |
7 | For people who live in their web browser
8 |
9 |
10 | Drag this link into your bookmarks toolbar
11 | DoneCal
12 |
13 |
14 |
15 | If you want want to know more about the Bookmarklet and how to use it check out this introduction video .
16 |
17 | {% end %}
18 |
19 |
--------------------------------------------------------------------------------
/apps/templates/user/forgotten.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Forgotten password
6 | {% if success %}
7 | {{ success }}
8 | {% else %}
9 |
24 | {% end %}
25 |
26 | {% end %}
27 |
28 |
29 | {% block extrajs %}
30 | {% module Static("forgotten.js") %}
31 | {% end %}
32 |
--------------------------------------------------------------------------------
/apps/templates/modules/footer.html:
--------------------------------------------------------------------------------
1 |
2 | ©
3 | {% if calendar_link %}
4 | Home/Calendar
5 | {% end %}
6 | About
7 | Help
8 | Developers' API
9 | {% if user %}Bookmarklet {% end %}
10 | {% if user %}Feature requests {% end %}
11 | {% if user %}Email reminders {% end %}
12 | {% if user %}Power Users {% end %}
13 |
14 |
--------------------------------------------------------------------------------
/apps/github/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from bson.objectid import ObjectId
3 | from apps.main.models import BaseDocument, register
4 | #git log master --date=short --pretty=format:"%h%x09%an%x09%ad%x09%s"
5 |
6 |
7 | @register
8 | class GitHubRepo(BaseDocument):
9 | __collection__ = 'githubrepos'
10 | structure = {
11 | 'username': unicode,
12 | 'repo': unicode,
13 | 'branch': unicode,
14 | 'full_url': unicode,
15 | 'last_pull_date': datetime.datetime,
16 | 'last_load_date': datetime.datetime,
17 | }
18 | required_fields = ['username', 'repo']
19 | default_values = {
20 | 'branch': u'master',
21 | }
22 |
23 | #git log master --date=short --pretty=format:"%h%x09%an%x09%ad%x09%s"
24 |
--------------------------------------------------------------------------------
/static/play-sounds.js:
--------------------------------------------------------------------------------
1 | /* Call to create and partially download the audo element.
2 | * You can all this as much as you like. */
3 | function preload_sound(key) {
4 | var id = 'sound-' + key;
5 | if (!document.getElementById(id)) {
6 | if (!SOUND_URLS[key]) {
7 | throw "Sound for '" + key + "' not defined";
8 | } else if (SOUND_URLS[key].search(/\.ogg/i) == -1) {
9 | throw "Sound for '" + key + "' must be .ogg URL";
10 | }
11 | var a = document.createElement('audio');
12 | a.setAttribute('id', id);
13 | a.setAttribute('src', SOUND_URLS[key]);
14 | document.body.appendChild(a);
15 | }
16 | return id;
17 | }
18 |
19 | function play_sound(key) {
20 | document.getElementById(preload_sound(key)).play();
21 | }
22 |
--------------------------------------------------------------------------------
/static/css/calendar.css:
--------------------------------------------------------------------------------
1 | h1#calendar-h1 {
2 | color: #292E35;
3 | font-size: 30px;
4 | font-weight:bolder;
5 | margin-top: 10px;
6 | margin-left: 49px;
7 | font-family:"droid-sans-1","droid-sans-2",Arial,Verdana,sans-serif;
8 | }
9 | h2.fc-header-title {
10 | font-size: 130%;
11 | font-weight: bold;
12 | }
13 | #calendar {
14 | margin: 10px;
15 | }
16 | input.placeholdervalue, textarea.placeholdervalue { color:#ccc; }
17 | #settings-box {
18 | margin-top:50px;
19 | }
20 | a.share-hider {
21 | font-size:10px;
22 | float:right;
23 | }
24 | .ui-tooltip, .qtip { max-width:540px; }
25 | .ui-tooltip-wrapper {
26 | background-color:#EEEEEE;
27 | border-color:#cdcdcd;
28 | }
29 |
--------------------------------------------------------------------------------
/apps/templates/powerusers/index.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}Power Users{% end %}
4 |
5 | {% block extra_head %}
6 |
8 | {% end %}
9 |
10 | {% block sidebar %}
11 | {% end %}
12 |
13 | {% block content_inner %}
14 |
15 | Power Users
16 |
17 |
18 | Check out these power users:
19 | {% for entry in power_users %}
20 | {{ entry['name'] }}
21 | {% end %}
22 |
23 |
24 | {% if user %}
25 | Think you're a power user? If so please contact
26 | mail@peterbe.com
27 |
28 | {% end %}
29 |
30 | {% end %}
31 |
--------------------------------------------------------------------------------
/apps/templates/premium/checkout.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/templates/testimonials.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
It is so easy to use that no person could resist giving it a try at the very least.
4 |
DoneCal.Com - A Very Simple Calendar
5 |
6 |
DoneCal is a lightweight and dead simple online calendar to help you keep track of dates and events. This app provides a different approach in working with online timesheets and task records and also serves as a great alternative for calendars like Google Calendar and iCal.
7 |
makeusof.com
8 |
9 |
Compared to a few corporate calendaring apps I've used, yours looks nice.
10 |
Gerard Flanagan
11 |
12 |
13 |
--------------------------------------------------------------------------------
/apps/eventlog/migrations/001_user_reference.py:
--------------------------------------------------------------------------------
1 | from bson.objectid import ObjectId
2 | from apps.main.models import connection
3 | import apps.eventlog.models
4 | import settings
5 |
6 | db = connection[settings.DATABASE_NAME]
7 | collection = db.EventLog.collection
8 | collection.drop_indexes()
9 |
10 | c = 0
11 | for msg in db.EventLog.find():
12 | if type(msg['user']) is ObjectId:
13 | pass
14 | else:
15 | if isinstance(msg, dict):
16 | try:
17 | msg['user'] = msg['user']['_id']
18 | except TypeError:
19 | # a DBRef
20 | msg['user'] = msg['user'].id
21 | msg.save()
22 | c += 1
23 | else:
24 | print repr(msg['user']), type(msg['user'])
25 |
26 | print "Fixed", c
27 |
--------------------------------------------------------------------------------
/apps/templates/eventlog/activity.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}Activity log{% end %}
4 |
5 | {% block extra_head %}
6 | {% module Static("css/ext/jquery.jqplot.min.css", "css/logs.css") %}
7 | {% end %}
8 |
9 |
10 | {% block sidebar %}
11 |
12 | {% end %}
13 |
14 | {% block content_inner %}
15 |
16 | Activity log
17 |
18 |
19 |
20 |
21 |
22 | {% end %}
23 |
24 |
25 | {% block extrajs %}
26 |
30 | {% module Static("activitylog.js") %}
31 | {% end %}
32 |
--------------------------------------------------------------------------------
/apps/eventlog/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 | def log_event(db, user, event, action, context, comment=None):
3 | try:
4 | event_log = db.EventLog()
5 | event_log.user = user._id
6 | event_log.event = event
7 | event_log.action = action
8 | event_log.context = context
9 | if comment is not None:
10 | event_log.comment = unicode(comment)
11 | event_log.save()
12 | except:
13 | logging.error("Unable to log event", exc_info=True)
14 |
15 | class Empty:
16 | pass
17 | import constants
18 | actions = Empty()
19 | contexts = Empty()
20 | for each in dir(constants):
21 | if each.startswith('ACTION_'):
22 | setattr(actions, each, getattr(constants, each))
23 | if each.startswith('CONTEXT_'):
24 | setattr(contexts, each, getattr(constants, each))
25 |
--------------------------------------------------------------------------------
/static/css/report.css:
--------------------------------------------------------------------------------
1 | tbody.head th { font-weight:bold; }
2 | #report tr.total td { font-weight:bold }
3 | #report td.label { padding-left:10px; }
4 | #report .label { width:200px; }
5 |
6 | #export-options p {
7 | background-repeat: no-repeat;
8 | background-position: 0 50%;
9 | padding:15px 10px 15px 5px;
10 | }
11 | #export-options p a {
12 | padding-left:50px;
13 | }
14 | #excel-export {
15 | background-image: url('icons/x-office-spreadsheet.png') ;
16 | }
17 | #csv-export {
18 | background-image: url('icons/text-csv.png') ;
19 | }
20 | #pdf-export {
21 | background-image: url('icons/application-pdf.png') ;
22 | }
23 |
24 |
25 | #export-options {
26 | margin-top:150px;
27 | }
28 |
29 | #export-options h2 {
30 | font-size: 19px;
31 | margin-bottom:20px;
32 | }
33 |
34 | #report h2 {
35 | margin-top:40px;
36 | margin-bottom:0;
37 | }
38 |
--------------------------------------------------------------------------------
/apps/qunit/handlers.py:
--------------------------------------------------------------------------------
1 | from glob import glob
2 | import os
3 | import tornado.web
4 |
5 | from tornado_utils.routes import route
6 |
7 | @route('/qunit/?')
8 | class QUnitHandler(tornado.web.RequestHandler):
9 | def get(self):
10 | tmpl_dir = os.path.normpath(os.path.join(__file__, '../../templates/qunit'))
11 | tmpls = glob(tmpl_dir + '/*.html')
12 |
13 | options = {'urls':[]}
14 | for tmpl in tmpls:
15 | if tmpl.endswith('index.html'):
16 | continue
17 | basename = os.path.basename(tmpl)
18 | options['urls'].append(self.reverse_url('qunit_file', basename))
19 |
20 | self.render('qunit/index.html', **options)
21 |
22 | @route('/qunit/(\w+\.html)', name="qunit_file")
23 | class QUnitHandler(tornado.web.RequestHandler):
24 | def get(self, filename):
25 | self.render(os.path.join('qunit', filename))
26 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | TITLE = u"DoneCal"
2 | APPS = (
3 | 'main',
4 | 'smartphone',
5 | 'emailreminders',
6 | 'eventlog',
7 | 'qunit',
8 | 'github',
9 | )
10 |
11 | DATABASE_NAME = "worklog"
12 |
13 | LOGIN_URL = "/auth/login/"
14 |
15 | REDIS_HOST = 'localhost'
16 | REDIS_PORT = 6379
17 |
18 | COOKIE_SECRET = "11o3TzKsxQAGAYdkl5gmGEJJFu4h7EQnp1XdTP10/"
19 |
20 | WEBMASTER = 'noreply@donecal.com'
21 | ADMIN_EMAILS = ['peterbe@gmail.com']
22 |
23 | EMAIL_REMINDER_SENDER = 'reminder+%(id)s@donecal.com'
24 | EMAIL_REMINDER_NOREPLY = 'noreplyplease@donecal.com'
25 |
26 | # commented out because it's on by default but driven by dont_embed_static_url option instead
27 | ## if you do this, for the static files, instead of getting something like
28 | ## '/static/foo.png?v=123556' we get '/static/v-123556/foo.png'
29 | #EMBED_STATIC_URL_TIMESTAMP = True
30 |
31 | try:
32 | from local_settings import *
33 | except ImportError:
34 | pass
35 |
--------------------------------------------------------------------------------
/apps/templates/help/internet-explorer.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Internet Explorer
6 |
7 | DoneCal is built specifically to use the latest web techniques and some of these are not
8 | supported on Internet Explorer. Especially older versions that are sadly still quite popular.
9 |
10 | Nothing is done deliberately to make it not work in Internet Explorer but there are
11 | no intentions to fix potential problems in DoneCal just to make it work with
12 | Internet Explorer's idiosyncrasies.
13 |
14 | If you are unfortunate enough to be stuck with an old version of Internet Explorer your
15 | options are either to upgrade to the latest version or, better, change to a free and secure browser
16 | such as Google Chrome or Mozilla Firefox .
17 |
18 |
19 | {% end %}
20 |
21 |
--------------------------------------------------------------------------------
/utils/send_mail/backends/locmem.py:
--------------------------------------------------------------------------------
1 | """
2 | Backend for test environment.
3 | """
4 | import sys
5 | #print sys.path
6 | import utils.send_mail as mail
7 | #from utils.send_mail import send_email as mail
8 | from utils.send_mail.backends.base import BaseEmailBackend
9 |
10 | class EmailBackend(BaseEmailBackend):
11 | """A email backend for use during test sessions.
12 |
13 | The test connection stores email messages in a dummy outbox,
14 | rather than sending them out on the wire.
15 |
16 | The dummy outbox is accessible through the outbox instance attribute.
17 | """
18 | def __init__(self, *args, **kwargs):
19 | super(EmailBackend, self).__init__(*args, **kwargs)
20 | if not hasattr(mail, 'outbox'):
21 | mail.outbox = []
22 |
23 | def send_messages(self, messages):
24 | """Redirect messages to the dummy outbox"""
25 | mail.outbox.extend(messages)
26 | return len(messages)
27 |
--------------------------------------------------------------------------------
/utils/git.py:
--------------------------------------------------------------------------------
1 | import os, re
2 | import logging
3 | from subprocess import Popen, PIPE
4 |
5 | def get_git_revision():
6 | return _get_git_revision()
7 |
8 | def _get_git_revision():
9 | # this is actually very fast. Takes about 0.01 seconds on my machine!
10 | home = os.path.dirname(__file__)
11 | proc = Popen('cd %s;git log --no-color -n 1 --date=iso' % home,
12 | shell=True, stdout=PIPE, stderr=PIPE)
13 | output = proc.communicate()
14 | try:
15 | commit = re.findall('commit (\w+)', output[0])[0]
16 | date = [x.split('Date:')[1].split('+')[0].strip() for x in
17 | output[0].splitlines() if x.startswith('Date:')][0]
18 | date_wo_tz = re.split('-\d{4}', date)[0].strip()
19 | return '%s (%s)' % (commit, date_wo_tz)
20 | except IndexError:
21 | logging.debug("OUTPUT=%r" % output[0], exc_info=True)
22 | logging.debug("ERROR=%r" % output[1])
23 | return 'unknown'
24 |
--------------------------------------------------------------------------------
/run_cleanup_deleted_events.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import datetime
3 | from mongokit import Connection
4 | from apps.main.models import User, Event
5 |
6 | def get_db():
7 | con = Connection()
8 | con.register([User, Event])
9 | return con['worklog']
10 |
11 | def main(verbose=True):
12 | db = get_db()
13 | undoer = db.User.one({'guid': u"UNDOER"})
14 | search = {'user.$id': undoer._id}
15 | minute_ago = datetime.datetime.now()
16 | minute_ago -= datetime.timedelta(minutes=1)
17 | search['add_date'] = {'$lt': minute_ago}
18 | if verbose:
19 | print "Removing", db.Event.find(search).count(), "events"
20 |
21 | db[Event.__collection__].remove(search)
22 |
23 |
24 |
25 | def run(*args):
26 | verbose = True
27 | if '-q' in args or '--quiet' in args:
28 | verbose = False
29 | main(verbose=verbose)
30 | return 0
31 |
32 | if __name__ == '__main__':
33 | import sys
34 | sys.exit(run(*sys.argv[1:]))
--------------------------------------------------------------------------------
/static/css/ext/jquery.autocomplete.css:
--------------------------------------------------------------------------------
1 | .ac_results {
2 | padding: 0px;
3 | border: 1px solid black;
4 | background-color: white;
5 | overflow: hidden;
6 | z-index: 99999;
7 | }
8 |
9 | .ac_results ul {
10 | width: 100%;
11 | list-style-position: outside;
12 | list-style: none;
13 | padding: 0;
14 | margin: 0;
15 | }
16 |
17 | .ac_results li {
18 | margin: 0px;
19 | padding: 2px 5px;
20 | cursor: default;
21 | display: block;
22 | /*
23 | if width will be 100% horizontal scrollbar will apear
24 | when scroll mode will be used
25 | */
26 | /*width: 100%;*/
27 | font: menu;
28 | font-size: 12px;
29 | /*
30 | it is very important, if line-height not setted or setted
31 | in relative units scroll will be broken in firefox
32 | */
33 | line-height: 16px;
34 | overflow: hidden;
35 | }
36 |
37 | .ac_loading {
38 | background: white url('indicator.gif') right center no-repeat;
39 | }
40 |
41 | .ac_odd {
42 | background-color: #eee;
43 | }
44 |
45 | .ac_over {
46 | background-color: #0A246A;
47 | color: white;
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/bin/_run_tests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import unittest
3 | import here
4 |
5 | TEST_MODULES = [
6 | 'apps.main.tests.test_handlers',
7 | 'apps.main.tests.test_api',
8 | 'apps.main.tests.test_models',
9 | 'apps.main.tests.test_utils',
10 | 'apps.emailreminders.tests.test_models',
11 | 'apps.emailreminders.tests.test_handlers',
12 | 'apps.emailreminders.tests.test_utils',
13 | 'apps.eventlog.tests.test_handlers',
14 | ]
15 |
16 | def all():
17 | try:
18 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES)
19 | except AttributeError, e:
20 | if "'module' object has no attribute 'test_handlers'" in str(e):
21 | # most likely because of an import error
22 | for m in TEST_MODULES:
23 | __import__(m, globals(), locals())
24 | raise
25 |
26 |
27 | if __name__ == '__main__':
28 | import tornado.testing
29 | #import cProfile, pstats
30 | #cProfile.run('tornado.testing.main()')
31 | try:
32 | tornado.testing.main()
33 | except KeyboardInterrupt:
34 | pass # exit
35 |
--------------------------------------------------------------------------------
/apps/eventlog/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from bson.objectid import ObjectId
3 | from apps.main.models import BaseDocument, Event, register
4 |
5 |
6 | @register
7 | class EventLog(BaseDocument):
8 | __collection__ = 'event_log'
9 | structure = {
10 | 'event': Event,
11 | 'user': ObjectId,
12 | 'action': int,
13 | 'context': unicode,
14 | 'comment': unicode,
15 | }
16 |
17 | # we're not using autorefs here because then we don't have to cascade
18 | # deletes
19 | use_autorefs = False
20 |
21 | @staticmethod
22 | def get_eventlogs_by_event(event):
23 | """return a cursor for all the eventlogs related to this parameter
24 | event.
25 |
26 | The reason why this is a static method here and not part of the
27 | apps.main.models.Event class is because I don't want to clutter the main
28 | app with something like the eventlog which is much less important."""
29 | return event.db.EventLog.find({'event':event})
30 |
31 | @property
32 | def user(self):
33 | return self.db.User.find_one({'_id': self['user']})
34 |
--------------------------------------------------------------------------------
/apps/templates/help/feature-requests.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Feature requests
6 |
7 | The Feature requests page is a simple application for collecting
8 | feedback from actual users about what new features (or changes) should be added to DoneCal next.
9 | Due to lack of time, all feature requests can't be satisfied so with this application it becomes
10 | possible to know what matters the most and hence should be attacked first.
11 |
12 |
13 | Anybody can suggest a new feature and doing so automatically puts your vote on it. Then,
14 | once added, you can hope that other people will vote it up too.
15 |
16 | Voting weight is basically a count of the number of event entries you have.
17 | If you have many entries you are most likely a frequent user so your vote counts for more. To be able to
18 | vote and to add new feature requests a prerequisite is that you have created an account.
19 |
20 | Go to the Feature requests page
21 |
22 | {% end %}
--------------------------------------------------------------------------------
/static/activitylog.js:
--------------------------------------------------------------------------------
1 | head.ready(function(){
2 |
3 | $.getJSON('/log/activity.json', {}, function(response) {
4 | var plot = $.jqplot('users-plot', [response.users], {
5 | title:'Active Users',
6 | gridPadding:{right:35},
7 | axes:{
8 | yaxis: { min:0 },
9 | xaxis:{
10 | renderer:$.jqplot.DateAxisRenderer,
11 | tickOptions:{formatString:'%#d %b -%y'}
12 | //min:'May 30, 2008',
13 | // tickInterval:'1 month'
14 | }
15 | },
16 | series:[{lineWidth:2}]
17 | });
18 |
19 | var plot = $.jqplot('events-plot', [response.events], {
20 | title:'Active Events',
21 | gridPadding:{right:35},
22 | axes:{
23 | yaxis: { min:0 },
24 | xaxis:{
25 | renderer:$.jqplot.DateAxisRenderer,
26 | tickOptions:{formatString:'%#d %b -%y'}
27 | //min:'May 30, 2008',
28 | // tickInterval:'1 month'
29 | }
30 | },
31 | series:[{lineWidth:2}]
32 | });
33 |
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/apps/templates/powerusers/users/jerome-ferdinands.html:
--------------------------------------------------------------------------------
1 | {% extends "power_user_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Power User: {{ user.first_name }} {{ user.last_name }}
6 |
7 |
8 |
9 |
10 |
DoneCal: Hi Jerome, what do you use DoneCal for?
11 |
12 |
Jerome:
13 | I use DoneCal as a timesheeting tool for my work here at eBay.
14 | I enter my time into blocks based on projects which is why I find the @ functionality
15 | very useful. I then export a monthly timesheet into excel and provide it along with my
16 | invoice. The best feature of DoneCal is its simplicity by providing basic core
17 | features which is a rarity these days in software where the standard is to provide a
18 | laundry list of features that haven't been thought through effectively. It also took me
19 | all of 10 mins to work out how to use it. The tutorial video is simple and effective.
20 | My life's work has been around UI and usability so saying that DoneCal is easy to use
21 | comes with 12 years of UX experience behind me.
22 |
23 |
24 |
25 |
26 | {% end %}
27 |
--------------------------------------------------------------------------------
/bin/_cleanup_old_combined_files.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from collections import defaultdict
4 | def run(directory, verbose):
5 | names = defaultdict(list)
6 | for f in os.listdir(directory):
7 | ff = os.path.join(directory, f)
8 | if os.path.isdir(ff):
9 | run(ff, verbose)
10 | elif os.path.isfile(ff):
11 | wo = re.sub('\d{10}\.', '', f)
12 | d = int(re.findall('(\d{10})\.', f)[0])
13 | names[wo].append((d, ff))
14 | #from pprint import pprint
15 | #pprint(dict(names))
16 | for name, versions in names.items():
17 | #print name
18 | versions.sort()
19 | versions.reverse()
20 | #pprint(versions[1:])
21 | for ts, oldname in versions[1:]:
22 | #print "\t", oldname
23 | if verbose:
24 | print "DELETE", oldname
25 | os.remove(oldname)
26 | return 0
27 | if __name__ == '__main__':
28 | import sys
29 | directory = sys.argv[1]
30 | args = sys.argv[2:]
31 | if '-v' in args or '--verbose' in args:
32 | verbose = True
33 | else:
34 | verbose = False
35 | sys.exit(run(directory, verbose))
--------------------------------------------------------------------------------
/static/emailreminders.js:
--------------------------------------------------------------------------------
1 | head.ready(function() {
2 | var format_ampm = function(h, m) {
3 | var s='am';
4 | if (h > 12) {
5 | h = (h - 12);
6 | s = 'pm';
7 | }
8 | if (m) {
9 | return h + '.' + m + s;
10 | } else {
11 | return h + s;
12 | }
13 | };
14 |
15 | $('form select').change(function() {
16 | var h = parseInt($('select[name="time_hour"]').val());
17 | var m = parseInt($('select[name="time_minute"]').val());
18 | $('#as_ampm').text('(' + format_ampm(h, m) + ')');
19 | });
20 | $('form select').trigger('change');
21 |
22 | if ($('#id_tz_offset').size() && !$('#id_tz_offset').val()) {
23 | var d = new Date();
24 | var gmtHours = -d.getTimezoneOffset()/60;
25 | $('#id_tz_offset').val(gmtHours);
26 | }
27 |
28 | $('form#reminders').submit(function() {
29 | if (!$('input[name="weekdays"]:checked').size()) {
30 | alert("Please select at least one weekday");
31 | return false;
32 | }
33 | return true;
34 | });
35 |
36 | $('input[name="cancel"]').click(function() {
37 | window.location = '/emailreminders/';
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/apps/templates/help/secure-passwords.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Secure passwords
6 |
7 | On DoneCal all passwords for registered users are stored encrypted .
8 | It's a one way encryption
9 | meaning that once encrypted it's not possible to get it back into clear text. So, if our database
10 | was hacked it would not be possible to figure out what your password was before it was encrypted.
11 |
12 |
13 | What hackers can do is instead something called
14 | "brute force attack" which
15 | means they try every single letter combination until something works. On DoneCal that's not
16 | possible because DoneCal encrypts its passwords with bcrypt . It's the
17 | most secure encryption possible for passwords because due to the nature of its encryption
18 | it's not possible to check any faster even if you have a supercomputer.
19 |
20 | You can read more about bcrypt and what Coda Hale has to say on it on:
21 | How To Safely Store A Password
22 |
23 | {% end %}
24 |
--------------------------------------------------------------------------------
/apps/templates/help/index.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Help
6 |
7 | Introduction video
8 |
9 |
10 | Introduction to DoneCal.com
11 | on Vimeo .
12 |
13 |
14 | Tagging
15 |
16 |
17 | With tagging you can separate events into different projects or categories.
18 | A tag is a word that starts with an @ or a # sign.
19 | It can contain hyphens or underscores but not spaces.
20 |
21 | Don't worry about the case. Whichever way you spell it the last time, all other tags
22 | care then rewritten in the same case.
23 |
24 |
25 |
26 | Bookmarklet
27 |
28 |
29 | Get your Bookmarklet set up now .
30 |
31 | {% end %}
32 |
33 |
--------------------------------------------------------------------------------
/apps/templates/powerusers/users/power_user_base.html:
--------------------------------------------------------------------------------
1 | {% extends "../../base.html" %}
2 |
3 | {% block title %}Power User: {{ user.first_name }} {{ user.last_name }}{% end %}
4 |
5 | {% block extra_head %}
6 |
17 | {% end %}
18 |
19 | {% block sidebar %}
20 | Numbers
21 |
22 | {% if member_no < 10 %}
23 | Member number:
24 | {{ member_no }}
25 | {% end %}
26 |
27 | Days of membership:
28 | {{ membership_length }}
29 |
30 | Total number of events:
31 | {{ no_events }}
32 |
33 | Average number of events per week:
34 | {{ events_per_week }}
35 |
36 | {% if prefers_all_day_events %}
37 | Total number of days:
38 | {{ total_days }} days
39 | {% else %}
40 | Total number of hours:
41 | {{ total_hours }} hours
42 | {% end %}
43 |
44 | {% if no_tags > 10 %}
45 | Number of different tags:
46 | {{ no_tags }}
47 | {% end %}
48 |
49 | {% end %}
--------------------------------------------------------------------------------
/apps/main/export/csv_export.py:
--------------------------------------------------------------------------------
1 | import csv
2 | def utf_8_encoder(unicode_csv_data):
3 | for line in unicode_csv_data:
4 | if line is None:
5 | yield ''
6 | else:
7 | yield line.encode('utf-8')
8 |
9 | def export_events(events, out_file, user=None, encoding='utf-8'):
10 | writer = csv.writer(out_file)
11 | writer.writerow([
12 | 'Event', 'Date', 'Days', 'Hours', 'Description'
13 | ])
14 |
15 | total_days = total_hours = 0
16 |
17 | for event in events:
18 | row = [
19 | event['title'],
20 | event['start'].strftime('%Y-%m-%d')
21 | ]
22 | if event['all_day']:
23 | days = event['end'] - event['start']
24 | row.append(str(days.days + 1))
25 | total_days += days.days + 1
26 | row.append('')
27 | else:
28 | row.append('')
29 | hours = (event['end'] - event['start']).seconds / 3600.0
30 | row.append(str(round(hours, 1)))
31 | total_hours += hours
32 | row.append(event['description'])
33 | writer.writerow(list(utf_8_encoder(row)))
34 | row = ['TOTAL:', '', str(total_days), str(total_hours), '']
35 | writer.writerow(row)
36 |
37 |
38 |
--------------------------------------------------------------------------------
/apps/main/migrations/013_share_featurerequest_featureeq_comment.py:
--------------------------------------------------------------------------------
1 | from bson.objectid import ObjectId
2 | from apps.main.models import User, Share, FeatureRequest, FeatureRequestComment, connection
3 | import settings
4 |
5 | db = connection[settings.DATABASE_NAME]
6 | collection = db.Share.collection
7 | collection.drop_indexes()
8 | collection = db.FeatureRequest.collection
9 | collection.drop_indexes()
10 | collection = db.FeatureRequestComment.collection
11 | collection.drop_indexes()
12 |
13 | c = 0
14 | for msg in db.Share.find():
15 | if msg['users'] and type(msg['users'][0]) is not ObjectId:
16 | msg['users'] = [x.id for x in msg['users']]
17 | if type(msg['user']) is not ObjectId:
18 | msg['user'] = msg['user'].id
19 | msg.save()
20 | c += 1
21 |
22 |
23 | print "Fixed", c, "shares"
24 |
25 |
26 | c = 0
27 | for msg in db.FeatureRequest.find():
28 | if type(msg['author']) is not ObjectId:
29 | msg['author'] = msg['author'].id
30 | msg.save()
31 | c += 1
32 |
33 | print "Fixed", c, "feature requests"
34 |
35 | c = 0
36 | for msg in db.FeatureRequestComment.find():
37 | if type(msg['user']) is not ObjectId:
38 | msg['user'] = msg['user'].id
39 | msg.save()
40 | c += 1
41 |
42 | print "Fixed", c, "feature request comments"
43 |
--------------------------------------------------------------------------------
/apps/templates/emailreminders/send_reminder.txt:
--------------------------------------------------------------------------------
1 | {% if first_name %}Hi {{ first_name }},{% else %}Hi,{% end %}
2 | What did you do {% if about_today %}*today*{% else %}*yesterday*{% end %}?
3 |
4 |
5 | {% if include_instructions %}INSTRUCTIONS:
6 | Just hit reply and type at the start of the email as you would enter
7 | an event normally on DoneCal with tags and everything. To enter
8 | multiple events separate them by two line breaks. A single line break
9 | sets the description of the event.
10 |
11 | If you want it to be an hourly even write a number followed by the letter 'h'. Like this for example:
12 |
13 | {{ hour_example_1 }} Working on #projectX like Sam said
14 |
15 | If you want to specify exactly when it happened enter it like a 24 hour clock or with AM/PM. Like this for example:
16 |
17 | {{ hour_example_2 }} 1h Helped Santa with gift wrapping
18 |
19 | If you don't start the line with a time, but do start the line with a duration it will assume it's noon your local time.
20 | {% end %}{% if summary_events %}{% if about_today %}SUMMARY OF EVENTS TODAY{% else %}SUMMARY OF EVENTS YESTERDAY{% end %}:
21 | {% for summary in summary_events %}
22 | {{ summary }}
23 | {% end %}{% end %}
24 |
25 | SETTINGS:
26 | If you want to change your email reminders simply go to this page:
27 | {{ email_reminder_edit_url }}
28 |
29 |
--------------------------------------------------------------------------------
/static/base.js:
--------------------------------------------------------------------------------
1 | function L() {
2 | if (window.console && window.console.log)
3 | console.log.apply(console, arguments);
4 | }
5 |
6 | function increment_total_no_events(new_no) {
7 | total_no_events += typeof new_no != 'undefined' ? new_no : 1;
8 | $('#total_no_events').text(total_no_events);
9 | }
10 |
11 | function decrement_total_no_events(new_no) {
12 | total_no_events -= typeof new_no != 'undefined' ? new_no : 1;
13 | $('#total_no_events').text(total_no_events);
14 | }
15 |
16 |
17 | /*
18 | $.getJSON('/auth/logged_in.json', {url:location.href}, function(r) {
19 | if (r.redirect_to) {
20 | window.location.href = r.redirect_to;
21 | return;
22 | }
23 | if (r.xsrf) {
24 | XSRF = r.xsrf;
25 | }
26 | if (r.user_name) {
27 | $('#login').removeClass('login_not');
28 | if (r.premium) {
29 | $('#login').addClass('login_premium');
30 | $('a.account','#login').attr('title','You\'re a premium user!');
31 | } else {
32 | $('#login').addClass('login_user');
33 | }
34 | $('a.account','#login').text('Hi ' + r.user_name);
35 | $('#login p').append($('', {text:'log out',
36 | href:'/auth/logout/'}).addClass('log-out'));
37 | $('#report-link').show();
38 | } else {
39 | $('#introduction').show();
40 | }
41 | });
42 | */
--------------------------------------------------------------------------------
/utils/send_mail/backends/console.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import threading
3 |
4 | from utils.send_mail.backends.base import BaseEmailBackend
5 |
6 | class EmailBackend(BaseEmailBackend):
7 | def __init__(self, *args, **kwargs):
8 | self.stream = kwargs.pop('stream', sys.stdout)
9 | self._lock = threading.RLock()
10 | super(EmailBackend, self).__init__(*args, **kwargs)
11 |
12 | def send_messages(self, email_messages):
13 | """Write all messages to the stream in a thread-safe way."""
14 | if not email_messages:
15 | return
16 | self._lock.acquire()
17 | try:
18 | # The try-except is nested to allow for
19 | # Python 2.4 support (Refs #12147)
20 | try:
21 | stream_created = self.open()
22 | for message in email_messages:
23 | self.stream.write('%s\n' % message.message().as_string())
24 | self.stream.write('-'*79)
25 | self.stream.write('\n')
26 | self.stream.flush() # flush after each message
27 | if stream_created:
28 | self.close()
29 | except:
30 | if not self.fail_silently:
31 | raise
32 | finally:
33 | self._lock.release()
34 | return len(email_messages)
35 |
--------------------------------------------------------------------------------
/utils/send_mail/importlib.py:
--------------------------------------------------------------------------------
1 | # Taken from Python 2.7 with permission from/by the original author.
2 | import sys
3 |
4 | def _resolve_name(name, package, level):
5 | """Return the absolute name of the module to be imported."""
6 | if not hasattr(package, 'rindex'):
7 | raise ValueError("'package' not set to a string")
8 | dot = len(package)
9 | for x in xrange(level, 1, -1):
10 | try:
11 | dot = package.rindex('.', 0, dot)
12 | except ValueError:
13 | raise ValueError("attempted relative import beyond top-level "
14 | "package")
15 | return "%s.%s" % (package[:dot], name)
16 |
17 |
18 | def import_module(name, package=None):
19 | """Import a module.
20 |
21 | The 'package' argument is required when performing a relative import. It
22 | specifies the package to use as the anchor point from which to resolve the
23 | relative import to an absolute import.
24 |
25 | """
26 | if name.startswith('.'):
27 | if not package:
28 | raise TypeError("relative imports require the 'package' argument")
29 | level = 0
30 | for character in name:
31 | if character != '.':
32 | break
33 | level += 1
34 | name = _resolve_name(name[level:], package, level)
35 | __import__(name)
36 | return sys.modules[name]
37 |
--------------------------------------------------------------------------------
/utils/send_mail/backends/base.py:
--------------------------------------------------------------------------------
1 | """Base email backend class."""
2 |
3 | class BaseEmailBackend(object):
4 | """
5 | Base class for email backend implementations.
6 |
7 | Subclasses must at least overwrite send_messages().
8 | """
9 | def __init__(self, fail_silently=False, **kwargs):
10 | self.fail_silently = fail_silently
11 |
12 | def open(self):
13 | """Open a network connection.
14 |
15 | This method can be overwritten by backend implementations to
16 | open a network connection.
17 |
18 | It's up to the backend implementation to track the status of
19 | a network connection if it's needed by the backend.
20 |
21 | This method can be called by applications to force a single
22 | network connection to be used when sending mails. See the
23 | send_messages() method of the SMTP backend for a reference
24 | implementation.
25 |
26 | The default implementation does nothing.
27 | """
28 | pass
29 |
30 | def close(self):
31 | """Close a network connection."""
32 | pass
33 |
34 | def send_messages(self, email_messages):
35 | """
36 | Sends one or more EmailMessage objects and returns the number of email
37 | messages sent.
38 | """
39 | raise NotImplementedError
40 |
41 |
--------------------------------------------------------------------------------
/apps/templates/event/edit.html:
--------------------------------------------------------------------------------
1 |
47 |
--------------------------------------------------------------------------------
/apps/templates/powerusers/users/bill-mitchell.html:
--------------------------------------------------------------------------------
1 | {% extends "power_user_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Power User: {{ user.first_name }} {{ user.last_name }}
6 |
7 |
8 |
9 |
10 |
DoneCal: Hi Bill, what do you use DoneCal for
11 |
12 |
Bill:
13 | I use it for project management for my own commercial endeavors, I put tentative dates on start and
14 | finish of selected projects and I mark down when our start up company has conferences or meetings
15 | that I want to schedule.
16 |
17 |
18 |
22 |
23 |
24 |
DoneCal: What in particular do you like about DoneCal?
25 |
26 |
Bill:
27 | I like the clean user interface, not tool cluttered very straight forward and doesn't insult the intelligence.
28 | I also like the ability to place more than one task or memo on a selected date and love the ability to share the calendar quickly with others.
29 |
30 |
31 |
32 |
33 | {% end %}
34 |
--------------------------------------------------------------------------------
/bin/run_shell.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import code, re
4 | import here
5 | import settings
6 |
7 | if __name__ == '__main__':
8 |
9 | from apps.main.models import *
10 | from apps.main import models
11 | from apps.eventlog.models import EventLog
12 | from apps.emailreminders.models import EmailReminder
13 | from mongokit import Connection, Document as mongokit_Document
14 | from bson.objectid import InvalidId, ObjectId
15 | from apps.main.models import connection
16 |
17 | import settings
18 | model_classes = []
19 | for app_name in settings.APPS:
20 | _models = __import__('apps.%s' % app_name, globals(), locals(),
21 | ['models'], -1)
22 | try:
23 | models = _models.models
24 | except AttributeError:
25 | # this app simply doesn't have a models.py file
26 | continue
27 | for name in [x for x in dir(models)
28 | if re.findall('[A-Z]\w+', x)]:
29 | thing = getattr(models, name)
30 | if issubclass(thing, mongokit_Document):
31 | model_classes.append(thing)
32 |
33 | #connection.register(model_classes)
34 |
35 |
36 | db = connection[settings.DATABASE_NAME]
37 | print "AVAILABLE:"
38 | print '\n'.join(['\t%s'%x for x in
39 | sorted(locals().keys(), lambda x,y: cmp(x.lower(), y.lower()))
40 | if re.findall('[A-Z]\w+|db|con', x)])
41 | print "Database available as 'db'"
42 | code.interact(local=locals())
43 |
--------------------------------------------------------------------------------
/apps/templates/premium/index.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}Upgrade to a Premium Account{% end %}
4 | {% block extra_head %}
5 |
12 | {% end %}
13 |
14 | {% block sidebar %}
15 |
16 | {% end %}
17 |
18 |
19 | {% block content_inner %}
20 |
21 | Upgrade to a Premium Account
22 |
23 |
24 |
25 |
26 |
27 | FREE
28 | PREMIUM ACCOUNT
29 |
30 |
31 |
32 |
33 | HTTPS on everything
34 |
35 | CHECK
36 |
37 |
38 | Limits on adding events
39 | Max. 100 per month
40 | None!
41 |
42 |
43 | Limits on shared calendars
44 | Max. 1
45 | Unlimited!
46 |
47 |
48 | Speed/Performance
49 | 100%
50 | 150%!
51 |
52 |
53 |
54 |
55 |
71 |
72 |
73 |
74 | {% end %}
75 |
--------------------------------------------------------------------------------
/static/css/ext/fullcalendar.print.css:
--------------------------------------------------------------------------------
1 | /*
2 | * FullCalendar v1.5.1 Print Stylesheet
3 | *
4 | * Include this stylesheet on your page to get a more printer-friendly calendar.
5 | * When including this stylesheet, use the media='print' attribute of the tag.
6 | * Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
7 | *
8 | * Copyright (c) 2011 Adam Shaw
9 | * Dual licensed under the MIT and GPL licenses, located in
10 | * MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
11 | *
12 | * Date: Sat Apr 9 14:09:51 2011 -0700
13 | *
14 | */
15 |
16 |
17 | /* Events
18 | -----------------------------------------------------*/
19 |
20 | .fc-event-skin {
21 | background: none !important;
22 | color: #000 !important;
23 | }
24 |
25 | /* horizontal events */
26 |
27 | .fc-event-hori {
28 | border-width: 0 0 1px 0 !important;
29 | border-bottom-style: dotted !important;
30 | border-bottom-color: #000 !important;
31 | padding: 1px 0 0 0 !important;
32 | }
33 |
34 | .fc-event-hori .fc-event-inner {
35 | border-width: 0 !important;
36 | padding: 0 1px !important;
37 | }
38 |
39 | /* vertical events */
40 |
41 | .fc-event-vert {
42 | border-width: 0 0 0 1px !important;
43 | border-left-style: dotted !important;
44 | border-left-color: #000 !important;
45 | padding: 0 1px 0 0 !important;
46 | }
47 |
48 | .fc-event-vert .fc-event-inner {
49 | border-width: 0 !important;
50 | padding: 1px 0 !important;
51 | }
52 |
53 | .fc-event-bg {
54 | display: none !important;
55 | }
56 |
57 | .fc-event .ui-resizable-handle {
58 | display: none !important;
59 | }
60 |
61 |
62 |
--------------------------------------------------------------------------------
/apps/templates/splash.html:
--------------------------------------------------------------------------------
1 |
9 | Welcome to DoneCal, first timer!
10 |
11 | You can immediately start adding events, change them and export and report on them in many different ways.
12 |
13 | 1.
14 |
15 | To get started, click on any day cell and create an event.
16 |
17 |
18 | 2.
19 |
20 | Tag events by prefixing words with a # or a @ sign.
21 |
22 |
23 | 3.
24 |
25 | Once added you can move it, resize it, edit it or remove it
26 |
27 |
28 |
29 |
30 | There are many other ways to get events in such as by email or API.
31 | See the Help if you ever get stuck.
32 |
33 |
34 | Let's begin! Close this
35 |
31 |
--------------------------------------------------------------------------------
/static/eventlog.js:
--------------------------------------------------------------------------------
1 | function display_sidebar_stats() {
2 | //var days_pie, hours_pie;
3 | var color_map = {};
4 | var seriesColors;
5 | var unused_colors;
6 | $.getJSON('/log/stats.json', {},
7 | function(response) {
8 | $('#actions-plot').html('');
9 | //alert(response.actions);
10 | if (response.actions) {
11 | $('#actions-plot:hidden').show();
12 | $.jqplot('actions-plot', [response.actions], {
13 | //seriesColors: response.days_colors,
14 | title: 'Actions',
15 | grid: { drawGridLines: false, gridLineColor: '#fff', background: '#fff', borderColor: '#fff', borderWidth: 1, shadow: false },
16 | highlighter: {sizeAdjust: 7.5},
17 | seriesDefaults:{renderer:$.jqplot.PieRenderer, rendererOptions:{sliceMargin:3, padding:7, border:false}},
18 | legend:{show:true}
19 | });
20 | } else {
21 | $('#actions-plot:visible').hide();
22 | }
23 |
24 | $('#contexts-plot').html('');
25 | if (response.contexts) {
26 | $('#contexts-plot:hidden').show();
27 | $.jqplot('contexts-plot', [response.contexts], {
28 | //seriesColors: response.hours_colors,
29 | title: 'Contexts',
30 | grid: { drawGridLines: false, gridLineColor: '#fff', background: '#fff', borderColor: '#fff', borderWidth: 1, shadow: false },
31 | seriesDefaults:{renderer:$.jqplot.PieRenderer, rendererOptions:{sliceMargin:3, padding:7, border:false}},
32 | legend:{show:true}
33 | });
34 | } else {
35 | $('#contexts-plot:visible').hide();
36 | }
37 |
38 | });
39 | }
40 |
41 | head.ready(function() {
42 | display_sidebar_stats();
43 | });
--------------------------------------------------------------------------------
/bin/download_static_urls.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from time import time
3 | import lxml.html
4 | from lxml import etree
5 | from lxml.cssselect import CSSSelector
6 | from urllib import urlopen
7 | from urlparse import urlparse
8 |
9 | def wrap(url, uri):
10 | if uri.startswith('/'):
11 | parts = list(urlparse(url))
12 | parts[2] = uri
13 | url = parts[0]
14 | url += '://'
15 | if parts[2].startswith('//'):
16 | url += parts[2][2:]
17 | else:
18 | url += parts[1]
19 | url += parts[2]
20 | return url
21 |
22 | def get_static_urls(url):
23 | html = urlopen(url).read()
24 | parser = etree.HTMLParser()
25 | tree = etree.fromstring(html.strip(), parser).getroottree()
26 | page = tree.getroot()
27 | for link in CSSSelector('link')(page):
28 | yield wrap(url, link.attrib['href'])
29 |
30 | for link in CSSSelector('script')(page):
31 |
32 | try:
33 | src = wrap(url, link.attrib['src'])
34 | if not src.count('googleapis'):
35 | yield wrap(url, link.attrib['src'])
36 | except KeyError:
37 | # block
38 | pass
39 |
40 |
41 | def main(*args):
42 | for arg in args:
43 | for url in get_static_urls(arg):
44 | #print url
45 | #continue
46 | t0=time()
47 | content = urlopen(url).read()
48 | t1=time()
49 | t = round((t1-t0) * 1000, 2)
50 | print ("%s Kb" % (len(content)/1024)).ljust(10),
51 | print ("%sms"%t).ljust(10),
52 | print url#, "%sms"%t, "%s Kb" % (len(content)/1024)
53 | return 0
54 | if __name__ == '__main__':
55 | import sys
56 | sys.exit(main(*sys.argv[1:]))
--------------------------------------------------------------------------------
/static/ext/jquery.lazy.js:
--------------------------------------------------------------------------------
1 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(9($){$.4=9(8){7(C 8.2==\'S\'){8.2=[8.2]}$.x(8.2,9(i){e 1=8.1,2=8.2[i],m=8.m||D,w=8.w||D,u=8.u||D,3,6,g={};$.4.5[1]={\'b\':\'E\',\'f\':[]};9 J(1,a,3,2,6){$.4.5[1].b="n";e h=F.Q(\'P\');h.I=\'R/k\';h.O=\'U\';h.T=1;h.V=\'K\';F.N("M")[0].L(h);$.4.5[1].b=\'z\';7(a)a(3,2,6)}9 y(1,a,3,2,6){$.4.5[1].b="n";$.12({I:"15",14:1,m:m,16:"17",18:9(){$.4.5[1].b=\'z\';7(a){a(3,2,6)}}})}9 p(3,2,6){9 a(){7(C 3==\'g\'){7(6.c>0){$(3)[2].o(3,6)}d{$(3)[2]()}}d{$[2].o(W,6)}$.x($.4.5[1].f,9(i){e l=$.4.5[1].f[i];g[l.2].o(l.3,l.t)});$.4.5[1].f=[]}y(1,a,3,2,6)}9 G(){e 3=q;6=t;7($.4.5[1].b===\'z\'){$.x(q,9(){$(q)[2].o(3,6)})}d 7($.4.5[1].b===\'n\'){$.4.5[1].f.X({\'2\':2,\'3\':3,\'t\':6})}d{$.4.5[1].b=\'n\';7(8.B){e k=8.B.k||[],s=8.B.s||[];e r=k.c+s.c;9 v(j,a,Z){e c=j.c,1;j=j.10();11(c--&&r--){1=j[c];7(C $.4.5[1]==\'19\'){$.4.5[1]={\'b\':\'E\',\'f\':[]}}7($.4.5[1].b===\'E\'){7(!r){a(1,9(){p(3,2,6)})}d{a(1)}}d 7(!r){p(3,2,6)}}}v(k,J);v(s,y)}d{p(3,2,6)}}Y q};g[2]=G;7(u){A.13.H(g)}7(w){A.H(g)}})};$.4.5={}})(A);',62,72,'|src|name|self|lazy|archive|arg|if|options|function|callback|status|length|else|var|que|object|node||array|css|queItem|cache|loading|apply|loadPlugin|this|total|js|arguments|isMethod|loadDependencies|isFunction|each|loadJS|loaded|jQuery|dependencies|typeof|true|unloaded|document|proxy|extend|type|loadCSS|screen|appendChild|head|getElementsByTagName|rel|link|createElement|text|string|href|stylesheet|media|null|push|return|callbackCallback|reverse|while|ajax|fn|url|GET|dataType|script|success|undefined'.split('|'),0,{}))
--------------------------------------------------------------------------------
/apps/templates/help/google-calendar.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block extra_head %}
4 |
15 | {% end %}
16 |
17 | {% block content_inner %}
18 |
19 | DoneCal vs. Google Calendar
20 |
21 |
22 | Personally, I use DoneCal to track what I have done (e.g. hours spent on a support job)
23 | and Google Calendar to track what I will do (e.g. meeting at Cafe 10.30am next Tuesday).
24 |
25 | When you realise that DoneCal is a database of event entries with a calendar as the user interface it
26 | becomes more clear what the distinction is.
27 |
28 |
29 |
30 |
Google Calendar
31 |
32 |
33 | A calendar with lots of powerful features
34 | Ability to see shared calendars
35 | Ability to import calendars from other web services (i.e. CalDAV)
36 | Great reminders (email, SMS, popups) for when events are about to happen
37 | Interfaces and syncs with various mobile devices
38 | A powerful but complex API
39 |
40 |
41 |
42 |
43 |
DoneCal
44 |
45 |
46 | Ability to tag entered events easily
47 | A simple and powerful calendar interface for entering and editing events
48 | Reporting capabilites per month, per week or per day
49 | Excel and CSV export functions
50 | A super simple API for programmatically entering events
51 | A bookmarklet for quickly entering events in the browser
52 |
53 |
54 |
55 |
56 |
57 |
58 | {% end %}
59 |
--------------------------------------------------------------------------------
/apps/templates/sharing/share.html:
--------------------------------------------------------------------------------
1 |
8 |
9 | Share your calendar
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Give this URL to friends and family and they'll be able to view
18 | (but not edit) your calendar:
19 |
20 |
21 |
22 |
23 |
24 | saving...
25 | more options
26 |
27 |
28 |
29 |
Specific tags:
30 |
31 | {% for tag in chosen_tags %}{{ tag }} {% end %}
32 | {% for tag in available_tags %}{{ tag }} {% end %}
33 |
34 |
35 |
43 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/sendmail.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | import datetime
4 | import urllib2
5 | import os.path
6 |
7 | import logging
8 | LOG_DIR = '/tmp'
9 | LOG_FILENAME = 'sendmail.py.log'
10 | logging.basicConfig(filename=os.path.join(LOG_DIR, LOG_FILENAME),
11 | level=logging.DEBUG,
12 | datefmt="%Y-%m-%d %H:%M:%S",
13 | format="%(asctime)s %(levelname)s %(name)s %(message)s",
14 | )
15 |
16 |
17 | def main(domain=None, protocol=None):
18 | if not domain:
19 | domain = 'donecal.com'
20 | if not protocol:
21 | protocol = 'http'
22 | now = datetime.datetime.now()
23 | save_as_file = os.path.join(LOG_DIR,
24 | now.strftime('%Y-%m-%d_%H%M%S_%f.email'))
25 | open(save_as_file, 'w').write(sys.stdin.read())
26 | logging.debug("Incoming email (%s)" % save_as_file)
27 | url = '%s://%s/emailreminders/receive/' % (protocol, domain)
28 | req = urllib2.Request(url, open(save_as_file).read())
29 | try:
30 | response = urllib2.urlopen(req)
31 | logging.info(response.read())
32 | except:
33 | logging.error(
34 | "Error on opening receiver. Look into %s" % save_as_file,
35 | exc_info=True)
36 |
37 | def run(*args):
38 | domain = None
39 | protocol = None
40 | _next_is_domain = False
41 | _next_is_protocol = False
42 | for arg in args:
43 | if _next_is_domain:
44 | domain = arg
45 | _next_is_domain = False
46 | elif _next_is_protocol:
47 | protocol = arg
48 | _next_is_protocol = False
49 | elif arg in ('--domain', '-d'):
50 | _next_is_domain = True
51 | elif arg in ('--protocol', '-p'):
52 | _next_is_protocol = True
53 |
54 | main(domain=domain, protocol=protocol)
55 |
56 | if __name__ == '__main__':
57 | import sys
58 | sys.exit(run(*sys.argv[1:]))
--------------------------------------------------------------------------------
/apps/templates/user/account.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Already have an account
4 |
5 |
6 |
7 | Email:
8 |
9 | Password:
10 |
11 |
12 |
13 |
14 | {% module xsrf_form_html() %}
15 |
16 |
17 |
18 | Or just use...
19 |
20 |
21 |
22 |
23 | Forgotten your password?
24 |
25 |
26 |
27 |
29 | Create a new account
30 |
31 |
32 |
33 |
There are still some errors in the form. Please have a look.
34 |
35 |
36 |
40 |
41 |
42 | E-mail:
43 |
44 | Choose a password:
45 |
46 |
47 | First name:
48 |
49 |
50 | Last name:
51 |
52 |
53 |
54 |
55 |
56 |
57 | {% module xsrf_form_html() %}
58 |
59 |
--------------------------------------------------------------------------------
/apps/templates/eventlog/index.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}Event Log{% end %}
4 |
5 | {% block extra_head %}
6 | {% module Static("css/ext/jquery.jqplot.min.css", "css/logs.css") %}
7 | {% end %}
8 |
9 |
10 | {% block sidebar %}
11 | Statistics
12 |
13 |
14 |
15 |
16 |
17 |
18 | {% end %}
19 |
20 | {% block content_inner %}
21 |
22 | Event Log beta
23 |
24 | #{{ count_event_logs }} log entries
25 |
26 |
27 |
28 | {% if superuser %}
29 | USER
30 | {% end %}
31 | ACTION
32 | EVENT
33 | CONTEXT
34 | DATE
35 | COMMENT
36 |
37 |
38 | {% for entry in event_logs %}
39 |
40 | {% if superuser %}
41 | {% module VerboseEventLog(entry, 'user') %}
42 | {% end %}
43 | {% module VerboseEventLog(entry, 'event') %}
44 | {% module VerboseEventLog(entry, 'action') %}
45 | {{ entry.context }}
46 | {% module VerboseEventLog(entry, 'date') %}
47 | {% module VerboseEventLog(entry, 'comment') %}
48 |
49 |
50 | {% end %}
51 |
52 |
53 |
54 | {% for p in pages %}
55 | {% if p == page %}
56 | {{ p }}
57 | {% else %}
58 | {% if p == 1 %}
59 | {{ p }}
60 | {% else %}
61 | {{ p }}
62 | {% end %}
63 | {% end %}
64 | {% end %}
65 |
66 |
67 | {% end %}
68 |
69 |
70 | {% block extrajs %}
71 |
76 | {% module Static("eventlog.js") %}
77 | {% end %}
78 |
--------------------------------------------------------------------------------
/apps/templates/powerusers/users/peter-bengtsson.html:
--------------------------------------------------------------------------------
1 | {% extends "power_user_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | Power User: {{ user.first_name }} {{ user.last_name }}
6 |
7 |
8 |
9 |
DoneCal: Hi Peter, what do you use DoneCal for?
10 |
11 |
Peter:
12 | Tracking how much time I spend on various projects at work. I tag both personal projects and work
13 | projects as I'm quite interested in how much time I spend on each in a given week.
14 |
15 |
I'm still a big fan of Google Calendar but I only use that these days for future
16 | events such as upcoming meetings or sharing with my colleagues when I'm away on holiday and such.
17 |
18 |
19 |
20 |
DoneCal: How do you enter your events into DoneCal?
21 |
22 |
Peter:
23 | At first I would open the DoneCal website and click to type it in but found it easy to forget to do it.
24 | Then I started using the Bookmarklet. Especially if I had just closed an issue on our work issue tracker.
25 | Recently I've started to just use the email reminders. I've got them set up to email me every work day evening
26 | at 6pm and now I just reply with hours and the tags on which I've been working.
27 |
28 |
29 |
DoneCal: What in particular do you like about DoneCal?
30 |
31 |
Peter:
32 | That there are numerous and flexible ways to enter things and just that. No rules or restrictions.
33 | Within the space of a few months
34 | I've already changed my ways of entering events many times. For example, I used to write quite a lot; now I just
35 | write the tag with a hash sign.
36 | Oh and I like the graphical reporting so I can glance roughly where I put my time in any chosen time interval.
37 |
38 |
39 |
40 |
41 | {% end %}
42 |
--------------------------------------------------------------------------------
/apps/templates/featurerequests/feature_request.html:
--------------------------------------------------------------------------------
1 |
2 | {% if thanks_instead %}
3 |
Thanks!
4 | {% else %}
5 | {% if feature_request.implemented %}
6 |
Implemented!
7 | {% else %}
8 |
10 | {% end %}
11 | {% end %}
12 |
{{ feature_request.title }}
13 |
14 |
15 |
16 |
17 | {% if feature_request.description %}
18 | {% module RenderText(feature_request.description, feature_request.description_format) %}
19 | {% end %}
20 |
36 |
37 |
38 | {% if comments %}
39 |
50 | {% end %}
51 |
52 | {% if feature_request.response %}
53 |
54 |
Response:
55 | {% module RenderText(feature_request.response, feature_request.response_format) %}
56 |
57 | {% end %}
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | About worklog
2 | =============
3 |
4 | worklog is the project that runs [DoneCal.com](http://donecal.com)
5 | which is a free web app for maintaining a simple online calendar ideal
6 | for keeping track of what you have done.
7 |
8 |
9 | Dependencies, installation
10 | --------------------------
11 |
12 | See INSTALL.txt
13 |
14 |
15 | To run the unit tests
16 | ---------------------
17 |
18 | You can either run the tests one-off like this:
19 |
20 | ./run_tests.sh
21 |
22 | Or keep it running waiting for changes:
23 |
24 | ./run_tests.sh --autoreload
25 |
26 | To run the tests of an individual test module, you can do this:
27 |
28 | ./run_tests.sh tests.test_models
29 | or
30 | ./run_tests.sh tests.test_models.ModelsTestCase
31 | or
32 | ./run_tests.sh tests.test_models.ModelsTestCase.test_create_user
33 |
34 | To run the coverage tests
35 | -------------------------
36 |
37 | You start it like this:
38 |
39 | ./run_coverage_tests.sh
40 |
41 | It will cancel the report if the tests don't pass.
42 |
43 |
44 | Running the Javascript test suite
45 | ---------------------------------
46 |
47 | (this is currently under heavy development)
48 |
49 | Open static/tests/smartphone.html in Firefox as a static file.
50 |
51 | Getting a copy of the live database
52 | -----------------------------------
53 |
54 | First log in to the server:
55 |
56 | $ ssh tornado@donecal.fry-it.com
57 |
58 | and run mongodump anywhere:
59 |
60 | $ cd /tmp/
61 | $ rm -fr dump
62 | $ mongodump -d worklog
63 |
64 | Then copy those files to the local server:
65 |
66 | $ cd /tmp
67 | $ jan_rsync donecal.fry-it.com:/tmp/dump .
68 |
69 | Now you need to create a new database and start mongodb.
70 |
71 | $ cd ~/worklog
72 | $ mkdir live-db
73 | $ jed start_mongodb.sh
74 | $ ./start_mongodb.sh
75 |
76 | Now, run the restore:
77 |
78 | $ ~/worklog/mongodb/bin/mongorestore dump
79 |
80 |
81 | To test to send emails in
82 | -------------------------
83 |
84 | Simple as this:
85 |
86 | $ python sendmail.py -d worklog < mail.txt
87 |
88 |
--------------------------------------------------------------------------------
/apps/templates/powerusers/top-10.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}Power Users{% end %}
4 |
5 | {% block extra_head %}
6 |
19 | {% end %}
20 |
21 | {% block sidebar %}
22 |
23 |
24 | {% end %}
25 |
26 | {% block content_inner %}
27 |
28 | Top 10 Power Users
29 |
30 |
31 |
32 |
33 |
34 |
35 | Events
36 | Name
37 | Email
38 | Member since
39 | Usage
40 |
41 |
42 | {% for entry in power_users %}
43 |
44 | {{ entry['index'] }}
45 | {{ entry['count'] }}
46 | {{ entry['user'].first_name }} {{ entry['user'].last_name }}
47 | {{ entry['user'].email }}
48 | {{ entry['member_since'] }}
49 |
50 |
51 |
52 | Median:
53 | {{ '%.1f' % entry['stats']['median'] }}
54 |
55 |
56 | Average:
57 | {{ '%.1f' % entry['stats']['average'] }}
58 |
59 |
60 |
61 |
62 | {{ ','.join([str(x) for x in entry['stats']['counts']]) }}
63 |
64 |
65 | {% end %}
66 |
67 |
68 |
69 |
70 | In the last days
71 |
72 |
73 | {% end %}
74 |
75 |
76 | {% block extrajs %}
77 |
82 | {% end %}
83 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | * (HIGH) Upgrade jqplot
2 | https://bitbucket.org/cleonello/jqplot/downloads/
3 |
4 | * (HIGH) dragging tags when sharing doesn't work
5 |
6 | * (MEDIUM) Look into
7 | https://github.com/wycats/jquery-offline when doing Smartphone
8 | offline stuff.
9 |
10 | * (MEDIUM) Rewrite log_request() method (over ride)
11 |
12 | * (LOW) Build a PDF report based on Timesheet.doc
13 |
14 | * (LOW) Write a script that analyzes the timings of
15 | /var/log/nginx/donecal.timed.access.log
16 |
17 | * (MEDIUM) Look into using CNAMES for cloudfront so I can
18 | cdn.donecal.com or something.
19 |
20 | * (LOW) Possibly look into this to make things work in IE7 too
21 | http://code.google.com/p/ie7-js/
22 |
23 | * (MEDIUM) Try changing to use head.js
24 |
25 | * (LOW) When you need memcaching, consider
26 | https://github.com/gmr/Tinman/blob/master/tinman/cache.py
27 |
28 | * (LOW) spell correct tags
29 |
30 | * (MEDIUM) Replace with Tipped
31 | http://projects.nickstakenburg.com/tipped/
32 |
33 | * (MEDIUM) render the shared classes in calendar.html on page load (and
34 | see if that helps with the reloading etc)
35 |
36 | * (HIGH) Find out why all CSS is lost when an event is added
37 |
38 | * (HIGH) moving an event resets colour on shared calendars
39 |
40 | * (MEDIUM) Consider changing from using .json in AJAX to .js which is
41 | also JSON but much lighter since it doesn't have keys.
42 |
43 | * (MEDIUM) Help page about playing sounds
44 | http://www.schillmania.com/projects/soundmanager2/demo/flashblock/
45 |
46 | * (LOW) More API for editing or viewing an event
47 |
48 | * (LOW) tests for previewing and editing events in API
49 |
50 | * (MEDIUM) Write sample Ruby package for using API
51 |
52 | * (LOW) Whoosh and Carrot
53 | http://github.com/ask/carrot
54 |
55 | * (MEDIUM) Look at this for ideas on how to use a log file
56 | http://groups.google.com/group/python-tornado/browse_thread/thread/6d62aec54eb47015
57 |
58 | * (LOW) Experiment with http://vis.stanford.edu/protovis/ to write
59 | some nice stats
60 |
61 | * (LOW) Experiment with running mongodb in RAM for faster unit tests
62 | http://www.phpmoadmin.com/mongodb-in-memory-database
63 |
--------------------------------------------------------------------------------
/apps/templates/help/about.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | About
6 |
7 | This app is a simple calendar. You can either use it to keep track of
8 | future events or you can use it to record past events. A good use case
9 | is to use it as an calendar-based alternative to
10 | timesheets . It's
11 | free and all your data can be exported.
12 |
13 | You do not need an account . Until you have an account cookies are
14 | used to remember your events and if you use different computers (ie.
15 | different cookies) you can set up an account to save your events securely.
16 |
17 | You can share your calendar with other people simply by giving them
18 | your unique sharing URL.
19 |
20 | This web application was built by Peter Bengtsson of Mozilla , London, England. Project
23 | started in September 2010.
24 |
25 |
26 | The following stack is being used to make things run:
27 |
28 |
29 |
30 | Nginx for the front web server
31 | Tornado for the application server
32 | Python to run the application server
33 | MongoDB for the database
34 | MongoKit to connection Python to MongoDB
35 | jQuery for the client side rendering
36 | FullCalendar for rendering the main calendar
37 |
38 |
39 | Please please please send me feedback. Ideas, bug reports, praise, etc.
40 | My email address is p
41 |
42 |
43 | {% end %}
44 |
45 | {% block extrajs %}
46 |
52 | {% end %}
53 |
--------------------------------------------------------------------------------
/apps/templates/featurerequests/index.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block title %}Feature requests - DoneCal{% end %}
4 |
5 | {% block extra_head %}
6 | {% module Static("css/ext/jquery.qtip.css", "css/featurerequests.css") %}
7 | {% end %}
8 |
9 | {% block sidebar %}
10 | Help!
11 | Read more about what this is in the
12 | Help - Feature requests page.
13 | {% end %}
14 |
15 | {% block content_inner %}
16 |
17 | Feature requests
18 |
19 |
20 | {% for feature_request in feature_requests %}
21 |
22 | {% module ShowFeatureRequest(feature_request) %}
23 |
24 | {% end %}
25 |
26 |
27 | {% if can_add %}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {% module xsrf_form_html() %}
41 |
42 | {% else %}
43 |
44 |
You can add your own feature requests too! ...but first you have to create an account
45 |
46 | {% end %}
47 |
48 |
49 |
62 |
63 | {% end %}
64 |
65 | {% block extrajs %}
66 |
69 | {% module Static("ext/jquery.validate.min.js", "ext/jquery.qtip.min.js", "featurerequests.js") %}
70 | {% if have_voted_features %}
71 | {% end %}
72 | {% end %}
73 |
--------------------------------------------------------------------------------
/static/css/featurerequests.css:
--------------------------------------------------------------------------------
1 | .centered {
2 | margin-left:auto;
3 | margin-right:auto;
4 | width:65%;
5 | }
6 | input.title { width:98%; font-size:1.3em; }
7 | input.placeholdervalue, textarea.placeholdervalue { color:#999; font-weight:normal; }
8 |
9 | .request {
10 | border:1px solid #ccc;
11 | background-color:#efefef;
12 | margin-bottom:30px;
13 | -moz-border-radius: 6px;
14 | border-radius: 6px;
15 | }
16 |
17 | .request .head {
18 | background-color:#292E35;
19 | color:#fff;
20 | -moz-border-radius-topleft: 6px;
21 | -moz-border-radius-topright: 6px;
22 | border-top-left-radius: 6px;
23 | border-top-right-radius: 6px;
24 | }
25 | .request .description, .request .head {
26 | padding:10px;
27 | }
28 | .request .response {
29 | background-color:#ccc;
30 | }
31 | .head .votes { display:none }
32 | .head .title {
33 | font-size:15px;
34 | font-weight:bold;
35 | margin-bottom:0;
36 | }
37 | .metadata {
38 | width:100%;
39 | /*border-top:1px solid #666;*/
40 | color:#666;
41 | }
42 | .metadata p {
43 | font-size:0.8em;
44 | margin-bottom:3px;
45 | padding-top:5px;
46 |
47 | }
48 | p.vote_weight {
49 | float:right;
50 | font-weight:bold;
51 | }
52 | p.voteup {
53 | float:right;
54 | }
55 | p.voteup a:hover {
56 | background: transparent;
57 |
58 | }
59 | form.centered { margin-top:40px }
60 |
61 | .ui-tooltip, .qtip { max-width:800px; }
62 | .ui-tooltip-wrapper {
63 | background-color:#EEEEEE;
64 | border-color:#cdcdcd;
65 | color:black;
66 | }
67 |
68 |
69 | .comments {
70 | background-color:rgb(225,225,225);
71 | padding:10px;
72 | margin:15px;
73 | -moz-border-radius: 6px;
74 | border-radius: 6px;
75 | }
76 |
77 | .response {
78 | /*background-color:rgb(225,225,225);*/
79 | padding:10px;
80 | margin:15px;
81 | -moz-border-radius: 6px;
82 | border-radius: 6px;
83 | }
84 |
85 | .comments p {
86 | margin-bottom:1px;
87 | }
88 | .comment p.metadata {
89 | font-size:0.8em;
90 | }
91 | .comment {
92 | margin-left:15px;
93 | margin-top:15px;
94 | }
95 |
96 | .implemented {
97 | float:right;
98 | margin-left:40px;
99 | background-color:#faff78;
100 | color:black;
101 | font-size:70%;
102 | font-weight:bold;
103 | padding:5px;
104 | -moz-border-radius: 6px;
105 | border-radius: 6px;
106 |
107 | }
--------------------------------------------------------------------------------
/apps/templates/smartphone/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DoneCal
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {% module Static("css/ext/jquery.mobile/jquery.mobile-1.0a4.1.min.css") %}
32 |
38 |
39 | {% module Static("ext/json2.js") %}
40 | {% if debug %}
41 | {% module Static("ext/jquery-1.5.2.min.js") %}
42 | {% else %}
43 |
44 | {% end %}
45 |
46 | {% module Static("ext/jquery.store.js") %}
47 | {% module Static("smartphone.js") %}
48 | {% module Static("ext/jquery.mobile-1.0a4.1.js") %}
49 |
50 |
51 |
52 | {% block pages %}
53 | {% end %}
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/static/share.js:
--------------------------------------------------------------------------------
1 |
2 | if ($('#close-sharing-open-account').size()) {
3 | $('#close-sharing-open-account').click(function() {
4 | $('a.account').click();
5 | return false;
6 | });
7 | } else {
8 | if ($('#share_url').size()) {
9 | $('#share_url')[0].focus();
10 | $('#share_url')[0].select();
11 | }
12 | }
13 |
14 | $('a.more-options').click(function() {
15 | if ($('#more-options:visible').size()) {
16 | $('#more-options').hide();
17 | $(this).text("more options");
18 | } else {
19 | $('#more-options').show();
20 | $(this).text("hide options");
21 | }
22 | return false;
23 | });
24 |
25 | function __check(tag, toggle) {
26 | $('select[name="tags"] option').each(function(i, e) {
27 | if ($(e).val() == tag) {
28 | $(e).attr('selected', toggle);
29 | }
30 | });
31 | }
32 |
33 | function _check_option(tag) {
34 | __check(tag, true);
35 | }
36 | function _uncheck_option(tag) {
37 | __check(tag, false);
38 | }
39 |
40 | $('#tags-chosen').droppable({
41 | activeClass: 'ui-state-default',
42 | hoverClass: 'ui-state-hover',
43 | drop: function(event, ui) {
44 | $(this).find('.placeholder').remove();
45 | var tag_text = ui.draggable.text();
46 | _check_option(tag_text);
47 | $(' ').text(tag_text).appendTo($(' ').addClass('tag').appendTo($('ul', this)).draggable({
48 | revert:'invalid'
49 | }));
50 | ui.draggable.remove();
51 | _save_share_tags();
52 | }
53 | });
54 |
55 | $('#tags-available li.tag, #tags-chosen li.tag').draggable({
56 | revert: 'invalid'
57 | });
58 |
59 | $('#tags-available').droppable({
60 | drop: function(event, ui) {
61 | var tag_text = ui.draggable.text();
62 | _uncheck_option(tag_text);
63 | $(' ').text(tag_text).appendTo($(' ').addClass('tag').appendTo($('ul', this)).draggable({
64 | revert:'invalid'
65 | }));
66 | // here, reorder the UL?
67 | ui.draggable.remove();
68 | _save_share_tags();
69 | }
70 | });
71 |
72 |
73 | function _save_share_tags() {
74 | $('#save-note').show().text("saving");
75 | $('form#share').ajaxSubmit({
76 | success: function(response) {
77 | setTimeout(function() {
78 | $('#save-note').text("saved!");
79 | setTimeout(function() {
80 | $('#save-note').fadeOut('fast');
81 | }, 2*1000);
82 | }, 1000);
83 | }
84 | });
85 | }
86 |
87 | if ($('select[name="tags"] option:selected').size()) {
88 | $('a.more-options').click();
89 | }
--------------------------------------------------------------------------------
/apps/main/tests/base.py:
--------------------------------------------------------------------------------
1 | import re
2 | import unittest
3 | from cStringIO import StringIO
4 |
5 |
6 | from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase
7 |
8 | import app
9 | from apps.main.models import User, Event, UserSettings, Share, \
10 | FeatureRequest, FeatureRequestComment
11 | from tornado_utils.http_test_client import TestClient, HTTPClientMixin
12 |
13 |
14 | class BaseModelsTestCase(unittest.TestCase):
15 | _once = False
16 | def setUp(self):
17 | if not self._once:
18 | self._once = True
19 | from mongokit import Connection
20 | con = Connection()
21 | con.register([User, Event, UserSettings, Share,
22 | FeatureRequest, FeatureRequestComment])
23 | self.db = con.test
24 | self._emptyCollections()
25 |
26 | def _emptyCollections(self):
27 | [self.db.drop_collection(x) for x
28 | in self.db.collection_names()
29 | if x not in ('system.indexes',)]
30 |
31 | def tearDown(self):
32 | self._emptyCollections()
33 |
34 |
35 |
36 | class BaseHTTPTestCase(AsyncHTTPTestCase, LogTrapTestCase, HTTPClientMixin):
37 |
38 | _once = False
39 | def setUp(self):
40 | super(BaseHTTPTestCase, self).setUp()
41 | if not self._once:
42 | self._once = True
43 | self._emptyCollections()
44 |
45 | self._app.settings['email_backend'] = 'utils.send_mail.backends.locmem.EmailBackend'
46 | self._app.settings['email_exceptions'] = False
47 | self.client = TestClient(self)
48 |
49 |
50 | def _emptyCollections(self):
51 | db = self.get_db()
52 | [db.drop_collection(x) for x
53 | in db.collection_names()
54 | if x not in ('system.indexes',)]
55 |
56 | # replace self.get_db() with self.db one day
57 | @property
58 | def db(self):
59 | return self.get_db()
60 |
61 | def get_db(self):
62 | return self._app.con[self._app.database_name]
63 |
64 | def get_app(self):
65 | return app.Application(database_name='test',
66 | xsrf_cookies=False,
67 | optimize_static_content=False)
68 |
69 | def decode_cookie_value(self, key, cookie_value):
70 | try:
71 | return re.findall('%s=([\w=\|]+);' % key, cookie_value)[0]
72 | except IndexError:
73 | raise ValueError("couldn't find %r in %r" % (key, cookie_value))
74 |
75 | def reverse_url(self, *args, **kwargs):
76 | return self._app.reverse_url(*args, **kwargs)
77 |
--------------------------------------------------------------------------------
/utils/datatoxml.py:
--------------------------------------------------------------------------------
1 | import re
2 | try:
3 | from lxml import etree
4 | #print("running with lxml.etree")
5 | except ImportError:
6 | try:
7 | # Python 2.5
8 | import xml.etree.cElementTree as etree
9 | #print("running with cElementTree on Python 2.5+")
10 | except ImportError:
11 | try:
12 | # Python 2.5
13 | import xml.etree.ElementTree as etree
14 | #print("running with ElementTree on Python 2.5+")
15 | except ImportError:
16 | try:
17 | # normal cElementTree install
18 | import cElementTree as etree
19 | #print("running with cElementTree")
20 | except ImportError:
21 | try:
22 | # normal ElementTree install
23 | import elementtree.ElementTree as etree
24 | #print("running with ElementTree")
25 | except ImportError:
26 | raise ImportError(
27 | "Failed to import ElementTree from any known place")
28 |
29 |
30 | def dict_to_xml(dict_, root_node_tagname="Result"):
31 | root = etree.Element(root_node_tagname)
32 | assert isinstance(dict_, dict)
33 | _append_dict(root, dict_)
34 | return etree.tostring(root, pretty_print=True)
35 |
36 | def list_to_xml(list_, key, root_node_tagname="Result"):
37 | root = etree.Element(root_node_tagname)
38 | assert isinstance(list_, (list, tuple))
39 | _append_list(root, key, list_)
40 | return etree.tostring(root, pretty_print=True)
41 |
42 | def _append_dict(root, data):
43 | for key, value in data.items():
44 | element = etree.SubElement(root, key)
45 | if isinstance(value, dict):
46 | _append_dict(element, value)
47 | elif isinstance(value, list):
48 | list_key = re.sub('ies$', '', key)
49 | if list_key == key:
50 | list_key = re.sub('s$', '', key)
51 | if list_key == key:
52 | list_key += '_item'
53 |
54 | _append_list(element, list_key, value)
55 | else:
56 | _append_value(element, value)
57 |
58 | def _append_list(element, key, value):
59 | for v in value:
60 | sub_element = etree.SubElement(element, key)
61 | if isinstance(v, dict):
62 | _append_dict(sub_element, v)
63 | else:
64 | _append_value(sub_element, v)
65 |
66 | def _append_value(element, value):
67 | if value is not None:
68 | if isinstance(value, bool):
69 | value = value and 'true' or 'false'
70 | #element.set('type', str(type(value)))
71 | element.text = unicode(value)
72 |
73 |
--------------------------------------------------------------------------------
/apps/templates/qunit/smartphone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | QUnit Test Suite
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Calendar
24 |
Home
25 |
26 |
27 |
31 |
32 |
33 |
34 |
Loading...
35 |
Home
36 |
37 |
41 |
42 |
43 |
44 |
45 |
46 |
Not logged in
47 |
48 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/apps/emailreminders/tests/2011-02-23_114902_621456.email:
--------------------------------------------------------------------------------
1 | From peterbe@gmail.com Wed Feb 23 11:49:02 2011
2 | Return-Path:
3 | Received: from mail-ww0-f50.google.com (mail-ww0-f50.google.com [74.125.82.50])
4 | by donecal.fry-it.com (Postfix) with ESMTPS id 89C6720BFE622
5 | for ; Wed, 23 Feb 2011 11:49:02 +0000 (UTC)
6 | Received: by wwf26 with SMTP id 26so7794958wwf.7
7 | for ; Wed, 23 Feb 2011 03:49:02 -0800 (PST)
8 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
9 | d=gmail.com; s=gamma;
10 | h=domainkey-signature:mime-version:reply-to:in-reply-to:references
11 | :from:date:message-id:subject:to:content-type
12 | :content-transfer-encoding;
13 | bh=vRQUNVUvBRYSyOn2aCYfgK2SNkfGvZPNFsEp0QH5JGA=;
14 | b=N7LNoHXkGj/B+4r11lNPAf3Tix4XYlLkgMifP0DhwEJSZHaYALJ0lCArZEo1Cx7dO/
15 | tF9LRuj422KBaWK07utgILIRYzQJ1dArASbiMbvNT1M+9YL5FxbacGUJybJNpHP7R/SW
16 | pczDgzLWcO/53brAU/Rabp6pU/lHvDJTRMtFE=
17 | DomainKey-Signature: a=rsa-sha1; c=nofws;
18 | d=gmail.com; s=gamma;
19 | h=mime-version:reply-to:in-reply-to:references:from:date:message-id
20 | :subject:to:content-type:content-transfer-encoding;
21 | b=ZsIQwwNvwBqqMVU4TtJ7pAC/arxe41D8iji29mV/S2wTnrXmamj87LUXMViJYzrE0a
22 | KfKMdGAUQ576lGnbMNyELxvOqM0Lfd8krz+gxloqLAwCOg5N3e1RazvhzB3K42Ezphmi
23 | LqzSaMTVhAY6CBLA640DnY6R/oQ/6ddXi+yp8=
24 | Received: by 10.216.19.139 with SMTP id n11mr3223969wen.78.1298461742160; Wed,
25 | 23 Feb 2011 03:49:02 -0800 (PST)
26 | MIME-Version: 1.0
27 | Received: by 10.216.221.224 with HTTP; Wed, 23 Feb 2011 03:48:42 -0800 (PST)
28 | Reply-To: peter@fry-it.com
29 | In-Reply-To: <20110221184501.16161.38157@donecal>
30 | References: <20110221184501.16161.38157@donecal>
31 | From: Peter Bengtsson
32 | Date: Wed, 23 Feb 2011 11:48:42 +0000
33 | Message-ID:
34 | Subject: Re: [DoneCal] What did you do today?
35 | To: DoneCal
36 | Content-Type: text/plain; charset=ISO-8859-1
37 | Content-Transfer-Encoding: quoted-printable
38 |
39 | 1h =C4r du p=E5 plats i Libyen? SM
40 |
41 | 30 minutes #=C4r du @p=E5
42 |
43 | On 21 February 2011 18:45, DoneCal
44 | wrote:
45 | > Hi Peter,
46 | > What did you do *today*?
47 | >
48 | >
49 | >
50 | >
51 | > SETTINGS:
52 | > If you want to change your email reminders simply go to this page:
53 | > https://donecal.com/emailreminders/?edit=3D4d11e7b674a1f8361200006c
54 | >
55 | >
56 |
57 |
58 |
59 | --=20
60 | Peter Bengtsson,
61 | work www.fry-it.com
62 | home www.peterbe.com
63 | hobby www.issuetrackerproduct.com
64 | fun crosstips.org
65 |
--------------------------------------------------------------------------------
/apps/templates/help/news.html:
--------------------------------------------------------------------------------
1 | {% extends "help_base.html" %}
2 |
3 | {% block content_inner %}
4 |
5 | News
6 |
7 |
8 |
9 |
1 Oct 2011 - DoneCal.com source code released
10 |
All the code of DoneCal is now released on GitHub at
11 | https://github.com/peterbe/worklog .
12 |
13 |
2 Jan 2011 - DoneCal.com international visitors
14 |
Interesting blog on Peterbe.com
15 | about where DoneCal users come from.
16 |
17 |
1 Jan 2011 - AM/PM or 24 hour time format
18 |
It is now possible to change if you prefer AM/PM or 24 hour time format for your calendar.
19 |
20 |
31 Dec 2010 - Email reminders
21 |
A new feature is available to enter events simply by replying to a regular
22 | Email reminder .
23 |
24 |
19 Dec 2010 - Use a # sign to tag
25 |
Voices have been heard!
26 | You can now tag your events with a # sign instead of an @ sign.
27 |
28 |
17 Dec 2010 - Three new Help pages
29 |
Check out Google Calendar ,
30 | Feature requests and the page about
31 | Secure passwords .
32 |
33 |
34 |
15 Dec 2010 - Calendar view of choice remembered in a cookie
35 |
If you change month or change to a week view it used to be that this was encoded in the
36 | current URL as an anchor link. Now it's just stored in a cookie instead so that you
37 | automatically return to the view you last time.
38 |
39 |
2 Dec 2010 - Change of default window for events in API
40 |
Before the API would allow you to send in events that were not all day events with
41 | and date equal to the start date.
42 | See About dates when posting in the API docs.
43 |
44 |
28 Nov 2010 - Feature requests page
45 |
Lots of improvements can always be made but it's best if which
46 | features are added is based on what people want the most.
47 | On Feature requests logged in users can vote up their most desired feature requests.
48 |
49 |
50 |
27 Nov 2010 - Resizing events across multiple weeks
51 |
You can now make events longer than one week by dragging and resizing them across
52 | multiple weeks.
53 |
54 |
24 Nov 2010 - Logging in with Google account
55 |
You can use your Google account now to both register and to log in. Means you don't
56 | have to remember which email address or what password you chose for DoneCal.
57 |
58 |
59 |
60 |
61 | {% end %}
62 |
--------------------------------------------------------------------------------
/apps/templates/stats/index.html:
--------------------------------------------------------------------------------
1 | {% extends "../base.html" %}
2 |
3 | {% block extra_head %}
4 | {% module Static("css/ext/jquery.jqplot.min.css", "css/smoothness/jquery-ui-1.8.6.slider.datepicker.css", "css/stats.css") %}
5 | {% end %}
6 |
7 | {% block title %}Statistics on DoneCal{% end %}
8 |
9 | {% block content_inner %}
10 |
11 | Stats
12 |
13 |
14 |
15 |
16 |
17 | From:
18 |
19 | To:
20 |
21 |
22 |
23 | Cumulative
24 |
25 | or
26 | New
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {% end %}
47 |
48 | {% block sidebar %}
49 |
50 | Raw numbers
51 |
52 |
54 |
55 |
56 | {% end %}
57 |
58 |
59 | {% block extrajs %}
60 |
89 | {% end %}
90 |
--------------------------------------------------------------------------------
/apps/templates/bookmarklet/index.html:
--------------------------------------------------------------------------------
1 | {% extends "bookmarklet_base.html" %}
2 |
3 | {% block content %}
4 |
5 | Quickly save something on DoneCal
6 |
7 | {% if user %}
8 |
Currently logged in as {{ user.first_name }}
9 | on
10 | DoneCal
11 |
12 | {% else %}
13 |
Currently not logged in to DoneCal
14 | {% end %}
15 |
16 |
17 |
18 | {% if error_title %}
19 | Error: {{ error_title }}
20 | {% end %}
21 |
22 |
23 |
24 | {% if external_url %}
25 |
26 |
27 | Remember that it was here on
28 | {% module TruncateString(external_url, 35) %}
29 |
30 |
31 |
32 | {% end %}
33 |
34 | Length:
35 |
36 | all day
37 | half hour
38 | one hour
39 |
40 |
41 |
42 | more options
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 | {% module xsrf_form_html() %}
56 |
57 | {% end %}
58 |
59 |
60 | {% block extrajs %}
61 |
76 | {% end %}
77 |
--------------------------------------------------------------------------------
/static/css/ext/jquery.jqplot.min.css:
--------------------------------------------------------------------------------
1 | .jqplot-target{position:relative;color:#666;font-family:"Trebuchet MS",Arial,Helvetica,sans-serif;font-size:1em;}.jqplot-axis{font-size:.75em;}.jqplot-xaxis{margin-top:10px;}.jqplot-x2axis{margin-bottom:10px;}.jqplot-yaxis{margin-right:10px;}.jqplot-y2axis,.jqplot-y3axis,.jqplot-y4axis,.jqplot-y5axis,.jqplot-y6axis,.jqplot-y7axis,.jqplot-y8axis,.jqplot-y9axis{margin-left:10px;margin-right:10px;}.jqplot-axis-tick,.jqplot-xaxis-tick,.jqplot-yaxis-tick,.jqplot-x2axis-tick,.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{position:absolute;}.jqplot-xaxis-tick{top:0;left:15px;vertical-align:top;}.jqplot-x2axis-tick{bottom:0;left:15px;vertical-align:bottom;}.jqplot-yaxis-tick{right:0;top:15px;text-align:right;}.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{left:0;top:15px;text-align:left;}.jqplot-meterGauge-tick{font-size:.75em;color:#999;}.jqplot-meterGauge-label{font-size:1em;color:#999;}.jqplot-xaxis-label{margin-top:10px;font-size:11pt;position:absolute;}.jqplot-x2axis-label{margin-bottom:10px;font-size:11pt;position:absolute;}.jqplot-yaxis-label{margin-right:10px;font-size:11pt;position:absolute;}.jqplot-y2axis-label,.jqplot-y3axis-label,.jqplot-y4axis-label,.jqplot-y5axis-label,.jqplot-y6axis-label,.jqplot-y7axis-label,.jqplot-y8axis-label,.jqplot-y9axis-label{font-size:11pt;position:absolute;}table.jqplot-table-legend{margin-top:12px;margin-bottom:12px;margin-left:12px;margin-right:12px;}table.jqplot-table-legend,table.jqplot-cursor-legend{background-color:rgba(255,255,255,0.6);border:1px solid #ccc;position:absolute;font-size:.75em;}td.jqplot-table-legend{vertical-align:middle;}td.jqplot-seriesToggle:hover,td.jqplot-seriesToggle:active{cursor:pointer;}td.jqplot-table-legend>div{border:1px solid #ccc;padding:1px;}div.jqplot-table-legend-swatch{width:0;height:0;border-top-width:5px;border-bottom-width:5px;border-left-width:6px;border-right-width:6px;border-top-style:solid;border-bottom-style:solid;border-left-style:solid;border-right-style:solid;}.jqplot-title{top:0;left:0;padding-bottom:.5em;font-size:1.2em;}table.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em;}.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,0.5);padding:1px;}.jqplot-highlighter-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,0.5);padding:1px;}.jqplot-point-label{font-size:.75em;z-index:2;}td.jqplot-cursor-legend-swatch{vertical-align:middle;text-align:center;}div.jqplot-cursor-legend-swatch{width:1.2em;height:.7em;}.jqplot-error{text-align:center;}.jqplot-error-message{position:relative;top:46%;display:inline-block;}div.jqplot-bubble-label{font-size:.8em;padding-left:2px;padding-right:2px;color:rgb(20%,20%,20%);}div.jqplot-bubble-label.jqplot-bubble-label-highlight{background:rgba(90%,90%,90%,0.7);}
--------------------------------------------------------------------------------
/static/ext/head.load.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | Head JS The only script in your
3 | Copyright Tero Piirainen (tipiirai)
4 | License MIT / http://bit.ly/mit-license
5 | Version 0.9
6 |
7 | http://headjs.com
8 | */(function(a){var b=a.documentElement,c,d,e=[],f=[],g={},h={},i=a.createElement("script").async===true||"MozAppearance"in a.documentElement.style||window.opera;var j=window.head_conf&&head_conf.head||"head",k=window[j]=window[j]||function(){k.ready.apply(null,arguments)};var l=0,m=1,n=2,o=3;i?k.js=function(){var a=arguments,b=a[a.length-1],c=[];t(b)||(b=null),s(a,function(d,e){d!=b&&(d=r(d),c.push(d),x(d,b&&e==a.length-2?function(){u(c)&&p(b)}:null))});return k}:k.js=function(){var a=arguments,b=[].slice.call(a,1),d=b[0];if(!c){f.push(function(){k.js.apply(null,a)});return k}d?(s(b,function(a){t(a)||w(r(a))}),x(r(a[0]),t(d)?d:function(){k.js.apply(null,b)})):x(r(a[0]));return k},k.ready=function(a,b){if(a=="dom"){d?p(b):e.push(b);return k}t(a)&&(b=a,a="ALL");var c=h[a];if(c&&c.state==o||a=="ALL"&&u()&&d){p(b);return k}var f=g[a];f?f.push(b):f=g[a]=[b];return k},k.ready("dom",function(){c&&u()&&s(g.ALL,function(a){p(a)}),k.feature&&k.feature("domloaded",true)});function p(a){a._done||(a(),a._done=1)}function q(a){var b=a.split("/"),c=b[b.length-1],d=c.indexOf("?");return d!=-1?c.substring(0,d):c}function r(a){var b;if(typeof a=="object")for(var c in a)a[c]&&(b={name:c,url:a[c]});else b={name:q(a),url:a};var d=h[b.name];if(d&&d.url===b.url)return d;h[b.name]=b;return b}function s(a,b){if(a){typeof a=="object"&&(a=[].slice.call(a));for(var c=0;cFull report
11 |
12 |
13 |
14 |
15 |
16 | From:
17 |
18 | To:
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Days
27 |
28 |
29 |
30 |
31 | Days
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
Hours
44 |
45 |
46 |
47 |
48 | Hours
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {% end %}
61 | {% block sidebar %}
62 |
63 |
79 |
80 | {% end %}
81 | {% block extrajs %}
82 |
100 | {% end %}
101 |
--------------------------------------------------------------------------------
/apps/emailreminders/tests/2011-05-17_220706_318577.email:
--------------------------------------------------------------------------------
1 | From SRS0=aWoSaA=YH=fibertel.com.ar=aki@srs.bis7.eu.blackberry.com Tue May 17 22:07:06 2011
2 | Return-Path:
3 | Received: from smtp07.bis7.eu.blackberry.com (smtp07.bis7.eu.blackberry.com [178.239.85.12])
4 | by donecal.fry-it.com (Postfix) with ESMTP id 2908A116A1C
5 | for ; Tue, 17 May 2011 22:07:06 +0000 (UTC)
6 | Received: from b3.c2.bise7.blackberry ([192.168.0.103])
7 | by srs.bis7.eu.blackberry.com (8.13.7 TEAMON/8.13.7) with ESMTP id p4HM70SG007750
8 | for ; Tue, 17 May 2011 22:07:00 GMT
9 | Received: from 172.18.195.195 (cmp25.c2.bise7.blackberry [172.18.195.195])
10 | by b3.c2.bise7.blackberry (8.13.7 TEAMON/8.13.7) with ESMTP id p4HM6x85026469
11 | for ; Tue, 17 May 2011 22:06:59 GMT
12 | X-rim-org-msg-ref-id: 2031999381
13 | Message-ID: <2031999381-1305670017-cardhu_decombobulator_blackberry.rim.net-1956640253-@b27.c2.bise7.blackberry>
14 | Content-Transfer-Encoding: base64
15 | Reply-To: aki@fibertel.com.ar
16 | X-Priority: Normal
17 | Sensitivity: Normal
18 | Importance: Normal
19 | Subject: Re: [DoneCal] What did you do today?
20 | To: "DoneCal"
21 | From: "Aki Ohki"
22 | Date: Tue, 17 May 2011 22:06:54 +0000
23 | Content-Type: text/plain; charset="Windows-1252"
24 | MIME-Version: 1.0
25 |
26 | MTI6MDAgMWggRmF0ZTogY29uZmlndXJhciBzY2FubmVyIFdDMzUNCi0tLS0tLU1lbnNhamUgb3Jp
27 | Z2luYWwtLS0tLS0NCkRlOiBEb25lQ2FsDQpQYXJhOiBBa2kgT2hraQ0KQXN1bnRvOiBbRG9uZUNh
28 | bF0gV2hhdCBkaWQgeW91IGRvIHRvZGF5Pw0KRW52aWFkbzogMTcgZGUgbWF5LCAyMDExIDE4OjAw
29 | DQoNCkhpIEFraSwNCldoYXQgZGlkIHlvdSBkbyAqdG9kYXkqPw0KDQoNCklOU1RSVUNUSU9OUzoN
30 | Ckp1c3QgaGl0IHJlcGx5IGFuZCB0eXBlIGF0IHRoZSBzdGFydCBvZiB0aGUgZW1haWwgYXMgeW91
31 | IHdvdWxkIGVudGVyDQphbiBldmVudCBub3JtYWxseSBvbiBEb25lQ2FsIHdpdGggdGFncyBhbmQg
32 | ZXZlcnl0aGluZy4gVG8gZW50ZXINCm11bHRpcGxlIGV2ZW50cyBzZXBhcmF0ZSB0aGVtIGJ5IHR3
33 | byBsaW5lIGJyZWFrcy4gQSBzaW5nbGUgbGluZSBicmVhaw0Kc2V0cyB0aGUgZGVzY3JpcHRpb24g
34 | b2YgdGhlIGV2ZW50LiANCg0KSWYgeW91IHdhbnQgaXQgdG8gYmUgYW4gaG91cmx5IGV2ZW4gd3Jp
35 | dGUgYSBudW1iZXIgZm9sbG93ZWQgYnkgdGhlIGxldHRlciAnaCcuIExpa2UgdGhpcyBmb3IgZXhh
36 | bXBsZToNCg0KIDE0OjQ1IFdvcmtpbmcgb24gI3Byb2plY3RYIGxpa2UgU2FtIHNhaWQNCg0KSWYg
37 | eW91IHdhbnQgdG8gc3BlY2lmeSBleGFjdGx5IHdoZW4gaXQgaGFwcGVuZWQgZW50ZXIgaXQgbGlr
38 | ZSBhIDI0IGhvdXIgY2xvY2sgb3Igd2l0aCBBTS9QTS4gTGlrZSB0aGlzIGZvciBleGFtcGxlOg0K
39 | DQogMTA6MzAgMWggSGVscGVkIFNhbnRhIHdpdGggZ2lmdCB3cmFwcGluZw0KDQpJZiB5b3UgZG9u
40 | J3Qgc3RhcnQgdGhlIGxpbmUgd2l0aCBhIHRpbWUsIGJ1dCBkbyBzdGFydCB0aGUgbGluZSB3aXRo
41 | IGEgZHVyYXRpb24gaXQgd2lsbCBhc3N1bWUgaXQncyBub29uIHlvdXIgbG9jYWwgdGltZS4NClNV
42 | TU1BUlkgT0YgRVZFTlRTIFRPREFZOg0KDQogICgxMDozMHBtLCAxIGhvdXIpIFJldW5p824gQ05B
43 | DQoNCiAgKDFwbSwgNiBob3VycykgQ29uZmlndXJhciBTY2FuIGEgQmFzZSAtIEF2b24NCg0KICAo
44 | NXBtLCAxIGhvdXIpIFZlciBwcm9ibGVtYXMgZGUgcmVkIE5va2lhIFNpZW1lbnMgTmV0d29yaw0K
45 | DQogICg0cG0sIDUgaG91cnMpIEFzYWRvIEFpY2hpIGtlbg0KDQogIChBbGwgZGF5KSBDdW1wbGVh
46 | 8W9zIGRlIEFraQ0KDQoNClNFVFRJTkdTOg0KSWYgeW91IHdhbnQgdG8gY2hhbmdlIHlvdXIgZW1h
47 | aWwgcmVtaW5kZXJzIHNpbXBseSBnbyB0byB0aGlzIHBhZ2U6DQpodHRwOi8vZG9uZWNhbC5jb20v
48 | ZW1haWxyZW1pbmRlcnMvP2VkaXQ9NGRkMmNhN2M3NGExZjgwMzRjMDAwMDM5DQoNCg0K
49 |
--------------------------------------------------------------------------------
/apps/emailreminders/tests/2011-03-28_170726_751460.email:
--------------------------------------------------------------------------------
1 | From ghayoun@gmail.com Mon Mar 28 17:07:26 2011
2 | Return-Path:
3 | Received: from mail-vw0-f44.google.com (mail-vw0-f44.google.com [209.85.212.44])
4 | by donecal.fry-it.com (Postfix) with ESMTPS id 77BAD803B21EC
5 | for ; Mon, 28 Mar 2011 17:07:26 +0000 (UTC)
6 | Received: by vws12 with SMTP id 12so2865453vws.31
7 | for ; Mon, 28 Mar 2011 10:07:25 -0700 (PDT)
8 | DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
9 | d=gmail.com; s=gamma;
10 | h=domainkey-signature:mime-version:in-reply-to:references:from:date
11 | :message-id:subject:to:content-type:content-transfer-encoding;
12 | bh=wHCQQoLHrOSt/wa4Q4dKej2TsNzII+A4+9gRCc2/47U=;
13 | b=fRVHL1onoucXmGHUt70G76DZGmYBSWZUt19a8YsD3IyBglk1Yygq3NiDliq+PwAVu2
14 | 6Fh8oRR7IGsSin6N2SQiv154kLsGE85LspAM8oEWuEgMBfpLAddKuE1u/GLxEXHWXtlr
15 | USCbSS8xlJrHsUCvzwr0ts2N1OLGhBWPBI924=
16 | DomainKey-Signature: a=rsa-sha1; c=nofws;
17 | d=gmail.com; s=gamma;
18 | h=mime-version:in-reply-to:references:from:date:message-id:subject:to
19 | :content-type:content-transfer-encoding;
20 | b=REdJ36jemgg2cnkQWe9x/3vgUcytVMqv+F8QzM0QOL3CB8EJcGYlbN9CeJlz2+tmqm
21 | 7q/kGDJNrXxylijgGbLt/qUAzwajFmvy6xFV2LHOluAIWq6na6QPfAm0zuJiyr8frw1f
22 | vF73wdcKgPCAl4yO1Kpg3I6/YKuycy2xaaCxk=
23 | Received: by 10.220.89.144 with SMTP id e16mr1219549vcm.49.1301332045186; Mon,
24 | 28 Mar 2011 10:07:25 -0700 (PDT)
25 | MIME-Version: 1.0
26 | Received: by 10.220.87.66 with HTTP; Mon, 28 Mar 2011 10:07:05 -0700 (PDT)
27 | In-Reply-To: <20110328170001.18172.2571@donecal>
28 | References: <20110328170001.18172.2571@donecal>
29 | From: Gautier HAYOUN
30 | Date: Mon, 28 Mar 2011 18:07:05 +0100
31 | Message-ID:
32 | Subject: Re: [DoneCal] What did you do today?
33 | To: DoneCal
34 | Content-Type: text/plain; charset=UTF-8
35 | Content-Transfer-Encoding: quoted-printable
36 |
37 | 9:00 4h bounced back emails #waf
38 |
39 |
40 | 15:00 2h deployed test env #orangepic
41 |
42 |
43 | On Mon, Mar 28, 2011 at 6:00 PM, DoneCal
44 | wrote:
45 | > Hi Gautier,
46 | > What did you do *today*?
47 | >
48 | >
49 | > INSTRUCTIONS:
50 | > Just hit reply and type at the start of the email as you would enter
51 | > an event normally on DoneCal with tags and everything. To enter
52 | > multiple events separate them by two line breaks. A single line break
53 | > sets the description of the event.
54 | >
55 | > If you want it to be an hourly even write a number followed by the letter=
56 | 'h'. Like this for example:
57 | >
58 | > =C2=A014:45 Working on #projectX like Sam said
59 | >
60 | > If you want to specify exactly when it happened enter it like a 24 hour c=
61 | lock or with AM/PM. Like this for example:
62 | >
63 | > =C2=A010:30 1h Helped Santa with gift wrapping
64 | >
65 | > If you don't start the line with a time, but do start the line with a dur=
66 | ation it will assume it's noon your local time.
67 | >
68 | >
69 | > SETTINGS:
70 | > If you want to change your email reminders simply go to this page:
71 | > http://donecal.com/emailreminders/?edit=3D4d90a11374a1f8470e00005c
72 | >
73 | >
74 |
--------------------------------------------------------------------------------
/apps/emailreminders/reminder_utils.py:
--------------------------------------------------------------------------------
1 | import re
2 | from utils import valid_email
3 |
4 | class ParseEventError(Exception):
5 | pass
6 |
7 | def parse_time(text):
8 | time_regex_1 = re.compile('^(\d{1,2})(\.|:)(\d{2})(am|pm)\s', re.I)
9 | time_regex_2 = re.compile('^(\d{1,2})(am|pm)\s', re.I)
10 | time_regex_3 = re.compile('^(\d{1,2})(\.|:)(\d{2})\s', re.I)
11 |
12 | time_ = None
13 |
14 | if time_regex_1.findall(text):
15 | h, __, m, am_pm = time_regex_1.findall(text)[0]
16 | h = int(h)
17 | m = int(m)
18 | if am_pm.lower() == 'pm':
19 | h += 12
20 | time_ = (h, m)
21 | text = time_regex_1.sub('', text).strip()
22 | elif time_regex_2.findall(text):
23 | h, am_pm = time_regex_2.findall(text)[0]
24 | h = int(h)
25 | if am_pm.lower() == 'pm':
26 | h += 12
27 | time_ = (h, 0)
28 | text = time_regex_2.sub('', text).strip()
29 | elif time_regex_3.findall(text):
30 | h, __, m = time_regex_3.findall(text)[0]
31 | h = int(h)
32 | m = int(m)
33 | text = time_regex_3.sub('', text).strip()
34 | time_ = (h, m)
35 |
36 | if time_:
37 | # make sure it makes sense
38 | if time_[0] < 0 or time_[0] > 23:
39 | raise ParseEventError("Hour part not in range 0..24")
40 | if time_[1] < 0 or time_[1] > 59:
41 | raise ParseEventError("Minute part not in range 0..24")
42 |
43 | return text, time_
44 |
45 |
46 | def parse_duration(text):
47 | """return the new text and and the duration in minutes as a tuple"""
48 | duration = None
49 | duration_regex = re.compile(
50 | '^(\d{1,2})(\.\d{1,2})*\s*(d |h |m |min |hours|hour|minutes|minute|days|day)',
51 | re.I)
52 | if duration_regex.findall(text):
53 | n1, n2, d = duration_regex.findall(text)[0]
54 | n1 = int(n1)
55 | if n2:
56 | n2 = float(n2)
57 | else:
58 | n2 = 0.0
59 | if d in ('d ', 'day', 'days'):
60 | duration = 24 * 60 * (n1 + n2)
61 | elif d in ('m ','min ','minute', 'minutes'):
62 | duration = n1 + n2
63 | elif d in ('h ','hour', 'hours'):
64 | duration = (n1 + n2) * 60
65 | else:
66 | raise NotImplementedError(d)
67 | text = duration_regex.sub('', text).strip()
68 | return text, duration
69 |
70 |
71 | def parse_email_line(es):
72 | """ find all email addresses in the string 'es'.
73 | For example, if the input is 'Peter '
74 | then return ['mail@peterbe.com']
75 | In other words, strip out all junk that isn't valid email addresses.
76 | """
77 |
78 | real_emails = []
79 | sep = ','
80 | local_domain_email_regex = re.compile(r'\b\w+@\w+\b')
81 |
82 | for chunk in [x.strip() for x in es.replace(';',',').split(',') if x.strip()]:
83 |
84 | # if the chunk is something like this:
85 | # 'SnapExpense info@snapexpense.com '
86 | # then we want to favor the part in <...>
87 | found = re.findall('<([^@]+@[^@]+)>', chunk)
88 | if found:
89 | chunk = found[0].strip()
90 |
91 | if valid_email(chunk) or local_domain_email_regex.findall(chunk):
92 | if chunk.lower() not in [x.lower() for x in real_emails]:
93 | real_emails.append(chunk)
94 |
95 | return real_emails
96 |
--------------------------------------------------------------------------------
/apps/emailreminders/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from apps.emailreminders import reminder_utils as utils
3 | class UtilsTestCase(unittest.TestCase):
4 |
5 | def test_parse_time(self):
6 | text = "3pm something 10.30"
7 | text, time = utils.parse_time(text)
8 | self.assertEqual(text, "something 10.30")
9 | self.assertEqual(time, (15, 0))
10 |
11 | text = "3am something 10.30"
12 | text, time = utils.parse_time(text)
13 | self.assertEqual(text, "something 10.30")
14 | self.assertEqual(time, (3, 0))
15 |
16 | text = "0am something 10.30"
17 | text, time = utils.parse_time(text)
18 | self.assertEqual(text, "something 10.30")
19 | self.assertEqual(time, (0, 0))
20 |
21 | text = "3.20pm something 10.30"
22 | text, time = utils.parse_time(text)
23 | self.assertEqual(text, "something 10.30")
24 | self.assertEqual(time, (15, 20))
25 |
26 | text = "3.50am something 10.30"
27 | text, time = utils.parse_time(text)
28 | self.assertEqual(text, "something 10.30")
29 | self.assertEqual(time, (3, 50))
30 |
31 | text = "10.50pm something 10.30"
32 | text, time = utils.parse_time(text)
33 | self.assertEqual(text, "something 10.30")
34 | self.assertEqual(time, (22, 50))
35 |
36 | text = "Not first 3pm something 10.30"
37 | text, time = utils.parse_time(text)
38 | self.assertEqual(text, "Not first 3pm something 10.30")
39 | self.assertEqual(time, None)
40 |
41 | text = "9.15 something 10.30"
42 | text, time = utils.parse_time(text)
43 | self.assertEqual(text, "something 10.30")
44 | self.assertEqual(time, (9, 15))
45 |
46 | text = "9:15 something 10.30"
47 | text, time = utils.parse_time(text)
48 | self.assertEqual(text, "something 10.30")
49 | self.assertEqual(time, (9, 15))
50 |
51 | text = "9.1500 precision"
52 | text, time = utils.parse_time(text)
53 | self.assertEqual(text, "9.1500 precision")
54 | self.assertEqual(time, None)
55 |
56 | def test_parse_time_failing(self):
57 | self.assertRaises(utils.ParseEventError,
58 | utils.parse_time, '20pm something')
59 |
60 | self.assertRaises(utils.ParseEventError,
61 | utils.parse_time, '10:70pm something')
62 |
63 |
64 | def test_duration(self):
65 | text = "60min something"
66 | text, duration = utils.parse_duration(text)
67 | self.assertEqual(duration, 60)
68 | self.assertEqual(text, "something")
69 |
70 | text = "1 day something"
71 | text, duration = utils.parse_duration(text)
72 | self.assertEqual(duration, 60 * 24)
73 | self.assertEqual(text, "something")
74 |
75 | text = "1.5 days something"
76 | text, duration = utils.parse_duration(text)
77 | self.assertEqual(duration, 60 * 24 + 60 * 12)
78 | self.assertEqual(text, "something")
79 |
80 | text = "2.5 minutes something"
81 | text, duration = utils.parse_duration(text)
82 | self.assertEqual(duration, 2.5)
83 | self.assertEqual(text, "something")
84 |
85 | text = "2.5h something"
86 | text, duration = utils.parse_duration(text)
87 | self.assertEqual(duration, 2.5 * 60)
88 | self.assertEqual(text, "something")
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/bin/run_pyflakes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import pyflakes.checker
3 | import compiler, sys
4 | import os
5 | import here
6 |
7 | def check(codeString, filename):
8 | """
9 | Check the Python source given by C{codeString} for flakes.
10 |
11 | @param codeString: The Python source to check.
12 | @type codeString: C{str}
13 |
14 | @param filename: The name of the file the source came from, used to report
15 | errors.
16 | @type filename: C{str}
17 |
18 | @return: The number of warnings emitted.
19 | @rtype: C{int}
20 | """
21 | # Since compiler.parse does not reliably report syntax errors, use the
22 | # built in compiler first to detect those.
23 | try:
24 | try:
25 | compile(codeString, filename, "exec")
26 | except MemoryError:
27 | # Python 2.4 will raise MemoryError if the source can't be
28 | # decoded.
29 | if sys.version_info[:2] == (2, 4):
30 | raise SyntaxError(None)
31 | raise
32 | except (SyntaxError, IndentationError), value:
33 | msg = value.args[0]
34 |
35 | (lineno, offset, text) = value.lineno, value.offset, value.text
36 |
37 | # If there's an encoding problem with the file, the text is None.
38 | if text is None:
39 | # Avoid using msg, since for the only known case, it contains a
40 | # bogus message that claims the encoding the file declared was
41 | # unknown.
42 | return ["%s: problem decoding source" % (filename, )]
43 | else:
44 | line = text.splitlines()[-1]
45 |
46 | if offset is not None:
47 | offset = offset - (len(text) - len(line))
48 |
49 | return ['%s:%d: %s' % (filename, lineno, msg)]
50 | else:
51 | # Okay, it's syntactically valid. Now parse it into an ast and check
52 | # it.
53 | tree = compiler.parse(codeString)
54 | w = pyflakes.checker.Checker(tree, filename)
55 |
56 | lines = codeString.split('\n')
57 | messages = [message for message in w.messages
58 | if lines[message.lineno-1].find('pyflakes:ignore') < 0]
59 | messages.sort(lambda a, b: cmp(a.lineno, b.lineno))
60 |
61 | return messages
62 |
63 |
64 | def checkPath(filename):
65 | """
66 | Check the given path, printing out any warnings detected.
67 |
68 | @return: the number of warnings printed
69 | """
70 | try:
71 | return check(file(filename, 'U').read() + '\n', filename)
72 | except IOError, msg:
73 | return ["%s: %s" % (filename, msg.args[1])]
74 |
75 | def checkPaths(filenames):
76 | warnings = []
77 | for arg in filenames:
78 | if os.path.isdir(arg):
79 | for dirpath, dirnames, filenames in os.walk(arg):
80 | for filename in filenames:
81 | if filename.endswith('.py'):
82 | warnings.extend(checkPath(os.path.join(dirpath, filename)))
83 | else:
84 | warnings.extend(checkPath(arg))
85 | return warnings
86 |
87 | import settings
88 |
89 | def run(*filenames):
90 | if not filenames:
91 | filenames = getattr(settings, 'PYFLAKES_DEFAULT_ARGS', ['apps'])
92 | warnings = checkPaths(filenames)
93 | for warning in warnings:
94 | print warning
95 | if warnings:
96 | print 'Total warnings: %d' % len(warnings)
97 | return 0
98 |
99 | if __name__ == '__main__':
100 | import sys
101 | sys.exit(run(*sys.argv[1:]))
102 |
--------------------------------------------------------------------------------
/static/account.js:
--------------------------------------------------------------------------------
1 | /* this is loaded automatically when the account page is shown */
2 |
3 | var f = $('form.login');
4 | var e = $('input[name="email"]', f);
5 | /*
6 | if (!e.val())
7 | e.val($('input[name="placeholdervalue"]', f).val())
8 | .addClass('placeholdervalue');
9 | e.bind('focus', function() {
10 | if ($(this).val() == $('input[name="placeholdervalue"]', f).val())
11 | $(this).val('').removeClass('placeholdervalue');
12 | }).bind('blur', function() {
13 | if (!$.trim($(this).val()))
14 | $(this).val($('input[name="placeholdervalue"]', f).val())
15 | .addClass('placeholdervalue');
16 | });
17 | */
18 |
19 |
20 | $.getScript(JS_URLS.validate, function() {
21 | var validated_emails = {};
22 |
23 | $('form.login').validate({
24 | rules: {
25 | email: {
26 | required: true,
27 | email: true
28 | },
29 | password: {
30 | required: true,
31 | minlength: 4
32 | }
33 | },
34 | onkeyup: false,
35 | success: function(label_) {
36 | if (label_.attr('for') == 'email') {
37 | var value = $('input[name="email"]', 'form.login').val();
38 | if ($.inArray(value, validated_emails) == -1) {
39 | $.getJSON('/user/signup/', {validate_email:value}, function(response) {
40 | if (!(response.error && response.error == 'taken')) {
41 | $('label[for="email"]', 'form.login').text("No user by that email");
42 | }
43 | validated_emails[value] = response;
44 | });
45 | }
46 | }
47 | }
48 | });
49 |
50 |
51 | $('form#signup').validate({
52 | invalidHandler: function(form, validator) {
53 | var errors = validator.numberOfInvalids();
54 | if (errors)
55 | $('#invalid-anything:hidden').show(200);
56 | },
57 | rules: {
58 | email: {
59 | required: true,
60 | email: true
61 | },
62 | password: {
63 | required: true,
64 | minlength: 4
65 | }
66 | },
67 | onkeyup: false,
68 | success: function(label_) {
69 | if (!$('input.error', 'form#signup').size())
70 | $('#invalid-anything:visible').hide(400);
71 | var text = "OK!";
72 | if (label_.attr('for') == 'email') {
73 | var value = $('input[name="' + label_.attr('for') + '"]', 'form#signup').val();
74 |
75 | if ($.inArray(value, validated_emails) == -1) {
76 | $.getJSON('/user/signup/', {validate_email:value}, function(response) {
77 | if (response.error) {
78 | $('#invalid-email').show(200);
79 | $('label[for="email"]', 'form#signup').text("Not ok").addClass('error').removeClass('valid');
80 | } else {
81 | $('#invalid-email:visible').hide();
82 | $('label[for="email"]', 'form#signup').remove();
83 | //$('label[for="email"]', 'form#signup').text("OK!").removeClass('error').addClass('valid');
84 | }
85 | validated_emails[value] = response;
86 | });
87 | }
88 | }
89 | label_.addClass("valid").text(text);
90 | }//,
91 |
92 | //errorLabelContainer: "form#signup .errormsgs",
93 | //wrapper: "li"
94 |
95 |
96 | });
97 | });
98 | //var email_pattern = new RegExp(/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i);
99 | //var f2 = $('form.signup');
100 | //$('input[name="email"]', f2).change(function() {
101 | //
102 | //});
103 |
--------------------------------------------------------------------------------
/apps/emailreminders/models.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from bson.objectid import ObjectId
3 | from mongokit import ValidationError
4 | from apps.main.models import BaseDocument, register
5 |
6 |
7 | @register
8 | class EmailReminder(BaseDocument):
9 | __collection__ = 'email_reminders'
10 | structure = {
11 | 'user': ObjectId,
12 | 'weekdays': [unicode],
13 | 'time': (int, int),
14 | 'tz_offset': float,
15 | 'disabled': bool,
16 | 'include_instructions': bool,
17 | 'include_summary': bool,
18 | '_next_send_date': datetime.datetime,
19 | }
20 |
21 | MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = \
22 | (u'Monday', u'Tuesday', u'Wednesday', u'Thursday', u'Friday',
23 | u'Saturday', u'Sunday')
24 |
25 | WEEKDAYS = MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
26 |
27 | required_fields = [
28 | 'user', 'weekdays', 'time', 'tz_offset',
29 | ]
30 |
31 | default_values = {
32 | 'disabled': False,
33 | 'include_summary': False,
34 | 'include_instructions': True,
35 | 'tz_offset': 0.0,
36 | 'weekdays': [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY],
37 | }
38 |
39 | validators = {
40 | 'tz_offset': lambda x: x > -12 and x < 12, #?????
41 | 'time': lambda x: 0 <= x[0] <= 23 and 0 <= x[1] <= 59,
42 | }
43 |
44 | @property
45 | def user(self):
46 | return self.db.User.find_one({'_id': self['user']})
47 |
48 | def validate(self, *args, **kwargs):
49 | if isinstance(self['tz_offset'], int):
50 | self['tz_offset'] = float(self['tz_offset'])
51 | not_weekdays = set(self['weekdays']) - \
52 | set((self.MONDAY,
53 | self.TUESDAY,
54 | self.WEDNESDAY,
55 | self.THURSDAY,
56 | self.FRIDAY,
57 | self.SATURDAY,
58 | self.SUNDAY))
59 | if not_weekdays:
60 | raise ValidationError("Unrecognized weekdays %r" % not_weekdays)
61 |
62 | super(EmailReminder, self).validate(*args, **kwargs)
63 |
64 | def set_next_send_date(self, utcnow=None):
65 | if utcnow is None:
66 | utcnow = datetime.datetime.utcnow()
67 | _next_send_date = utcnow
68 |
69 | # first create the time with any random date
70 | start = datetime.datetime(2000,01,01, self.time[0], self.time[1], 0)
71 | result = start - datetime.timedelta(hours=self.tz_offset)
72 |
73 | h = result.hour
74 | m = result.minute
75 | if result > start:
76 | day_diff = (result - start).days
77 | else:
78 | day_diff = (start - result).days
79 |
80 | _next_send_date = datetime.datetime(_next_send_date.year,
81 | _next_send_date.month,
82 | _next_send_date.day,
83 | h, m)
84 | if day_diff:
85 | _next_send_date += datetime.timedelta(days=day_diff)
86 |
87 | assert self.weekdays
88 | # iterate until we hit the next weekday that is in this list
89 | while _next_send_date.strftime('%A') not in self.weekdays or _next_send_date <= utcnow:
90 | _next_send_date += datetime.timedelta(days=1)
91 |
92 | if day_diff:
93 | _next_send_date += datetime.timedelta(days=day_diff)
94 |
95 | assert _next_send_date > utcnow, _next_send_date
96 | self._next_send_date = _next_send_date
97 |
98 |
99 | @register
100 | class EmailReminderLog(BaseDocument):
101 | __collection__ = 'email_reminders_log'
102 | structure = {
103 | 'email_reminder': EmailReminder,
104 | 'replies': int,
105 | }
106 | # remember, every BaseDocument has add_date and modify_date
107 |
108 | default_values = {
109 | 'replies': 0,
110 | }
111 |
--------------------------------------------------------------------------------
/apps/eventlog/tests/test_handlers.py:
--------------------------------------------------------------------------------
1 | from time import mktime
2 | from bson.objectid import ObjectId
3 | import datetime
4 | from apps.main.tests.base import BaseHTTPTestCase
5 | from tornado_utils.http_test_client import TestClient
6 | from apps.eventlog.constants import *
7 |
8 | class EventLogsTestCase(BaseHTTPTestCase):
9 |
10 | def test_posting_editing_deleting_restoring(self):
11 | db = self.get_db()
12 | client = TestClient(self)
13 |
14 | today = datetime.date.today()
15 | data = {'title': "Foo",
16 | 'date': mktime(today.timetuple()),
17 | 'all_day': 'yes'}
18 | response = client.post('/events/', data)
19 | self.assertEqual(response.code, 200)
20 | event = db.Event.one()
21 |
22 | from apps.eventlog.models import EventLog
23 |
24 | self.assertEqual(db.EventLog.find().count(), 1)
25 | assert db.EventLog.one({'action': ACTION_ADD})
26 |
27 | data = {'id': str(event._id),
28 | 'all_day':'1',
29 | 'title': 'New title'}
30 | response = client.post('/event/edit/', data)
31 | event = db.Event.one()
32 | assert event.title == 'New title'
33 |
34 | self.assertEqual(db.EventLog.find().count(), 2)
35 | self.assertTrue(db.EventLog.one({'action': ACTION_EDIT}))
36 |
37 | data['days'] = '3'
38 | data['minutes'] = 0
39 | response = client.post('/event/resize/', data)
40 | self.assertEqual(response.code, 200)
41 | assert 'error' not in response.body
42 |
43 | event = db.Event.one({'_id': ObjectId(data['id'])})
44 | assert (event.end - event.start).days == 3
45 |
46 | self.assertEqual(db.EventLog.find().count(), 3)
47 | self.assertEqual(db.EventLog.find({'action': ACTION_EDIT}).count(), 2)
48 | self.assertTrue(db.EventLog.one({'comment': u'resize'}))
49 |
50 | response = client.post('/event/delete/', data)
51 | self.assertEqual(response.code, 200)
52 | assert 'error' not in response.body
53 | event = db.Event.one({'_id': ObjectId(data['id'])})
54 | assert event.user.guid == 'UNDOER'
55 |
56 | self.assertTrue(db.EventLog.one({'action': ACTION_DELETE}))
57 |
58 | response = client.post('/event/undodelete/', {'id': data['id']})
59 | self.assertEqual(response.code, 200)
60 | assert 'error' not in response.body
61 | event = db.Event.one({'_id': ObjectId(data['id'])})
62 | assert event.user.guid != 'UNDOER'
63 |
64 | self.assertTrue(db.EventLog.one({'action': ACTION_RESTORE}))
65 |
66 | def test_bookmarkleting_and_log(self):
67 | db = self.get_db()
68 |
69 | client = TestClient(self)
70 |
71 | today = datetime.date.today()
72 | data = {'title': "Foo",
73 | 'date': mktime(today.timetuple()),
74 | 'all_day': 'yes'}
75 | response = client.post('/events/', data)
76 | self.assertEqual(response.code, 200)
77 |
78 | future = datetime.datetime.now() + datetime.timedelta(hours=2)
79 | data = dict(now=mktime(future.timetuple()),
80 | title="somethign")
81 | response = client.post('/bookmarklet/', data)
82 | self.assertEqual(response.code, 200)
83 | assert db.Event.one({'title': data['title']})
84 |
85 | self.assertTrue(db.EventLog.one(
86 | {'action': ACTION_ADD, 'context': CONTEXT_BOOKMARKLET}))
87 |
88 | def test_posting_with_api(self):
89 | db = self.get_db()
90 | today = datetime.date.today()
91 |
92 | peter = self.get_db().users.User()
93 | assert peter.guid
94 | peter.save()
95 | data = dict(guid=peter.guid,
96 | title="SOmething",
97 | data=mktime(today.timetuple()))
98 | response = self.post('/api/events.json', data)
99 | self.assertEqual(response.code, 201)
100 |
101 | self.assertTrue(db.EventLog.one(
102 | {'action': ACTION_ADD, 'context': CONTEXT_API}))
103 |
--------------------------------------------------------------------------------
/utils/send_mail/backends/smtp.py:
--------------------------------------------------------------------------------
1 | """SMTP email backend class."""
2 |
3 | import smtplib
4 | import socket
5 | import threading
6 |
7 | from utils.send_mail.backends.base import BaseEmailBackend
8 | from utils.send_mail.dns_name import DNS_NAME
9 | from utils.send_mail import config
10 |
11 | class EmailBackend(BaseEmailBackend):
12 | """
13 | A wrapper that manages the SMTP network connection.
14 | """
15 | def __init__(self, host=None, port=None, username=None, password=None,
16 | use_tls=None, fail_silently=False, **kwargs):
17 | super(EmailBackend, self).__init__(fail_silently=fail_silently)
18 | self.host = host or config.EMAIL_HOST
19 | self.port = port or config.EMAIL_PORT
20 | self.username = username or config.EMAIL_HOST_USER
21 | self.password = password or config.EMAIL_HOST_PASSWORD
22 | if use_tls is None:
23 | self.use_tls = config.EMAIL_USE_TLS
24 | else:
25 | self.use_tls = use_tls
26 | self.connection = None
27 | self._lock = threading.RLock()
28 |
29 | def open(self):
30 | """
31 | Ensures we have a connection to the email server. Returns whether or
32 | not a new connection was required (True or False).
33 | """
34 | if self.connection:
35 | # Nothing to do if the connection is already open.
36 | return False
37 | try:
38 | # If local_hostname is not specified, socket.getfqdn() gets used.
39 | # For performance, we use the cached FQDN for local_hostname.
40 | self.connection = smtplib.SMTP(self.host, self.port,
41 | local_hostname=DNS_NAME.get_fqdn())
42 | if self.use_tls:
43 | self.connection.ehlo()
44 | self.connection.starttls()
45 | self.connection.ehlo()
46 | if self.username and self.password:
47 | self.connection.login(self.username, self.password)
48 | return True
49 | except:
50 | if not self.fail_silently:
51 | raise
52 |
53 | def close(self):
54 | """Closes the connection to the email server."""
55 | try:
56 | try:
57 | self.connection.quit()
58 | except socket.sslerror:
59 | # This happens when calling quit() on a TLS connection
60 | # sometimes.
61 | self.connection.close()
62 | except:
63 | if self.fail_silently:
64 | return
65 | raise
66 | finally:
67 | self.connection = None
68 |
69 | def send_messages(self, email_messages):
70 | """
71 | Sends one or more EmailMessage objects and returns the number of email
72 | messages sent.
73 | """
74 | if not email_messages:
75 | return
76 | self._lock.acquire()
77 | try:
78 | new_conn_created = self.open()
79 | if not self.connection:
80 | # We failed silently on open().
81 | # Trying to send would be pointless.
82 | return
83 | num_sent = 0
84 | for message in email_messages:
85 | sent = self._send(message)
86 | if sent:
87 | num_sent += 1
88 | if new_conn_created:
89 | self.close()
90 | finally:
91 | self._lock.release()
92 | return num_sent
93 |
94 | def _send(self, email_message):
95 | """A helper method that does the actual sending."""
96 | if not email_message.recipients():
97 | return False
98 | try:
99 | self.connection.sendmail(email_message.from_email,
100 | email_message.recipients(),
101 | email_message.message().as_string())
102 | except:
103 | if not self.fail_silently:
104 | raise
105 | return False
106 | return True
107 |
--------------------------------------------------------------------------------
Comments:
41 | {% for comment in comments %} 42 |Added by {{ comment['first_name'] }}
46 | {% end %} 47 |