Compare commits

...

200 Commits

Author SHA1 Message Date
d25070829d Bump up release version to 0.0.29 2016-05-20 18:49:10 +03:00
d194502b27 Merge pull request #485 from mailpoet/standard_scheduling_fix
Prevent user from manually editing Standard newsletter scheduling date
2016-05-20 15:09:04 +02:00
2cef99de2b Merge pull request #484 from mailpoet/re_adding_localestring
Re-adding toLocaleString on Segments listing
2016-05-20 08:50:02 -04:00
05d3756a1c Change readOnly field to boolean type 2016-05-20 15:27:46 +03:00
b3d9fc54fe Prevent user from manually editing standard scheduling date 2016-05-20 14:19:38 +03:00
20841eb5e8 Re-adding toLocaleString on Segments listing 2016-05-20 09:25:40 +02:00
9e23326d45 Merge pull request #483 from mailpoet/react_forms
Forms / WP Sync
2016-05-19 10:20:47 -04:00
f81d639b19 Merge pull request #482 from mailpoet/export_fix
Export fix
2016-05-19 15:49:12 +02:00
ab3e272020 extra space 2016-05-19 15:40:14 +02:00
4c265d1339 convert some React fields to ES6
- renamed empty label to placeholder
2016-05-19 15:19:31 +02:00
da08de0e74 - Fixes unit test
- Enables check for ZIP extension
2016-05-19 09:10:26 -04:00
6074aa927b Fix WP Sync
- added missing translations in listings (responsive view)
2016-05-19 14:32:40 +02:00
046127eeba fixed react forms (new bug discovered on new forms with default values not saved) 2016-05-19 14:30:33 +02:00
0c8a8c6854 Merge pull request #473 from mailpoet/standard_scheduling
Standard newsletter scheduling
2016-05-19 13:56:44 +03:00
acfcfdd730 - Refactors scheduler worker and updates logic responsible for scheduling
a new post notification queue
2016-05-18 15:40:41 -04:00
8f935df12a Merge branch 'standard_scheduling' of mailpoet:mailpoet/mailpoet into standard_scheduling 2016-05-18 14:37:11 -04:00
2784bb7282 - Adds queue rescheduling for post notifications 2016-05-18 14:33:55 -04:00
08a8ca4969 Change segments handling for notification newsletters 2016-05-18 21:27:58 +03:00
5a03eb9a17 Fix newsletter calls to use array access and not object access 2016-05-18 19:03:06 +03:00
91bb215e4d Change Date picker to use Wordpress date format 2016-05-18 18:38:59 +03:00
0b3a388a78 - Remove space after if;
- Change Scheduler::standard function name to a more descriptive one
2016-05-18 18:38:58 +03:00
66a93768e1 Remove unnecessary function calls 2016-05-18 18:14:54 +03:00
61df4899cd - Refactor WP DateTime helper
- Add a unit test helper to stub out Wordpress functions
2016-05-18 18:14:54 +03:00
f8b1e153be Make date picker translatable 2016-05-18 18:14:53 +03:00
f322433875 Add standard newsletter scheduling backend 2016-05-18 18:14:53 +03:00
be155c38bc Fix datepicker onChange handler 2016-05-18 17:51:39 +03:00
13ee338fb0 Add jQuery UI datepicker with Melon skin 2016-05-18 17:51:39 +03:00
f8628c1f4e - Add input validation
- Unmount datepicker on destruction
- Change prop propagation pipeline
2016-05-18 17:51:39 +03:00
f7c70be5eb Add standard newsletter scheduling UI 2016-05-18 17:51:39 +03:00
65df28d52e Merge pull request #478 from mailpoet/post_notification_update
Post notification update
2016-05-18 16:10:23 +02:00
3e658033da - Updates conditional check 2016-05-18 10:00:38 -04:00
31e082eb2b - Removes space between IF and statement 2016-05-18 10:00:38 -04:00
bf1ab3a593 - Removes space between (int) and variable 2016-05-18 10:00:38 -04:00
8540c51679 - Changes newsletter object to array in SendingQueue router
- Schedules post notification right after its creation
2016-05-18 10:00:09 -04:00
d2a6b6bd4e - Updates based on code review comments 2016-05-18 10:00:09 -04:00
06417c1e88 - Fix ALC post amount handling of boundary values;
- Change magic number to named constant
2016-05-18 10:00:09 -04:00
be238f4c67 - Fixes unit test
- Corrects method name in scheduler
2016-05-18 10:00:09 -04:00
343da0fdcc - Saves sent posts during rendering by sending queue worker
- Prevents empty notification emails from being sent
- Hooks to WP's post update and rewrite post notification logic
- Prevents scheduling multiple queues of the same newsletter
- Fixes issue with segments not updating when scheduling a newsletter
- Removes depreciated hash field & associated logic
2016-05-18 10:00:09 -04:00
dbb3c96300 Modify ALC to skip already sent posts 2016-05-18 10:00:09 -04:00
c8f7bea419 Let wordpress exclude already used posts in ALC selection 2016-05-18 10:00:09 -04:00
396ab50fa0 - Implements exclusion of duplicate posts from ALC 2016-05-18 10:00:09 -04:00
84e9ecef29 Merge pull request #477 from mailpoet/php7_mysql_fix
Fix for PHP7 & Mysql 5.7
2016-05-18 15:42:36 +02:00
57472d1a2e - Removes comment for an explicitly invoked condition 2016-05-18 09:39:29 -04:00
cb4f055263 - Updates export SQL to always use GROUP BY and aggregate columns 2016-05-17 11:07:12 -04:00
3f1bdd2c59 - Removed debug function
- Corrected comment
2016-05-17 10:01:06 -04:00
a6802a1925 - Fixes GROUP BY when using non-aggregate columns 2016-05-17 09:54:58 -04:00
a02b2d3aa0 - Updates migrator/schema to work with MySQL 5.7
- Fixes unit tests
- Fixes export's SQL query to work with strict ONLY_FULL_GROUP_BY option
2016-05-16 22:26:21 -04:00
607a151c23 Fix for PHP7 & Mysql 5.7
- added default values to not null columns
- fixed passing by reference issue on MailChimp->getDataCenter
- fixed a couple unit tests
2016-05-16 14:50:43 +02:00
c8748e5e6e Merge pull request #475 from mailpoet/uniform_listings
bulk actions / pagination and customizable limit per page for all
2016-05-16 13:37:14 +03:00
6e45a8e788 all listings have bulk actions / pagination and customizable limit per page 2016-05-13 16:53:48 +02:00
395e95bc54 Bump up release version to 0.0.28 2016-05-13 16:18:01 +03:00
a8309b436d Merge pull request #474 from mailpoet/UI_updates
Ui updates
2016-05-13 16:14:23 +03:00
77fe385645 - Disables input fields if subscriber is a WP user. #421 (5)
- Removes 'unconfirmed' status if subscriber is a WP user. #421 (9)
- Displays notice if subscriber is a WP user. #421 (6)
2016-05-12 19:38:34 -04:00
290f749220 - Renames "Update" link to "Force Sync" #421 (12)
- Adds new link "Read more" #421 (13)
2016-05-12 11:48:33 -04:00
8cb5b3729d Merge pull request #472 from mailpoet/UI_updates
Expands MailPoet admin menu for sub-subpages
2016-05-12 17:40:04 +03:00
ce4a3196ee - Highlight main MailPoet menu
- Expands menu for newsletter editor
2016-05-12 10:37:56 -04:00
40ea5a0b84 - Improves menu expansion code
::
2016-05-12 09:59:05 -04:00
af95380a62 - Expands MailPoet admin menu for sub-subpages. #431 2016-05-12 09:10:35 -04:00
483eaffc0e Merge pull request #471 from mailpoet/import_update
Import udate
2016-05-12 12:40:23 +03:00
7e86ee3266 Merge pull request #470 from mailpoet/UI_updates
Various UI fixes
2016-05-12 12:15:52 +03:00
65630e6726 - Formats numbers in export 2016-05-11 15:47:47 -04:00
23682011af - Prevents WP user's first/last name to be updated during import
- Resets "next step" button state when going to step 3 of import
- Closes #469
2016-05-11 14:44:29 -04:00
00eaa768a6 - Updates Segment model to return subscriber count only if subscriber's
status is "subscribed"
2016-05-11 13:06:26 -04:00
bc92d9a61e - Updates all datetime references to use WordPress format
- Removes depreciated datetime conversion method from Helpers
- Fixes translation & error display issue in import
Closes #432
2016-05-11 10:56:34 -04:00
a1d8dec047 - Properly formats subscriber count during import 2016-05-11 10:18:59 -04:00
ff030068b0 Merge pull request #468 from mailpoet/UI_updates
Various UI fixes
2016-05-11 16:39:28 +03:00
79e37018c9 - Fixes unit test
- Updates Settings page heading to h1
2016-05-11 08:56:37 -04:00
7ae8248339 Merge branch 'UI_updates' of mailpoet:mailpoet/mailpoet into UI_updates 2016-05-11 08:52:38 -04:00
30720975ab - Disables "confirm unconfirmed" batch action as per #432 2016-05-11 08:52:13 -04:00
b2359258d4 Update newsletter sending process to format numbers >1000 2016-05-11 11:47:23 +03:00
1bd7639cc2 - Formats all numbers >1000 to use comma
- Removes subscriber count from segments if its === 0
Fixes #431
2016-05-10 20:09:02 -04:00
2dab89135f - Sets MailPoet notice timeout to 5sec
- Removes custom timeout from import
2016-05-10 18:59:31 -04:00
c3368d69fd - Updates Segment model to return ASC sorted results
- Updates Subscriber listing to display segmnets with count
2016-05-10 18:37:37 -04:00
751d8e7852 - Adds missing page titles 2016-05-10 13:01:40 -04:00
8a4dec08e1 - Converts page title headings to H1
- Adds "back to list" to subpages
2016-05-10 10:13:57 -04:00
8b9f5ad5b1 Merge pull request #467 from mailpoet/cron_fix
- Corrects the daemon stop status
2016-05-10 15:51:53 +03:00
fc597a53bb - Corrects the daemon stop status 2016-05-09 20:28:02 -04:00
2f25dc6a20 Bump up release version to 0.0.27 2016-05-06 14:46:12 +03:00
fc38ee2f08 Merge pull request #466 from mailpoet/rendering_engine_update
Fixes empty paragraph spacing
2016-05-05 13:04:26 +03:00
33bebc6629 - Replaces empty paragraphs with single space according to specific
condition
2016-05-04 19:57:10 -04:00
14a8e02f99 Merge pull request #465 from mailpoet/browser_preview
Newsletter preview
2016-05-04 17:58:41 +03:00
0bf6c87ec7 - Fixes shortcode category name for view in browser url
- Updates shortcode regex
2016-05-04 10:45:30 -04:00
422fba2835 - Updates based on code review comments 2016-05-03 18:41:57 -04:00
f36dbb78e6 - Updates unit test 2016-05-02 13:44:36 -04:00
3213dd0d08 - Removes unused exception handlers 2016-05-02 13:19:10 -04:00
3f2199fd63 - Updates link tracking to use the same Public API data format as other
endpoints
2016-05-02 13:15:48 -04:00
a4477a9bd6 - Updates CONST values 2016-05-02 12:27:00 -04:00
52790d7bd6 - Updates code comment 2016-05-02 12:24:56 -04:00
0b9812210f - Updates CONST values 2016-05-02 11:30:37 -04:00
756dbb4641 - Updates daemon status to CONSTs
- Uncomments temporary commented out code
- Removes unnecessary exception handles in Public API
2016-05-02 10:32:34 -04:00
b38742ddc0 - Changes divider to CONST 2016-05-02 10:02:22 -04:00
49e38549ec - Fixes URLs not being properly generated when tracking is enabled 2016-05-02 10:00:35 -04:00
afcb0a0d7f - Upperases all constants 2016-05-02 09:47:54 -04:00
d18f0e50b5 - Removes debug backtrace
- Removes uncommented queue save
2016-05-02 09:23:37 -04:00
6868a07ead - Moves link saving logic into Links class 2016-05-01 21:55:24 -04:00
cca76d0d97 - Adds custom shortcode processing logic
- Updates shortcode categories code
- Completely rewrites shortcodes unit tess
2016-05-01 14:07:06 -04:00
70fa77d333 - Creates temporary folder if it doesn't exist 2016-04-30 22:38:55 -04:00
412201d965 - Updates unit test 2016-04-30 22:38:39 -04:00
045a92c7d6 - Standardizes variable names 2016-04-30 22:21:03 -04:00
2ba2e3eca5 - Refactors sending queue worker 2016-04-30 22:19:59 -04:00
90c294f60e - Updates editor/router to use the new browser preview class 2016-04-30 22:19:59 -04:00
57b953dd14 - Rewrites shortcode processing class to work with other changes 2016-04-30 22:19:59 -04:00
0f81a8db60 - Updates method to utilize constant value 2016-04-30 22:19:59 -04:00
2d6971f8df - Refactors link tracking class 2016-04-30 22:19:59 -04:00
0abe8b5371 - Implements view in browser 2016-04-30 22:19:59 -04:00
5bad682879 - Merges all shortcode links under one category
- Adds filter hooks for custom links
- Renames old shortcodes in the editor and elsewhere
2016-04-30 22:19:59 -04:00
fa9d32c230 Bump up release version to 0.0.26 2016-04-29 21:09:41 +03:00
1b3ceca7b2 Merge pull request #464 from mailpoet/undo_go_back
remove attempt at 'goback' after editing an entity
2016-04-29 21:07:10 +03:00
9be326b45d remove attempt at 'goback' after editing an entity 2016-04-29 18:35:52 +02:00
2d2e1298c4 Merge pull request #462 from mailpoet/segments_listing
React components update + bugfixes
2016-04-29 14:18:05 +03:00
5dc3a4386e Merge pull request #463 from mailpoet/queue_processed_date_fix
Updates queue processed date to account for time offset
2016-04-29 12:16:51 +03:00
b617dde266 - Updates queue processed date to account for time offset 2016-04-28 12:42:16 -04:00
962e91f9dc fixed merge fuck 2016-04-28 17:15:07 +02:00
4047b41a7f Updated all React components to their latest version
- updated code due to deprecated warnings (mostly router stuff & input default value)
- set default sender based on settings when creating new newsletter
- fixed erroneous UTC offset when displaying dates (PHP takes care of it)
2016-04-28 17:02:24 +02:00
59199140bf Merge pull request #448 from mailpoet/twig_template_cache
modified temp path/url for cache/export folders
2016-04-28 10:18:53 -04:00
1079c0beae Merge pull request #458 from mailpoet/click_rate_update
fixes click rate
2016-04-28 17:01:05 +03:00
1694baab7d Merge branch 'click_rate_update' of mailpoet:mailpoet/mailpoet into click_rate_update 2016-04-28 09:37:04 -04:00
335ac9c778 - Updates statistics to display only 1 click event from a given subscriber
Fixes #417 (7)
2016-04-28 09:34:59 -04:00
65efd234f9 - Fixes duplicate status for different newsletters
- Updates statistics not to display NaN values
2016-04-28 09:25:09 -04:00
f726d943db Merge pull request #460 from mailpoet/text_rendering_fix
Text rendering fix
2016-04-28 12:55:08 +03:00
2d1e950097 Merge pull request #459 from mailpoet/scheduled_status
Updates newsletter listing status
2016-04-28 12:52:46 +03:00
ca2f16970b - Fixes and issue with lists not retaining links and other HTML content 2016-04-27 21:22:39 -04:00
4a123f8fe9 - Displays proper status for scheduled newsletters 2016-04-27 20:59:04 -04:00
7b224328e1 - Updates statistics to display only 1 click event from a given subscriber
Fixes #417 (7)
2016-04-27 20:55:19 -04:00
699dfa19f0 - Removes leftover console command 2016-04-27 20:14:06 -04:00
06d56fe19d - Adds "scheduled for" status for notifications
- Adds post notification/welcome activation message
- Implements schedule update when editing newsletter
Implements #405 (3 & 7)
2016-04-27 20:08:45 -04:00
1b7ac62b5c Merge pull request #453 from mailpoet/queue_statistics
Send statistics
2016-04-27 21:28:42 +03:00
ae358ce13e - Updates statistics SQL query
- Fixes tests + expands coverage
- Removes leftover console command
2016-04-27 21:26:47 +03:00
18f35b5e91 - Displays statistics in newsletter listing 2016-04-27 21:26:47 +03:00
67ca305b7f - Adds withStatistics method to the Newsletter model
- Updates Newsletter router to return statistics
- Returns only completed/running queues
2016-04-27 21:26:47 +03:00
cb0908fc70 Merge pull request #454 from mailpoet/post_title_shortcode_fix
Updates newsletter post title shortcode
2016-04-27 21:24:34 +03:00
42df59076d Merge pull request #457 from mailpoet/forms_listing
Forms listing
2016-04-27 15:15:53 +03:00
ff8be3bdc6 Merge pull request #456 from mailpoet/manage_subscription_custom_pages
don't enforce our content on custom subscription pages
2016-04-27 15:03:27 +03:00
f7b6dcf409 - Remove bulk actions from forms
- form name is now a link to the edit form page
- added "row-title" class so that the name is "bigger"
2016-04-27 13:31:30 +02:00
940f3848dd Merge pull request #452 from mailpoet/form_stats
Form subscription stats
2016-04-27 14:14:42 +03:00
e847ad2df2 don't enforce our content on custom subscription pages 2016-04-27 11:13:36 +02:00
2459a103fd - Ignores sticky posts when fetching latest post title
- Updates unit test
2016-04-26 19:53:23 -04:00
0dd3f2178e updated form stats to record individual subscription 2016-04-26 18:12:05 +02:00
de873eca71 Unit test for StatisticsForms model
- improved incrementation of subscriptions count so that we don't need to fetch the record after an update
2016-04-26 15:38:42 +02:00
ef461da77f Statistics for Form Subscriptions
- added statistics_forms table
- added corresponding model to record stats
- record stats whenever someone subscribes via a form
2016-04-26 15:16:37 +02:00
caf6dcddfa Merge pull request #451 from mailpoet/react_select_fix
Subscribers bulk actions with an extra select
2016-04-26 14:00:32 +03:00
9684c88651 refactor 2016-04-26 12:43:48 +02:00
256bca8ed9 Fix selection.jsx by adding a check before trying to destroy the select2 instance 2016-04-26 12:19:39 +02:00
8d56e8582f Merge pull request #450 from mailpoet/unsubscribe_tracking
Implements tracking of unsubscribe events
2016-04-25 21:32:10 +03:00
56959f2f49 Terminate execution after redirecting a click 2016-04-25 20:33:17 +03:00
bbc7de6898 - Removes commit leftovers 2016-04-25 12:02:56 -04:00
0df246da15 Merge pull request #447 from mailpoet/tracking_fix
Fixes click tracking that would end request when calling open tracking
2016-04-25 18:37:15 +03:00
757e18355d Merge pull request #446 from mailpoet/welcome_email_fix
welcome email fix
2016-04-25 18:36:57 +03:00
4b29b04bd1 - Implements tracking of unsubscribe events 2016-04-25 11:06:01 -04:00
6cb94bc413 - Updates open rate tracking class constructor 2016-04-25 09:43:39 -04:00
f624c891ab - Capitalizes CONST values 2016-04-25 09:42:02 -04:00
b83abf0ac5 modified temp path/url 2016-04-25 14:26:27 +02:00
ef1b0036e5 - Fixes click tracking that would end request when calling open tracking 2016-04-22 20:30:16 -04:00
5efa9f65c6 - Fixes an issue with welcome email not being sent to WP user with any
role
2016-04-22 18:51:51 -04:00
a8e3dd424e Bump up release version to 0.0.25 2016-04-22 23:25:28 +03:00
26628ba156 Merge pull request #445 from mailpoet/sending_fix
Fixes check for changed newsletter content
2016-04-22 23:22:30 +03:00
78cabde9e1 Select an existing segment as initial value for Welcome segment select 2016-04-22 16:08:45 -04:00
1ec0372c2d - Fixes check for changed newsletter content 2016-04-22 16:08:45 -04:00
31e2d5e771 Merge pull request #444 from mailpoet/fix_router
fixed Router when data represents a single variable (not an array)
2016-04-22 18:05:38 +03:00
d2dbf86a9c fixed Router when data represents a single variable (not an array) 2016-04-22 16:59:28 +02:00
1c39d39078 Merge pull request #442 from mailpoet/sending_subscriber_recalculation
Dynamically updates queue's subscriber count
2016-04-22 17:03:09 +03:00
6d0795abba Merge pull request #438 from mailpoet/non_ajax_subscription
Non ajax subscription
2016-04-22 09:51:47 -04:00
5f45c6cc74 - Extracts subscriber recalculation into a spearate method
- Updates subscriber recalculation logic
2016-04-22 09:47:53 -04:00
d4d806e247 Merge pull request #443 from mailpoet/editor_rendering
Editor rendering update
2016-04-22 15:38:54 +02:00
758a545eb6 Merge pull request #441 from mailpoet/shortcodes_fix
Updates the newsletter shortcode
2016-04-22 13:45:15 +03:00
578088d2e5 Add fallback fonts for all editor fonts 2016-04-22 12:38:59 +03:00
e17dba2b07 Add blockquote bottom margin 2016-04-22 12:30:57 +03:00
7e5cf533f0 Fixes class name 2016-04-22 12:30:57 +03:00
f7c656aed5 Fixes blockquote content duplication 2016-04-22 12:30:57 +03:00
2e42305710 Updates line breaks in footer/header to match the editor 2016-04-22 12:30:57 +03:00
b3e310652e Remove bottom margin from paragraphs 2016-04-22 12:30:57 +03:00
6737158130 - Change paragraph bottom margin to equal line-height
- Remove horizontal blockquote margin to match Renderer;
- Removed bottom padding from last text elements in column;
- Removed bottom padding from full width images;
2016-04-22 12:30:57 +03:00
906558a772 Apply paragraph styles to LI tags 2016-04-22 12:30:57 +03:00
d559483c7b - Add bottom margin to headings
- Fix top and bottom margin for UL tags
- Add bottom margin to LI tags
2016-04-22 12:30:57 +03:00
bec3e02285 Remove vertical button padding, following renderer 2016-04-22 12:30:57 +03:00
9d6d72dd8c Use fallback font stack for Lucida font 2016-04-22 12:30:57 +03:00
3b76f838d1 Merge pull request #440 from mailpoet/sending_fix
Adds condition that prevents duplicate and/or stuck post notifications
2016-04-22 11:32:14 +03:00
909ad86e33 - Recalculates the number of processed/to process subscribers if the
original count does not the current count from the database
2016-04-21 21:36:37 -04:00
dd8f58e35e - Fixes incorrect newsletter issue number display
- Updates newsletter 'last post title' logic
- Updates shortcode extraction regex to allow limiting extraction of only
  specific categories
