├── .gitignore ├── .gitmodules ├── COMPETITION.txt ├── INSTALL.txt ├── README.md ├── TODO.txt ├── app.py ├── apps ├── __init__.py ├── emailreminders │ ├── __init__.py │ ├── handlers.py │ ├── migrations │ │ ├── 001_include_summary_and_instructions.py │ │ └── 002_emailreminders_user.py │ ├── models.py │ ├── reminder_utils.py │ ├── tests │ │ ├── 2011-02-23_003928_692010.email │ │ ├── 2011-02-23_114902_621456.email │ │ ├── 2011-03-28_170726_751460.email │ │ ├── 2011-05-17_220706_318577.email │ │ ├── 2011-09-19_090052_194569.email │ │ ├── __init__.py │ │ ├── test_handlers.py │ │ ├── test_models.py │ │ └── test_utils.py │ └── ui_modules.py ├── eventlog │ ├── __init__.py │ ├── constants.py │ ├── handlers.py │ ├── indexes.py │ ├── migrations │ │ └── 001_user_reference.py │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_handlers.py │ │ └── test_models.py │ └── ui_modules.py ├── github │ ├── __init__.py │ ├── handlers.py │ └── models.py ├── main │ ├── __init__.py │ ├── config.py │ ├── export │ │ ├── __init__.py │ │ ├── csv_export.py │ │ └── excel_export.py │ ├── handlers.py │ ├── indexes.py │ ├── migrations │ │ ├── 001_url_and_external_url.py │ │ ├── 002_settings_disable_sound.py │ │ ├── 003_offline_mode_user_settings.py │ │ ├── 004_event_description.py │ │ ├── 005_settings_hash_tags.py │ │ ├── 006_featurerequests_implemented.py │ │ ├── 007_user_premium.py │ │ ├── 008_ampm_format_settings.py │ │ ├── 009_first_hour_settings.py │ │ ├── 010_settings_newsletter_optout.py │ │ ├── 011_0second_not_all_day_events.py │ │ ├── 012_usersettings_user.py │ │ ├── 013_share_featurerequest_featureeq_comment.py │ │ └── example.py.txt │ ├── models.py │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── mock_data.py │ │ ├── test_api.py │ │ ├── test_handlers.py │ │ ├── test_models.py │ │ └── test_utils.py │ └── ui_modules.py ├── qunit │ ├── __init__.py │ └── handlers.py ├── smartphone │ ├── __init__.py │ └── handlers.py └── templates │ ├── base.html │ ├── bookmarklet │ ├── bookmarklet_base.html │ ├── index.html │ └── posted.html │ ├── calendar.html │ ├── emailreminders │ ├── base.html │ ├── index.html │ ├── send_reminder.txt │ └── show_weekday_reminders.html │ ├── event │ └── edit.html │ ├── eventlog │ ├── activity.html │ └── index.html │ ├── featurerequests │ ├── feature_request.html │ └── index.html │ ├── help │ ├── about.html │ ├── api.html │ ├── bookmarklet.html │ ├── feature-requests.html │ ├── google-calendar.html │ ├── help_base.html │ ├── index.html │ ├── internet-explorer.html │ ├── news.html │ ├── secure-passwords.html │ ├── see_also.html │ └── show_vimeo_video.html │ ├── modules │ ├── eventpreview.html │ ├── footer.html │ └── settings.html │ ├── powerusers │ ├── index.html │ ├── top-10.html │ └── users │ │ ├── bill-mitchell.html │ │ ├── jerome-ferdinands.html │ │ ├── peter-bengtsson.html │ │ └── power_user_base.html │ ├── premium │ ├── checkout.html │ └── index.html │ ├── qunit │ ├── index.html │ └── smartphone.html │ ├── report │ └── index.html │ ├── sharing │ ├── cant-share-yet.html │ └── share.html │ ├── smartphone │ ├── base.html │ ├── base.html.boilerplate │ ├── index.html │ └── logged_in.html │ ├── sound │ └── test.html │ ├── splash.html │ ├── stats │ └── index.html │ ├── testimonials.html │ └── user │ ├── account.html │ ├── change-account.html │ ├── forgotten.html │ ├── recover_forgotten.html │ ├── reset_password.txt │ ├── settings.html │ └── sharing.html ├── bin ├── README ├── _cleanup_old_combined_files.py ├── _run_coverage_tests.py ├── _run_tests.py ├── cleanup_old_combined_files.sh ├── download_static_urls.py ├── ensure_indexes.py ├── find_console.log.sh ├── here.py ├── run_coverage_tests.sh ├── run_development_server.sh ├── run_migrations.py ├── run_pyflakes.py ├── run_shell.py └── run_tests.sh ├── export_newsletter_subscribers.py ├── marketing └── generate_emails_csv.py ├── requirements.txt ├── robots.txt ├── run_cleanup_deleted_events.py ├── sendmail.py ├── settings.py ├── static ├── account.js ├── activitylog.js ├── ajaxproxy.js ├── base.js ├── bookmarklet.js ├── calendar.js ├── compiler.jar ├── css │ ├── bookmarklet.css │ ├── calendar.css │ ├── ext │ │ ├── fancybox │ │ │ ├── blank.gif │ │ │ ├── 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 │ │ │ ├── fancybox-x.png │ │ │ ├── fancybox-y.png │ │ │ └── fancybox.png │ │ ├── fullcalendar.css │ │ ├── fullcalendar.print.css │ │ ├── indicator.gif │ │ ├── jquery.autocomplete.css │ │ ├── jquery.fancybox-1.3.2.css │ │ ├── jquery.fancybox-1.3.4.css │ │ ├── jquery.jqplot.css │ │ ├── jquery.jqplot.min.css │ │ ├── jquery.mobile │ │ │ ├── images │ │ │ │ ├── ajax-loader.png │ │ │ │ ├── form-check-off.png │ │ │ │ ├── form-check-on.png │ │ │ │ ├── form-radio-off.png │ │ │ │ ├── form-radio-on.png │ │ │ │ ├── icon-search-black.png │ │ │ │ ├── icons-18-black.png │ │ │ │ ├── icons-18-white.png │ │ │ │ ├── icons-36-black.png │ │ │ │ └── icons-36-white.png │ │ │ ├── jquery.mobile-1.0a3.css │ │ │ ├── jquery.mobile-1.0a3.min.css │ │ │ ├── jquery.mobile-1.0a4.1.css │ │ │ └── jquery.mobile-1.0a4.1.min.css │ │ ├── jquery.qtip.css │ │ └── qtip │ │ │ ├── close-blue.png │ │ │ ├── close-dark.png │ │ │ ├── close-green.png │ │ │ ├── close-light.png │ │ │ ├── close-red.png │ │ │ └── close.png │ ├── featurerequests.css │ ├── icons │ │ ├── application-pdf.png │ │ ├── text-csv.png │ │ └── x-office-spreadsheet.png │ ├── logs.css │ ├── report.css │ ├── smoothness │ │ ├── images │ │ │ ├── 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 │ │ │ ├── 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 │ │ └── jquery-ui-1.8.6.slider.datepicker.css │ ├── stats.css │ └── style.css ├── emailreminders.js ├── eventlog.js ├── ext │ ├── fullcalendar.js │ ├── fullcalendar.min.js │ ├── head.load.min.js │ ├── jqplot.barRenderer.js │ ├── jqplot.barRenderer.min.js │ ├── jqplot.categoryAxisRenderer.js │ ├── jqplot.categoryAxisRenderer.min.js │ ├── jqplot.dateAxisRenderer.js │ ├── jqplot.dateAxisRenderer.min.js │ ├── jqplot.highlighter.js │ ├── jqplot.highlighter.min.js │ ├── jqplot.pieRenderer.js │ ├── jqplot.pieRenderer.min.js │ ├── jqplot.pointLabels.js │ ├── jqplot.pointLabels.min.js │ ├── jquery-1.5.2.min.js │ ├── jquery-ui-1.8.4.draggable-resizable.min.js │ ├── jquery-ui-1.8.6.slider.datepicker.min.js │ ├── jquery-ui-1.8.9.fullcalendar.min.js │ ├── jquery.autocomplete.js │ ├── jquery.autocomplete.pack.js │ ├── jquery.cookie.js │ ├── jquery.cookie.min.js │ ├── jquery.fancybox-1.3.4.js │ ├── jquery.fancybox-1.3.4.pack.js │ ├── jquery.form.js │ ├── jquery.jqplot.js │ ├── jquery.jqplot.min.js │ ├── jquery.lazy.js │ ├── jquery.mobile-1.0a4.1.js │ ├── jquery.mobile-1.0a4.1.min.js │ ├── jquery.qtip-1.0.0-rc3.js │ ├── jquery.qtip-1.0.0-rc3.min.js │ ├── jquery.qtip.js │ ├── jquery.qtip.min.js │ ├── jquery.qtip.pack.js │ ├── jquery.sparkline.min.js │ ├── jquery.store.js │ ├── jquery.ui.droppable.min.js │ ├── jquery.validate.js │ ├── jquery.validate.min.js │ └── json2.js ├── featurerequests.js ├── forgotten.js ├── images │ ├── blank_button.png │ ├── bookmarklet_close.png │ ├── calendar_home.png │ ├── favicon.ico │ ├── google_openid.png │ ├── key.gif │ ├── locked.gif │ ├── premium.png │ ├── smartphone │ │ ├── icon_114x114.png │ │ └── icon_57x57.png │ ├── splash │ │ ├── take-1.png │ │ ├── take-2.png │ │ └── take-3.png │ ├── tagging.png │ ├── thumbup.png │ ├── tiny_close.png │ ├── twitter.png │ ├── ui-bg_glass_100_fdf5ce_1x400.png │ ├── ui-bg_highlight-soft_100_eeeeee_1x100.png │ └── vimeovideo.png ├── play-sounds.js ├── qunit │ ├── qunit.css │ ├── qunit.js │ └── test_smartphone.js ├── recover.js ├── report.js ├── share.js ├── sidebar.js ├── smartphone.js ├── sounds │ ├── README.txt │ ├── add.ogg │ └── delete.ogg ├── stats.js └── yuicompressor-2.4.2.jar ├── utils ├── __init__.py ├── datatoxml.py ├── decorators.py ├── git.py ├── send_mail │ ├── __init__.py │ ├── backends │ │ ├── __init__.py │ │ ├── base.py │ │ ├── console.py │ │ ├── locmem.py │ │ └── smtp.py │ ├── config.py │ ├── dns_name.py │ ├── importlib.py │ └── send_email.py ├── truncate.py └── utils.py └── vendor └── vendor.pth /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- 1 | # perhaps more magic can be put here -------------------------------------------------------------------------------- /apps/emailreminders/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /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/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/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/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/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/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/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-09-19_090052_194569.email: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/apps/emailreminders/tests/2011-09-19_090052_194569.email -------------------------------------------------------------------------------- /apps/emailreminders/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_models import * 2 | from test_handlers import * 3 | from test_utils import * -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/eventlog/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from test_models import * 2 | from test_handlers import * 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/eventlog/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from mongokit import RequireFieldError, ValidationError 2 | import datetime 3 | import unittest 4 | from apps.main.models import Event, User 5 | from apps.eventlog import actions, contexts 6 | from apps.eventlog.models import EventLog 7 | from apps.main.tests.base import BaseModelsTestCase 8 | 9 | class ModelsTestCase(BaseModelsTestCase): 10 | def setUp(self): 11 | super(ModelsTestCase, self).setUp() 12 | self.db.connection.register([EventLog, Event, User]) 13 | 14 | def test_static_get_eventlogs_by_event(self): 15 | user = self.db.User() 16 | event = self.db.Event() 17 | event.user = user 18 | event.title = u"Something" 19 | event.start = event.end = datetime.datetime.now() 20 | event.all_day = False 21 | event.save() 22 | 23 | eventlogs = EventLog.get_eventlogs_by_event(event) 24 | self.assertEqual(eventlogs.count(), 0) 25 | 26 | eventlog1 = self.db.EventLog() 27 | eventlog1.event = event 28 | eventlog1.user = user 29 | eventlog1.action = actions.ACTION_ADD 30 | eventlog1.context = contexts.CONTEXT_CALENDAR 31 | eventlog1.save() 32 | 33 | eventlogs = EventLog.get_eventlogs_by_event(event) 34 | self.assertEqual(eventlogs.count(), 1) 35 | first = list(eventlogs)[0] 36 | self.assertEqual(first.action, actions.ACTION_ADD) 37 | 38 | # add another event and we should be able to sort them 39 | eventlog2 = self.db.EventLog() 40 | eventlog2.event = event 41 | eventlog2.user = user 42 | eventlog2.action = actions.ACTION_EDIT 43 | eventlog2.context = contexts.CONTEXT_CALENDAR 44 | eventlog2.modify_date = datetime.datetime.now() + datetime.timedelta(seconds=1) 45 | eventlog2.save() 46 | 47 | eventlogs = EventLog.get_eventlogs_by_event(event) 48 | self.assertEqual(eventlogs.count(), 2) 49 | first, second = list(eventlogs.sort('add_date', 1)) 50 | self.assertEqual(first.action, actions.ACTION_ADD) 51 | self.assertEqual(second.action, actions.ACTION_EDIT) 52 | eventlogs.rewind() 53 | second, first = list(eventlogs.sort('add_date', -1)) 54 | self.assertEqual(first.action, actions.ACTION_ADD) 55 | self.assertEqual(second.action, actions.ACTION_EDIT) 56 | 57 | # now add some other junk related to some other event 58 | event2 = self.db.Event() 59 | event2.user = user 60 | event2.title = u"Else" 61 | event2.start = event2.end = datetime.datetime.now() 62 | event2.all_day = False 63 | event2.save() 64 | 65 | 66 | eventlog1 = self.db.EventLog() 67 | eventlog1.event = event2 68 | eventlog1.user = user 69 | eventlog1.action = actions.ACTION_ADD 70 | eventlog1.context = contexts.CONTEXT_CALENDAR 71 | eventlog1.save() 72 | 73 | eventlogs = EventLog.get_eventlogs_by_event(event) 74 | self.assertEqual(eventlogs.count(), 2) 75 | 76 | eventlogs = EventLog.get_eventlogs_by_event(event2) 77 | self.assertEqual(eventlogs.count(), 1) 78 | -------------------------------------------------------------------------------- /apps/eventlog/ui_modules.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | from apps.main.models import User, Event 3 | from models import EventLog 4 | import constants 5 | 6 | class VerboseEventLog(tornado.web.UIModule): 7 | def render(self, entry, what): 8 | if what == 'action': 9 | return constants.ACTIONS_HUMAN_READABLE.get(entry.action, "*unknown*") 10 | elif what == 'date': 11 | return entry.add_date.strftime('%Y-%m-%d')# %H:%M:%S') 12 | elif what == 'comment': 13 | return entry.comment and entry.comment or u'' 14 | elif what == 'event': 15 | if isinstance(entry.event, dict): 16 | event = entry.event 17 | else: 18 | event = self._get_event(entry.event) 19 | if event: 20 | return event['title'] 21 | else: 22 | return '*deleted*' 23 | elif what == 'user': 24 | #print "ENTRY.USER" 25 | #print entry.user 26 | user = entry.user 27 | #if isinstance(entry.user, dict): 28 | # user = entry.user 29 | #else: 30 | # user = entry.user 31 | 32 | if user: 33 | return self._brief_email(user['email']) 34 | else: 35 | return '*deleted*' 36 | else: 37 | raise NotImplementedError(what) 38 | 39 | def _brief_email(self, email): 40 | return email 41 | 42 | def _get_user(self, ref): 43 | print "REF", repr(ref) 44 | 45 | return self.handler.db.User.collection.one({'_id': ref.id}) 46 | 47 | def _get_event(self, ref): 48 | return self.handler.db.Event.collection.one({'_id': ref.id}) 49 | -------------------------------------------------------------------------------- /apps/github/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/apps/github/__init__.py -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/main/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /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/main/export/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/qunit/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/smartphone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/apps/smartphone/__init__.py -------------------------------------------------------------------------------- /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 | 30 | 31 |