- Updates unit tests
- Closes #380
2016-04-21 20:04:02 -04:00
4b2061fcfa - Adds condition that prevents duplicate and/or stuck post notifications 2016-04-21 18:20:13 -04:00
8e6ca502b3 Merge pull request #439 from mailpoet/sending_fix
Sending fix
2016-04-21 19:05:49 +03:00
ef85834db5 - Removes debugging function 2016-04-21 11:32:29 -04:00
2bdcd0eb42 - Prevents duplicate newsletters from being sent 2016-04-21 10:45:00 -04:00
3b90be4122 - Fixes scheduling of notifications 2016-04-21 10:44:43 -04:00
375e70d84b success messages in both context when subscribing to a form 2016-04-21 12:39:14 +02:00
42d586610e form subscription only using Router now 2016-04-20 16:59:18 +02:00
e4a5438512 Merge pull request #435 from mailpoet/open_tracking
Implements open tracking
2016-04-20 14:32:20 +03:00
11b22fa63a - Enables newsletter open tracking when links are clicked 2016-04-19 21:01:27 -04:00
45b933d635 - Implements open tracking 2016-04-19 21:01:27 -04:00
15cf087d40 - Implements post-processing filter during rendering 2016-04-19 21:01:27 -04:00
ed09c3e5d4 Merge pull request #427 from mailpoet/batch_sending_fix
Adds missing rendered subject to the queue during bulk processing
2016-04-18 18:27:24 +03:00
285a556f21 Merge pull request #426 from mailpoet/subscriber_selection_update
Updates subscriber selection logic
2016-04-18 17:41:03 +03:00
dc1ef2af47 - Removes check for signup confirmation when selecting subscribers 2016-04-18 17:33:08 +03:00
6f17f0d2d1 Merge branch 'subscriber_selection_update' 2016-04-18 17:28:53 +03:00
91076580ef - Enables check for subscribers who has "subscribed" status during
newsletter sending
- Enforces "signup_confirmation" option when selecting subscribers for
  newsletter sending
- Updates unit tests
2016-04-18 17:28:37 +03:00
d4abaa7150 Merge branch 'link_tracking_update' 2016-04-18 16:57:56 +03:00
d2bc0fd24a - Removes check for signup confirmation when selecting subscribers 2016-04-18 08:45:38 -04:00
1f26079b5f - Removes unused properties from Links class and converts methods to
static
2016-04-16 16:48:35 -04:00
87a1211a55 - Adds missing rendered subject to the queue during bulk processing 2016-04-15 20:06:35 -04:00
0ca0a7d029 - Enables check for subscribers who has "subscribed" status during
newsletter sending
- Enforces "signup_confirmation" option when selecting subscribers for
  newsletter sending
- Updates unit tests
2016-04-15 20:02:44 -04:00
185 changed files with 4386 additions and 1653 deletions

View File

@ -1,6 +1,7 @@
@import 'nib'
@require 'select2/dist/css/select2.css'
@require 'datepicker/datepicker'
@require 'common'
@require 'modal'
@ -18,4 +19,4 @@
@require 'settings'
@require 'progress_bar'
@require 'subscribers'
@require 'subscribers'

View File

@ -0,0 +1,2 @@
@require 'jquery-ui-1.10.1.css'
@require 'melon.datepicker.css'

View File

@ -0,0 +1,649 @@
/*! jQuery UI - v1.10.1 - 2013-03-10
* http://jqueryui.com
* Includes: jquery.ui.core.css, jquery.ui.datepicker.css
* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
* Copyright (c) 2013 jQuery Foundation and other contributors Licensed MIT */
/* Layout helpers
----------------------------------*/
.ui-helper-hidden {
display: none;
}
.ui-helper-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.ui-helper-reset {
margin: 0;
padding: 0;
border: 0;
outline: 0;
line-height: 1.3;
text-decoration: none;
font-size: 100%;
list-style: none;
}
.ui-helper-clearfix:before,
.ui-helper-clearfix:after {
content: "";
display: table;
border-collapse: collapse;
}
.ui-helper-clearfix:after {
clear: both;
}
.ui-helper-clearfix {
min-height: 0; /* support: IE7 */
}
.ui-helper-zfix {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
opacity: 0;
filter:Alpha(Opacity=0);
}
.ui-front {
z-index: 100;
}
/* Interaction Cues
----------------------------------*/
.ui-state-disabled {
cursor: default !important;
}
/* Icons
----------------------------------*/
/* states and images */
.ui-icon {
display: block;
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
}
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ui-datepicker {
width: 17em;
padding: .2em .2em 0;
display: none;
}
.ui-datepicker .ui-datepicker-header {
position: relative;
padding: .2em 0;
}
.ui-datepicker .ui-datepicker-prev,
.ui-datepicker .ui-datepicker-next {
position: absolute;
top: 2px;
width: 1.8em;
height: 1.8em;
}
.ui-datepicker .ui-datepicker-prev-hover,
.ui-datepicker .ui-datepicker-next-hover {
top: 1px;
}
.ui-datepicker .ui-datepicker-prev {
left: 2px;
}
.ui-datepicker .ui-datepicker-next {
right: 2px;
}
.ui-datepicker .ui-datepicker-prev-hover {
left: 1px;
}
.ui-datepicker .ui-datepicker-next-hover {
right: 1px;
}
.ui-datepicker .ui-datepicker-prev span,
.ui-datepicker .ui-datepicker-next span {
display: block;
position: absolute;
left: 50%;
margin-left: -8px;
top: 50%;
margin-top: -8px;
}
.ui-datepicker .ui-datepicker-title {
margin: 0 2.3em;
line-height: 1.8em;
text-align: center;
}
.ui-datepicker .ui-datepicker-title select {
font-size: 1em;
margin: 1px 0;
}
.ui-datepicker select.ui-datepicker-month-year {
width: 100%;
}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year {
width: 49%;
}
.ui-datepicker table {
width: 100%;
font-size: .9em;
border-collapse: collapse;
margin: 0 0 .4em;
}
.ui-datepicker th {
padding: .7em .3em;
text-align: center;
font-weight: bold;
border: 0;
}
.ui-datepicker td {
border: 0;
padding: 1px;
}
.ui-datepicker td span,
.ui-datepicker td a {
display: block;
padding: .2em;
text-align: right;
text-decoration: none;
}
.ui-datepicker .ui-datepicker-buttonpane {
background-image: none;
margin: .7em 0 0 0;
padding: 0 .2em;
border-left: 0;
border-right: 0;
border-bottom: 0;
}
.ui-datepicker .ui-datepicker-buttonpane button {
float: right;
margin: .5em .2em .4em;
cursor: pointer;
padding: .2em .6em .3em .6em;
width: auto;
overflow: visible;
}
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
float: left;
}
/* with multiple calendars */
.ui-datepicker.ui-datepicker-multi {
width: auto;
}
.ui-datepicker-multi .ui-datepicker-group {
float: left;
}
.ui-datepicker-multi .ui-datepicker-group table {
width: 95%;
margin: 0 auto .4em;
}
.ui-datepicker-multi-2 .ui-datepicker-group {
width: 50%;
}
.ui-datepicker-multi-3 .ui-datepicker-group {
width: 33.3%;
}
.ui-datepicker-multi-4 .ui-datepicker-group {
width: 25%;
}
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
border-left-width: 0;
}
.ui-datepicker-multi .ui-datepicker-buttonpane {
clear: left;
}
.ui-datepicker-row-break {
clear: both;
width: 100%;
font-size: 0;
}
/* RTL support */
.ui-datepicker-rtl {
direction: rtl;
}
.ui-datepicker-rtl .ui-datepicker-prev {
right: 2px;
left: auto;
}
.ui-datepicker-rtl .ui-datepicker-next {
left: 2px;
right: auto;
}
.ui-datepicker-rtl .ui-datepicker-prev:hover {
right: 1px;
left: auto;
}
.ui-datepicker-rtl .ui-datepicker-next:hover {
left: 1px;
right: auto;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane {
clear: right;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane button {
float: left;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
.ui-datepicker-rtl .ui-datepicker-group {
float: right;
}
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
border-right-width: 0;
border-left-width: 1px;
}
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
font-size: 1.1em;
}
.ui-widget .ui-widget {
font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;
font-size: 1em;
}
.ui-widget-content {
border: 1px solid #dddddd;
background: #eeeeee url(../img/datepicker/ui-bg_highlight-soft_100_eeeeee_1x100.png) 50% top repeat-x;
color: #333333;
}
.ui-widget-content a {
color: #333333;
}
.ui-widget-header {
border: 1px solid #e78f08;
background: #f6a828 url(../img/datepicker/ui-bg_gloss-wave_35_f6a828_500x100.png) 50% 50% repeat-x;
color: #ffffff;
font-weight: bold;
}
.ui-widget-header a {
color: #ffffff;
}
/* Interaction states
----------------------------------*/
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default {
border: 1px solid #cccccc;
background: #f6f6f6 url(../img/datepicker/ui-bg_glass_100_f6f6f6_1x400.png) 50% 50% repeat-x;
font-weight: bold;
color: #1c94c4;
}
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited {
color: #1c94c4;
text-decoration: none;
}
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
.ui-state-focus,
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus {
border: 1px solid #fbcb09;
background: #fdf5ce url(../img/datepicker/ui-bg_glass_100_fdf5ce_1x400.png) 50% 50% repeat-x;
font-weight: bold;
color: #c77405;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited {
color: #c77405;
text-decoration: none;
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active {
border: 1px solid #fbd850;
background: #ffffff url(../img/datepicker/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;
font-weight: bold;
color: #eb8f00;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
color: #eb8f00;
text-decoration: none;
}
/* Interaction Cues
----------------------------------*/
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
border: 1px solid #fed22f;
background: #ffe45c url(../img/datepicker/ui-bg_highlight-soft_75_ffe45c_1x100.png) 50% top repeat-x;
color: #363636;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
color: #363636;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
border: 1px solid #cd0a0a;
background: #b81900 url(../img/datepicker/ui-bg_diagonals-thick_18_b81900_40x40.png) 50% 50% repeat;
color: #ffffff;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
color: #ffffff;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
color: #ffffff;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
opacity: .7;
filter:Alpha(Opacity=70);
font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
opacity: .35;
filter:Alpha(Opacity=35);
background-image: none;
}
.ui-state-disabled .ui-icon {
filter:Alpha(Opacity=35); /* For IE8 - See #6059 */
}
/* Icons
----------------------------------*/
/* states and images */
.ui-icon {
width: 16px;
height: 16px;
background-position: 16px 16px;
}
.ui-icon,
.ui-widget-content .ui-icon {
background-image: url(../img/datepicker/ui-icons_222222_256x240.png);
}
.ui-widget-header .ui-icon {
background-image: url(../img/datepicker/ui-icons_ffffff_256x240.png);
}
.ui-state-default .ui-icon {
background-image: url(../img/datepicker/ui-icons_ef8c08_256x240.png);
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon {
background-image: url(../img/datepicker/ui-icons_ef8c08_256x240.png);
}
.ui-state-active .ui-icon {
background-image: url(../img/datepicker/ui-icons_ef8c08_256x240.png);
}
.ui-state-highlight .ui-icon {
background-image: url(../img/datepicker/ui-icons_228ef1_256x240.png);
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
background-image: url(../img/datepicker/ui-icons_ffd27a_256x240.png);
}
/* positioning */
.ui-icon-carat-1-n { background-position: 0 0; }
.ui-icon-carat-1-ne { background-position: -16px 0; }
.ui-icon-carat-1-e { background-position: -32px 0; }
.ui-icon-carat-1-se { background-position: -48px 0; }
.ui-icon-carat-1-s { background-position: -64px 0; }
.ui-icon-carat-1-sw { background-position: -80px 0; }
.ui-icon-carat-1-w { background-position: -96px 0; }
.ui-icon-carat-1-nw { background-position: -112px 0; }
.ui-icon-carat-2-n-s { background-position: -128px 0; }
.ui-icon-carat-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -64px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -64px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
border-top-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
border-top-right-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
border-bottom-left-radius: 4px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
border-bottom-right-radius: 4px;
}
/* Overlays */
.ui-widget-overlay {
background: #666666 url(../img/datepicker/ui-bg_diagonals-thick_20_666666_40x40.png) 50% 50% repeat;
opacity: .5;
filter: Alpha(Opacity=50);
}
.ui-widget-shadow {
margin: -5px 0 0 -5px;
padding: 5px;
background: #000000 url(../img/datepicker/ui-bg_flat_10_000000_40x100.png) 50% 50% repeat-x;
opacity: .2;
filter: Alpha(Opacity=20);
border-radius: 5px;
}

View File

@ -0,0 +1,115 @@
/**
* Melon skin from: https://github.com/rtsinani/jquery-datepicker-skins
*/
.wp-admin {
font-size: 90%;
}
.wp-admin .ui-widget {
font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
background: #2e3641;
border: none;
border-radius: 0;
-webkit-border-radius: 0;
-moz-border-radius: 0;
}
.wp-admin .ui-datepicker {
padding: 0;
}
.wp-admin .ui-datepicker-header {
border: none;
background: transparent;
font-weight: normal;
font-size: 15px;
}
.wp-admin .ui-datepicker-header .ui-state-hover {
background: transparent;
border-color: transparent;
cursor: pointer;
border-radius: 0;
-webkit-border-radius: 0;
-moz-border-radius: 0;
}
.wp-admin .ui-datepicker .ui-datepicker-title {
margin-top: .4em;
margin-bottom: .3em;
color: #e9f0f4;
}
.wp-admin .ui-datepicker .ui-datepicker-prev-hover,
.wp-admin .ui-datepicker .ui-datepicker-next-hover,
.wp-admin .ui-datepicker .ui-datepicker-next,
.wp-admin .ui-datepicker .ui-datepicker-prev {
top: .9em;
border:none;
}
.wp-admin .ui-datepicker .ui-datepicker-prev-hover {
left: 2px;
}
.wp-admin .ui-datepicker .ui-datepicker-next-hover {
right: 2px;
}
.wp-admin .ui-datepicker .ui-datepicker-next span,
.wp-admin .ui-datepicker .ui-datepicker-prev span {
background-image: url(../img/datepicker/ui-icons_ffffff_256x240.png);
background-position: -32px 0;
margin-top: 0;
top: 0;
font-weight: normal;
}
.wp-admin .ui-datepicker .ui-datepicker-prev span {
background-position: -96px 0;
}
.wp-admin .ui-datepicker table {
margin: 0;
}
.wp-admin .ui-datepicker th {
padding: 1em 0;
color: #ccc;
font-size: 13px;
font-weight: normal;
border: none;
border-top: 1px solid #3a414d;
}
.wp-admin .ui-datepicker td {
background: #f97e76;
border: none;
padding: 0;
}
.wp-admin td .ui-state-default {
background: transparent;
border: none;
text-align: center;
padding: .5em;
margin: 0;
font-weight: normal;
color: #efefef;
font-size: 16px;
}
.wp-admin .ui-state-disabled {
opacity: 1;
}
.wp-admin .ui-state-disabled .ui-state-default {
color: #fba49e;
}
.wp-admin td .ui-state-active,
.wp-admin td .ui-state-hover {
background: #2e3641;
}

View File

@ -1,2 +1,5 @@
.mailpoet_form
margin: 0 0 20px 0
.mailpoet_form td
vertical-align: top !important
vertical-align: top !important

View File

@ -1,4 +1,5 @@
$block-hover-highlight-color = $primary-active-color
$block-text-line-height = $text-line-height
.mailpoet_block
box-sizing: border-box
@ -30,8 +31,34 @@ $block-hover-highlight-color = $primary-active-color
.mailpoet_content
position: relative
line-height: 1.61803398875
line-height: $block-text-line-height
p, h1, h2, h3, h4, h5, h6
line-height: 1.61803398875
line-height: $block-text-line-height
padding: 0
margin: 0
font-style: normal
font-weight: normal
h1, h2, h3, h4, h5, h6
margin-bottom: 0.3em
p
margin-top: 0
margin-bottom: 0
font-weight: normal
ul
padding: 0
margin-top: 10px
margin-bottom: 10px
li
margin-top: 0
font-weight: normal
margin-bottom: 10px
blockquote
margin: 0 0 $block-text-line-height
padding-left: 10px
border-left: 2px #565656 solid

View File

@ -1,9 +1,4 @@
$button-vertical-padding = 13px
.mailpoet_button_block
padding-top: $button-vertical-padding
padding-bottom: $button-vertical-padding
overflow: hidden
.mailpoet_editor_button

View File

@ -6,5 +6,5 @@
.mailpoet_content
padding: 10px 20px
p
margin: 0
& > *:last-child
margin-bottom: 0

View File

@ -6,5 +6,5 @@
.mailpoet_content
padding: 10px 20px
p
margin: 0
& > *:last-child
margin-bottom: 0

View File

@ -1,4 +1,4 @@
$social-block-vertical-padding = 11px
$social-block-vertical-padding = 0px
$social-icon-width = 32px
$active-social-icon-set-border-color = #adadad

View File

@ -9,16 +9,5 @@
padding-left: 20px
padding-right: 20px
h1, h2, h3, h4, h5, h6
padding: 0
margin: 0
font-weight: normal
p
margin-top: 0
font-weight: normal
blockquote
margin: 1em
padding-left: 1em
border-left: 2px #565656 solid
& > *:last-child
margin-bottom: 0

View File

@ -24,3 +24,5 @@ $error-text-color = #d54e21
// Dimensions
$newsletter-width = 660px
$text-line-height = 1.6em

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -40,11 +40,18 @@ define('date',
return this;
},
format: function(date, options) {
options = options || {};
this.init(options);
return Moment.utc(date)
.local()
return Moment(date, this.convertFormat(options.parseFormat))
.format(this.convertFormat(this.options.format));
},
toDate: function(date, options) {
options = options || {};
this.init(options);
return Moment(date, this.convertFormat(options.parseFormat)).toDate();
},
short: function(date) {
return this.format(date, {
format: 'F, j Y'
@ -115,6 +122,8 @@ define('date',
}
};
if (!format || format.length <= 0) return format;
const replacements = format_mappings['date'];
let outputFormat = '';
@ -133,4 +142,4 @@ define('date',
return outputFormat;
}
};
});
});

View File