32 | {% end %} 33 | 34 |


35 |     36 | 37 | 38 | 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 | -------------------------------------------------------------------------------- /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/templates/emailreminders/base.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /apps/templates/emailreminders/show_weekday_reminders.html: -------------------------------------------------------------------------------- 1 | {% for reminder in reminders %} 2 | 4 | {% end %} 5 | -------------------------------------------------------------------------------- /apps/templates/event/edit.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | {% if url %} 12 |
13 | {{ url }} 14 |
15 | {% end %} 16 | 17 |
18 | 19 |
20 | {% if url %} 21 | 22 | {% else %} 23 | 24 | {% end %} 25 | 26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 |

37 | Delete? 38 | 42 |

43 |
44 | 45 | {% module xsrf_form_html() %} 46 |
47 | -------------------------------------------------------------------------------- /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/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 | 30 | {% end %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% for entry in event_logs %} 39 | 40 | {% if superuser %} 41 | 42 | {% end %} 43 | 44 | 45 | 46 | 47 | 48 | 50 | {% end %} 51 |
USERACTIONEVENTCONTEXTDATECOMMENT
{% module VerboseEventLog(entry, 'user') %}{% module VerboseEventLog(entry, 'event') %}{% module VerboseEventLog(entry, 'action') %}{{ entry.context }}{% module VerboseEventLog(entry, 'date') %}{% module VerboseEventLog(entry, 'comment') %} 49 |
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/featurerequests/feature_request.html: -------------------------------------------------------------------------------- 1 |
2 | {% if thanks_instead %} 3 |

Thanks!

4 | {% else %} 5 | {% if feature_request.implemented %} 6 | Implemented! 7 | {% else %} 8 |

Vote up

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 |
40 |

Comments:

41 | {% for comment in comments %} 42 |
43 | {% module RenderText(comment['comment']) %} 44 | {% if comment['first_name'] %} 45 | 46 | {% end %} 47 |
48 | {% end %} 49 |
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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 %} -------------------------------------------------------------------------------- /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/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 |
12 |

13 | Twitter
15 | Follow DoneCal on Twitter 17 |

18 | 19 |
20 | {% end %} 21 | -------------------------------------------------------------------------------- /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 | Tagging 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/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 | -------------------------------------------------------------------------------- /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/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/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 |
-------------------------------------------------------------------------------- /apps/templates/help/show_vimeo_video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Introduction to DoneCal.com 5 | 6 | 7 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | 26 | -------------------------------------------------------------------------------- /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/templates/modules/settings.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /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/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 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for entry in power_users %} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 61 | 64 | 65 | {% end %} 66 |
 EventsNameEmailMember sinceUsage
{{ entry['index'] }}{{ entry['count'] }}{{ entry['user'].first_name }} {{ entry['user'].last_name }}{{ entry['user'].email }}{{ entry['member_since'] }} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
Median:{{ '%.1f' % entry['stats']['median'] }}
Average:{{ '%.1f' % entry['stats']['average'] }}
60 |
62 | {{ ','.join([str(x) for x in entry['stats']['counts']]) }} 63 |
67 | 68 | 69 |
70 |