@ -9,6 +9,13 @@ define([
render() {
const yearsRange = 100;
const years = [];
if (this.props.placeholder !== undefined) {
years.push((
<option value="" key={ 0 }>{ this.props.placeholder }</option>
));
}
const currentYear = Moment().year();
for (let i = currentYear; i >= currentYear - yearsRange; i--) {
years.push((
@ -33,6 +40,13 @@ define([
class FormFieldDateMonth extends React.Component {
render() {
const months = [];
if (this.props.placeholder !== undefined) {
months.push((
<option value="" key={ 0 }>{ this.props.placeholder }</option>
));
}
for (let i = 1; i <= 12; i++) {
months.push((
<option
@ -56,6 +70,13 @@ define([
class FormFieldDateDay extends React.Component {
render() {
const days = [];
if (this.props.placeholder !== undefined) {
days.push((
<option value="" key={ 0 }>{ this.props.placeholder }</option>
));
}
for (let i = 1; i <= 31; i++) {
days.push((
<option
@ -81,9 +102,9 @@ define([
constructor(props) {
super(props);
this.state = {
year: Moment().year(),
month: 1,
day: 1
year: undefined,
month: undefined,
day: undefined
}
}
componentDidMount() {
@ -98,7 +119,6 @@ define([
}
extractTimeStamp() {
const timeStamp = parseInt(this.props.item[this.props.field.name], 10);
this.setState({
year: Moment.unix(timeStamp).year(),
// Moment returns the month as [0..11]
@ -112,7 +132,7 @@ define([
`${this.state.month}/${this.state.day}/${this.state.year}`,
'M/D/YYYY'
).valueOf();
if (!isNaN(newTimeStamp) && parseInt(newTimeStamp, 10) > 0) {
if (~~(newTimeStamp) > 0) {
// convert milliseconds to seconds
newTimeStamp /= 1000;
return this.props.onValueChange({
@ -158,6 +178,7 @@ define([
key={ 'year' }
name={ this.props.field.name }
year={ this.state.year }
placeholder={ this.props.field.year_placeholder }
/>);
break;
@ -169,6 +190,7 @@ define([
name={ this.props.field.name }
month={ this.state.month }
monthNames={ monthNames }
placeholder={ this.props.field.month_placeholder }
/>);
break;
@ -179,6 +201,7 @@ define([
key={ 'day' }
name={ this.props.field.name }
day={ this.state.day }
placeholder={ this.props.field.day_placeholder }
/>);
break;
}

View File

@ -1,40 +1,54 @@
define([
'react'
],
function(
React
) {
const FormFieldSelect = React.createClass({
render: function() {
if (this.props.field.values === undefined) {
return false;
}
import React from 'react'
const options = Object.keys(this.props.field.values).map(
(value, index) => {
return (
<option
key={ 'option-' + index }
value={ value }>
{ this.props.field.values[value] }
</option>
);
}
);
const FormFieldSelect = React.createClass({
render() {
if (this.props.field.values === undefined) {
return false;
}
return (
<select
name={ this.props.field.name }
id={ 'field_'+this.props.field.name }
value={ this.props.item[this.props.field.name] }
onChange={ this.props.onValueChange }
{...this.props.field.validation}
>
{options}
</select>
let filter = false;
let placeholder = false;
if (this.props.field.placeholder !== undefined) {
placeholder = (
<option value="">{ this.props.field.placeholder }</option>
);
}
});
return FormFieldSelect;
});
if (this.props.field['filter'] !== undefined) {
filter = this.props.field.filter;
}
const options = Object.keys(this.props.field.values).map(
(value, index) => {
if (filter !== false && filter(this.props.item, value) === false) {
return;
}
return (
<option
key={ 'option-' + index }
value={ value }>
{ this.props.field.values[value] }
</option>
);
}
);
return (
<select
name={ this.props.field.name }
id={ 'field_'+this.props.field.name }
value={ this.props.item[this.props.field.name] }
onChange={ this.props.onValueChange }
{...this.props.field.validation}
>
{placeholder}
{options}
</select>
);
}
});
module.exports = FormFieldSelect;

View File

@ -13,14 +13,22 @@ function(
getInitialState: function() {
return {
items: [],
initialized: false
select2: false
};
},
componentWillMount: function() {
this.loadCachedItems();
},
allowMultipleValues: function() {
return (this.props.field.multiple === true);
},
isSelect2Initialized: function() {
return (this.state.select2 === true);
},
componentDidMount: function() {
this.setupSelect2();
if(this.allowMultipleValues()) {
this.setupSelect2();
}
},
componentDidUpdate: function(prevProps, prevState) {
if(
@ -33,14 +41,17 @@ function(
}
},
componentWillUnmount: function() {
jQuery('#'+this.refs.select.id).select2('destroy');
if(this.allowMultipleValues()) {
this.destroySelect2();
}
},
destroySelect2: function() {
if(this.isSelect2Initialized()) {
jQuery('#'+this.refs.select.id).select2('destroy');
}
},
setupSelect2: function() {
if(
!this.props.field.multiple
|| this.state.initialized === true
|| this.refs.select === undefined
) {
if(this.isSelect2Initialized()) {
return;
}
@ -72,7 +83,7 @@ function(
select2.on('change', this.handleChange);
this.setState({ initialized: true });
this.setState({ select2: true });
},
getSelectedValues: function() {
if(this.props.field['selected'] !== undefined) {

View File

@ -1,33 +1,35 @@
define([
'react'
],
function(
React
) {
var FormFieldText = React.createClass({
render: function() {
var value = this.props.item[this.props.field.name];
if(!value) { value = null; }
return (
<input
type="text"
className={ (this.props.field.size) ? '' : 'regular-text' }
size={
(this.props.field.size !== 'auto' && this.props.field.size > 0)
? this.props.field.size
: false
}
name={ this.props.field.name }
id={ 'field_'+this.props.field.name }
value={ value }
placeholder={ this.props.field.placeholder }
defaultValue={ this.props.field.defaultValue }
onChange={ this.props.onValueChange }
{...this.props.field.validation}
/>
);
}
});
import React from 'react'
return FormFieldText;
const FormFieldText = React.createClass({
render() {
let value = this.props.item[this.props.field.name];
if (value === undefined) {
value = this.props.field.defaultValue || '';
}
return (
<input
type="text"
disabled={
(this.props.field['disabled'] !== undefined)
? this.props.field.disabled(this.props.item)
: false
}
className={ (this.props.field.size) ? '' : 'regular-text' }
size={
(this.props.field.size !== 'auto' && this.props.field.size > 0)
? this.props.field.size
: false
}
name={ this.props.field.name }
id={ 'field_'+this.props.field.name }
value={ value }
placeholder={ this.props.field.placeholder }
onChange={ this.props.onValueChange }
{...this.props.field.validation}
/>
);
}
});
module.exports = FormFieldText;

View File

@ -13,10 +13,11 @@ define(
Router,
FormField
) {
var Form = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
getDefaultProps: function() {
return {
params: {},
@ -36,9 +37,13 @@ define(
return this.props.errors ? this.props.errors : this.state.errors;
},
componentDidMount: function() {
if(this.props.params.id !== undefined) {
if(this.isMounted()) {
if(this.isMounted()) {
if(this.props.params.id !== undefined) {
this.loadItem(this.props.params.id);
} else {
this.setState({
item: jQuery('.mailpoet_form').serializeObject()
});
}
}
},
@ -68,7 +73,7 @@ define(
loading: false,
item: {}
}, function() {
this.history.pushState(null, '/new');
this.context.router.push('/new');
}.bind(this));
} else {
this.setState({
@ -101,7 +106,6 @@ define(
item[field.name] = this.state.item[field.name];
}
}.bind(this));
// set id if specified
if(this.props.params.id !== undefined) {
item.id = this.props.params.id;
@ -118,7 +122,7 @@ define(
if(this.props.onSuccess !== undefined) {
this.props.onSuccess();
} else {
this.history.pushState(null, '/')
this.context.router.push('/');
}
if(this.props.params.id !== undefined) {
@ -166,6 +170,17 @@ define(
{ 'mailpoet_form_loading': this.state.loading || this.props.loading }
);
var beforeFormContent = false;
var afterFormContent = false;
if (this.props.beforeFormContent !== undefined) {
beforeFormContent = this.props.beforeFormContent(this.getValues());
}
if (this.props.afterFormContent !== undefined) {
afterFormContent = this.props.afterFormContent(this.getValues());
}
var fields = this.props.fields.map(function(field, i) {
return (
<FormField
@ -190,26 +205,30 @@ define(
}
return (
<form
id={ this.props.id }
ref="form"
className={ formClasses }
onSubmit={
(this.props.onSubmit !== undefined)
? this.props.onSubmit
: this.handleSubmit
}
>
{ errors }
<div>
{ beforeFormContent }
<form
id={ this.props.id }
ref="form"
className={ formClasses }
onSubmit={
(this.props.onSubmit !== undefined)
? this.props.onSubmit
: this.handleSubmit
}
>
{ errors }
<table className="form-table">
<tbody>
{fields}
</tbody>
</table>
<table className="form-table">
<tbody>
{fields}
</tbody>
</table>
{ actions }
</form>
{ actions }
</form>
{ afterFormContent }
</div>
);
}
});

View File

@ -1,10 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute } from 'react-router'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import FormList from 'forms/list.jsx'
import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({
render() {
@ -12,7 +12,7 @@ const App = React.createClass({
}
});
let container = document.getElementById('forms_container');
const container = document.getElementById('forms_container');
if(container) {
ReactDOM.render((

View File

@ -71,6 +71,14 @@ const messages = {
}
};
const bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
const item_actions = [
{
name: 'edit',
@ -102,14 +110,6 @@ const item_actions = [
}
];
const bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
const FormList = React.createClass({
createForm() {
MailPoet.Ajax.post({
@ -138,7 +138,10 @@ const FormList = React.createClass({
<div>
<td className={ row_classes }>
<strong>
<a>{ form.name }</a>
<a
className="row-title"
href={ `admin.php?page=mailpoet-form-editor&id=${form.id}` }
>{ form.name }</a>
</strong>
{ actions }
</td>
@ -146,7 +149,7 @@ const FormList = React.createClass({
{ segments }
</td>
<td className="column-date" data-colname={MailPoet.I18n.t('createdOn')}>
<abbr>{ MailPoet.Date.full(form.created_at) }</abbr>
<abbr>{ MailPoet.Date.format(form.created_at) }</abbr>
</td>
</div>
);
@ -154,20 +157,20 @@ const FormList = React.createClass({
render() {
return (
<div>
<h2 className="title">
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <a
className="add-new-h2"
className="page-title-action"
href="javascript:;"
onClick={ this.createForm }
>{MailPoet.I18n.t('new')}</a>
</h2>
</h1>
<Listing
limit={ mailpoet_listing_per_page }
location={ this.props.location }
params={ this.props.params }
messages={ messages }
search={ false }
limit={ 1000 }
endpoint="forms"
onRenderItem={ this.renderItem }
columns={ columns }

View File

@ -140,5 +140,20 @@ define('handlebars_helpers', ['handlebars'], function(Handlebars) {
return parseInt(string, 10);
});
Handlebars.registerHelper('fontWithFallback', function(font) {
switch(font) {
case 'Arial': return new Handlebars.SafeString("Arial, 'Helvetica Neue', Helvetica, sans-serif");
case 'Comic Sans MS': return new Handlebars.SafeString("'Comic Sans MS', 'Marker Felt-Thin', Arial, sans-serif");
case 'Courier New': return new Handlebars.SafeString("'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace");
case 'Georgia': return new Handlebars.SafeString("Georgia, Times, 'Times New Roman', serif");
case 'Lucida': return new Handlebars.SafeString("'Lucida Sans Unicode', 'Lucida Grande', sans-serif");
case 'Tahoma': return new Handlebars.SafeString("Tahoma, Verdana, Segoe, sans-serif");
case 'Times New Roman': return new Handlebars.SafeString("'Times New Roman', Times, Baskerville, Georgia, serif");
case 'Trebuchet MS': return new Handlebars.SafeString("'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif");
case 'Verdana': return new Handlebars.SafeString("Verdana, Geneva, sans-serif");
default: return font;
}
});
window.Handlebars = Handlebars;
});

View File

@ -21,7 +21,7 @@ define(['react', 'classnames'], function(React, classNames) {
href="javascript:;"
className={classes}
onClick={this.handleSelect.bind(this, group.name)} >
{group.label} <span className="count">({ group.count })</span>
{group.label} <span className="count">({ group.count.toLocaleString() })</span>
</a>
</li>
);

View File

@ -295,9 +295,9 @@ define(
});
var Listing = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
loading: false,
@ -359,9 +359,12 @@ define(
}
})
}
// default overrides
if(this.props.limit !== undefined) {
state.limit = Math.abs(~~this.props.limit);
}
this.setState(state, function() {
this.getItems();
}.bind(this));
@ -398,7 +401,7 @@ define(
if(this.props.location) {
if(this.props.location.pathname !== params) {
this.history.pushState(null, `${params}`)
this.context.router.push(`${params}`);
}
}
},
@ -730,6 +733,7 @@ define(
{ search }
<div className="tablenav top clearfix">
<ListingBulkActions
count={ this.state.count }
bulk_actions={ bulk_actions }
selection={ this.state.selection }
selected_ids={ this.state.selected_ids }
@ -753,8 +757,8 @@ define(
onSort={ this.handleSort }
onSelectItems={ this.handleSelectItems }
selection={ this.state.selection }
sort_by={ this.state.sort_by }
sort_order={ this.state.sort_order }
sort_by={ sort_by }
sort_order={ sort_order }
columns={ this.props.columns }
is_selectable={ bulk_actions.length > 0 } />
</thead>
@ -783,8 +787,8 @@ define(
onSort={ this.handleSort }
onSelectItems={ this.handleSelectItems }
selection={ this.state.selection }
sort_by={ this.state.sort_by }
sort_order={ this.state.sort_order }
sort_by={ sort_by }
sort_order={ sort_order }
columns={ this.props.columns }
is_selectable={ bulk_actions.length > 0 } />
</tfoot>
@ -792,6 +796,7 @@ define(
</table>
<div className="tablenav bottom">
<ListingBulkActions
count={ this.state.count }
bulk_actions={ bulk_actions }
selection={ this.state.selection }
selected_ids={ this.state.selected_ids }

View File

@ -148,7 +148,7 @@ define([
className="current-page" />
&nbsp;{MailPoet.I18n.t('pageOutOf')}&nbsp;
<span className="total-pages">
{Math.ceil(this.props.count / this.props.limit)}
{Math.ceil(this.props.count / this.props.limit).toLocaleString()}
</span>
</span>
&nbsp;
@ -167,7 +167,7 @@ define([
return (
<div className={ classes }>
<span className="displaying-num">{
MailPoet.I18n.t('numberOfItems').replace('%$1d', this.props.count)
MailPoet.I18n.t('numberOfItems').replace('%$1d', this.props.count.toLocaleString())
}</span>
{ pagination }
</div>

View File

@ -16,7 +16,7 @@ define([
defaults: function() {
return this._getDefaults({
type: 'footer',
text: '<a href="[subscription:unsubscribe_url]">Unsubscribe</a> | <a href="[subscription:manage_url]">Manage subscription</a><br /><b>Add your postal address here!</b>',
text: '<a href="[link:subscription_unsubscribe_url]">Unsubscribe</a> | <a href="[link:subscription_manage_url]">Manage subscription</a><br /><b>Add your postal address here!</b>',
styles: {
block: {
backgroundColor: 'transparent',

View File

@ -16,7 +16,7 @@ define([
defaults: function() {
return this._getDefaults({
type: 'header',
text: 'Display problems? <a href="[newsletter:view_in_browser_url]">View it in your browser</a>',
text: 'Display problems? <a href="[link:newsletter_view_in_browser_url]">View it in your browser</a>',
styles: {
block: {
backgroundColor: 'transparent',

View File

@ -284,7 +284,7 @@ define([
}
if (App.getConfig().get('validation.validateUnsubscribeLinkPresent') &&
JSON.stringify(jsonObject).indexOf("[subscription:unsubscribe_url]") < 0) {
JSON.stringify(jsonObject).indexOf("[link:subscription_unsubscribe_url]") < 0) {
this.showValidationError(MailPoet.I18n.t('unsubscribeLinkMissing'));
return;
}

View File

@ -245,14 +245,20 @@ define([
MailPoet.Ajax.post({
endpoint: 'newsletters',
action: 'render',
action: 'showPreview',
data: json,
}).done(function(response){
MailPoet.Modal.loading(false);
window.open('data:text/html;charset=utf-8,' + encodeURIComponent(response.rendered_body), '_blank');
if (response.result === true) {
window.open(response.data.url, '_blank')
}
MailPoet.Notice.error(response.errors);
}).fail(function(error) {
MailPoet.Modal.loading(false);
alert('Something went wrong, check console');
MailPoet.Notice.error(
MailPoet.I18n.t('newsletterPreviewFailed')
);
});
},
sendPreview: function() {

View File

@ -14,9 +14,6 @@ define(
var Link = Router.Link;
var Breadcrumb = React.createClass({
mixins: [
Router.History
],
getInitialState: function() {
return {
step: null,

View File

@ -31,6 +31,10 @@ define(
name: 'segments',
label: MailPoet.I18n.t('lists')
},
{
name: 'statistics',
label: MailPoet.I18n.t('statistics')
},
{
name: 'created_at',
label: MailPoet.I18n.t('createdOn'),
@ -142,6 +146,11 @@ define(
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
} else {
if (item.queue.status === 'scheduled') {
return (
<span>{MailPoet.I18n.t('scheduledFor')} { MailPoet.Date.format(item.queue.scheduled_at) } </span>
)
}
var progressClasses = classNames(
'mailpoet_progress',
{ 'mailpoet_progress_complete': item.queue.status === 'completed'}
@ -205,6 +214,31 @@ define(
);
}
},
renderStatistics: function(item) {
if(!item.statistics || !item.queue || item.queue.count_processed == 0 || item.queue.status === 'scheduled') {
return (
<span>
{MailPoet.I18n.t('notSentYet')}
</span>
);
}
var percentage_clicked = Math.round(
(item.statistics.clicked * 100) / (item.queue.count_processed)
);
var percentage_opened = Math.round(
(item.statistics.opened * 100) / (item.queue.count_processed)
);
var percentage_unsubscribed = Math.round(
(item.statistics.unsubscribed * 100) / (item.queue.count_processed)
);
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
},
renderItem: function(newsletter, actions) {
var rowClasses = classNames(
'manage-column',
@ -216,6 +250,13 @@ define(
return segment.name
}).join(', ');
var statistics_column =
(!mailpoet_settings.tracking || !mailpoet_settings.tracking.enabled) ?
false :
<td className="column {statistics_class}" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
</td>;
return (
<div>
<td className={ rowClasses }>
@ -224,29 +265,34 @@ define(
</strong>
{ actions }
</td>
<td className="column" data-colname="Lists">
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderStatus(newsletter) }
</td>
<td className="column" data-colname="Lists">
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
</td>
<td className="column-date" data-colname="Subscribed on">
<abbr>{ MailPoet.Date.full(newsletter.created_at) }</abbr>
{ statistics_column }
<td className="column-date" data-colname={ MailPoet.I18n.t('createdOn') }>
<abbr>{ MailPoet.Date.format(newsletter.created_at) }</abbr>
</td>
<td className="column-date" data-colname="Last modified on">
<abbr>{ MailPoet.Date.full(newsletter.updated_at) }</abbr>
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
<abbr>{ MailPoet.Date.format(newsletter.updated_at) }</abbr>
</td>
</div>
);
},
render: function() {
if (!mailpoet_settings.tracking || !mailpoet_settings.tracking.enabled) {
columns = _.without(columns, _.findWhere(columns, {name: 'statistics'}));
}
return (
<div>
<h2 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="add-new-h2" to="/new">{MailPoet.I18n.t('new')}</Link>
</h2>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
</h1>
<Listing
limit={ mailpoet_listing_per_page }
params={ this.props.params }
endpoint="newsletters"
onRenderItem={this.renderItem}

View File

@ -1,6 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute, Link } from 'react-router'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import NewsletterList from 'newsletters/list.jsx'
import NewsletterTypes from 'newsletters/types.jsx'
import NewsletterTemplates from 'newsletters/templates.jsx'
@ -8,9 +9,8 @@ import NewsletterSend from 'newsletters/send.jsx'
import NewsletterStandard from 'newsletters/types/standard.jsx'
import NewsletterWelcome from 'newsletters/types/welcome/welcome.jsx'
import NewsletterNotification from 'newsletters/types/notification/notification.jsx'
import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({
render() {
@ -18,7 +18,7 @@ const App = React.createClass({
}
});
let container = document.getElementById('newsletters_container');
const container = document.getElementById('newsletters_container');
if(container) {
ReactDOM.render((

View File

@ -23,9 +23,9 @@ define(
) {
var NewsletterSend = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
fields: [],
@ -68,7 +68,7 @@ define(
loading: false,
item: {},
}, function() {
this.history.pushState(null, '/new');
this.context.router.push('/new');
}.bind(this));
} else {
this.setState({
@ -106,10 +106,8 @@ define(
}).done((response) => {
this.setState({ loading: false });
if(response.result === true) {
this.history.pushState(null, '/');
MailPoet.Notice.success(
MailPoet.I18n.t('newsletterIsBeingSent')
);
this.context.router.push('/');
MailPoet.Notice.success(response.data.message);
} else {
if(response.errors) {
MailPoet.Notice.error(response.errors);
@ -135,7 +133,7 @@ define(
this.setState({ loading: false });
if(response.result === true) {
this.history.pushState(null, '/');
this.context.router.push('/');
MailPoet.Notice.success(
MailPoet.I18n.t('newsletterUpdated')
);

View File

@ -42,7 +42,7 @@ define(
getLabel: function(segment) {
var name = segment.name;
if (segment.subscribers > 0) {
name += ' (%$1s)'.replace('%$1s', segment.subscribers);
name += ' (%$1s)'.replace('%$1s', parseInt(segment.subscribers).toLocaleString());
}
return name;
},
@ -60,7 +60,6 @@ define(
name: 'sender_name',
type: 'text',
placeholder: MailPoet.I18n.t('senderNamePlaceholder'),
defaultValue: (settings.sender !== undefined) ? settings.sender.name : '',
validation: {
'data-parsley-required': true
}
@ -69,7 +68,6 @@ define(
name: 'sender_address',
type: 'text',
placeholder: MailPoet.I18n.t('senderAddressPlaceholder'),
defaultValue: (settings.sender !== undefined) ? settings.sender.address : '',
validation: {
'data-parsley-required': true,
'data-parsley-type': 'email'
@ -86,15 +84,13 @@ define(
{
name: 'reply_to_name',
type: 'text',
placeholder: MailPoet.I18n.t('replyToNamePlaceholder'),
defaultValue: (settings.reply_to !== undefined) ? settings.reply_to.name : '',
placeholder: MailPoet.I18n.t('replyToNamePlaceholder')
},
{
name: 'reply_to_address',
type: 'text',
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder'),
defaultValue: (settings.reply_to !== undefined) ? settings.reply_to.address : ''
},
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder')
}
]
}
];

View File

@ -1,12 +1,320 @@
define(
[
'mailpoet'
'react',
'jquery',
'underscore',
'mailpoet',
'form/fields/checkbox.jsx',
'form/fields/select.jsx',
'form/fields/text.jsx',
],
function(
MailPoet
React,
jQuery,
_,
MailPoet,
Checkbox,
Select,
Text
) {
var settings = window.mailpoet_settings || {};
var settings = window.mailpoet_settings || {},
currentTime = window.mailpoet_current_time || '00:00',
defaultDateTime = window.mailpoet_current_date + ' ' + '00:00:00';
timeOfDayItems = window.mailpoet_schedule_time_of_day,
dateDisplayFormat = window.mailpoet_date_display_format,
dateStorageFormat = window.mailpoet_date_storage_format;
var datepickerTranslations = {
closeText: MailPoet.I18n.t('close'),
currentText: MailPoet.I18n.t('today'),
nextText: MailPoet.I18n.t('next'),
prevText: MailPoet.I18n.t('previous'),
monthNames: [
MailPoet.I18n.t('january'),
MailPoet.I18n.t('february'),
MailPoet.I18n.t('march'),
MailPoet.I18n.t('april'),
MailPoet.I18n.t('may'),
MailPoet.I18n.t('june'),
MailPoet.I18n.t('july'),
MailPoet.I18n.t('august'),
MailPoet.I18n.t('september'),
MailPoet.I18n.t('october'),
MailPoet.I18n.t('november'),
MailPoet.I18n.t('december')
],
monthNamesShort: [
MailPoet.I18n.t('januaryShort'),
MailPoet.I18n.t('februaryShort'),
MailPoet.I18n.t('marchShort'),
MailPoet.I18n.t('aprilShort'),
MailPoet.I18n.t('mayShort'),
MailPoet.I18n.t('juneShort'),
MailPoet.I18n.t('julyShort'),
MailPoet.I18n.t('augustShort'),
MailPoet.I18n.t('septemberShort'),
MailPoet.I18n.t('octoberShort'),
MailPoet.I18n.t('novemberShort'),
MailPoet.I18n.t('decemberShort')
],
dayNames: [
MailPoet.I18n.t('sunday'),
MailPoet.I18n.t('monday'),
MailPoet.I18n.t('tuesday'),
MailPoet.I18n.t('wednesday'),
MailPoet.I18n.t('thursday'),
MailPoet.I18n.t('friday'),
MailPoet.I18n.t('saturday')
],
dayNamesShort: [
MailPoet.I18n.t('sundayShort'),
MailPoet.I18n.t('mondayShort'),
MailPoet.I18n.t('tuesdayShort'),
MailPoet.I18n.t('wednesdayShort'),
MailPoet.I18n.t('thursdayShort'),
MailPoet.I18n.t('fridayShort'),
MailPoet.I18n.t('saturdayShort')
],
dayNamesMin: [
MailPoet.I18n.t('sundayMin'),
MailPoet.I18n.t('mondayMin'),
MailPoet.I18n.t('tuesdayMin'),
MailPoet.I18n.t('wednesdayMin'),
MailPoet.I18n.t('thursdayMin'),
MailPoet.I18n.t('fridayMin'),
MailPoet.I18n.t('saturdayMin')
],
};
var DateText = React.createClass({
onChange: function(event) {
// Swap display format to storage format
var displayDate = event.target.value,
storageDate = this.getStorageDate(displayDate);
event.target.value = storageDate;
this.props.onChange(event);
},
componentDidMount: function() {
var $element = jQuery(this.refs.dateInput),
that = this;
if ($element.datepicker) {
// Override jQuery UI datepicker Date parsing and formatting
jQuery.datepicker.parseDate = function(format, value) {
// Transform string format to Date object
return MailPoet.Date.toDate(value, {
parseFormat: dateDisplayFormat,
format: format
});
};
jQuery.datepicker.formatDate = function(format, value) {
// Transform Date object to string format
var newValue = MailPoet.Date.format(value, {
format: format
});
return newValue;
};
$element.datepicker(_.extend({
dateFormat: this.props.displayFormat,
isRTL: false,
onSelect: function(value) {
that.onChange({
target: {
name: that.getFieldName(),
value: value,
},
});
},
}, datepickerTranslations));
this.datepickerInitialized = true;
}
},
componentWillUnmount: function() {
if (this.datepickerInitialized) {
jQuery(this.refs.dateInput).datepicker('destroy');
}
},
getFieldName: function() {
return this.props.name || 'date';
},
getDisplayDate: function(date) {
return MailPoet.Date.format(date, {
parseFormat: this.props.storageFormat,
format: this.props.displayFormat
});
},
getStorageDate: function(date) {
return MailPoet.Date.format(date, {
parseFormat: this.props.displayFormat,
format: this.props.storageFormat
});
},
render: function() {
return (
<input
type="text"
size="10"
name={this.getFieldName()}
value={this.getDisplayDate(this.props.value)}
readOnly={ true }
onChange={this.onChange}
ref="dateInput"
{...this.props.validation} />
);
},
});
var TimeSelect = React.createClass({
render: function() {
const options = Object.keys(timeOfDayItems).map(
(value, index) => {
return (
<option
key={ 'option-' + index }
value={ value }>
{ timeOfDayItems[value] }
</option>
);
}
);
return (
<select
name={this.props.name || 'time'}
value={this.props.value}
onChange={this.props.onChange}
{...this.props.validation}
>
{options}
</select>
);
}
});
var DateTime = React.createClass({
_DATE_TIME_SEPARATOR: " ",
getInitialState: function() {
return this._buildStateFromProps(this.props);
},
componentWillReceiveProps: function(nextProps) {
this.setState(this._buildStateFromProps(nextProps));
},
_buildStateFromProps: function(props) {
let value = props.value || defaultDateTime;
const [date, time] = value.split(this._DATE_TIME_SEPARATOR)
return {
date: date,
time: time,
};
},
handleChange: function(event) {
var newState = {};
newState[event.target.name] = event.target.value;
this.setState(newState, function() {
this.propagateChange();
});
},
propagateChange: function() {
if (this.props.onChange) {
return this.props.onChange({
target: {
name: this.props.name || '',
value: this.getDateTime(),
}
})
}
},
getDateTime: function() {
return [this.state.date, this.state.time].join(this._DATE_TIME_SEPARATOR);
},
render: function() {
return (
<span>
<DateText
name="date"
value={this.state.date}
onChange={this.handleChange}
displayFormat={dateDisplayFormat}
storageFormat={dateStorageFormat}
validation={this.props.dateValidation}/>
<TimeSelect
name="time"
value={this.state.time}
onChange={this.handleChange}
validation={this.props.timeValidation} />
</span>
);
}
});
var StandardScheduling = React.createClass({
_getCurrentValue: function() {
return this.props.item[this.props.field.name] || {};
},
handleValueChange: function(event) {
var oldValue = this._getCurrentValue(),
newValue = {};
newValue[event.target.name] = event.target.value;
return this.props.onValueChange({
target: {
name: this.props.field.name,
value: _.extend({}, oldValue, newValue)
}
});
},
handleCheckboxChange: function(event) {
event.target.value = this.refs.isScheduled.checked ? '1' : '0';
return this.handleValueChange(event);
},
isScheduled: function() {
return this._getCurrentValue().isScheduled === '1';
},
getDateValidation: function() {
return {
'data-parsley-required': true,
'data-parsley-required-message': MailPoet.I18n.t('noScheduledDateError'),
'data-parsley-errors-container': '#mailpoet_scheduling',
};
},
render: function() {
var schedulingOptions;
if (this.isScheduled()) {
schedulingOptions = (
<span id="mailpoet_scheduling">
<DateTime
name="scheduledAt"
value={this._getCurrentValue().scheduledAt}
onChange={this.handleValueChange}
dateValidation={this.getDateValidation()} />
&nbsp;
<span>
{MailPoet.I18n.t('websiteTimeIs')} <code>{currentTime}</code>
</span>
</span>
);
}
return (
<div>
<input
ref="isScheduled"
type="checkbox"
value="1"
checked={this.isScheduled()}
name="isScheduled"
onChange={this.handleCheckboxChange} />
{schedulingOptions}
</div>
);
},
});
var fields = [
{
@ -34,7 +342,7 @@ define(
getLabel: function(segment) {
var name = segment.name;
if (segment.subscribers > 0) {
name += ' (%$1s)'.replace('%$1s', segment.subscribers);
name += ' (%$1s)'.replace('%$1s', parseInt(segment.subscribers).toLocaleString());
}
return name;
},
@ -52,7 +360,6 @@ define(
name: 'sender_name',
type: 'text',
placeholder: MailPoet.I18n.t('senderNamePlaceholder'),
defaultValue: (settings.sender !== undefined) ? settings.sender.name : '',
validation: {
'data-parsley-required': true
}
@ -61,7 +368,6 @@ define(
name: 'sender_address',
type: 'text',
placeholder: MailPoet.I18n.t('senderAddressPlaceholder'),
defaultValue: (settings.sender !== undefined) ? settings.sender.address : '',
validation: {
'data-parsley-required': true,
'data-parsley-type': 'email'
@ -78,16 +384,20 @@ define(
{
name: 'reply_to_name',
type: 'text',
placeholder: MailPoet.I18n.t('replyToNamePlaceholder'),
defaultValue: (settings.reply_to !== undefined) ? settings.reply_to.name : '',
placeholder: MailPoet.I18n.t('replyToNamePlaceholder')
},
{
name: 'reply_to_address',
type: 'text',
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder'),
defaultValue: (settings.reply_to !== undefined) ? settings.reply_to.address : ''
},
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder')
}
]
},
{
name: 'options',
label: MailPoet.I18n.t('scheduleIt'),
type: 'reactComponent',
component: StandardScheduling,
}
];

View File

@ -36,7 +36,6 @@ define(
name: 'sender_name',
type: 'text',
placeholder: MailPoet.I18n.t('senderNamePlaceholder'),
defaultValue: (settings.sender !== undefined) ? settings.sender.name : '',
validation: {
'data-parsley-required': true
}
@ -45,7 +44,6 @@ define(
name: 'sender_address',
type: 'text',
placeholder: MailPoet.I18n.t('senderAddressPlaceholder'),
defaultValue: (settings.sender !== undefined) ? settings.sender.address : '',
validation: {
'data-parsley-required': true,
'data-parsley-type': 'email'
@ -62,15 +60,13 @@ define(
{
name: 'reply_to_name',
type: 'text',
placeholder: MailPoet.I18n.t('replyToNamePlaceholder'),
defaultValue: (settings.reply_to !== undefined) ? settings.reply_to.name : '',
placeholder: MailPoet.I18n.t('replyToNamePlaceholder')
},
{
name: 'reply_to_address',
type: 'text',
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder'),
defaultValue: (settings.reply_to !== undefined) ? settings.reply_to.address : ''
},
placeholder: MailPoet.I18n.t('replyToAddressPlaceholder')
}
]
}
];

View File

@ -80,9 +80,6 @@ define(
});
var NewsletterTemplates = React.createClass({
mixins: [
Router.History
],
getInitialState: function() {
return {
loading: false,

View File

@ -12,12 +12,12 @@ define(
Breadcrumb
) {
var NewsletterTypes = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
setupNewsletter: function(type) {
if(type !== undefined) {
this.history.pushState(null, `/new/${type}`);
this.context.router.push(`/new/${type}`);
}
},
createNewsletter: function(type) {
@ -30,7 +30,7 @@ define(
}
}).done(function(response) {
if(response.result && response.newsletter.id) {
this.history.pushState(null, `/template/${response.newsletter.id}`);
this.context.router.push(`/template/${response.newsletter.id}`);
} else {
if(response.errors.length > 0) {
response.errors.map(function(error) {

View File

@ -24,9 +24,9 @@ define(
};
var NewsletterNotification = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
options: {
@ -64,7 +64,7 @@ define(
}.bind(this));
},
showTemplateSelection: function(newsletterId) {
this.history.pushState(null, `/template/${newsletterId}`);
this.context.router.push(`/template/${newsletterId}`);
},
render: function() {
return (

View File

@ -13,11 +13,11 @@ define(
) {
var NewsletterStandard = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
showTemplateSelection: function(newsletterId) {
this.history.pushState(null, `/template/${newsletterId}`);
this.context.router.push(`/template/${newsletterId}`);
},
componentDidMount: function() {
// No options for this type, create a newsletter upon mounting

View File

@ -34,7 +34,7 @@ define(
function(segment) {
var name = segment.name;
if (segment.subscribers > 0) {
name += ' (%$1d)'.replace('%$1d', segment.subscribers);
name += ' (%$1s)'.replace('%$1s', parseInt(segment.subscribers).toLocaleString());
}
return [segment.id, name];
}
@ -65,6 +65,9 @@ define(
};
var WelcomeScheduling = React.createClass({
contextTypes: {
router: React.PropTypes.object.isRequired
},
_getCurrentValue: function() {
return this.props.item[this.props.field.name] || {};
},
@ -131,7 +134,7 @@ define(
}.bind(this));
},
showTemplateSelection: function(newsletterId) {
this.history.pushState(null, `/template/${newsletterId}`);
this.context.router.push(`/template/${newsletterId}`);
},
render: function() {
var value = this._getCurrentValue(),

View File

@ -23,15 +23,22 @@ define(
component: Scheduling,
};
var availableSegments = window.mailpoet_segments || {},
defaultSegment = 1;
if (_.size(availableSegments) > 0) {
defaultSegment = _.first(availableSegments).id;
}
var NewsletterWelcome = React.createClass({
mixins: [
Router.History
],
contextTypes: {
router: React.PropTypes.object.isRequired
},
getInitialState: function() {
return {
options: {
event: 'segment',
segment: 1,
segment: defaultSegment,
role: 'subscriber',
afterTimeNumber: 1,
afterTimeType: 'immediate',
@ -64,7 +71,7 @@ define(
}.bind(this));
},
showTemplateSelection: function(newsletterId) {
this.history.pushState(null, `/template/${newsletterId}`);
this.context.router.push(`/template/${newsletterId}`);
},
render: function() {
return (

View File

@ -51,7 +51,7 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
id: null,
positionAfter: false,
scroll: false,
timeout: 2000,
timeout: 5000,
onOpen: null,
onClose: null
},

View File

@ -22,7 +22,7 @@ function(
form.parsley().on('form:validated', function(parsley) {
// clear messages
form.find('.mailpoet_message').html('');
form.find('.mailpoet_message > p').hide();
// resize iframe
if(window.frameElement !== null) {
@ -45,27 +45,18 @@ function(
action: 'subscribe',
data: data
}).done(function(response) {
if(response.result !== true) {
// errors
$.each(response.errors, function(index, error) {
form
.find('.mailpoet_message')
.append('<p class="mailpoet_validate_error">'+
error+
'</p>');
});
if(response.result === false) {
form.find('.mailpoet_validate_error').html(
response.errors.join('<br />')
).show();
} else {
// successfully subscribed
if(response.page !== undefined) {
// go to page
window.location.href = response.page;
} else if(response.message !== undefined) {
} else {
// display success message
form
.find('.mailpoet_message')
.html('<p class="mailpoet_validate_success">'+
response.message+
'</p>');
form.find('.mailpoet_validate_success').show();
}
// reset form

View File

@ -21,7 +21,8 @@ define(
{
name: 'description',
label: MailPoet.I18n.t('description'),
type: 'textarea'
type: 'textarea',
tip: MailPoet.I18n.t('segmentDescriptionTip')
}
];
@ -34,16 +35,16 @@ define(
}
};
var Link = Router.Link;
const SegmentForm = React.createClass({
mixins: [
Router.History
],
render: function() {
return (
<div>
<h2 className="title">
<h1 className="title">
{MailPoet.I18n.t('segment')}
</h2>
<Link className="page-title-action" to="/">{MailPoet.I18n.t('backToList')}</Link>
</h1>
<Form
endpoint="segments"

View File

@ -88,10 +88,17 @@ const messages = {
}
};
const bulk_actions = [
{
name: 'trash',
label: MailPoet.I18n.t('trash'),
onSuccess: messages.onTrash
}
];
const item_actions = [
{
name: 'edit',
label: MailPoet.I18n.t('edit'),
link: function(item) {
return (
<Link to={ `/edit/${item.id}` }>{MailPoet.I18n.t('edit')}</Link>
@ -103,7 +110,7 @@ const item_actions = [
},
{
name: 'duplicate_segment',
label: 'Duplicate',
label: MailPoet.I18n.t('duplicate'),
onClick: function(item, refresh) {
return MailPoet.Ajax.post({
endpoint: 'segments',
@ -120,10 +127,23 @@ const item_actions = [
return (segment.type !== 'wp_users');
}
},
{
name: 'read_more',
link: function(item) {
return (
<a
href="https://www.mailpoet.com/#TODO"
target="_blank"
>{MailPoet.I18n.t('readMore')}</a>
);
},
display: function(segment) {
return (segment.type === 'wp_users');
}
},
{
name: 'synchronize_segment',
label: MailPoet.I18n.t('update'),
className: 'update',
label: MailPoet.I18n.t('forceSync'),
onClick: function(item, refresh) {
MailPoet.Modal.loading(true);
MailPoet.Ajax.post({
@ -159,9 +179,6 @@ const item_actions = [
}
];
const bulk_actions = [
];
const SegmentList = React.createClass({
renderItem: function(segment, actions) {
var rowClasses = classNames(
@ -169,28 +186,42 @@ const SegmentList = React.createClass({
'column-primary',
'has-row-actions'
);
const subscribed = ~~(segment.subscribers_count.subscribed || 0);
const unconfirmed = ~~(segment.subscribers_count.unconfirmed || 0);
const unsubscribed = ~~(segment.subscribers_count.unsubscribed || 0);
let segment_name = (
<Link to={ `/edit/${segment.id}` }>{ segment.name }</Link>
);
// the WP users segment is not editable so just display its name
if (segment.type === 'wp_users') {
segment_name = segment.name;
}
return (
<div>
<td className={ rowClasses }>
<strong>
<a>{ segment.name }</a>
{ segment_name }
</strong>
{ actions }
</td>
<td className="column-date" data-colname="Description">
<td className="column-date" data-colname={ MailPoet.I18n.t('description') }>
<abbr>{ segment.description }</abbr>
</td>
<td className="column-date" data-colname="Subscribed">
<abbr>{ segment.subscribers_count.subscribed || 0 }</abbr>
<td className="column-date" data-colname={ MailPoet.I18n.t('subscribed') }>
<abbr>{ subscribed.toLocaleString() }</abbr>
</td>
<td className="column-date" data-colname="Unconfirmed">
<abbr>{ segment.subscribers_count.unconfirmed || 0 }</abbr>
<td className="column-date" data-colname={ MailPoet.I18n.t('unconfirmed') }>
<abbr>{ unconfirmed.toLocaleString() }</abbr>
</td>
<td className="column-date" data-colname="Unsubscribed">
<abbr>{ segment.subscribers_count.unsubscribed || 0 }</abbr>
<td className="column-date" data-colname={ MailPoet.I18n.t('unsubscribed') }>
<abbr>{ unsubscribed.toLocaleString() }</abbr>
</td>
<td className="column-date" data-colname="Created on">
<abbr>{ MailPoet.Date.full(segment.created_at) }</abbr>
<td className="column-date" data-colname={ MailPoet.I18n.t('createdOn') }>
<abbr>{ MailPoet.Date.format(segment.created_at) }</abbr>
</td>
</div>
);
@ -198,16 +229,16 @@ const SegmentList = React.createClass({
render: function() {
return (
<div>
<h2 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="add-new-h2" to="/new">{MailPoet.I18n.t('new')}</Link>
</h2>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
</h1>
<Listing
limit={ mailpoet_listing_per_page }
location={ this.props.location }
params={ this.props.params }
messages={ messages }
search={ false }
limit={ 1000 }
endpoint="segments"
onRenderItem={ this.renderItem }
columns={ columns }

View File

@ -1,11 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute, Link } from 'react-router'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import SegmentList from 'segments/list.jsx'
import SegmentForm from 'segments/form.jsx'
import createHashHistory from 'history/lib/createHashHistory'
let history = createHashHistory({ queryKey: false })
const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({
render() {
@ -13,7 +13,7 @@ const App = React.createClass({
}
});
let container = document.getElementById('segments_container');
const container = document.getElementById('segments_container');
if(container) {
ReactDOM.render((

View File

@ -3,38 +3,55 @@ define(
'react',
'react-router',
'mailpoet',
'form/form.jsx'
'form/form.jsx',
'react-string-replace'
],
function(
React,
Router,
MailPoet,
Form
Form,
ReactStringReplace
) {
var fields = [
{
name: 'email',
label: MailPoet.I18n.t('email'),
type: 'text'
type: 'text',
disabled: function(subscriber) {
return ~~(subscriber.wp_user_id > 0);
}
},
{
name: 'first_name',
label: MailPoet.I18n.t('firstname'),
type: 'text'
type: 'text',
disabled: function(subscriber) {
return ~~(subscriber.wp_user_id > 0);
}
},
{
name: 'last_name',
label: MailPoet.I18n.t('lastname'),
type: 'text'
type: 'text',
disabled: function(subscriber) {
return ~~(subscriber.wp_user_id > 0);
}
},
{
name: 'status',
label: MailPoet.I18n.t('status'),
type: 'select',
values: {
'unconfirmed': MailPoet.I18n.t('unconfirmed'),
'subscribed': MailPoet.I18n.t('subscribed'),
'unconfirmed': MailPoet.I18n.t('unconfirmed'),
'unsubscribed': MailPoet.I18n.t('unsubscribed')
},
filter: function(subscriber, value) {
if (~~(subscriber.wp_user_id) > 0 && value === 'unconfirmed') {
return false;
}
return true;
}
},
{
@ -100,6 +117,19 @@ define(
field.values = custom_field.params.values;
}
// add placeholders for selects (date, select)
switch(custom_field.type) {
case 'date':
field.year_placeholder = MailPoet.I18n.t('year');
field.month_placeholder = MailPoet.I18n.t('month');
field.day_placeholder = MailPoet.I18n.t('day');
break;
case 'select':
field.placeholder = '-';
break;
}
fields.push(field);
});
@ -112,24 +142,54 @@ define(
}
};
var beforeFormContent = function(subscriber) {
if (~~(subscriber.wp_user_id) > 0) {
return (
<p className="description">
{ ReactStringReplace(
MailPoet.I18n.t('WPUserEditNotice'),
/\[link\](.*?)\[\/link\]/g,
(match, i) => (
<a
key={ i }
href={`user-edit.php?user_id=${ subscriber.wp_user_id }`}
>{ match }</a>
)
)
}
</p>
);
}
};
var afterFormContent = function(subscriber) {
return (
<p className="description">
<strong>
{ MailPoet.I18n.t('tip') }
</strong> { MailPoet.I18n.t('customFieldsTip') }
</p>
);
}
var Link = Router.Link;
var SubscriberForm = React.createClass({
mixins: [
Router.History
],
render: function() {
return (
<div>
<h2 className="title">
<h1 className="title">
{MailPoet.I18n.t('subscriber')}
</h2>
<Link className="page-title-action" to="/">{MailPoet.I18n.t('backToList')}</Link>
</h1>
<Form
endpoint="subscribers"
fields={ fields }
params={ this.props.params }
messages={ messages }
beforeFormContent={ beforeFormContent }
afterFormContent={ afterFormContent }
/>
</div>
);

View File

@ -43,12 +43,12 @@ define(
width: '20em',
templateResult: function (item) {
return (item.subscriberCount > 0)
? item.name + ' (' + item.subscriberCount + ')'
? item.name + ' (' + parseInt(item.subscriberCount).toLocaleString() + ')'
: item.name;
},
templateSelection: function (item) {
return (item.subscriberCount > 0)
? item.name + ' (' + item.subscriberCount + ')'
? item.name + ' (' + parseInt(item.subscriberCount).toLocaleString() + ')'
: item.name;
}
})
@ -151,7 +151,7 @@ define(
MailPoet.Notice.error(response.errors);
} else {
resultMessage = MailPoet.I18n.t('exportMessage')
.replace('%1$s', '<strong>' + response.data.totalExported + '</strong>')
.replace('%1$s', '<strong>' + parseInt(response.data.totalExported).toLocaleString() + '</strong>')
.replace('[link]', '<a href="' + response.data.exportFileURL + '" target="_blank" >')
.replace('[/link]', '</a>');
jQuery('#export_result_notice > ul > li').html(resultMessage);

View File

@ -24,7 +24,6 @@ define(
return;
}
jQuery(document).ready(function () {
var noticeTimeout = 3000;
jQuery('input[name="select_method"]').attr('checked', false);
// configure router
router = new (Backbone.Router.extend({
@ -128,9 +127,7 @@ define(
// get an approximate size of textarea paste in bytes
var pasteSize = encodeURI(pasteInputElement.val()).split(/%..|./).length - 1;
if (pasteSize > maxPostSizeBytes) {
MailPoet.Notice.error(MailPoet.I18n.t('maxPostSizeNotice'), {
timeout: noticeTimeout,
});
MailPoet.Notice.error(MailPoet.I18n.t('maxPostSizeNotice'));
return;
}
// delay loading indicator for 10ms or else it's just too fast :)
@ -148,9 +145,7 @@ define(
var ext = this.value.match(/\.(.+)$/);
if (ext === null || ext[1].toLowerCase() !== 'csv') {
this.value = '';
MailPoet.Notice.error(MailPoet.I18n.t('wrongFileFormat'), {
timeout: noticeTimeout,
});
MailPoet.Notice.error(MailPoet.I18n.t('wrongFileFormat'));
}
toggleNextStepButton(
@ -198,9 +193,7 @@ define(
}).done(function (response) {
if (response.result === false) {
MailPoet.Notice.hide();
MailPoet.Notice.error(response.errors, {
timeout: noticeTimeout,
});
MailPoet.Notice.error(response.errors);
jQuery('.mailpoet_mailchimp-key-status')
.removeClass()
.addClass('mailpoet_mailchimp-key-status mailpoet_mailchimp-error');
@ -223,9 +216,7 @@ define(
}).error(function (error) {
MailPoet.Modal.loading(false);
MailPoet.Notice.error(
MailPoet.I18n.t('serverError') + error.statusText.toLowerCase() + '.', {
timeout: noticeTimeout,
}
MailPoet.I18n.t('serverError') + error.statusText.toLowerCase() + '.'
);
});
MailPoet.Modal.loading(false);
@ -250,17 +241,13 @@ define(
}
else {
MailPoet.Notice.hide();
MailPoet.Notice.error(response.errors, {
timeout: noticeTimeout,
});
MailPoet.Notice.error(response.errors);
}
MailPoet.Modal.loading(false);
}).error(function () {
MailPoet.Modal.loading(false);
MailPoet.Notice.error(
MailPoet.I18n.t('serverError') + result.statusText.toLowerCase() + '.', {
timeout: noticeTimeout,
}
MailPoet.I18n.t('serverError') + result.statusText.toLowerCase() + '.'
);
});
});
@ -350,9 +337,7 @@ define(
comments: advancedOptionComments,
error: function () {
MailPoet.Notice.hide();
MailPoet.Notice.error(MailPoet.I18n.t('dataProcessingError'), {
timeout: noticeTimeout,
});
MailPoet.Notice.error(MailPoet.I18n.t('dataProcessingError'));
},
complete: function (CSV) {
for (var rowCount in CSV.data) {
@ -434,9 +419,7 @@ define(
var errorNotice = MailPoet.I18n.t('noValidRecords');
errorNotice = errorNotice.replace('[link]', MailPoet.I18n.t('csvKBLink'));
errorNotice = errorNotice.replace('[/link]', '</a>');
MailPoet.Notice.error(errorNotice, {
timeout: noticeTimeout,
});
MailPoet.Notice.error(errorNotice);
}
}
}
@ -507,7 +490,7 @@ define(
),
invalid: (subscribers.invalid.length)
? MailPoet.I18n.t('importNoticeInvalid')
.replace('%1$s', '<strong>' + subscribers.invalid.length + '</strong>')
.replace('%1$s', '<strong>' + subscribers.invalid.length.toLocaleString() + '</strong>')
.replace('%2$s', subscribers.invalid.join(', '))
: null,
duplicate: (subscribers.duplicate.length)
@ -550,13 +533,15 @@ define(
data: segments,
width: '20em',
templateResult: function (item) {
item.subscriberCount = parseInt(item.subscriberCount);
return (item.subscriberCount > 0)
? item.name + ' (' + item.subscriberCount + ')'
? item.name + ' (' + item.subscriberCount.toLocaleString() + ')'
: item.name;
},
templateSelection: function (item) {
item.subscriberCount = parseInt(item.subscriberCount);
return (item.subscriberCount > 0)
? item.name + ' (' + item.subscriberCount + ')'
? item.name + ' (' + item.subscriberCount.toLocaleString() + ')'
: item.name;
}
})
@ -566,7 +551,6 @@ define(
if (!segmentSelectionNotice.length) {
MailPoet.Notice.error(MailPoet.I18n.t('segmentSelectionRequired'), {
static: true,
timeout: noticeTimeout,
scroll: true,
id: 'segmentSelection',
hideClose: true
@ -644,18 +628,14 @@ define(
else {
MailPoet.Modal.close();
MailPoet.Notice.error(
MailPoet.I18n.t('segmentCreateError') + response.message + '.', {
timeout: noticeTimeout,
}
MailPoet.I18n.t('segmentCreateError') + response.message + '.'
);
}
})
.error(function (error) {
MailPoet.Modal.close();
MailPoet.Notice.error(
MailPoet.I18n.t('serverError') + error.statusText.toLowerCase() + '.', {
timeout: noticeTimeout
}
MailPoet.I18n.t('serverError') + error.statusText.toLowerCase() + '.'
);
});
}
@ -733,7 +713,7 @@ define(
}
// if we're on the last line, show the total count of subscribers data
else if (index === (subscribers.subscribers.length - 1)) {
return subscribers.subscribersCount;
return subscribers.subscribersCount.toLocaleString();
} else {
return index + 1;
}
@ -860,18 +840,14 @@ define(
filterSubscribers();
}
else {
MailPoet.Notice.error(MailPoet.I18n.t('customFieldCreateError'), {
timeout: noticeTimeout,
});
MailPoet.Notice.error(MailPoet.I18n.t('customFieldCreateError'));
}
MailPoet.Modal.loading(false);
})
.error(function (error) {
MailPoet.Modal.loading(false);
MailPoet.Notice.error(
MailPoet.I18n.t('serverError') + error.statusText.toLowerCase() + '.', {
timeout: noticeTimeout,
}
MailPoet.I18n.t('serverError') + error.statusText.toLowerCase() + '.'
);
});
}
@ -935,7 +911,6 @@ define(
if (!jQuery('[data-id="notice_invalidEmail"]').length) {
MailPoet.Notice.error(MailPoet.I18n.t('columnContainsInvalidElement'), {
static: true,
timeout: noticeTimeout,
scroll: true,
hideClose: true,
id: 'invalidEmail'
@ -1001,7 +976,7 @@ define(
data[matchedColumn] +=
'<span class="mailpoet_data_match" title="'
+ MailPoet.I18n.t('verifyDateMatch') + '">'
+ date + '</span>';
+ MailPoet.Date.format(date) + '</span>';
}
else {
data[matchedColumn] +=
@ -1015,7 +990,6 @@ define(
if (preventNextStep && !jQuery('.mailpoet_invalidDate').length) {
MailPoet.Notice.error(MailPoet.I18n.t('columnContainsInvalidDate'), {
static: true,
timeout: noticeTimeout,
scroll: true,
hideClose: true,
id: 'invalidDate'
@ -1127,9 +1101,7 @@ define(
queue.onComplete(function () {
MailPoet.Modal.loading(false);
if (importResults.errors.length > 0 && !importResults.updated && !importResults.created) {
MailPoet.Notice.error(_.flatten(importResults.errors), {
timeout: noticeTimeout,
}
MailPoet.Notice.error(_.flatten(importResults.errors)
);
}
else {
@ -1140,6 +1112,7 @@ define(
});
importData.step2 = importResults;
enableSegmentSelection(mailpoetSegments);
toggleNextStepButton('off');
router.navigate('step3', {trigger: true});
}
});
@ -1158,9 +1131,7 @@ define(
showCurrentStep();
if (importData.step2.errors.length > 0) {
MailPoet.Notice.error(_.flatten(importData.step2.errors), {
timeout: noticeTimeout,
});
MailPoet.Notice.error(_.flatten(importData.step2.errors));
}
// display statistics
@ -1172,12 +1143,12 @@ define(
importResults = {
created: (importData.step2.created)
? MailPoet.I18n.t('subscribersCreated')
.replace('%1$s', '<strong>' + importData.step2.created + '</strong>')
.replace('%1$s', '<strong>' + importData.step2.created.toLocaleString() + '</strong>')
.replace('%2$s', '"' + importData.step2.segments.join('", "') + '"')
: false,
updated: (importData.step2.updated)
? MailPoet.I18n.t('subscribersUpdated')
.replace('%1$s', '<strong>' + importData.step2.updated + '</strong>')
.replace('%1$s', '<strong>' + importData.step2.updated.toLocaleString() + '</strong>')
.replace('%2$s', '"' + importData.step2.segments.join('", "') + '"')
: false,
noaction: (!importData.step2.updated && !importData.step2.created)

View File

@ -106,6 +106,11 @@ const bulk_actions = [
return !!(
!segment.deleted_at && segment.type === 'default'
);
},
getLabel: function (segment) {
return (segment.subscribers > 0) ?
segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')' :
segment.name;
}
};
@ -137,6 +142,11 @@ const bulk_actions = [
return !!(
!segment.deleted_at && segment.type === 'default'
);
},
getLabel: function (segment) {
return (segment.subscribers > 0) ?
segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')' :
segment.name;
}
};
@ -168,6 +178,11 @@ const bulk_actions = [
return !!(
segment.type === 'default'
);
},
getLabel: function (segment) {
return (segment.subscribers > 0) ?
segment.name + ' (' + parseInt(segment.subscribers).toLocaleString() + ')' :
segment.name;
}
};
@ -198,16 +213,6 @@ const bulk_actions = [
);
}
},
{
name: 'confirmUnconfirmed',
label: MailPoet.I18n.t('confirmUnconfirmed'),
onSuccess: function(response) {
MailPoet.Notice.success(
MailPoet.I18n.t('multipleSubscribersConfirmed')
.replace('%$1d', ~~response)
);
}
},
{
name: 'sendConfirmationEmail',
label: MailPoet.I18n.t('resendConfirmationEmail'),
@ -275,10 +280,15 @@ const SubscriberList = React.createClass({
}
let segments = false;
let subscribed_segments = [];
// WordPress Users
if (~~(subscriber.wp_user_id) > 0) {
subscribed_segments.push(MailPoet.I18n.t('WPUsersSegment'));
}
// Subscriptions
if (subscriber.subscriptions.length > 0) {
let subscribed_segments = [];
subscriber.subscriptions.map((subscription) => {
const segment = this.getSegmentFromId(subscription.segment_id);
if(segment === false) return;
@ -286,15 +296,14 @@ const SubscriberList = React.createClass({
subscribed_segments.push(segment.name);
}
});
segments = (
<span>
<span className="mailpoet_segments_subscribed">
{ subscribed_segments.join(', ') }
</span>
</span>
);
}
segments = (
<span>
{ subscribed_segments.join(', ') }
</span>
);
let avatar = false;
if(subscriber.avatar_url) {
avatar = (
@ -326,10 +335,10 @@ const SubscriberList = React.createClass({
{ segments }
</td>
<td className="column-date" data-colname={MailPoet.I18n.t('subscribedOn')}>
<abbr>{ MailPoet.Date.full(subscriber.created_at) }</abbr>
<abbr>{ MailPoet.Date.format(subscriber.created_at) }</abbr>
</td>
<td className="column-date" data-colname={MailPoet.I18n.t('lastModifiedOn')}>
<abbr>{ MailPoet.Date.full(subscriber.updated_at) }</abbr>
<abbr>{ MailPoet.Date.format(subscriber.updated_at) }</abbr>
</td>
</div>
);
@ -340,11 +349,11 @@ const SubscriberList = React.createClass({
render: function() {
return (
<div>
<h2 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="add-new-h2" to="/new">{MailPoet.I18n.t('new')}</Link>
<a className="add-new-h2" href="?page=mailpoet-import#step1">{MailPoet.I18n.t('import')}</a>
<a id="mailpoet_export_button" className="add-new-h2" href="?page=mailpoet-export">{MailPoet.I18n.t('export')}</a>
</h2>
<h1 className="title">
{MailPoet.I18n.t('pageTitle')} <Link className="page-title-action" to="/new">{MailPoet.I18n.t('new')}</Link>
<a className="page-title-action" href="?page=mailpoet-import#step1">{MailPoet.I18n.t('import')}</a>
<a id="mailpoet_export_button" className="page-title-action" href="?page=mailpoet-export">{MailPoet.I18n.t('export')}</a>
</h1>
<Listing
limit={ mailpoet_listing_per_page }

View File

@ -1,11 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRoute, Link } from 'react-router'
import { Router, Route, IndexRoute, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import SubscriberList from 'subscribers/list.jsx'
import SubscriberForm from 'subscribers/form.jsx'
import createHashHistory from 'history/lib/createHashHistory'
const history = createHashHistory({ queryKey: false })
const history = useRouterHistory(createHashHistory)({ queryKey: false });
const App = React.createClass({
render() {

262
composer.lock generated
View File

@ -251,16 +251,16 @@
},
{
"name": "phpmailer/phpmailer",
"version": "v5.2.14",
"version": "v5.2.15",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "e774bc9152de85547336e22b8926189e582ece95"
"reference": "d0186171b28af4f06ac2ad8a84a8f3d6cbc3ba6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e774bc9152de85547336e22b8926189e582ece95",
"reference": "e774bc9152de85547336e22b8926189e582ece95",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d0186171b28af4f06ac2ad8a84a8f3d6cbc3ba6c",
"reference": "d0186171b28af4f06ac2ad8a84a8f3d6cbc3ba6c",
"shasum": ""
},
"require": {
@ -271,8 +271,7 @@
"phpunit/phpunit": "4.7.*"
},
"suggest": {
"league/oauth2-client": "Needed for XOAUTH2 authentication",
"league/oauth2-google": "Needed for Gmail XOAUTH2"
"league/oauth2-google": "Needed for Google XOAUTH2 authentication"
},
"type": "library",
"autoload": {
@ -308,7 +307,7 @@
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"time": "2015-11-01 10:15:28"
"time": "2016-05-10 18:39:36"
},
{
"name": "phpseclib/phpseclib",
@ -494,16 +493,16 @@
},
{
"name": "swiftmailer/swiftmailer",
"version": "v5.4.1",
"version": "v5.4.2",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421"
"reference": "d8db871a54619458a805229a057ea2af33c753e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/0697e6aa65c83edf97bb0f23d8763f94e3f11421",
"reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/d8db871a54619458a805229a057ea2af33c753e8",
"reference": "d8db871a54619458a805229a057ea2af33c753e8",
"shasum": ""
},
"require": {
@ -543,7 +542,7 @@
"mail",
"mailer"
],
"time": "2015-06-06 14:19:39"
"time": "2016-05-01 08:45:47"
},
{
"name": "symfony/polyfill-mbstring",
@ -606,16 +605,16 @@
},
{
"name": "symfony/translation",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "b7b4ebadd2b5e614ff7d2d6fc63e0ed0578909c7"
"reference": "d60b8e076d22953aabebeebda53bf334438e7aca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/b7b4ebadd2b5e614ff7d2d6fc63e0ed0578909c7",
"reference": "b7b4ebadd2b5e614ff7d2d6fc63e0ed0578909c7",
"url": "https://api.github.com/repos/symfony/translation/zipball/d60b8e076d22953aabebeebda53bf334438e7aca",
"reference": "d60b8e076d22953aabebeebda53bf334438e7aca",
"shasum": ""
},
"require": {
@ -666,7 +665,7 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2016-02-02 09:49:18"
"time": "2016-03-25 01:40:30"
},
{
"name": "tburry/pquery",
@ -785,16 +784,16 @@
"packages-dev": [
{
"name": "codeception/codeception",
"version": "2.1.7",
"version": "2.1.8",
"source": {
"type": "git",
"url": "https://github.com/Codeception/Codeception.git",
"reference": "65971b0dee4972710365b6102154cd412a9bf7b1"
"reference": "f3daa61f0f11c531b33eb3623ab0daa599d88a79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codeception/Codeception/zipball/65971b0dee4972710365b6102154cd412a9bf7b1",
"reference": "65971b0dee4972710365b6102154cd412a9bf7b1",
"url": "https://api.github.com/repos/Codeception/Codeception/zipball/f3daa61f0f11c531b33eb3623ab0daa599d88a79",
"reference": "f3daa61f0f11c531b33eb3623ab0daa599d88a79",
"shasum": ""
},
"require": {
@ -862,7 +861,7 @@
"functional testing",
"unit testing"
],
"time": "2016-03-12 01:15:25"
"time": "2016-04-15 02:56:43"
},
{
"name": "codeception/verify",
@ -899,16 +898,16 @@
},
{
"name": "codegyre/robo",
"version": "0.7.1",
"version": "0.7.2",
"source": {
"type": "git",
"url": "https://github.com/Codegyre/Robo.git",
"reference": "1f4e0621fdc37521e2eaca67f33d1c4e240dc7d8"
"reference": "9982edecb19f59420031dd9855b0d31e861b8b68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codegyre/Robo/zipball/1f4e0621fdc37521e2eaca67f33d1c4e240dc7d8",
"reference": "1f4e0621fdc37521e2eaca67f33d1c4e240dc7d8",
"url": "https://api.github.com/repos/Codegyre/Robo/zipball/9982edecb19f59420031dd9855b0d31e861b8b68",
"reference": "9982edecb19f59420031dd9855b0d31e861b8b68",
"shasum": ""
},
"require": {
@ -950,7 +949,7 @@
}
],
"description": "Modern task runner",
"time": "2016-02-25 19:28:51"
"time": "2016-04-13 20:14:17"
},
{
"name": "doctrine/instantiator",
@ -1051,16 +1050,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "6.1.1",
"version": "6.2.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "c6851d6e48f63b69357cbfa55bca116448140e0c"
"reference": "d094e337976dff9d8e2424e8485872194e768662"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/c6851d6e48f63b69357cbfa55bca116448140e0c",
"reference": "c6851d6e48f63b69357cbfa55bca116448140e0c",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/d094e337976dff9d8e2424e8485872194e768662",
"reference": "d094e337976dff9d8e2424e8485872194e768662",
"shasum": ""
},
"require": {
@ -1076,7 +1075,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.1-dev"
"dev-master": "6.2-dev"
}
},
"autoload": {
@ -1109,7 +1108,7 @@
"rest",
"web service"
],
"time": "2015-11-23 00:47:50"
"time": "2016-03-21 20:02:09"
},
{
"name": "guzzlehttp/promises",
@ -1164,16 +1163,16 @@
},
{
"name": "guzzlehttp/psr7",
"version": "1.2.3",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b"
"reference": "31382fef2889136415751badebbd1cb022a4ed72"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/2e89629ff057ebb49492ba08e6995d3a6a80021b",
"reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/31382fef2889136415751badebbd1cb022a4ed72",
"reference": "31382fef2889136415751badebbd1cb022a4ed72",
"shasum": ""
},
"require": {
@ -1218,7 +1217,7 @@
"stream",
"uri"
],
"time": "2016-02-18 21:54:00"
"time": "2016-04-13 19:56:01"
},
{
"name": "henrikbjorn/lurker",
@ -1976,16 +1975,16 @@
},
{
"name": "sebastian/environment",
"version": "1.3.5",
"version": "1.3.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf"
"reference": "2292b116f43c272ff4328083096114f84ea46a56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
"reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/2292b116f43c272ff4328083096114f84ea46a56",
"reference": "2292b116f43c272ff4328083096114f84ea46a56",
"shasum": ""
},
"require": {
@ -2022,7 +2021,7 @@
"environment",
"hhvm"
],
"time": "2016-02-26 18:40:46"
"time": "2016-05-04 07:59:13"
},
{
"name": "sebastian/exporter",
@ -2231,16 +2230,16 @@
},
{
"name": "symfony/browser-kit",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
"reference": "dde849a0485b70a24b36f826ed3fb95b904d80c3"
"reference": "e07127ac31230b30887c2dddf3708d883d239b14"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/dde849a0485b70a24b36f826ed3fb95b904d80c3",
"reference": "dde849a0485b70a24b36f826ed3fb95b904d80c3",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/e07127ac31230b30887c2dddf3708d883d239b14",
"reference": "e07127ac31230b30887c2dddf3708d883d239b14",
"shasum": ""
},
"require": {
@ -2284,20 +2283,20 @@
],
"description": "Symfony BrowserKit Component",
"homepage": "https://symfony.com",
"time": "2016-01-27 11:34:55"
"time": "2016-03-04 07:55:57"
},
{
"name": "symfony/config",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "0f8f94e6a32b5c480024eed5fa5cbd2790d0ad19"
"reference": "edbbcf33cffa2a85104fc80de8dc052cc51596bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/0f8f94e6a32b5c480024eed5fa5cbd2790d0ad19",
"reference": "0f8f94e6a32b5c480024eed5fa5cbd2790d0ad19",
"url": "https://api.github.com/repos/symfony/config/zipball/edbbcf33cffa2a85104fc80de8dc052cc51596bb",
"reference": "edbbcf33cffa2a85104fc80de8dc052cc51596bb",
"shasum": ""
},
"require": {
@ -2337,20 +2336,20 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2016-02-22 16:12:45"
"time": "2016-04-20 18:52:26"
},
{
"name": "symfony/console",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "2ed5e2706ce92313d120b8fe50d1063bcfd12e04"
"reference": "34a214710e0714b6efcf40ba3cd1e31373a97820"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/2ed5e2706ce92313d120b8fe50d1063bcfd12e04",
"reference": "2ed5e2706ce92313d120b8fe50d1063bcfd12e04",
"url": "https://api.github.com/repos/symfony/console/zipball/34a214710e0714b6efcf40ba3cd1e31373a97820",
"reference": "34a214710e0714b6efcf40ba3cd1e31373a97820",
"shasum": ""
},
"require": {
@ -2397,20 +2396,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2016-02-28 16:24:34"
"time": "2016-04-28 09:48:42"
},
{
"name": "symfony/css-selector",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "6605602690578496091ac20ec7a5cbd160d4dff4"
"reference": "65e764f404685f2dc20c057e889b3ad04b2e2db0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/6605602690578496091ac20ec7a5cbd160d4dff4",
"reference": "6605602690578496091ac20ec7a5cbd160d4dff4",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/65e764f404685f2dc20c057e889b3ad04b2e2db0",
"reference": "65e764f404685f2dc20c057e889b3ad04b2e2db0",
"shasum": ""
},
"require": {
@ -2450,20 +2449,20 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"time": "2016-01-27 05:14:46"
"time": "2016-03-04 07:55:57"
},
{
"name": "symfony/dom-crawler",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "981c8edb4538f88ba976ed44bdcaa683fce3d6c6"
"reference": "49b588841225b205700e5122fa01911cabada857"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/981c8edb4538f88ba976ed44bdcaa683fce3d6c6",
"reference": "981c8edb4538f88ba976ed44bdcaa683fce3d6c6",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/49b588841225b205700e5122fa01911cabada857",
"reference": "49b588841225b205700e5122fa01911cabada857",
"shasum": ""
},
"require": {
@ -2506,20 +2505,20 @@
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
"time": "2016-02-28 16:24:34"
"time": "2016-04-12 18:09:53"
},
{
"name": "symfony/event-dispatcher",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "78c468665c9568c3faaa9c416a7134308f2d85c3"
"reference": "a158f13992a3147d466af7a23b564ac719a4ddd8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/78c468665c9568c3faaa9c416a7134308f2d85c3",
"reference": "78c468665c9568c3faaa9c416a7134308f2d85c3",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a158f13992a3147d466af7a23b564ac719a4ddd8",
"reference": "a158f13992a3147d466af7a23b564ac719a4ddd8",
"shasum": ""
},
"require": {
@ -2566,20 +2565,20 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2016-01-27 05:14:19"
"time": "2016-05-03 18:59:18"
},
{
"name": "symfony/filesystem",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "65cb36b6539b1d446527d60457248f30d045464d"
"reference": "dee379131dceed90a429e951546b33edfe7dccbb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/65cb36b6539b1d446527d60457248f30d045464d",
"reference": "65cb36b6539b1d446527d60457248f30d045464d",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/dee379131dceed90a429e951546b33edfe7dccbb",
"reference": "dee379131dceed90a429e951546b33edfe7dccbb",
"shasum": ""
},
"require": {
@ -2615,20 +2614,20 @@
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
"time": "2016-02-22 15:02:30"
"time": "2016-04-12 18:01:21"
},
{
"name": "symfony/finder",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "623bda0abd9aa29e529c8e9c08b3b84171914723"
"reference": "c54e407b35bc098916704e9fd090da21da4c4f52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/623bda0abd9aa29e529c8e9c08b3b84171914723",
"reference": "623bda0abd9aa29e529c8e9c08b3b84171914723",
"url": "https://api.github.com/repos/symfony/finder/zipball/c54e407b35bc098916704e9fd090da21da4c4f52",
"reference": "c54e407b35bc098916704e9fd090da21da4c4f52",
"shasum": ""
},
"require": {
@ -2664,20 +2663,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2016-01-27 05:14:46"
"time": "2016-03-10 11:13:05"
},
{
"name": "symfony/form",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/form.git",
"reference": "25b71aec36879b4a84c2ea06603dbe7498b31f06"
"reference": "922807a06e25c0c6e06b86f83ffe4710080a8b2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/form/zipball/25b71aec36879b4a84c2ea06603dbe7498b31f06",
"reference": "25b71aec36879b4a84c2ea06603dbe7498b31f06",
"url": "https://api.github.com/repos/symfony/form/zipball/922807a06e25c0c6e06b86f83ffe4710080a8b2e",
"reference": "922807a06e25c0c6e06b86f83ffe4710080a8b2e",
"shasum": ""
},
"require": {
@ -2738,20 +2737,20 @@
],
"description": "Symfony Form Component",
"homepage": "https://symfony.com",
"time": "2016-02-28 15:05:09"
"time": "2016-04-28 09:59:09"
},
{
"name": "symfony/intl",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
"reference": "cafee6f65148dab9058cdb6de5631222aca820d0"
"reference": "a602fe5a36cfc6367c5495b38a88c443570e75da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/cafee6f65148dab9058cdb6de5631222aca820d0",
"reference": "cafee6f65148dab9058cdb6de5631222aca820d0",
"url": "https://api.github.com/repos/symfony/intl/zipball/a602fe5a36cfc6367c5495b38a88c443570e75da",
"reference": "a602fe5a36cfc6367c5495b38a88c443570e75da",
"shasum": ""
},
"require": {
@ -2813,20 +2812,20 @@
"l10n",
"localization"
],
"time": "2016-02-23 15:16:06"
"time": "2016-04-01 06:34:33"
},
{
"name": "symfony/options-resolver",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "d1e6e9182d9e5af6367bf85175e708f8b4a828c0"
"reference": "5e4a8ee6e823428257f2002f6daf52de854d8384"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/d1e6e9182d9e5af6367bf85175e708f8b4a828c0",
"reference": "d1e6e9182d9e5af6367bf85175e708f8b4a828c0",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/5e4a8ee6e823428257f2002f6daf52de854d8384",
"reference": "5e4a8ee6e823428257f2002f6daf52de854d8384",
"shasum": ""
},
"require": {
@ -2867,7 +2866,7 @@
"configuration",
"options"
],
"time": "2016-01-21 09:05:51"
"time": "2016-05-09 18:12:35"
},
{
"name": "symfony/polyfill-intl-icu",
@ -2929,16 +2928,16 @@
},
{
"name": "symfony/process",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "dfecef47506179db2501430e732adbf3793099c8"
"reference": "53f9407c0bb1c5a79127db8f7bfe12f0f6f3dcdb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/dfecef47506179db2501430e732adbf3793099c8",
"reference": "dfecef47506179db2501430e732adbf3793099c8",
"url": "https://api.github.com/repos/symfony/process/zipball/53f9407c0bb1c5a79127db8f7bfe12f0f6f3dcdb",
"reference": "53f9407c0bb1c5a79127db8f7bfe12f0f6f3dcdb",
"shasum": ""
},
"require": {
@ -2974,20 +2973,20 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2016-02-02 13:44:19"
"time": "2016-04-14 15:30:28"
},
{
"name": "symfony/property-access",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/property-access.git",
"reference": "f138bcc0cdaf6a6aba99ecab20d235b394f46eba"
"reference": "d9deeca5d2f0dffd90f9547d3d9a2007bd6ee61b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/property-access/zipball/f138bcc0cdaf6a6aba99ecab20d235b394f46eba",
"reference": "f138bcc0cdaf6a6aba99ecab20d235b394f46eba",
"url": "https://api.github.com/repos/symfony/property-access/zipball/d9deeca5d2f0dffd90f9547d3d9a2007bd6ee61b",
"reference": "d9deeca5d2f0dffd90f9547d3d9a2007bd6ee61b",
"shasum": ""
},
"require": {
@ -3034,20 +3033,20 @@
"property path",
"reflection"
],
"time": "2016-02-13 09:23:44"
"time": "2016-04-20 18:53:54"
},
{
"name": "symfony/routing",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "ae38e64bae52753c0f4a1a6583f5ba9bb2b742ab"
"reference": "0d814e0c1dc07cb688ca2298bbf7b32ace80cee1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/ae38e64bae52753c0f4a1a6583f5ba9bb2b742ab",
"reference": "ae38e64bae52753c0f4a1a6583f5ba9bb2b742ab",
"url": "https://api.github.com/repos/symfony/routing/zipball/0d814e0c1dc07cb688ca2298bbf7b32ace80cee1",
"reference": "0d814e0c1dc07cb688ca2298bbf7b32ace80cee1",
"shasum": ""
},
"require": {
@ -3109,20 +3108,20 @@
"uri",
"url"
],
"time": "2016-02-04 13:53:00"
"time": "2016-05-03 12:21:46"
},
{
"name": "symfony/twig-bridge",
"version": "v2.8.3",
"version": "v2.8.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/twig-bridge.git",
"reference": "ba898e4714734948dc00c4d776e1d6be165ff8c4"
"reference": "789ffb6f63574d8d5573520ba12156f5c395fcd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/ba898e4714734948dc00c4d776e1d6be165ff8c4",
"reference": "ba898e4714734948dc00c4d776e1d6be165ff8c4",
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/789ffb6f63574d8d5573520ba12156f5c395fcd5",
"reference": "789ffb6f63574d8d5573520ba12156f5c395fcd5",
"shasum": ""
},
"require": {
@ -3134,7 +3133,7 @@
"symfony/console": "~2.8|~3.0.0",
"symfony/expression-language": "~2.4|~3.0.0",
"symfony/finder": "~2.3|~3.0.0",
"symfony/form": "~2.8",
"symfony/form": "~2.8.4",
"symfony/http-kernel": "~2.8|~3.0.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/routing": "~2.2|~3.0.0",
@ -3190,20 +3189,20 @@
],
"description": "Symfony Twig Bridge",
"homepage": "https://symfony.com",
"time": "2016-02-22 15:02:30"
"time": "2016-03-30 10:37:34"
},
{
"name": "symfony/yaml",
"version": "v3.0.3",
"version": "v3.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "b5ba64cd67ecd6887f63868fa781ca094bd1377c"
"reference": "0047c8366744a16de7516622c5b7355336afae96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/b5ba64cd67ecd6887f63868fa781ca094bd1377c",
"reference": "b5ba64cd67ecd6887f63868fa781ca094bd1377c",
"url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96",
"reference": "0047c8366744a16de7516622c5b7355336afae96",
"shasum": ""
},
"require": {
@ -3239,7 +3238,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2016-02-23 15:16:06"
"time": "2016-03-04 07:55:57"
},
{
"name": "twig/extensions",
@ -3344,23 +3343,23 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.2.0",
"version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "9caf304153dc2288e4970caec6f1f3b3bc205412"
"reference": "63f37b9395e8041cd4313129c08ece896d06ca8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/9caf304153dc2288e4970caec6f1f3b3bc205412",
"reference": "9caf304153dc2288e4970caec6f1f3b3bc205412",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/63f37b9395e8041cd4313129c08ece896d06ca8e",
"reference": "63f37b9395e8041cd4313129c08ece896d06ca8e",
"shasum": ""
},
"require": {
"php": ">=5.3.9"
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.0"
"phpunit/phpunit": "^4.8 || ^5.0"
},
"type": "library",
"extra": {
@ -3375,7 +3374,7 @@
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD"
"BSD-3-Clause-Attribution"
],
"authors": [
{
@ -3385,13 +3384,12 @@
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"homepage": "http://github.com/vlucas/phpdotenv",
"keywords": [
"dotenv",
"env",
"environment"
],
"time": "2015-12-29 15:10:30"
"time": "2016-04-15 10:48:49"
}
],
"aliases": [],

View File

@ -38,8 +38,11 @@ class Env {
self::$assets_path = self::$path . '/assets';
self::$assets_url = plugins_url('/assets', $file);
$wp_upload_dir = wp_upload_dir();
self::$temp_path = $wp_upload_dir['path'];
self::$temp_url = $wp_upload_dir['url'];
self::$temp_path = $wp_upload_dir['basedir'].'/'.self::$plugin_name;
if(!is_dir(self::$temp_path)) {
mkdir(self::$temp_path);
}
self::$temp_url = $wp_upload_dir['baseurl'].'/'.self::$plugin_name;
self::$languages_path = self::$path . '/lang';
self::$lib_path = self::$path . '/lib';
self::$plugin_prefix = 'mailpoet_';

View File

@ -9,14 +9,14 @@ class Hooks {
}
function init() {
$this->setupSubscribe();
$this->setupWPUsers();
$this->setupImageSize();
$this->setupListing();
$this->setupManageSubscription();
$this->setupSubscriptionEvents();
$this->setupPostNotifications();
}
function setupSubscribe() {
function setupSubscriptionEvents() {
$subscribe = Setting::getValue('subscribe', array());
// Subscribe in comments
if(
@ -98,7 +98,7 @@ class Hooks {
add_action(
'profile_update',
'\MailPoet\Segments\WP::synchronizeUser',
1,2
1, 2
);
add_action(
'delete_user',
@ -140,18 +140,6 @@ class Hooks {
);
}
function setupManageSubscription() {
// handle subscription form submission
add_action(
'admin_post_mailpoet_subscription_update',
'\MailPoet\Subscription\Manage::onSave'
);
add_action(
'admin_post_nopriv_mailpoet_subscription_update',
'\MailPoet\Subscription\Manage::onSave'
);
}
function setScreenOption($status, $option, $value) {
if(preg_match('/^mailpoet_(.*)_per_page$/', $option)) {
return $value;
@ -159,4 +147,12 @@ class Hooks {
return $status;
}
}
}
function setupPostNotifications() {
add_filter(
'publish_post',
'\MailPoet\Newsletter\Scheduler\Scheduler::schedulePostNotification',
10, 1
);
}
}

View File

@ -82,8 +82,12 @@ class Initializer {
$newsletter_option_fields = Env::$db_prefix . 'newsletter_option_fields';
$newsletter_option = Env::$db_prefix . 'newsletter_option';
$newsletter_links = Env::$db_prefix . 'newsletter_links';
$newsletter_posts = Env::$db_prefix . 'newsletter_posts';
$statistics_newsletters = Env::$db_prefix . 'statistics_newsletters';
$statistics_clicks = Env::$db_prefix . 'statistics_clicks';
$statistics_opens = Env::$db_prefix . 'statistics_opens';
$statistics_unsubscribes = Env::$db_prefix . 'statistics_unsubscribes';
$statistics_forms = Env::$db_prefix . 'statistics_forms';
define('MP_SETTINGS_TABLE', $settings);
define('MP_SEGMENTS_TABLE', $segments);
@ -98,9 +102,13 @@ class Initializer {
define('MP_NEWSLETTER_SEGMENT_TABLE', $newsletter_segment);
define('MP_NEWSLETTER_OPTION_FIELDS_TABLE', $newsletter_option_fields);
define('MP_NEWSLETTER_LINKS_TABLE', $newsletter_links);
define('MP_NEWSLETTER_POSTS_TABLE', $newsletter_posts);
define('MP_NEWSLETTER_OPTION_TABLE', $newsletter_option);
define('MP_STATISTICS_NEWSLETTERS_TABLE', $statistics_newsletters);
define('MP_STATISTICS_CLICKS_TABLE', $statistics_clicks);
define('MP_STATISTICS_OPENS_TABLE', $statistics_opens);
define('MP_STATISTICS_UNSUBSCRIBES_TABLE', $statistics_unsubscribes);
define('MP_STATISTICS_FORMS_TABLE', $statistics_forms);
}
function runMigrator() {

View File

@ -14,6 +14,7 @@ use MailPoet\Subscribers\ImportExport\BootStrapMenu;
use MailPoet\Util\DKIM;
use MailPoet\Util\Permissions;
use MailPoet\Listing;
use MailPoet\WP\DateTime;
if(!defined('ABSPATH')) exit;
@ -46,9 +47,9 @@ class Menu {
$this->assets_url . '/img/menu_icon.png',
30
);
add_submenu_page(
$newsletters_page = add_submenu_page(
'mailpoet',
__('Newsletters'),
$this->setPageTitle(__('Newsletters')),
__('Newsletters'),
'manage_options',
'mailpoet-newsletters',
@ -57,9 +58,21 @@ class Menu {
'newsletters'
)
);
add_submenu_page(
// add limit per page to screen options
add_action('load-'.$newsletters_page, function() {
add_screen_option('per_page', array(
'label' => _x(
'Number of newsletters per page',
'newsletters per page (screen options)'
),
'option' => 'mailpoet_newsletters_per_page'
));
});
$forms_page = add_submenu_page(
'mailpoet',
__('Forms'),
$this->setPageTitle(__('Forms')),
__('Forms'),
'manage_options',
'mailpoet-forms',
@ -68,9 +81,20 @@ class Menu {
'forms'
)
);
// add limit per page to screen options
add_action('load-'.$forms_page, function() {
add_screen_option('per_page', array(
'label' => _x(
'Number of forms per page',
'forms per page (screen options)'
),
'option' => 'mailpoet_forms_per_page'
));
});
$subscribers_page = add_submenu_page(
'mailpoet',
__('Subscribers'),
$this->setPageTitle(__('Subscribers')),
__('Subscribers'),
'manage_options',
'mailpoet-subscribers',
@ -90,9 +114,9 @@ class Menu {
));
});
add_submenu_page(
$segments_page = add_submenu_page(
'mailpoet',
__('Segments'),
$this->setPageTitle(__('Segments')),
__('Segments'),
'manage_options',
'mailpoet-segments',
@ -101,9 +125,21 @@ class Menu {
'segments'
)
);
// add limit per page to screen options
add_action('load-'.$segments_page, function() {
add_screen_option('per_page', array(
'label' => _x(
'Number of segments per page',
'segments per page (screen options)'
),
'option' => 'mailpoet_segments_per_page'
));
});
add_submenu_page(
'mailpoet',
__('Settings'),
$this->setPageTitle( __('Settings')),
__('Settings'),
'manage_options',
'mailpoet-settings',
@ -113,8 +149,8 @@ class Menu {
)
);
add_submenu_page(
null,
__('Import'),
'admin.php?page=mailpoet-subscribers',
$this->setPageTitle( __('Import')),
__('Import'),
'manage_options',
'mailpoet-import',
@ -123,9 +159,10 @@ class Menu {
'import'
)
);
add_submenu_page(
null,
__('Export'),
true,
$this->setPageTitle(__('Export')),
__('Export'),
'manage_options',
'mailpoet-export',
@ -136,8 +173,8 @@ class Menu {
);
add_submenu_page(
null,
__('Welcome'),
true,
$this->setPageTitle(__('Welcome')),
__('Welcome'),
'manage_options',
'mailpoet-welcome',
@ -148,8 +185,8 @@ class Menu {
);
add_submenu_page(
null,
__('Update'),
true,
$this->setPageTitle(__('Update')),
__('Update'),
'manage_options',
'mailpoet-update',
@ -160,8 +197,8 @@ class Menu {
);
add_submenu_page(
null,
__('Form editor'),
true,
$this->setPageTitle(__('Form')),
__('Form editor'),
'manage_options',
'mailpoet-form-editor',
@ -172,8 +209,8 @@ class Menu {
);
add_submenu_page(
null,
__('Newsletter editor'),
true,
$this->setPageTitle(__('Newsletter')),
__('Newsletter editor'),
'manage_options',
'mailpoet-newsletter-editor',
@ -185,7 +222,7 @@ class Menu {
add_submenu_page(
'mailpoet',
__('Cron'),
$this->setPageTitle(__('Cron')),
__('Cron'),
'manage_options',
'mailpoet-cron',
@ -317,15 +354,8 @@ class Menu {
function subscribers() {
$data = array();
// listing: limit per page
$listing_per_page = get_user_meta(
get_current_user_id(), 'mailpoet_subscribers_per_page', true
);
$data['per_page'] = (!empty($listing_per_page))
? (int)$listing_per_page
: Listing\Handler::DEFAULT_LIMIT_PER_PAGE;
$data['segments'] = Segment::findArray();
$data['items_per_page'] = $this->getLimitPerPage('subscribers');
$data['segments'] = Segment::getSegmentsWithSubscriberCount();
$data['custom_fields'] = array_map(function($field) {
$field['params'] = unserialize($field['params']);
@ -349,11 +379,14 @@ class Menu {
function segments() {
$data = array();
$data['items_per_page'] = $this->getLimitPerPage('segments');
echo $this->renderer->render('segments.html', $data);
}
function forms() {
$data = array();
$data['items_per_page'] = $this->getLimitPerPage('forms');
$data['segments'] = Segment::findArray();
echo $this->renderer->render('forms.html', $data);
@ -364,11 +397,24 @@ class Menu {
$data = array();
$data['items_per_page'] = $this->getLimitPerPage('newsletters');
$data['segments'] = Segment::getSegmentsWithSubscriberCount();
$data['settings'] = Setting::getAll();
$data['roles'] = $wp_roles->get_names();
$data['roles']['mailpoet_all'] = __('In any WordPress role');
$date_time = new DateTime();
$data['current_date'] = $date_time->getCurrentDate(DateTime::DEFAULT_DATE_FORMAT);
$data['current_time'] = $date_time->getCurrentTime();
$data['schedule_time_of_day'] = $date_time->getTimeInterval(
'00:00:00',
'+1 hour',
24
);
wp_enqueue_script('jquery-ui');
wp_enqueue_script('jquery-ui-datepicker');
echo $this->renderer->render('newsletters.html', $data);
}
@ -383,6 +429,7 @@ class Menu {
$data = array(
'customFields' => $custom_fields,
'settings' => Setting::getAll(),
'sub_menu' => 'mailpoet-newsletters'
);
wp_enqueue_media();
wp_enqueue_script('tinymce-wplink', includes_url('js/tinymce/plugins/wplink/plugin.js'));
@ -393,17 +440,19 @@ class Menu {
function import() {
$import = new BootStrapMenu('import');
$data = $import->bootstrap();
$data['sub_menu'] = 'mailpoet-subscribers';
echo $this->renderer->render('subscribers/importExport/import.html', $data);
}
function export() {
$export = new BootStrapMenu('export');
$data = $export->bootstrap();
$data['sub_menu'] = 'mailpoet-subscribers';
echo $this->renderer->render('subscribers/importExport/export.html', $data);
}
function formEditor() {
$id = (isset($_GET['id']) ? (int) $_GET['id'] : 0);
$id = (isset($_GET['id']) ? (int)$_GET['id'] : 0);
$form = Form::findOne($id);
if($form !== false) {
$form = $form->asArray();
@ -412,12 +461,12 @@ class Menu {
$data = array(
'form' => $form,
'pages' => Pages::getAll(),
'segments' => Segment::getPublic()
->findArray(),
'segments' => Segment::getPublic()->findArray(),
'styles' => FormRenderer::getStyles($form),
'date_types' => Block\Date::getDateTypes(),
'date_formats' => Block\Date::getDateFormats(),
'month_names' => Block\Date::getMonthNames()
'month_names' => Block\Date::getMonthNames(),
'sub_menu' => 'mailpoet-forms'
);
echo $this->renderer->render('form/editor.html', $data);
@ -426,4 +475,25 @@ class Menu {
function cron() {
echo $this->renderer->render('cron.html');
}
function setPageTitle($title) {
return sprintf(
'%s - %s',
__('MailPoet'),
$title
);
}
function getLimitPerPage($model = null) {
if($model === null) {
return Listing\Handler::DEFAULT_LIMIT_PER_PAGE;
}
$listing_per_page = get_user_meta(
get_current_user_id(), 'mailpoet_'.$model.'_per_page', true
);
return (!empty($listing_per_page))
? (int)$listing_per_page
: Listing\Handler::DEFAULT_LIMIT_PER_PAGE;
}
}

View File

@ -23,9 +23,13 @@ class Migrator {
'newsletter_option',
'newsletter_segment',
'newsletter_links',
'newsletter_posts',
'forms',
'statistics_newsletters',
'statistics_clicks',
'statistics_opens',
'statistics_unsubscribes',
'statistics_forms'
);
}
@ -56,10 +60,10 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'type varchar(90) NOT NULL DEFAULT "default",',
'description varchar(250) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'description varchar(250) NOT NULL DEFAULT "",',
'created_at TIMESTAMP NULL,',
'deleted_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name (name)'
);
@ -98,7 +102,6 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'newsletter_rendered_body longtext,',
'newsletter_rendered_body_hash varchar(250) NULL DEFAULT NULL,',
'newsletter_rendered_subject varchar(250) NULL DEFAULT NULL,',
'subscribers longtext,',
'status varchar(12) NULL DEFAULT NULL,',
@ -107,11 +110,11 @@ class Migrator {
'count_processed mediumint(9) NOT NULL DEFAULT 0,',
'count_to_process mediumint(9) NOT NULL DEFAULT 0,',
'count_failed mediumint(9) NOT NULL DEFAULT 0,',
'scheduled_at TIMESTAMP NOT NULL DEFAULT 0,',
'processed_at TIMESTAMP NOT NULL DEFAULT 0,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'scheduled_at TIMESTAMP NULL,',
'processed_at TIMESTAMP NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -121,13 +124,13 @@ class Migrator {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'wp_user_id bigint(20) NULL,',
'first_name tinytext NOT NULL,',
'last_name tinytext NOT NULL,',
'first_name tinytext NOT NULL DEFAULT "",',
'last_name tinytext NOT NULL DEFAULT "",',
'email varchar(150) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "unconfirmed",',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id),',
'UNIQUE KEY email (email)'
);
@ -140,8 +143,8 @@ class Migrator {
'subscriber_id mediumint(9) NOT NULL,',
'segment_id mediumint(9) NOT NULL,',
'status varchar(12) NOT NULL DEFAULT "subscribed",',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY subscriber_segment (subscriber_id,segment_id)'
);
@ -153,9 +156,9 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'subscriber_id mediumint(9) NOT NULL,',
'custom_field_id mediumint(9) NOT NULL,',
'value varchar(255) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'value varchar(255) NOT NULL DEFAULT "",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY subscriber_id_custom_field_id (subscriber_id,custom_field_id)'
);
@ -165,17 +168,17 @@ class Migrator {
function newsletters() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'subject varchar(250) NOT NULL,',
'subject varchar(250) NOT NULL DEFAULT "",',
'type varchar(20) NOT NULL DEFAULT "standard",',
'sender_address varchar(150) NOT NULL,',
'sender_name varchar(150) NOT NULL,',
'reply_to_address varchar(150) NOT NULL,',
'reply_to_name varchar(150) NOT NULL,',
'preheader varchar(250) NOT NULL,',
'sender_address varchar(150) NOT NULL DEFAULT "",',
'sender_name varchar(150) NOT NULL DEFAULT "",',
'reply_to_address varchar(150) NOT NULL DEFAULT "",',
'reply_to_name varchar(150) NOT NULL DEFAULT "",',
'preheader varchar(250) NOT NULL DEFAULT "",',
'body longtext,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -189,8 +192,8 @@ class Migrator {
'body LONGTEXT,',
'thumbnail LONGTEXT,',
'readonly TINYINT(1) DEFAULT 0,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -201,8 +204,8 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'name varchar(90) NOT NULL,',
'newsletter_type varchar(90) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY name_newsletter_type (newsletter_type,name)'
);
@ -214,9 +217,9 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'option_field_id mediumint(9) NOT NULL,',
'value varchar(255) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'value varchar(255) NOT NULL DEFAULT "",',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY newsletter_id_option_field_id (newsletter_id,option_field_id)'
);
@ -228,8 +231,8 @@ class Migrator {
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'segment_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'UNIQUE KEY newsletter_segment (newsletter_id,segment_id)'
);
@ -243,6 +246,18 @@ class Migrator {
'queue_id mediumint(9) NOT NULL,',
'url varchar(255) NOT NULL,',
'hash varchar(20) NOT NULL,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function newsletter_posts() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'post_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
@ -257,9 +272,9 @@ class Migrator {
'body longtext,',
'settings longtext,',
'styles longtext,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'deleted_at TIMESTAMP NULL,',
'PRIMARY KEY (id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -271,7 +286,7 @@ class Migrator {
'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'sent_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'sent_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
@ -285,14 +300,49 @@ class Migrator {
'queue_id mediumint(9) NOT NULL,',
'link_id mediumint(9) NOT NULL,',
'count mediumint(9) NOT NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT 0,',
'deleted_at TIMESTAMP NULL DEFAULT NULL,',
'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'created_at TIMESTAMP NULL,',
'updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_opens() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_unsubscribes() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'newsletter_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'queue_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'PRIMARY KEY (id)',
);
return $this->sqlify(__FUNCTION__, $attributes);
}
function statistics_forms() {
$attributes = array(
'id mediumint(9) NOT NULL AUTO_INCREMENT,',
'form_id mediumint(9) NOT NULL,',
'subscriber_id mediumint(9) NOT NULL,',
'created_at TIMESTAMP NULL,',
'PRIMARY KEY (id),',
'UNIQUE KEY form_subscriber (form_id,subscriber_id)'
);
return $this->sqlify(__FUNCTION__, $attributes);
}
private function sqlify($model, $attributes) {
$table = $this->prefix . $model;

View File

@ -10,7 +10,7 @@ use \MailPoet\Segments\WP;
use \MailPoet\Models\Setting;
use \MailPoet\Settings\Pages;
if (!defined('ABSPATH')) exit;
if(!defined('ABSPATH')) exit;
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
@ -36,7 +36,7 @@ class Populator {
$column_conditions = array_map(function($key) use ($field) {
return $key . '=' . $field[$key];
}, $field);
if ($wpdb->get_var("SELECT COUNT(*) FROM " . $table . " WHERE " . implode(' AND ', $column_conditions)) === 0) {
if($wpdb->get_var("SELECT COUNT(*) FROM " . $table . " WHERE " . implode(' AND ', $column_conditions)) === 0) {
$wpdb->insert(
$table,
$field
@ -140,6 +140,14 @@ class Populator {
function newsletter_option_fields() {
return array(
array(
'name' => 'isScheduled',
'newsletter_type' => 'standard',
),
array(
'name' => 'scheduledAt',
'newsletter_type' => 'standard',
),
array(
'name' => 'event',
'newsletter_type' => 'welcome',
@ -184,10 +192,6 @@ class Populator {
array(
'name' => 'schedule',
'newsletter_type' => 'notification',
),
array(
'name' => 'segments',
'newsletter_type' => 'notification',
)
);
}
@ -207,7 +211,7 @@ class Populator {
$_this = $this;
array_map(function($row) use ($_this, $table) {
if (!$_this->rowExists($table, $row)) {
if(!$_this->rowExists($table, $row)) {
$_this->insertRow($table, $row);
}
}, $rows);

View File

@ -142,7 +142,7 @@ class BlankTemplate {
"blocks" => array(
array(
"type" => "footer",
"text" => "<a href=\"[subscription:unsubscribe_url]\">Unsubscribe</a> | <a href=\"[subscription:manage_url]\">Manage subscription</a><br /><b>Add your postal address here!</b>",
"text" => "<a href=\"[link:subscription_unsubscribe_url]\">Unsubscribe</a> | <a href=\"[link:subscription_manage_url]\">Manage subscription</a><br /><b>Add your postal address here!</b>",
"styles" => array(
"block" => array(
"backgroundColor" => "transparent"

View File

@ -46,7 +46,7 @@ class FranksRoastHouseTemplate {
"blocks" => array(
array(
"type" => "header",
"text" => __("Display problems?&nbsp;<a href=\"[newsletter:view_in_browser_url]\">View it in your browser</a>"),
"text" => __("Display problems?&nbsp;<a href=\"[link:newsletter_view_in_browser_url]\">View it in your browser</a>"),
"styles" => array(
"block" => array(
"backgroundColor" => "#ccc6c6"
@ -280,7 +280,7 @@ class FranksRoastHouseTemplate {
"blocks" => array(
array(
"type" => "footer",
"text" => __("<p><a href=\"[subscription:unsubscribe_url]\">Unsubscribe</a> | <a href=\"[subscription:manage_url]\">Manage subscription</a><br />12345 MailPoet Drive, EmailVille, 76543</p>"),
"text" => __("<p><a href=\"[link:subscription_unsubscribe_url]\">Unsubscribe</a> | <a href=\"[link:subscription_manage_url]\">Manage subscription</a><br />12345 MailPoet Drive, EmailVille, 76543</p>"),
"styles" => array(
"block" => array(
"backgroundColor" => "#a9a7a7"

View File

@ -242,7 +242,7 @@ class PostNotificationsBlankTemplate {
"blocks" => array(
array(
"type" => "footer",
"text" => __("<a href=\"[subscription:unsubscribe_url]\">Unsubscribe</a> | <a href=\"[subscription:manage_url]\">Manage subscription</a><br /><b>Add your postal address here!</b>"),
"text" => __("<a href=\"[link:subscription_unsubscribe_url]\">Unsubscribe</a> | <a href=\"[link:subscription_manage_url]\">Manage subscription</a><br /><b>Add your postal address here!</b>"),
"styles" => array(
"block" => array(
"backgroundColor" => "transparent"

View File

@ -46,7 +46,7 @@ class WelcomeTemplate {
"blocks" => array(
array(
"type" => "header",
"text" => __("Display problems?&nbsp;<a href=\"[newsletter:view_in_browser_url]\">View it in your browser</a>"),
"text" => __("Display problems?&nbsp;<a href=\"[link:newsletter_view_in_browser_url]\">View it in your browser</a>"),
"styles" => array(
"block" => array(
"backgroundColor" => "transparent"
@ -224,7 +224,7 @@ class WelcomeTemplate {
"blocks" => array(
array(
"type" => "footer",
"text" => __("<a href=\"[subscription:unsubscribe_url]\">Unsubscribe</a> | <a href=\"[subscription:manage_url]\">Manage subscription</a><br /><b>Add your postal address here!</b>"),
"text" => __("<a href=\"[link:subscription_unsubscribe_url]\">Unsubscribe</a> | <a href=\"[link:subscription_manage_url]\">Manage subscription</a><br /><b>Add your postal address here!</b>"),
"styles" => array(
"block" => array(
"backgroundColor" => "transparent"

View File

@ -2,8 +2,10 @@
namespace MailPoet\Config;
use MailPoet\Cron\Daemon;
use MailPoet\Subscription;
use MailPoet\Newsletter\Viewer\ViewInBrowser;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
use MailPoet\Subscription;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
@ -24,7 +26,7 @@ class PublicAPI {
Helpers::underscoreToCamelCase($_GET['action']) :
false;
$this->data = isset($_GET['data']) ?
$_GET['data'] :
unserialize(base64_decode($_GET['data'])) :
false;
}
@ -34,30 +36,29 @@ class PublicAPI {
}
function queue() {
try {
$queue = new Daemon($this->_decodeData());
$this->_checkAndCallMethod($queue, $this->action);
} catch(\Exception $e) {
}
$queue = new Daemon($this->data);
$this->_checkAndCallMethod($queue, $this->action);
}
function subscription() {
try {
$subscription = new Subscription\Pages($this->action, $this->_decodeData());
$this->_checkAndCallMethod($subscription, $this->action);
} catch(\Exception $e) {
}
$subscription = new Subscription\Pages($this->action, $this->data);
$this->_checkAndCallMethod($subscription, $this->action);
}
function track() {
try {
if($this->action === 'click') {
$track_class = new Clicks($this->data);
}
if(!isset($track_class)) return;
$track_class->track();
} catch(\Exception $e) {
if($this->action === 'click') {
$track_class = new Clicks($this->data);
}
if($this->action === 'open') {
$track_class = new Opens($this->data);
}
if(!isset($track_class)) return;
$track_class->track();
}
function viewInBrowser() {
$viewer = new ViewInBrowser($this->data);
$viewer->view();
}
private function _checkAndCallMethod($class, $method, $terminate_request = false) {
@ -73,12 +74,4 @@ class PublicAPI {
)
);
}
private function _decodeData() {
if($this->data !== false) {
return unserialize(base64_decode($this->data));
} else {
return array();
}
}
}

View File

@ -7,13 +7,13 @@ use MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
class CronHelper {
const daemon_execution_limit = 20;
const daemon_execution_timeout = 35;
const daemon_request_timeout = 2;
const DAEMON_EXECUTION_LIMIT = 20;
const DAEMON_EXECUTION_TIMEOUT = 35;
const DAEMON_REQUEST_TIMEOUT = 2;
static function createDaemon($token) {
$daemon = array(
'status' => 'starting',
'status' => Daemon::STATUS_STARTING,
'counter' => 0,
'token' => $token
);
@ -37,7 +37,7 @@ class CronHelper {
return Security::generateRandomString();
}
static function accessDaemon($token, $timeout = self::daemon_request_timeout) {
static function accessDaemon($token, $timeout = self::DAEMON_REQUEST_TIMEOUT) {
$data = serialize(array('token' => $token));
$url = '/?mailpoet&endpoint=queue&action=run&data=' .
base64_encode($data);
@ -71,7 +71,7 @@ class CronHelper {
static function checkExecutionTimer($timer) {
$elapsed_time = microtime(true) - $timer;
if($elapsed_time >= self::daemon_execution_limit) {
if($elapsed_time >= self::DAEMON_EXECUTION_LIMIT) {
throw new \Exception(__('Maximum execution time reached.'));
}
}

View File

@ -12,12 +12,15 @@ class Daemon {
public $daemon;
public $data;
public $refreshed_token;
const daemon_request_timeout = 5;
const STATUS_STOPPED = 'stopped';
const STATUS_STOPPING = 'stopping';
const STATUS_STARTED = 'started';
const STATUS_STARTING = 'starting';
const REQUEST_TIMEOUT = 5;
private $timer;
function __construct($data) {
if(empty($data)) $this->abortWithError(__('Invalid or missing cron data.'));
set_time_limit(0);
ignore_user_abort();
$this->daemon = CronHelper::getDaemon();
$this->token = CronHelper::createToken();
@ -27,6 +30,7 @@ class Daemon {
function run() {
$daemon = $this->daemon;
set_time_limit(0);
if(!$daemon) {
$this->abortWithError(__('Daemon does not exist.'));
}
@ -42,10 +46,11 @@ class Daemon {
$queue = new SendingQueue();
$queue->process($this->timer);
} catch(\Exception $e) {
// continue processing, no need to catch errors
}
$elapsed_time = microtime(true) - $this->timer;
if($elapsed_time < CronHelper::daemon_execution_limit) {
sleep(CronHelper::daemon_execution_limit - $elapsed_time);
if($elapsed_time < CronHelper::DAEMON_EXECUTION_LIMIT) {
sleep(CronHelper::DAEMON_EXECUTION_LIMIT - $elapsed_time);
}
// after each execution, re-read daemon data in case it was deleted or
// its status has changed
@ -55,8 +60,8 @@ class Daemon {
}
$daemon['counter']++;
$this->abortIfStopped($daemon);
if($daemon['status'] === 'starting') {
$daemon['status'] = 'started';
if($daemon['status'] === self::STATUS_STARTING) {
$daemon['status'] = self::STATUS_STARTED;
}
$daemon['token'] = $this->token;
CronHelper::saveDaemon($daemon);
@ -64,9 +69,9 @@ class Daemon {
}
function abortIfStopped($daemon) {
if($daemon['status'] === 'stopped') exit;
if($daemon['status'] === 'stopping') {
$daemon['status'] = 'stopped';
if($daemon['status'] === self::STATUS_STOPPED) exit;
if($daemon['status'] === self::STATUS_STOPPING) {
$daemon['status'] = self::STATUS_STOPPED;
CronHelper::saveDaemon($daemon);
exit;
}
@ -77,7 +82,7 @@ class Daemon {
}
function callSelf() {
CronHelper::accessDaemon($this->token, self::daemon_request_timeout);
CronHelper::accessDaemon($this->token, self::REQUEST_TIMEOUT);
exit;
}
}

View File

@ -23,25 +23,25 @@ class Supervisor {
// if the daemon is stopped, return its status and do nothing
if(!$this->force_run &&
isset($daemon['status']) &&
$daemon['status'] === 'stopped'
$daemon['status'] === Daemon::STATUS_STOPPED
) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
$elapsed_time = time() - (int) $daemon['updated_at'];
$elapsed_time = time() - (int)$daemon['updated_at'];
// if it's been less than 40 seconds since last execution and we're not
// force-running the daemon, return its status and do nothing
if($elapsed_time < CronHelper::daemon_execution_timeout && !$this->force_run) {
if($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT && !$this->force_run) {
return $this->formatDaemonStatusMessage($daemon['status']);
}
// if it's been less than 40 seconds since last execution, we are
// force-running the daemon and it's either being started or stopped,
// return its status and do nothing
elseif($elapsed_time < CronHelper::daemon_execution_timeout &&
elseif($elapsed_time < CronHelper::DAEMON_EXECUTION_TIMEOUT &&
$this->force_run &&
in_array($daemon['status'], array(
'stopping',
'starting'
Daemon::STATUS_STOPPING,
Daemon::STATUS_STARTING
))
) {
return $this->formatDaemonStatusMessage($daemon['status']);

View File

@ -2,14 +2,13 @@
namespace MailPoet\Cron\Workers;
use Carbon\Carbon;
use Cron\CronExpression as Cron;
use MailPoet\Cron\CronHelper;
use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Subscriber;
use MailPoet\Models\SubscriberSegment;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Util\Helpers;
use MailPoet\Newsletter\Scheduler\Scheduler as NewsletterScheduler;
require_once(ABSPATH . 'wp-includes/pluggable.php');
@ -17,6 +16,7 @@ if(!defined('ABSPATH')) exit;
class Scheduler {
public $timer;
const UNCONFIRMED_SUBSCRIBER_RESCHEDULE_TIMEOUT = 5;
function __construct($timer = false) {
$this->timer = ($timer) ? $timer : microtime(true);
@ -28,17 +28,17 @@ class Scheduler {
->whereLte('scheduled_at', Carbon::createFromTimestamp(current_time('timestamp')))
->findMany();
if(!count($scheduled_queues)) return;
foreach($scheduled_queues as $queue) {
foreach($scheduled_queues as $i => $queue) {
$newsletter = Newsletter::filter('filterWithOptions')
->findOne($queue->newsletter_id);
if(!$newsletter || $newsletter->deleted_at !== null) {
$queue->delete();
}
else if($newsletter->type === 'welcome') {
} elseif($newsletter->type === 'welcome') {
$this->processWelcomeNewsletter($newsletter, $queue);
}
else if($newsletter->type === 'notification') {
} elseif($newsletter->type === 'notification') {
$this->processPostNotificationNewsletter($newsletter, $queue);
} elseif($newsletter->type === 'standard') {
$this->processScheduledStandardNewsletter($newsletter, $queue);
}
CronHelper::checkExecutionTimer($this->timer);
}
@ -46,16 +46,20 @@ class Scheduler {
function processWelcomeNewsletter($newsletter, $queue) {
$subscriber = unserialize($queue->subscribers);
$subscriber_id = $subscriber['to_process'][0];
if(empty($subscriber['to_process'][0])) {
$queue->delete();
return;
}
$subscriber_id = (int)$subscriber['to_process'][0];
if($newsletter->event === 'segment') {
if ($this->verifyMailPoetSubscriber($subscriber_id, $newsletter, $queue) === false) {
if($this->verifyMailPoetSubscriber($subscriber_id, $newsletter, $queue) === false) {
return;
}
}
else if($newsletter->event === 'user') {
if ($this->verifyWPSubscriber($subscriber_id, $newsletter) === false) {
$queue->delete();
return;
} else {
if($newsletter->event === 'user') {
if($this->verifyWPSubscriber($subscriber_id, $newsletter, $queue) === false) {
return;
}
}
}
$queue->status = null;
@ -63,17 +67,50 @@ class Scheduler {
}
function processPostNotificationNewsletter($newsletter, $queue) {
$next_run_date = $this->getQueueNextRunDate($newsletter->schedule);
$segments = unserialize($newsletter->segments);
$subscribers = SubscriberSegment::whereIn('segment_id', $segments)
$segments = $newsletter->segments()->findArray();
if(empty($segments)) {
$this->deleteQueueOrUpdateNextRunDate($queue, $newsletter);
return;
}
$segment_ids = array_map(function($segment) {
return $segment['id'];
}, $segments);
$subscribers = Subscriber::getSubscribedInSegments($segment_ids)
->findArray();
$subscribers = Helpers::arrayColumn($subscribers, 'subscriber_id');
$subscribers = array_unique($subscribers);
if(!count($subscribers) || !$this->checkIfNewsletterChanged($newsletter)) {
$queue->scheduled_at = $next_run_date;
$queue->save();
if(empty($subscribers)) {
$this->deleteQueueOrUpdateNextRunDate($queue, $newsletter);
return;
}
// schedule new queue if the post notification is not destined for immediate delivery
if ($newsletter->intervalType !== NewsletterScheduler::INTERVAL_IMMEDIATELY) {
$new_queue = SendingQueue::create();
$new_queue->newsletter_id = $newsletter->id;
$new_queue->status = NewsletterScheduler::STATUS_SCHEDULED;
self::deleteQueueOrUpdateNextRunDate($new_queue, $newsletter);
}
$queue->subscribers = serialize(
array(
'to_process' => $subscribers
)
);
$queue->count_total = $queue->count_to_process = count($subscribers);
$queue->status = null;
$queue->save();
}
function processScheduledStandardNewsletter($newsletter, $queue) {
$segments = $newsletter->segments()->findArray();
$segment_ids = array_map(function($segment) {
return $segment['id'];
}, $segments);
$subscribers = Subscriber::getSubscribedInSegments($segment_ids)
->findArray();
$subscribers = Helpers::arrayColumn($subscribers, 'subscriber_id');
$subscribers = array_unique($subscribers);
// update current queue
$queue->subscribers = serialize(
array(
@ -83,66 +120,58 @@ class Scheduler {
$queue->count_total = $queue->count_to_process = count($subscribers);
$queue->status = null;
$queue->save();
// schedule newsletter for next delivery
$new_queue = SendingQueue::create();
$new_queue->newsletter_id = $newsletter->id;
$new_queue->scheduled_at = $next_run_date;
$new_queue->status = 'scheduled';
$new_queue->save();
}
private function verifyMailPoetSubscriber($subscriber_id, $newsletter, $queue) {
function verifyMailPoetSubscriber($subscriber_id, $newsletter, $queue) {
$subscriber = Subscriber::findOne($subscriber_id);
// check if subscriber is in proper segment
$subscriber_in_segment =
SubscriberSegment::where('subscriber_id', $subscriber_id)
->where('segment_id', $newsletter->segment)
->where('status', 'subscribed')
->findOne();
if (!$subscriber_in_segment) {
if(!$subscriber || !$subscriber_in_segment) {
$queue->delete();
return false;
}
// check if subscriber is confirmed (subscribed)
$subscriber = $subscriber_in_segment->subscriber()->findOne();
if ($subscriber->status !== 'subscribed') {
if($subscriber->status !== 'subscribed') {
// reschedule delivery in 5 minutes
$scheduled_at = Carbon::createFromTimestamp(current_time('timestamp'));
$queue->scheduled_at = $scheduled_at->addMinutes(5);
$queue->scheduled_at = $scheduled_at->addMinutes(
self::UNCONFIRMED_SUBSCRIBER_RESCHEDULE_TIMEOUT
);
$queue->save();
return false;
}
return true;
}
private function verifyWPSubscriber($subscriber_id, $newsletter) {
function verifyWPSubscriber($subscriber_id, $newsletter, $queue) {
// check if user has the proper role
$subscriber = Subscriber::findOne($subscriber_id);
if(!$subscriber || $subscriber->wp_user_id === null) {
$queue->delete();
return false;
}
$wp_user = (array) get_userdata($subscriber->wp_user_id);
if(!in_array($newsletter->role, $wp_user['roles'])) {
if($newsletter->role !== \MailPoet\Newsletter\Scheduler\Scheduler::WORDPRESS_ALL_ROLES
&& !in_array($newsletter->role, $wp_user['roles'])
) {
$queue->delete();
return false;
}
return true;
}
private function checkIfNewsletterChanged($newsletter) {
$last_run_queue = SendingQueue::where('status', 'completed')
->where('newsletter_id', $newsletter->id)
->orderByDesc('id')
->findOne();
if(!$last_run_queue) return true;
$renderer = new Renderer($newsletter->asArray());
$rendered_newsletter = $renderer->render();
$new_hash = md5($rendered_newsletter['html']);
$old_hash = $last_run_queue->newsletter_rendered_body_hash;
return $new_hash === $old_hash;
}
private function getQueueNextRunDate($schedule) {
$schedule = Cron::factory($schedule);
return $schedule->getNextRunDate(current_time('mysql'))
->format('Y-m-d H:i:s');
private function deleteQueueOrUpdateNextRunDate($queue, $newsletter) {
if($newsletter->intervalType === NewsletterScheduler::INTERVAL_IMMEDIATELY) {
$queue->delete();
} else {
$next_run_date = NewsletterScheduler::getNextRunDate($newsletter->schedule);
$queue->scheduled_at = $next_run_date;
$queue->save();
}
return;
}
}

View File

@ -4,11 +4,12 @@ namespace MailPoet\Cron\Workers;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterLink;
use MailPoet\Models\NewsletterPost;
use MailPoet\Models\Setting;
use MailPoet\Models\StatisticsNewsletters;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Links\Links;
use MailPoet\Newsletter\Renderer\PostProcess\OpenTracking;
use MailPoet\Newsletter\Renderer\Renderer;
use MailPoet\Newsletter\Shortcodes\Shortcodes;
use MailPoet\Util\Helpers;
@ -19,9 +20,10 @@ class SendingQueue {
public $mta_config;
public $mta_log;
public $processing_method;
public $divider = '***MailPoet***';
private $timer;
const batch_size = 50;
const BATCH_SIZE = 50;
const DIVIDER = '***MailPoet***';
const STATUS_COMPLETED = 'completed';
function __construct($timer = false) {
$this->mta_config = $this->getMailerConfig();
@ -42,6 +44,12 @@ class SendingQueue {
}
$newsletter = $newsletter->asArray();
$newsletter['body'] = $this->getOrRenderNewsletterBody($queue, $newsletter);
if($newsletter['type'] === 'notification' &&
strpos($newsletter['body']['html'], 'data-post-id') === false
){
$queue->delete();
continue;
}
$queue->subscribers = (object) unserialize($queue->subscribers);
if(!isset($queue->subscribers->processed)) {
$queue->subscribers->processed = array();
@ -50,10 +58,21 @@ class SendingQueue {
$queue->subscribers->failed = array();
}
$mailer = $this->configureMailer($newsletter);
foreach(array_chunk($queue->subscribers->to_process, self::batch_size) as
foreach(array_chunk($queue->subscribers->to_process, self::BATCH_SIZE) as
$subscribers_ids) {
$subscribers = Subscriber::whereIn('id', $subscribers_ids)
->findArray();
if(count($subscribers_ids) !== count($subscribers)) {
$queue->subscribers->to_process = $this->recalculateSubscriberCount(
Helpers::arrayColumn($subscribers, 'id'),
$subscribers_ids,
$queue->subscribers->to_process
);
}
if(!count($queue->subscribers->to_process)) {
$this->updateQueue($queue);
continue;
}
$queue->subscribers = call_user_func_array(
array(
$this,
@ -74,11 +93,15 @@ class SendingQueue {
// check if newsletter has been rendered, in which case return its contents
// or render and save for future reuse
if($queue->newsletter_rendered_body === null) {
// render newsletter
$rendered_newsletter = $this->renderNewsletter($newsletter);
if((boolean) Setting::getValue('tracking.enabled')) {
// extract and replace links
$processed_newsletter = $this->processLinks(
// insert tracking code
add_filter('mailpoet_rendering_post_process', function($template) {
return OpenTracking::process($template);
});
// render newsletter
$rendered_newsletter = $this->renderNewsletter($newsletter);
// process link shortcodes, extract and save links in the database
$processed_newsletter = $this->processLinksAndShortcodes(
$this->joinObject($rendered_newsletter),
$newsletter['id'],
$queue->id
@ -87,10 +110,14 @@ class SendingQueue {
$this->splitObject($processed_newsletter);
}
else {
$newsletter['body'] = $rendered_newsletter;
// render newsletter
$newsletter['body'] = $this->renderNewsletter($newsletter);
}
$this->extractAndSaveNewsletterPosts(
$newsletter['id'],
$newsletter['body']['html']
);
$queue->newsletter_rendered_body = json_encode($newsletter['body']);
$queue->newsletter_rendered_body_hash = md5($newsletter['body']['text']);
$queue->save();
} else {
$newsletter['body'] = json_decode($queue->newsletter_rendered_body);
@ -101,7 +128,10 @@ class SendingQueue {
function processBulkSubscribers($mailer, $newsletter, $subscribers, $queue) {
foreach($subscribers as $subscriber) {
$processed_newsletters[] =
$this->processNewsletter($newsletter, $subscriber, $queue);
$this->processNewsletterBeforeSending($newsletter, $subscriber, $queue);
if(!$queue->newsletter_rendered_subject) {
$queue->newsletter_rendered_subject = $processed_newsletters[0]['subject'];
}
$transformed_subscribers[] =
$mailer->transformSubscriber($subscriber);
}
@ -142,7 +172,7 @@ class SendingQueue {
function processIndividualSubscriber($mailer, $newsletter, $subscribers, $queue) {
foreach($subscribers as $subscriber) {
$this->checkSendingLimit();
$processed_newsletter = $this->processNewsletter($newsletter, $subscriber, $queue);
$processed_newsletter = $this->processNewsletterBeforeSending($newsletter, $subscriber, $queue);
if(!$queue->newsletter_rendered_subject) {
$queue->newsletter_rendered_subject = $processed_newsletter['subject'];
}
@ -179,22 +209,26 @@ class SendingQueue {
return $renderer->render();
}
function processLinks($text, $newsletter_id, $queue_id) {
$links = new Links();
list($text, $processed_links) = $links->replace($text);
foreach($processed_links as $link) {
// save extracted and processed links
$newsletter_link = NewsletterLink::create();
$newsletter_link->newsletter_id = $newsletter_id;
$newsletter_link->queue_id = $queue_id;
$newsletter_link->hash = $link['hash'];
$newsletter_link->url = $link['url'];
$newsletter_link->save();
}
return $text;
function processLinksAndShortcodes($content, $newsletter_id, $queue_id) {
// process only link shortcodes
$shortcodes = new Shortcodes($newsletter = false, $subscriber = false, $queue_id);
$content = $shortcodes->replace(
$content,
$categories = array('link')
);
// extract and save links and link shortcodes
list($content, $processed_links) =
Links::process(
$content,
$links = false,
$process_link_shortcodes = true,
$queue_id
);
Links::save($processed_links, $newsletter_id, $queue_id);
return $content;
}
function processNewsletter($newsletter, $subscriber = false, $queue) {
function processNewsletterBeforeSending($newsletter, $subscriber = false, $queue) {
$data_for_shortcodes = array(
$newsletter['subject'],
$newsletter['body']['html'],
@ -203,10 +237,11 @@ class SendingQueue {
$processed_newsletter = $this->replaceShortcodes(
$newsletter,
$subscriber,
$queue,
$this->joinObject($data_for_shortcodes)
);
if((boolean) Setting::getValue('tracking.enabled')) {
$processed_newsletter = $this->replaceLinks(
$processed_newsletter = Links::replaceSubscriberData(
$newsletter['id'],
$subscriber['id'],
$queue->id,
@ -220,18 +255,11 @@ class SendingQueue {
return $newsletter;
}
function replaceLinks($newsletter_id, $subscriber_id, $queue_id, $body) {
return str_replace(
'[mailpoet_data]',
sprintf('%s-%s-%s', $newsletter_id, $subscriber_id, $queue_id),
$body
);
}
function replaceShortcodes($newsletter, $subscriber, $body) {
function replaceShortcodes($newsletter, $subscriber, $queue, $body) {
$shortcodes = new Shortcodes(
$newsletter,
$subscriber
$subscriber,
$queue
);
return $shortcodes->replace($body);
}
@ -292,8 +320,8 @@ class SendingQueue {
$queue->count_total =
$queue->count_processed + $queue->count_to_process;
if(!$queue->count_to_process) {
$queue->processed_at = date('Y-m-d H:i:s');
$queue->status = 'completed';
$queue->processed_at = current_time('mysql');
$queue->status = self::STATUS_COMPLETED;
}
$queue->subscribers = serialize((array) $queue->subscribers);
$queue->save();
@ -325,9 +353,9 @@ class SendingQueue {
}
function checkSendingLimit() {
$frequency_interval = (int) $this->mta_config['frequency']['interval'] * 60;
$frequency_limit = (int) $this->mta_config['frequency']['emails'];
$elapsed_time = time() - (int) $this->mta_log['started'];
$frequency_interval = (int)$this->mta_config['frequency']['interval'] * 60;
$frequency_limit = (int)$this->mta_config['frequency']['emails'];
$elapsed_time = time() - (int)$this->mta_log['started'];
if($this->mta_log['sent'] === $frequency_limit &&
$elapsed_time <= $frequency_interval
) {
@ -343,11 +371,28 @@ class SendingQueue {
return;
}
function recalculateSubscriberCount(
$found_subscriber, $existing_subscribers, $subscribers_to_process) {
$subscibers_to_exclude = array_diff($existing_subscribers, $found_subscriber);
return array_diff($subscribers_to_process, $subscibers_to_exclude);
}
function extractAndSaveNewsletterPosts($newletter_id, $content) {
preg_match_all('/data-post-id="(\d+)"/ism', $content, $posts);
$posts = $posts[1];
foreach($posts as $post) {
$newletter_post = NewsletterPost::create();
$newletter_post->newsletter_id = $newletter_id;
$newletter_post->post_id = $post;
$newletter_post->save();
}
}
private function joinObject($object = array()) {
return implode($this->divider, $object);
return implode(self::DIVIDER, $object);
}
private function splitObject($object = array()) {
return explode($this->divider, $object);
return explode(self::DIVIDER, $object);
}
}

View File

@ -109,6 +109,10 @@ class Date extends Base {
$month_names = static::getMonthNames();
$html = '';
// empty value label
$html .= '<option value="">'.__('Month').'</option>';
for($i = 1; $i < 13; $i++) {
$is_selected = ($i === $block['selected']) ? 'selected="selected"' : '';
$html .= '<option value="'.$i.'" '.$is_selected.'>';
@ -125,6 +129,7 @@ class Date extends Base {
'from' => (int)strftime('%Y') - 100,
'to' => (int)strftime('%Y')
);
// is default today
if(!empty($block['params']['is_default_today'])) {
$defaults['selected'] = (int)strftime('%Y');
@ -135,6 +140,9 @@ class Date extends Base {
$html = '';
// empty value label
$html .= '<option value="">'.__('Year').'</option>';
// return years as an array
for($i = (int)$block['to']; $i > (int)($block['from'] - 1); $i--) {
$is_selected = ($i === $block['selected']) ? 'selected="selected"' : '';
@ -158,6 +166,9 @@ class Date extends Base {
$html = '';
// empty value label
$html .= '<option value="">'.__('Day').'</option>';
// return days as an array
for($i = 1; $i < 32; $i++) {
$is_selected = ($i === $block['selected']) ? 'selected="selected"' : '';

View File

@ -13,9 +13,12 @@ class Select extends Base {
$html .= static::renderLabel($block);
$html .= '<select class="mailpoet_select" name="'.$field_name.'">';
if(isset($block['params']['label_within'])
&& $block['params']['label_within']) {
if(isset($block['params']['label_within']) && $block['params']['label_within']) {
$html .= '<option value="">'.static::getFieldLabel($block).'</option>';
} else {
if(empty($block['params']['required']) || !$block['params']['required']) {
$html .= '<option value="">-</option>';
}
}
$options = (!empty($block['params']['values'])

View File

@ -7,33 +7,12 @@ use \MailPoet\Models\Setting;
use \MailPoet\Models\Subscriber;
use \MailPoet\Form\Renderer as FormRenderer;
use \MailPoet\Form\Util;
use \MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
class Widget extends \WP_Widget {
function __construct () {
// add_action(
// 'wp_ajax_mailpoet_form_subscribe',
// array($this, 'subscribe')
// );
// add_action(
// 'wp_ajax_nopriv_mailpoet_form_subscribe',
// array($this, 'subscribe')
// );
// add_action(
// 'admin_post_nopriv_mailpoet_form_subscribe',
// array($this, 'subscribe')
// );
// add_action(
// 'admin_post_mailpoet_form_subscribe',
// array($this, 'subscribe')
// );
// add_action(
// 'init',
// array($this, 'subscribe')
// );
return parent::__construct(
'mailpoet_form',
__('MailPoet Form'),
@ -169,21 +148,15 @@ class Widget extends \WP_Widget {
'after_title' => (!empty($after_title) ? $after_title : '')
);
// if(isset($_GET['mailpoet_form']) && (int)$_GET['mailpoet_form'] === $form['id']) {
// // form messages (success / error)
// $output .= '<div class="mailpoet_message">';
// // success message
// if(isset($_GET['mailpoet_success'])) {
// $output .= '<p class="mailpoet_validate_success">'.strip_tags(urldecode($_GET['mailpoet_success']), '<a><strong><em><br><p>').'</p>';
// }
// // error message
// if(isset($_GET['mailpoet_error'])) {
// $output .= '<p class="mailpoet_validate_error">'.strip_tags(urldecode($_GET['mailpoet_error']), '<a><strong><em><br><p>').'</p>';
// }
// $output .= '</div>';
// } else {
// $output .= '<div class="mailpoet_message"></div>';
// }
// check if the form was successfully submitted via POST (non ajax)
$data['success'] = (
(isset($_GET['mailpoet_success']))
&&
((int)$_GET['mailpoet_success'] === (int)$form['id'])
);
// generate security token
$data['token'] = Security::generateToken();
// render form
$renderer = new Renderer();

View File

@ -100,9 +100,9 @@ class Mailer {
}
function getSender($sender = false) {
if(!$sender) {
$sender = Setting::getValue('sender', null);
if(!$sender['address']) throw new \Exception(__('Sender name and email are not configured.'));
if(empty($sender)) {
$sender = Setting::getValue('sender', array());
if(empty($sender['address'])) throw new \Exception(__('Sender name and email are not configured.'));
}
return array(
'from_name' => $sender['name'],

View File

@ -82,6 +82,45 @@ class Newsletter extends Model {
return $this;
}
function withStatistics() {
$statistics = $this->getStatistics();
if($statistics === false) {
$this->statistics = false;
} else {
$this->statistics = $statistics->asArray();
}
return $this;
}
function getStatistics() {
if($this->queue === false) {
return false;
}
return SendingQueue::tableAlias('queues')
->selectExpr(
'count(DISTINCT(clicks.subscriber_id)) as clicked, ' .
'count(DISTINCT(opens.subscriber_id)) as opened, ' .
'count(DISTINCT(unsubscribes.subscriber_id)) as unsubscribed '
)
->leftOuterJoin(
MP_STATISTICS_CLICKS_TABLE,
'queues.id = clicks.queue_id',
'clicks'
)
->leftOuterJoin(
MP_STATISTICS_OPENS_TABLE,
'queues.id = opens.queue_id',
'opens'
)
->leftOuterJoin(
MP_STATISTICS_UNSUBSCRIBES_TABLE,
'queues.id = unsubscribes.queue_id',
'unsubscribes'
)
->where('queues.id', $this->queue['id'])
->findOne();
}
static function search($orm, $search = '') {
return $orm->where_like('subject', '%' . $search . '%');
}
@ -180,6 +219,37 @@ class Newsletter extends Model {
if($newsletter === false) {
$newsletter = self::create();
// set default sender based on settings
if(empty($data['sender'])) {
$sender = Setting::getValue('sender', array());
$data['sender_name'] = (
!empty($sender['name'])
? $sender['name']
: ''
);
$data['sender_address'] = (
!empty($sender['address'])
? $sender['address']
: ''
);
}
// set default reply_to based on settings
if(empty($data['reply_to'])) {
$reply_to = Setting::getValue('reply_to', array());
$data['reply_to_name'] = (
!empty($reply_to['name'])
? $reply_to['name']
: ''
);
$data['reply_to_address'] = (
!empty($reply_to['address'])
? $reply_to['address']
: ''
);
}
$newsletter->hydrate($data);
} else {
unset($data['id']);

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class NewsletterPost extends Model {
public static $_table = MP_NEWSLETTER_POSTS_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -28,8 +28,8 @@ class NewsletterTemplate extends Model {
static function createOrUpdate($data = array()) {
$template = false;
if(isset($data['id']) && (int) $data['id'] > 0) {
$template = self::findOne((int) $data['id']);
if(isset($data['id']) && (int)$data['id'] > 0) {
$template = self::findOne((int)$data['id']);
}
if($template === false) {

View File

@ -126,18 +126,19 @@ class Segment extends Model {
static function getSegmentsWithSubscriberCount() {
return self::selectMany(array(self::$_table.'.id', self::$_table.'.name'))
->select_expr(
'COUNT('.MP_SUBSCRIBER_SEGMENT_TABLE.'.subscriber_id)', 'subscribers'
->selectExpr(
self::$_table.'.*, COUNT(IF('.MP_SUBSCRIBER_SEGMENT_TABLE.'.status="subscribed",1,NULL)) `subscribers`'
)
->left_outer_join(
->leftOuterJoin(
MP_SUBSCRIBER_SEGMENT_TABLE,
array(self::$_table.'.id', '=', MP_SUBSCRIBER_SEGMENT_TABLE.'.segment_id'))
->left_outer_join(
->leftOuterJoin(
MP_SUBSCRIBERS_TABLE,
array(MP_SUBSCRIBER_SEGMENT_TABLE.'.subscriber_id', '=', MP_SUBSCRIBERS_TABLE.'.id'))
->whereNull(MP_SUBSCRIBERS_TABLE.'.deleted_at')
->group_by(self::$_table.'.id')
->group_by(self::$_table.'.name')
->groupBy(self::$_table.'.id')
->groupBy(self::$_table.'.name')
->orderByAsc(self::$_table.'.name')
->where(self::$_table.'.type', 'default')
->whereNull(self::$_table.'.deleted_at')
->findArray();

View File

@ -1,7 +1,7 @@
<?php
namespace MailPoet\Models;
if (!defined('ABSPATH')) exit;
if(!defined('ABSPATH')) exit;
class Setting extends Model {
public static $_table = MP_SETTINGS_TABLE;

View File

@ -0,0 +1,33 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class StatisticsForms extends Model {
public static $_table = MP_STATISTICS_FORMS_TABLE;
function __construct() {
parent::__construct();
}
public static function record($form_id, $subscriber_id) {
if($form_id > 0 && $subscriber_id > 0) {
// check if we already have a record for today
$record = self::where('form_id', $form_id)
->where('subscriber_id', $subscriber_id)
->findOne();
if($record === false) {
// create a new entry
$record = self::create();
$record->hydrate(array(
'form_id' => $form_id,
'subscriber_id' => $subscriber_id
));
$record->save();
}
return $record;
}
return false;
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class StatisticsOpens extends Model {
public static $_table = MP_STATISTICS_OPENS_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MailPoet\Models;
if(!defined('ABSPATH')) exit;
class StatisticsUnsubscribes extends Model {
public static $_table = MP_STATISTICS_UNSUBSCRIBES_TABLE;
function __construct() {
parent::__construct();
}
}

View File

@ -124,6 +124,10 @@ class Subscriber extends Model {
return false;
}
static function verifyToken($email, $token) {
return (self::generateToken($email) === $token);
}
static function subscribe($subscriber_data = array(), $segment_ids = array()) {
if(empty($subscriber_data) or empty($segment_ids)) {
return false;
@ -163,7 +167,10 @@ class Subscriber extends Model {
}
// welcome email
Scheduler::welcomeForSegmentSubscription($subscriber->id, $segment_ids);
Scheduler::scheduleSubscriberWelcomeNotification(
$subscriber->id,
$segment_ids
);
}
}
@ -188,11 +195,15 @@ class Subscriber extends Model {
'label' => __('All segments'),
'value' => ''
);
$subscribers_without_segment = self::filter('withoutSegments')->count();
$subscribers_without_segment_label = sprintf(
__('Subscribers without a segment (%s)'),
number_format($subscribers_without_segment)
);
$segment_list[] = array(
'label' => sprintf(
__('Subscribers without a segment (%d)'),
self::filter('withoutSegments')->count()
),
'label' => str_replace(' (0)', '', $subscribers_without_segment_label),
'value' => 'none'
);
@ -201,8 +212,9 @@ class Subscriber extends Model {
->filter('groupBy', $group)
->count();
$label = sprintf('%s (%s)', $segment->name, number_format($subscribers_count));
$segment_list[] = array(
'label' => sprintf('%s (%d)', $segment->name, $subscribers_count),
'label' => str_replace(' (0)', '', $label),
'value' => $segment->id()
);
}
@ -300,9 +312,10 @@ class Subscriber extends Model {
$customFields = CustomField::findArray();
foreach ($customFields as $customField) {
$orm = $orm->selectExpr(
'CASE WHEN ' .
'MAX(CASE WHEN ' .
MP_CUSTOM_FIELDS_TABLE . '.id=' . $customField['id'] . ' THEN ' .
MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.value END as "' . $customField['id'].'"');
MP_SUBSCRIBER_CUSTOM_FIELD_TABLE . '.value END) as "' . $customField['id'].'"'
);
}
$orm = $orm
->leftOuterJoin(
@ -316,6 +329,19 @@ class Subscriber extends Model {
return $orm;
}
static function getSubscribedInSegments($segment_ids) {
$subscribers = SubscriberSegment::table_alias('relation')
->whereIn('relation.segment_id', $segment_ids)
->where('relation.status', 'subscribed')
->join(
MP_SUBSCRIBERS_TABLE,
'subscribers.id = relation.subscriber_id',
'subscribers'
)
->where('subscribers.status', 'subscribed');
return $subscribers;
}
function customFields() {
return $this->hasManyThrough(
__NAMESPACE__.'\CustomField',
@ -513,12 +539,6 @@ class Subscriber extends Model {
return false;
}
static function bulkConfirmUnconfirmed($orm) {
$subscribers = $orm->findResultSet();
$subscribers->set('status', self::STATUS_SUBSCRIBED)->save();
return $subscribers->count();
}
static function bulkSendConfirmationEmail($orm) {
$subscribers = $orm
->where('status', self::STATUS_UNCONFIRMED)

View File

@ -2,13 +2,41 @@
namespace MailPoet\Newsletter;
use MailPoet\Newsletter\Editor\Transformer;
use MailPoet\Util\Helpers;
if(!defined('ABSPATH')) exit;
class AutomatedLatestContent {
function getPosts($args) {
const DEFAULT_POSTS_PER_PAGE = 10;
function __construct($newsletter_id = false) {
$this->newsletter_id = $newsletter_id;
$this->_attachSentPostsFilter();
}
function __destruct() {
$this->_detachSentPostsFilter();
}
function filterOutSentPosts($where) {
$sentPostsQuery = 'SELECT ' . MP_NEWSLETTER_POSTS_TABLE . '.post_id FROM '
. MP_NEWSLETTER_POSTS_TABLE . ' WHERE '
. MP_NEWSLETTER_POSTS_TABLE . ".newsletter_id='" . $this->newsletter_id . "'";
$wherePostUnsent = 'ID NOT IN (' . $sentPostsQuery . ')';
if(!empty($where)) $wherePostUnsent = ' AND ' . $wherePostUnsent;
return $where . $wherePostUnsent;
}
function getPosts($args, $posts_to_exclude = array()) {
$posts_per_page = (!empty($args['amount']) && (int)$args['amount'] > 0)
? (int)$args['amount']
: self::DEFAULT_POSTS_PER_PAGE;
$parameters = array(
'posts_per_page' => (isset($args['amount'])) ? (int) $args['amount'] : 10,
'posts_per_page' => $posts_per_page,
'post_type' => (isset($args['contentType'])) ? $args['contentType'] : 'post',
'post_status' => (isset($args['postStatus'])) ? $args['postStatus'] : 'publish',
'orderby' => 'date',
@ -20,7 +48,17 @@ class AutomatedLatestContent {
if(isset($args['posts']) && is_array($args['posts'])) {
$parameters['post__in'] = $args['posts'];
}
if(!empty($posts_to_exclude)) {
$parameters['post__not_in'] = $posts_to_exclude;
}
$parameters['tax_query'] = $this->constructTaxonomiesQuery($args);
// This enables using posts query filters for get_posts, where by default
// it is disabled.
// However, it also enables other plugins and themes to hook in and alter
// the query.
$parameters['suppress_filters'] = false;
return get_posts($parameters);
}
@ -66,4 +104,16 @@ class AutomatedLatestContent {
}
return $taxonomies_query;
}
}
private function _attachSentPostsFilter() {
if($this->newsletter_id > 0) {
add_action('posts_where', array($this, 'filterOutSentPosts'));
}
}
private function _detachSentPostsFilter() {
if($this->newsletter_id > 0) {
remove_action('posts_where', array($this, 'filterOutSentPosts'));
}
}
}

View File

@ -11,14 +11,14 @@ class MetaInformationManager {
$position_field = $position . 'Text';
$text = array();
if ($args['showAuthor'] === $position_field) {
if($args['showAuthor'] === $position_field) {
$text[] = self::getPostAuthor(
$post->post_author,
$args['authorPrecededBy']
);
}
if ($args['showCategories'] === $position_field) {
if($args['showCategories'] === $position_field) {
$text[] = self::getPostCategories(
$post->ID,
$post->post_type,
@ -26,10 +26,10 @@ class MetaInformationManager {
);
}
if (!empty($text)) {
if(!empty($text)) {
$text = '<p>' . implode('<br />', $text) . '</p>';
if ($position === 'above') $content = $text . $content;
else if ($position === 'below') $content .= $text;
if($position === 'above') $content = $text . $content;
else if($position === 'below') $content .= $text;
}
}

Some files were not shown because too many files have changed in this diff Show More