In the last days

71 |
72 | 73 | {% end %} 74 | 75 | 76 | {% block extrajs %} 77 | 82 | {% end %} 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/templates/premium/checkout.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
-------------------------------------------------------------------------------- /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 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
 FREEPREMIUM ACCOUNT
HTTPS on everything CHECK
Limits on adding eventsMax. 100 per monthNone!
Limits on shared calendarsMax. 1Unlimited!
Speed/Performance100%150%!
54 | 55 |
56 | 57 | {% for product in products %} 58 |
59 |

{{ product['description'] }}

60 | {% module CheckoutCode(product['code'], currency) %} 61 | {% if product.get('discount') %} 62 |

Discount: 63 | {{ product['discount'] }} 64 |

65 | {% end %} 66 |
67 | {% end %} 68 | 69 | 70 |
71 | 72 | 73 | 74 | {% end %} 75 | -------------------------------------------------------------------------------- /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/qunit/smartphone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QUnit Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

QUnit Test Suite

15 |

16 |
17 |

18 |
    19 |
    20 | 21 |
    22 |
    23 |

    Calendar

    24 | Home 25 |
    26 | 27 |
    28 |
      29 |
    30 |
    31 |
    32 |
    33 |
    34 |

    Loading...

    35 | Home 36 |
    37 |
    38 |
      39 |
    40 |
    41 |
    42 | 43 | 44 |
    45 |
    46 |

    Not logged in

    47 |
    48 |
    49 |
    50 |
    51 | 52 |
    53 | 54 | 55 |
    56 |
    57 | 58 | 59 |
    60 | 61 | 62 |
    63 |
    64 |
    65 |
    66 | 67 | 68 | 69 | 70 |
    71 | 72 | 73 | -------------------------------------------------------------------------------- /apps/templates/report/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/report.css") %} 5 | {% end %} 6 | 7 | 8 | {% block content_inner %} 9 | 10 |

    Full 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 | 32 | 33 | 34 | 35 | 36 |
     Days
    37 |
    38 |
    39 | 40 |
    41 | 42 |
    43 |

    Hours

    44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
     Hours
    54 |
    55 |
    56 |
    57 | 58 |
    59 | 60 | {% end %} 61 | {% block sidebar %} 62 | 63 |
    64 |

    Export options

    65 |

    66 | Download as Excel file 67 |

    68 | 69 |

    70 | Download as CSV file 71 |

    72 | 73 | 78 |
    79 | 80 | {% end %} 81 | {% block extrajs %} 82 | 100 | {% end %} 101 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 25 | more options 26 |

    27 | 28 | 52 | 53 | 54 |
    55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 11 | 12 | 13 | {% end %} 14 | 15 | 16 | {% block extrajs %} 17 | {% module Static("play-sounds.js") %} 18 | 29 | {% end %} 30 | -------------------------------------------------------------------------------- /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 | Click any day cell 15 | To get started, click on any day cell and create an event. 16 |

    17 | 18 |

    2. 19 | Type in a single line title (fully description can come later) 20 | Tag events by prefixing words with a # or a @ sign. 21 |

    22 | 23 |

    3. 24 | Once added you can move it, resize it, edit it or remove it 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 | 89 | {% end %} 90 | -------------------------------------------------------------------------------- /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/templates/user/account.html: -------------------------------------------------------------------------------- 1 | 16 | 17 |

    18 | Or just use... 19 | Google OpenID 20 |

    21 | 22 |

    23 | Forgotten your password?

    24 | 25 |
    26 |

    27 | 29 | Create a new account 30 |

    31 | 32 | 35 | 36 | 40 | 41 |
    42 | 43 |
    44 | 45 |
    46 | 47 | 48 |
    49 | 50 | 51 |
    52 | 53 | 54 | 55 |
    56 | 57 | {% module xsrf_form_html() %} 58 |
    59 | -------------------------------------------------------------------------------- /apps/templates/user/change-account.html: -------------------------------------------------------------------------------- 1 |

    Change your account

    2 | 3 |
    4 | 5 |
    6 | 7 |
    8 | 9 | 10 |
    11 | 12 | 13 |
    14 | 15 | 16 | 17 |
    18 | 19 | {% module xsrf_form_html() %} 20 |
    21 | -------------------------------------------------------------------------------- /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 |
    10 | 11 | {% if error %} 12 |

    Error: {{ error }}

    13 | {% end %} 14 | 15 |
    16 | 17 |
    18 | 19 | 20 |
    21 | 22 | {% module xsrf_form_html() %} 23 |
    24 | {% end %} 25 | 26 | {% end %} 27 | 28 | 29 | {% block extrajs %} 30 | {% module Static("forgotten.js") %} 31 | {% end %} 32 | -------------------------------------------------------------------------------- /apps/templates/user/recover_forgotten.html: -------------------------------------------------------------------------------- 1 | {% extends "../base.html" %} 2 | 3 | {% block content_inner %} 4 | 5 |

    Recover forgotten password

    6 |
    7 | 8 | {% if error %} 9 |

    Error: {{ error }}

    10 | {% end %} 11 | 12 |
    13 | 14 |
    15 | 16 | 17 |
    18 | 19 | {% module xsrf_form_html() %} 20 |
    21 | 22 | {% end %} 23 | 24 | 25 | {% block extrajs %} 26 | {% module Static("recover.js") %} 27 | {% end %} 28 | -------------------------------------------------------------------------------- /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 }} -------------------------------------------------------------------------------- /apps/templates/user/settings.html: -------------------------------------------------------------------------------- 1 | 4 | 5 |

    User settings

    6 | 40 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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)) -------------------------------------------------------------------------------- /bin/_run_coverage_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | import coverage 4 | 5 | from _run_tests import TEST_MODULES 6 | 7 | COVERAGE_MODULES = [ 8 | 'app', 9 | 'apps.main.models', 10 | 'apps.main.handlers', 11 | 'apps.main.ui_modules', 12 | #'apps.smartphone.handlers', 13 | 'utils', 14 | 'apps.emailreminders.models', 15 | 'apps.emailreminders.handlers', 16 | 'apps.emailreminders.reminder_utils', 17 | 'apps.emailreminders.ui_modules', 18 | ] 19 | 20 | def all(): 21 | return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) 22 | 23 | if __name__ == '__main__': 24 | import tornado.testing 25 | 26 | cov = coverage.coverage() 27 | cov.use_cache(0) # Do not cache any of the coverage.py stuff 28 | cov.start() 29 | 30 | try: 31 | tornado.testing.main() 32 | except SystemExit, e: 33 | if e.code: 34 | # go ahead and raise the exit :( 35 | raise 36 | 37 | cov.stop() 38 | print '' 39 | print '----------------------------------------------------------------------' 40 | print ' Unit Test Code Coverage Results' 41 | print '----------------------------------------------------------------------' 42 | 43 | # Report code coverage metrics 44 | coverage_modules = [] 45 | for module in COVERAGE_MODULES: 46 | coverage_modules.append(__import__(module, globals(), locals(), [''])) 47 | cov.report(coverage_modules, show_missing=1) 48 | cov.html_report(coverage_modules, directory='coverage_report') 49 | # Print code metrics footer 50 | print '----------------------------------------------------------------------' 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/cleanup_old_combined_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python bin/_cleanup_old_combined_files.py /tmp/combined -v -------------------------------------------------------------------------------- /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:])) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/find_console.log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | gg console.log | grep -v apply | grep -v '/ext/' 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/run_development_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./app.py --debug --dont_combine --dont_embed_static_url --logging=debug 3 | -------------------------------------------------------------------------------- /bin/run_migrations.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import datetime 3 | import re 4 | from glob import glob 5 | import os 6 | import here 7 | 8 | def main(locations, patterns): 9 | def _filter(filename): 10 | if filename.endswith('.done') and not filename.endswith('.py'): 11 | return False 12 | if not re.findall('\d+_', os.path.basename(filename)): 13 | return False 14 | if os.path.isfile(filename + '.done'): 15 | return False 16 | return True 17 | def _repr(filename): 18 | return (int(re.findall('(\d+)_', os.path.basename(filename))[0]), 19 | filename) 20 | filenames = [] 21 | for location in locations: 22 | for pattern in patterns: 23 | filenames.extend([_repr(x) for x in glob(os.path.join(location, pattern)) 24 | if _filter(x)]) 25 | filenames.sort() 26 | for __, filename in filenames: 27 | sys.path.insert(0, os.path.abspath('.')) 28 | execfile(filename) 29 | t = datetime.datetime.now() 30 | t = t.strftime('%Y/%m/%d %H:%M:%S') 31 | done_filename = filename + '.done' 32 | open(done_filename, 'w').write("%s\n" % t) 33 | print done_filename 34 | 35 | from settings import APPS 36 | locations = [os.path.join('apps', x, 'migrations') 37 | for x in APPS 38 | if os.path.isdir(os.path.join('apps', x, 'migrations'))] 39 | 40 | def run(*args): 41 | if not args: 42 | patterns = ['*.py'] 43 | else: 44 | patterns = args 45 | main(locations, patterns) 46 | return 0 47 | 48 | if __name__ == '__main__': 49 | import sys 50 | sys.exit(run(*sys.argv[1:])) 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python bin/_run_tests.py --logging=error $@ 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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:])) -------------------------------------------------------------------------------- /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:])) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/ajaxproxy.js: -------------------------------------------------------------------------------- 1 | $.storage = new $.store(); 2 | 3 | var is_offline = false; 4 | 5 | function _make_key(url, data) { 6 | var key = url; 7 | if (data) { 8 | if (typeof data !== 'string') data = $.param(data); 9 | key += (url.match(/\?/) ? "&" : "?") + data; 10 | } 11 | return key; 12 | } 13 | 14 | var old_jquery_get = $.get; 15 | $.get = function(url, data, callback, type) { 16 | //L('type', type); 17 | if (type == 'script') { 18 | return old_jquery_get(url, data, callback, type); 19 | } 20 | 21 | //L('GET url', url); 22 | if ($.isFunction(data)) { 23 | callback = data; 24 | data = null; 25 | } 26 | var key = _make_key(url, data); 27 | 28 | if (is_offline) { 29 | // get from store 30 | // 31 | var stored_response = $.storage.get(key); 32 | //var stored_callback = $.storage.get(key + "__callback"); 33 | if (stored_response !== null && callback) 34 | callback(stored_response); 35 | //else 36 | // L("callback", callback); 37 | 38 | } else { 39 | var new_callback = function(response) { 40 | //L("RESPONSE", response); 41 | $.storage.set(key, response); 42 | //$.storage.set(key+"__callback", callback); 43 | callback(response); 44 | }; 45 | return old_jquery_get(url, data, new_callback, type); 46 | } 47 | return {}; 48 | }; 49 | 50 | 51 | var old_jquery_ajax = $.ajax; 52 | $.ajax = function( s ) { 53 | if (s.type.toLowerCase() != "post") { 54 | // we only want to proxy the POSTs 55 | return old_jquery_ajax(s); 56 | } 57 | L(s.dataType); 58 | 59 | if (is_offline) { 60 | s = $.extend(true, s, $.extend(true, {}, $.ajaxSettings, s)); 61 | if ( s.data && s.processData && typeof s.data !== "string" ) 62 | s.data = $.param(s.data); 63 | 64 | if ( $.isFunction( s.data ) ) { 65 | s.callback = s.data; 66 | s.data = {}; 67 | } 68 | //L('url', s.url, 'data', s.data); 69 | //var key = _make_key(s.url, s.data); 70 | //L("Storing", key); 71 | //$.storage.set(key, {url:s.url, data:s.data}); 72 | 73 | var queue = $.storage.get(s.url); 74 | if (!$.isArray(queue)) 75 | queue = new Array(); 76 | queue.push(s.data); 77 | $.storage.set(s.url, queue); 78 | 79 | var post_queue = $.storage.get('post-queue'); 80 | if (!$.isArray(post_queue)) 81 | post_queue = new Array(); 82 | post_queue.push(s.url); 83 | $.storage.set('post-queue', post_queue); 84 | 85 | } else { 86 | if ($.storage.get('post-queue')) { 87 | L("Back online!"); 88 | // old stuff to post 89 | $.each($.storage.get('post-queue'), function(i, url) { 90 | L('URL', url); 91 | var stored_posts = $.storage.get(url); 92 | $.each(stored_posts, function(i, data) { 93 | L("DATA", data); 94 | old_jquery_ajax({url:url, data:data, callback:function() {}, type:'post'}); 95 | }); 96 | }); 97 | } 98 | old_jquery_ajax(s); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /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 | */ -------------------------------------------------------------------------------- /static/compiler.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/compiler.jar -------------------------------------------------------------------------------- /static/css/bookmarklet.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/bookmarklet.css -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/css/ext/fancybox/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/blank.gif -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_close.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_loading.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_nav_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_nav_left.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_nav_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_nav_right.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_e.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_n.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_ne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_ne.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_nw.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_s.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_se.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_sw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_sw.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_shadow_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_shadow_w.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_title_left.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_title_main.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_over.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_title_over.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancy_title_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancy_title_right.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancybox-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancybox-x.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancybox-y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancybox-y.png -------------------------------------------------------------------------------- /static/css/ext/fancybox/fancybox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/fancybox/fancybox.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/css/ext/indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/indicator.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/css/ext/jquery.mobile/images/ajax-loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/ajax-loader.png -------------------------------------------------------------------------------- /static/css/ext/jquery.mobile/images/form-check-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/form-check-off.png -------------------------------------------------------------------------------- /static/css/ext/jquery.mobile/images/form-check-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/form-check-on.png -------------------------------------------------------------------------------- /static/css/ext/jquery.mobile/images/form-radio-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/form-radio-off.png -------------------------------------------------------------------------------- /static/css/ext/jquery.mobile/images/form-radio-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/form-radio-on.png -------------------------------------------------------------------------------- /static/css/ext/jquery.mobile/images/icon-search-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/icon-search-black.png -------------------------------------------------------------------------------- /static/css/ext/jquery.mobile/images/icons-18-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/jquery.mobile/images/icons-36-white.png -------------------------------------------------------------------------------- /static/css/ext/qtip/close-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/qtip/close-blue.png -------------------------------------------------------------------------------- /static/css/ext/qtip/close-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/qtip/close-dark.png -------------------------------------------------------------------------------- /static/css/ext/qtip/close-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/qtip/close-green.png -------------------------------------------------------------------------------- /static/css/ext/qtip/close-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/qtip/close-light.png -------------------------------------------------------------------------------- /static/css/ext/qtip/close-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/qtip/close-red.png -------------------------------------------------------------------------------- /static/css/ext/qtip/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/ext/qtip/close.png -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /static/css/icons/application-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/icons/application-pdf.png -------------------------------------------------------------------------------- /static/css/icons/text-csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/icons/text-csv.png -------------------------------------------------------------------------------- /static/css/icons/x-office-spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/icons/x-office-spreadsheet.png -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/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/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /static/css/smoothness/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/css/smoothness/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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;c 1 && (value === null || typeof value !== "object")) { 15 | options = jQuery.extend({}, options); 16 | 17 | if (value === null) { 18 | options.expires = -1; 19 | } 20 | 21 | if (typeof options.expires === 'number') { 22 | var days = options.expires, t = options.expires = new Date(); 23 | t.setDate(t.getDate() + days); 24 | } 25 | 26 | return (document.cookie = [ 27 | encodeURIComponent(key), '=', 28 | options.raw ? String(value) : encodeURIComponent(String(value)), 29 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 30 | options.path ? '; path=' + options.path : '', 31 | options.domain ? '; domain=' + options.domain : '', 32 | options.secure ? '; secure' : '' 33 | ].join('')); 34 | } 35 | 36 | // key and possibly options given, get cookie... 37 | options = value || {}; 38 | var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; 39 | return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; 40 | }; 41 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/ext/jquery.jqplot.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/ext/jquery.jqplot.js -------------------------------------------------------------------------------- /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,{})) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/images/blank_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/blank_button.png -------------------------------------------------------------------------------- /static/images/bookmarklet_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/bookmarklet_close.png -------------------------------------------------------------------------------- /static/images/calendar_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/calendar_home.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/google_openid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/google_openid.png -------------------------------------------------------------------------------- /static/images/key.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/key.gif -------------------------------------------------------------------------------- /static/images/locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/locked.gif -------------------------------------------------------------------------------- /static/images/premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/premium.png -------------------------------------------------------------------------------- /static/images/smartphone/icon_114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/smartphone/icon_114x114.png -------------------------------------------------------------------------------- /static/images/smartphone/icon_57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/smartphone/icon_57x57.png -------------------------------------------------------------------------------- /static/images/splash/take-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/splash/take-1.png -------------------------------------------------------------------------------- /static/images/splash/take-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/splash/take-2.png -------------------------------------------------------------------------------- /static/images/splash/take-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/splash/take-3.png -------------------------------------------------------------------------------- /static/images/tagging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/tagging.png -------------------------------------------------------------------------------- /static/images/thumbup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/thumbup.png -------------------------------------------------------------------------------- /static/images/tiny_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/tiny_close.png -------------------------------------------------------------------------------- /static/images/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/twitter.png -------------------------------------------------------------------------------- /static/images/ui-bg_glass_100_fdf5ce_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/ui-bg_glass_100_fdf5ce_1x400.png -------------------------------------------------------------------------------- /static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png -------------------------------------------------------------------------------- /static/images/vimeovideo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/images/vimeovideo.png -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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($('
  1. ').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($('
  2. ').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 | } -------------------------------------------------------------------------------- /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/sounds/add.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/sounds/add.ogg -------------------------------------------------------------------------------- /static/sounds/delete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/sounds/delete.ogg -------------------------------------------------------------------------------- /static/yuicompressor-2.4.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterbe/worklog/2eb547fda6a6315218b645fd55c0500ccc9b5656/static/yuicompressor-2.4.2.jar -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from utils import * -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /utils/send_mail/__init__.py: -------------------------------------------------------------------------------- 1 | from send_email import send_email -------------------------------------------------------------------------------- /utils/send_mail/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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) -------------------------------------------------------------------------------- /vendor/vendor.pth: -------------------------------------------------------------------------------- 1 | src/tornado 2 | src/mongokit 3 | src/tornado-utils 4 | --------------------------------------------------------------------------------