Compare commits

...

288 Commits

Author SHA1 Message Date
b3e23de837 Merge pull request 'development' (#1) from development into main
Reviewed-on: #1
2025-01-23 06:36:53 +00:00
bobbyvish
9d20583a78 fix: event multi image upload 2025-01-23 12:04:24 +05:30
bobbyvish
5693ec3c48 fixed:(customer): fixed issue of veiw button 2025-01-08 13:41:42 +05:30
bobbyvish
bec8f9d608 Update(Customer): added profile photo field 2025-01-08 12:37:07 +05:30
bobbyvish
b559c62926 updated(event): added link field in event 2024-12-24 17:00:59 +05:30
bobbyvish
24949ade3e feat(customer): customer cred and postcode for address 2024-12-20 19:43:44 +05:30
bobbyvish
3425d82891 updated : requirement.txt 2024-12-02 12:05:45 +05:30
bobbyvish
8ccec7012c refactor: custom command to update social key 2024-12-02 11:51:51 +05:30
bobbyvish
3f263b2af5 fixed: filter title with tag and calendar issue 2024-11-28 16:05:32 +05:30
bobbyvish
76ac9413c4 fix: report isssue and custom command to generate individual marchant report 2024-11-28 15:56:20 +05:30
bobbyvish
76323a1ea1 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into development 2024-11-06 14:40:28 +05:30
bobbyvish
eef12c8579 chore(email): adjust email configuration to reduce spam 2024-11-06 14:39:50 +05:30
BOBBY VISHWAKARMA
9ba960fcad Merge pull request #102 from WDI-Ideas/feature/key-guest-multiple
Updated : key guest for multiple
2024-10-31 17:49:19 +05:30
bobbyvish
6f07fe4877 Updated : key guest for multiple 2024-10-30 12:36:05 +05:30
bobbyvish
540442fa28 fix: stipe key issue 2024-10-30 12:33:53 +05:30
bobbyvish
449d8615c8 refactor: manage customer edit 2024-09-26 13:18:34 +05:30
bobbyvish
389dbbe390 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into development 2024-09-24 18:57:47 +05:30
bobbyvish
b6b4295d7e fixed: webhook, edit customer 2024-09-24 18:56:42 +05:30
BOBBY VISHWAKARMA
cb904b3057 Merge pull request #97 from WDI-Ideas/feature/module_8_socialmedia
feat(social media): added api for post to social media
2024-09-03 12:08:56 +05:30
bobbyvish
89639c7a38 feat(social media): added api for post to social media 2024-09-03 12:07:56 +05:30
BOBBY VISHWAKARMA
1caad5ceb5 Merge pull request #94 from WDI-Ideas/feature/module_9_coupons
Feature/module 9 coupons
2024-08-25 22:59:15 +05:30
bobbyvish
b3db97baf4 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_9_coupons 2024-08-25 22:57:47 +05:30
bobbyvish
bef5a52b55 fix: create subscription and update logic 2024-08-25 22:57:32 +05:30
BOBBY VISHWAKARMA
aefce76db3 Merge pull request #92 from WDI-Ideas/feature/module_9_coupons
Feature/module 9 coupons
2024-08-25 19:15:31 +05:30
bobbyvish
5dc82d28c7 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_9_coupons 2024-08-25 19:12:01 +05:30
bobbyvish
7ba73dff35 refator(subscription): update functionality, list view, subscription design 2024-08-25 19:11:49 +05:30
bobbyvish
70d807220e fix: age-group filter, url redirect 2024-08-23 17:25:08 +05:30
BOBBY VISHWAKARMA
3efc093de3 Merge pull request #90 from WDI-Ideas/feature/module_9_coupons
Feature/module 9 coupons
2024-08-23 12:30:40 +05:30
bobbyvish
1f580c099d refactor(subscription): removed unnecessary code 2024-08-23 12:26:09 +05:30
bobbyvish
5d107ad17a refactor(subscription): removed unnecessary complexity 2024-08-21 23:24:47 +05:30
bobbyvish
4866f0a5d4 refactor(subscription): removed unnecessary field and complexity 2024-08-20 16:57:19 +05:30
bobbyvish
342b36efc6 refactor(profile): added social link in profile and in organization 2024-08-16 15:08:43 +05:30
bobbyvish
88fd84bbdb fix: filter to get active user subscriptions 2024-08-14 12:53:44 +05:30
BOBBY VISHWAKARMA
b43bde15c2 Merge pull request #88 from WDI-Ideas/development
Development
2024-08-13 16:02:35 +05:30
BOBBY VISHWAKARMA
c57ff88ec0 Merge branch 'staging' into development 2024-08-13 16:02:26 +05:30
BOBBY VISHWAKARMA
1b3fc0e4db Merge pull request #87 from WDI-Ideas/feature/module_9_coupons
Feature/module 9 coupons
2024-08-13 15:48:01 +05:30
bobbyvish
7710f2be9b Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_9_coupons 2024-08-13 15:40:47 +05:30
BOBBY VISHWAKARMA
a8fee238c6 Merge pull request #86 from WDI-Ideas/feature/module_freeapp
refactor(profile): added preference count in extended data api
2024-08-13 15:27:25 +05:30
bobbyvish
5bd5b7a500 refactor(profile): added preference count in extended data api 2024-08-13 15:24:09 +05:30
BOBBY VISHWAKARMA
5b68e5643e Merge pull request #84 from WDI-Ideas/feature/module_1
Feature/module 1
2024-08-13 15:21:56 +05:30
bobbyvish
66bf95ef87 refactor(customer details): added social info in customer detail page 2024-08-13 15:19:30 +05:30
bobbyvish
797a089a50 refactor(add customer) add more field to add customer form and in excel import 2024-08-13 15:14:47 +05:30
rizwanisready
bc3a1d741b my subscriptions page 22 2024-08-13 15:12:26 +05:30
rizwanisready
52887b0328 my subscriptions page 21 2024-08-13 14:11:17 +05:30
rizwanisready
bd76a3a4cf my subscriptions page 20 2024-08-13 14:07:20 +05:30
rizwanisready
ab684a3e07 my subscriptions page 19 2024-08-13 14:05:00 +05:30
rizwanisready
8f6d577031 my subscriptions page 18 2024-08-13 14:01:18 +05:30
rizwanisready
e80b01a549 my subscriptions page 17 2024-08-13 13:15:42 +05:30
rizwanisready
a0dabe85ac my subscriptions page 16 2024-08-13 13:13:16 +05:30
rizwanisready
e57901de33 my subscriptions page 15 2024-08-13 13:10:33 +05:30
rizwanisready
9c2b484fa0 my subscriptions page 14 2024-08-13 13:08:25 +05:30
rizwanisready
7278ef6d92 my subscriptions page 13 2024-08-13 12:47:23 +05:30
rizwanisready
f22777a72f my subscriptions page 12 2024-08-13 12:40:44 +05:30
rizwanisready
8f59f81796 my subscriptions page 11 2024-08-12 20:47:36 +05:30
rizwanisready
cfff16393f my subscriptions page 10 2024-08-12 19:01:02 +05:30
rizwanisready
438f0005ee my subscriptions page 9 2024-08-12 15:39:47 +05:30
rizwanisready
efbdc47aab my subscriptions page 8 2024-08-12 15:33:01 +05:30
rizwanisready
e47a96c742 my subscriptions page 7 2024-08-12 15:31:41 +05:30
rizwanisready
5cee9f21ad my subscriptions page 6 2024-08-12 15:08:55 +05:30
rizwanisready
08adadffbf my subscriptions page 5 2024-08-12 14:58:44 +05:30
rizwanisready
6e93793665 my subscriptions page 4 2024-08-12 14:49:48 +05:30
rizwanisready
ee27d86010 my subscriptions page 3 2024-08-12 13:47:12 +05:30
rizwanisready
15a7308cb8 my subscriptions page 2 2024-08-12 12:57:22 +05:30
rizwanisready
1a2013e10b my subscriptions page 2024-08-12 12:54:29 +05:30
BOBBY VISHWAKARMA
e99095fb19 Merge pull request #83 from WDI-Ideas/feature/module_freeapp
Feature/module freeapp
2024-08-12 11:22:57 +05:30
bobbyvish
234e9e900a refactor(Profile):changed profile serializer 2024-08-11 21:10:52 +05:30
BOBBY VISHWAKARMA
390206548b Merge pull request #82 from WDI-Ideas/development
Development
2024-08-09 11:49:33 +05:30
rizwanisready
4c2d693ebf updated webhook module 2024-08-08 18:07:23 +05:30
rizwanisready
e0ed41f096 updated webhook module 2024-08-08 17:31:05 +05:30
rizwanisready
32a834726e updated webhook module 2024-08-08 17:21:10 +05:30
rizwanisready
70c5c815fa updated webhook module 2024-08-08 14:59:01 +05:30
rizwanisready
e8b90b8fdb updated webhook module 2024-08-08 14:07:59 +05:30
rizwanisready
33a11af520 updated webhook module 2024-08-08 13:58:45 +05:30
bobbyvish
87f711b3f5 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_freeapp 2024-08-08 12:40:14 +05:30
BOBBY VISHWAKARMA
9894a94b8c Merge pull request #81 from WDI-Ideas/feature/module_freeapp
feat(freeapp):add limitation to user preference and search filter
2024-08-08 12:38:09 +05:30
bobbyvish
593dac0a0c Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_freeapp 2024-08-08 00:03:44 +05:30
BOBBY VISHWAKARMA
b837749fe0 Merge pull request #80 from WDI-Ideas/feature/module_8_socialmedia
fix(socialmedia):allowed only active event to publish on social media
2024-08-07 23:54:50 +05:30
bobbyvish
8fb1a7eb41 fix(socialmedia):allowed only active event to publish on social media 2024-08-07 23:52:45 +05:30
bobbyvish
a81b5b4f7f feat(freeapp):add limitation to user preference and search filter 2024-08-07 22:58:22 +05:30
rizwanisready
6e8eb96f4f getting discounted amount from transaction 2 2024-08-06 21:12:30 +05:30
rizwanisready
48d18d6ef3 getting discounted amount from transaction 2024-08-06 21:04:00 +05:30
rizwanisready
93796ceb46 commented twisted-iocpsupport==1.0.4 : error on linux server 2024-08-06 13:43:22 +05:30
rizwanisready
6d4ebe533a added model admin for coupons 2024-08-06 13:37:31 +05:30
rizwanisready
5b29245ab2 Merge branch 'staging' of https://github.com/WDI-Ideas/goodtimes into feature/module_9_coupons 2024-08-06 12:48:30 +05:30
rizwanisready
7aabd0c9fa refactored webhook realted to coupon 4 2024-08-05 19:24:56 +05:30
rizwanisready
fccd7378f7 refactored webhook realted to coupon 3 2024-08-05 19:19:20 +05:30
rizwanisready
3ac7e0d522 refactored webhook realted to coupon 2 2024-08-05 18:51:49 +05:30
rizwanisready
73ca0ea974 refactored webhook realted to coupon 2024-08-05 18:40:34 +05:30
rizwanisready
85767e7b96 auto recurring testing phase 31 2024-08-02 19:16:11 +05:30
rizwanisready
06bfae09b3 auto recurring testing phase 30 2024-08-02 19:14:51 +05:30
rizwanisready
f595089b05 auto recurring testing phase 29 2024-08-02 18:13:16 +05:30
rizwanisready
a6dfdf8cd1 auto recurring testing phase 28 2024-08-02 18:07:06 +05:30
rizwanisready
dbed6999dc auto recurring testing phase 27 2024-08-02 18:01:53 +05:30
rizwanisready
33d1c3c94e auto recurring testing phase 26 2024-08-02 17:57:21 +05:30
rizwanisready
c52b42e67d auto recurring testing phase 25 2024-08-02 17:14:04 +05:30
rizwanisready
a75b579459 auto recurring testing phase 24 2024-08-02 17:10:26 +05:30
rizwanisready
0a2738496d auto recurring testing phase 23 2024-08-02 16:24:42 +05:30
rizwanisready
74425add85 auto recurring testing phase 22 2024-08-02 16:20:04 +05:30
rizwanisready
442ce193d4 auto recurring testing phase 21 2024-08-02 15:56:15 +05:30
rizwanisready
338f5c29c5 auto recurring testing phase 21 2024-08-02 15:49:52 +05:30
rizwanisready
4397ac52b8 auto recurring testing phase 20 2024-08-02 15:38:25 +05:30
rizwanisready
1711af39bc auto recurring testing phase 19 2024-08-02 15:30:19 +05:30
rizwanisready
9ef1593f33 auto recurring testing phase 18 2024-08-02 12:37:09 +05:30
rizwanisready
eb18a5d4dc auto recurring testing phase 17 2024-08-02 12:25:11 +05:30
rizwanisready
d1ab24dc0d auto recurring testing phase 16 2024-08-01 21:03:01 +05:30
rizwanisready
f7d904fee1 auto recurring testing phase 15 2024-08-01 16:25:49 +05:30
rizwanisready
0036e3246e auto recurring testing phase 14 2024-08-01 16:23:49 +05:30
rizwanisready
1cf66abbff auto recurring testing phase 13 2024-08-01 15:48:06 +05:30
rizwanisready
731f162531 auto recurring testing phase 12 2024-08-01 14:01:00 +05:30
rizwanisready
b958765b29 auto recurring testing phase 11 2024-08-01 13:21:10 +05:30
rizwanisready
deeb584c12 auto recurring testing phase 10 2024-08-01 13:01:25 +05:30
rizwanisready
41ddf0ebca auto recurring testing phase 9 2024-08-01 12:59:48 +05:30
rizwanisready
d585a8311b auto recurring testing phase 8 2024-08-01 12:52:06 +05:30
rizwanisready
0f7ec4418c auto recurring testing phase 8 2024-08-01 12:49:39 +05:30
rizwanisready
cf7f1a5b51 auto recurring testing phase 7 2024-08-01 12:32:57 +05:30
rizwanisready
4e1d533deb auto recurring testing phase 6 2024-08-01 12:23:00 +05:30
rizwanisready
54bdca8630 auto recurring testing phase 6 2024-08-01 12:21:04 +05:30
rizwanisready
0ba056cb63 auto recurring testing phase 5 2024-07-31 20:18:11 +05:30
rizwanisready
32afbd952e auto recurring testing phase 4 2024-07-31 20:15:57 +05:30
rizwanisready
31f9e768ea auto recurring testing phase 3 2024-07-31 20:10:21 +05:30
rizwanisready
e3b86d64fd auto recurring testing phase 2 2024-07-31 15:50:35 +05:30
rizwanisready
e3189344ea auto recurring testing 2024-07-31 13:12:17 +05:30
BOBBY VISHWAKARMA
6e0d34312c Merge pull request #78 from WDI-Ideas/development
feat(filter):refactor sorting logic
2024-07-30 12:43:12 +05:30
bobbyvish
ce676aa81d feat(filter):refactor sorting logic 2024-07-30 12:41:41 +05:30
BOBBY VISHWAKARMA
7f7ef04a25 Merge pull request #77 from WDI-Ideas/development
feat(filter): fixed nearest, latest and preference event
2024-07-29 15:42:09 +05:30
bobbyvish
27a124b0b2 feat(filter): fixed nearest, latest and preference event 2024-07-29 15:39:43 +05:30
rizwanisready
7992e456a4 debugging on stripe subscription 4 2024-07-29 12:56:22 +05:30
rizwanisready
77be3218b7 debugging on stripe subscription 3 2024-07-29 12:14:43 +05:30
rizwanisready
036b8ae935 debugging on stripe subscription 2 2024-07-29 12:08:56 +05:30
rizwanisready
d739d9bd91 debugging on stripe subscription 2024-07-29 11:51:03 +05:30
rizwanisready
cdf9d06e8c implementing stripe subscription 2024-07-29 11:41:49 +05:30
rizwanisready
53a572c19e debugging coupon on stripe 3 2024-07-26 18:04:17 +05:30
rizwanisready
2db723f12d debugging coupon on stripe 2 2024-07-26 17:58:28 +05:30
rizwanisready
9397b26708 debugging coupon on stripe 2024-07-26 17:54:07 +05:30
rizwanisready
4afbfb33f7 sync Wdipl with Staging 2 2024-07-26 16:27:41 +05:30
rizwanisready
8d212430ec sync Wdipl with Staging 2024-07-26 16:11:02 +05:30
BOBBY VISHWAKARMA
8bcc4bef7d Merge pull request #76 from WDI-Ideas/development
refactor(social media):change message of success and failure in socia…
2024-07-25 16:52:53 +05:30
bobbyvish
a68f114f99 refactor(social media):change message of success and failure in social media response 2024-07-25 16:45:20 +05:30
BOBBY VISHWAKARMA
66c5b8674a Merge pull request #75 from WDI-Ideas/development
Development
2024-07-25 00:54:18 +05:30
bobbyvish
33030ca728 feat(socialmedia):post event to social media 2024-07-25 00:48:24 +05:30
rizwanisready
6a8dc781d2 manage coupons list, add, edit, delete views 2024-07-22 21:08:56 +05:30
rizwanisready
473a961ab6 manage coupons 2024-07-19 21:18:18 +05:30
bobbyvish
ab80b06f54 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_8_socialmedia 2024-07-18 16:43:51 +05:30
BOBBY VISHWAKARMA
46802f202a Merge pull request #72 from WDI-Ideas/development
feat(module_2_filter):fixed migration file issue
2024-07-18 16:04:42 +05:30
bobbyvish
b226e1f584 feat(module_2_filter):fixed migration file issue 2024-07-18 16:02:15 +05:30
BOBBY VISHWAKARMA
81e35c1562 Merge pull request #71 from WDI-Ideas/development
feat(module_2_filter):changed age_group in admin side
2024-07-18 13:56:35 +05:30
bobbyvish
e96bfe9068 feat(module_2_filter):changed age_group in admin side 2024-07-18 13:52:48 +05:30
BOBBY VISHWAKARMA
f17f523e25 Merge pull request #70 from WDI-Ideas/development
feat(module_2_filter):change age group dynamically
2024-07-17 19:53:13 +05:30
bobbyvish
ee25cf5a8b feat(module_2_filter):change age group dynamically 2024-07-17 19:52:02 +05:30
bobbyvish
161cba37ab Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into feature/module_8_socialmedia 2024-07-17 15:39:17 +05:30
BOBBY VISHWAKARMA
f918a2f76f Merge pull request #68 from WDI-Ideas/development
feat(age_group): dynamically change age group in filter and event
2024-07-17 12:10:40 +05:30
bobbyvish
3da21b0c0b feat(age_group): dynamically change age group in filter and event 2024-07-17 12:09:13 +05:30
bobbyvish
29011dff06 feat(socialmedia):added twitter service 2024-07-17 00:37:06 +05:30
BOBBY VISHWAKARMA
7921c3f89d Merge pull request #66 from WDI-Ideas/development
feat(module_2_filter): fix nearest ordering issue
2024-07-16 19:05:52 +05:30
bobbyvish
d670a18599 feat(module_2_filter): fix nearest ordering issue 2024-07-16 19:03:46 +05:30
BOBBY VISHWAKARMA
8faa9eb81f Merge pull request #65 from WDI-Ideas/development
Development
2024-07-12 16:04:39 +05:30
bobbyvish
83efd28687 fix(module_2_filter):fixed order by latest 2024-07-12 16:00:55 +05:30
bobbyvish
91dba0308c fix(module_2_filter):fixed order by latest 2024-07-12 16:00:22 +05:30
bobbyvish
23bae8cf3d fix(module_2_filter):changed base query of event filter 2024-07-12 13:13:53 +05:30
BOBBY VISHWAKARMA
5d320cccfe Merge pull request #64 from WDI-Ideas/development
fix(module_2_filter):removed end date and changed price type
2024-07-11 12:49:53 +05:30
bobbyvish
38fad86990 fix(module_2_filter):removed end date and changed price type 2024-07-11 12:47:39 +05:30
BOBBY VISHWAKARMA
2db24f203c Merge pull request #63 from WDI-Ideas/development
fix(module_2_filter):improve location filter and ordering nearest event
2024-07-10 12:56:48 +05:30
bobbyvish
31e9d5a03c fix(module_2_filter):improve location filter and ordering nearest event 2024-07-10 12:53:48 +05:30
BOBBY VISHWAKARMA
b8ac4ed4e6 Merge pull request #62 from WDI-Ideas/development
feat(module_2_filter):Home page advance filter api
2024-07-10 00:45:05 +05:30
bobbyvish
1ebf7f6838 feat(module_2_filter):Home page advance filter api 2024-07-10 00:42:38 +05:30
BOBBY VISHWAKARMA
e25f402cea Merge pull request #61 from WDI-Ideas/development
Development
2024-07-06 21:38:48 +05:30
bobbyvish
3396f5ce01 Merge branch 'feature/module_1' into development 2024-07-06 21:38:04 +05:30
bobbyvish
d602988b81 fix(moduel_1):form validation 2024-07-06 21:37:48 +05:30
BOBBY VISHWAKARMA
d5b69ce6bd Merge pull request #59 from WDI-Ideas/development
Development
2024-07-02 19:24:43 +05:30
bobbyvish
dea6b60c43 Merge branch 'feature/module_1' into development 2024-07-02 19:23:41 +05:30
bobbyvish
fa05aee1b1 fix(module_1):default venue select in dropdown and remove deleted venue 2024-07-02 19:23:13 +05:30
bobbyvish
3ae21b747d Merge branch 'feature/module_1' into development 2024-07-02 18:54:37 +05:30
bobbyvish
04e428fcf6 fix(module_1):event form validation, venue validation 2024-07-02 18:54:18 +05:30
BOBBY VISHWAKARMA
f336c1f866 Merge pull request #58 from WDI-Ideas/development
Development
2024-07-02 15:39:59 +05:30
bobbyvish
92f597d830 Merge branch 'feature/module_1' into development 2024-07-02 15:38:40 +05:30
bobbyvish
a5ce725b30 feat(module 1):multiple image input, form validation 2024-07-02 15:37:50 +05:30
BOBBY VISHWAKARMA
5cb9c51bc2 Merge pull request #57 from WDI-Ideas/development
Development
2024-06-27 17:36:37 +05:30
bobbyvish
35d904603d Merge branch 'feature/module_1' into development 2024-06-27 17:34:02 +05:30
bobbyvish
8e7f2d6d69 feat(module 1): add customer, non transferlist, import , export 2024-06-27 17:24:52 +05:30
BOBBY VISHWAKARMA
a9cb97b406 Merge pull request #56 from WDI-Ideas/development
fix:event multiple image and add mobile no
2024-06-25 19:03:40 +05:30
bobbyvish
2398d91c93 fix:event multiple image and add mobile no 2024-06-25 19:02:59 +05:30
rizwanisready
9abfb25cc2 Merge pull request #55 from WDI-Ideas/development
updated requirements.txt
2024-06-21 18:13:49 +05:30
rizwanisready
ed92443e36 updated requirements.txt 2024-06-21 18:10:11 +05:30
rizwanisready
142803a5de Merge pull request #54 from WDI-Ideas/development
Development
2024-06-21 17:53:55 +05:30
rizwanisready
7a0f82b4a6 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into development 2024-06-21 17:52:24 +05:30
rizwanisready
a459742801 refactored consumers.py 2024-06-21 17:49:18 +05:30
rizwanisready
ef8f91809e Merge pull request #53 from WDI-Ideas/development
added coupon code
2024-06-20 15:07:06 +05:30
rizwanisready
1ed0d78fa6 added coupon code 2024-06-20 15:05:54 +05:30
rizwanisready
912c6e42d9 Merge pull request #51 from WDI-Ideas/development
in app notfctns one week
2024-06-10 20:02:18 +05:30
rizwanisready
ad649848cd in app notfctns one week 2024-06-10 19:41:32 +05:30
rizwanisready
79422fbf0c Merge pull request #50 from WDI-Ideas/development
text wrapping token field
2024-06-07 20:46:43 +05:30
rizwanisready
8ee574de52 text wrapping token field 2024-06-07 20:45:55 +05:30
rizwanisready
dd41d4856a Merge pull request #49 from WDI-Ideas/development
selling selected rewards 2
2024-06-07 20:41:31 +05:30
rizwanisready
c87cf3eea8 selling selected rewards 2 2024-06-07 20:40:45 +05:30
rizwanisready
5b201bf91f Merge pull request #48 from WDI-Ideas/development
selling selected rewards
2024-06-07 20:36:03 +05:30
rizwanisready
b6a0b0b537 selling selected rewards 2024-06-07 20:34:45 +05:30
rizwanisready
8ab25d6c2c Merge pull request #46 from WDI-Ideas/development
saving interested and going notifications into database if enabled False
2024-06-07 17:07:52 +05:30
rizwanisready
15265b1914 saving interested and going notifications into database if enabled False 2024-06-07 17:06:47 +05:30
rizwanisready
36912fec48 Merge pull request #45 from WDI-Ideas/development
updated report count
2024-06-07 16:00:28 +05:30
rizwanisready
d1f24f0867 updated report count 2024-06-07 15:59:35 +05:30
rizwanisready
346ff66eeb Merge pull request #44 from WDI-Ideas/development
removed the active and deleted from views and reviews
2024-06-07 15:52:21 +05:30
rizwanisready
9a3ff98253 removed the active and deleted from views and reviews 2024-06-07 15:51:03 +05:30
rizwanisready
c639e66ed8 Merge pull request #43 from WDI-Ideas/development
added model admin for favorites
2024-06-07 15:24:47 +05:30
rizwanisready
e9079f6c82 added model admin for favorites 2024-06-07 15:23:58 +05:30
rizwanisready
1454fc2838 Merge pull request #42 from WDI-Ideas/development
report problem 6
2024-06-05 20:53:39 +05:30
rizwanisready
f746f0c29a report problem 6 2024-06-05 20:52:54 +05:30
rizwanisready
b0e8df3437 Merge pull request #41 from WDI-Ideas/development
Development
2024-06-05 19:55:28 +05:30
rizwanisready
e6ca0c5c71 report problem 5 2024-06-05 19:54:40 +05:30
rizwanisready
59a9a5eb98 report problem 4 2024-06-05 19:48:48 +05:30
rizwanisready
2126660d9b report problem 3 2024-06-05 19:47:48 +05:30
rizwanisready
cc6345cc4c Merge pull request #40 from WDI-Ideas/development
report problem 2
2024-06-05 19:40:08 +05:30
rizwanisready
a303d1056d report problem 2 2024-06-05 19:39:23 +05:30
rizwanisready
fd73d1fb9c Merge pull request #39 from WDI-Ideas/development
Development
2024-06-05 19:17:55 +05:30
rizwanisready
9d502d5b60 report problem 2024-06-05 19:16:23 +05:30
rizwanisready
6ae855cfe1 testing 2024-06-04 16:12:52 +05:30
rizwanisready
25ab9b0341 Merge pull request #37 from WDI-Ideas/development
referral notifications correction
2024-06-04 15:36:43 +05:30
rizwanisready
9b37e0ee5b referral notifications correction 2024-06-04 15:35:14 +05:30
rizwanisready
c5ac748ca6 Merge pull request #36 from WDI-Ideas/development
corrected code
2024-06-04 15:03:24 +05:30
rizwanisready
d43d135123 corrected code 2024-06-04 15:01:20 +05:30
rizwanisready
6241c5253d Merge pull request #35 from WDI-Ideas/development
referral and reports
2024-06-04 14:57:00 +05:30
rizwanisready
ecd9246375 referral and reports 2024-06-04 14:01:18 +05:30
rizwanisready
ea80afb990 Merge pull request #34 from WDI-Ideas/development
report and share api 2
2024-06-03 21:07:35 +05:30
rizwanisready
b6a4567ff8 report and share api 2 2024-06-03 21:06:33 +05:30
rizwanisready
19363af620 Merge pull request #33 from WDI-Ideas/development
Development
2024-06-03 19:32:05 +05:30
rizwanisready
d09ff0f973 report and share api 2024-06-03 19:30:33 +05:30
rizwanisready
5ddccc0860 report 2024-06-03 16:49:19 +05:30
rizwanisready
10171170f4 reports pdf 2024-05-31 19:58:50 +05:30
rizwanisready
a53d221091 Merge pull request #32 from WDI-Ideas/development
event view count
2024-05-31 17:33:23 +05:30
rizwanisready
6262afea17 event view count 2024-05-31 17:16:30 +05:30
rizwanisready
bb99a3e049 Merge pull request #31 from WDI-Ideas/development
removed eventlistserializer in my events api
2024-05-28 13:18:48 +05:30
rizwanisready
051d8788bd removed eventlistserializer in my events api 2024-05-28 13:17:52 +05:30
rizwanisready
6e0812df38 Merge pull request #29 from WDI-Ideas/development
cron event bug solved
2024-05-27 20:33:04 +05:30
rizwanisready
e7b5b37e58 cron event bug solved 2024-05-27 20:31:48 +05:30
rizwanisready
b7309f1e68 Merge pull request #28 from WDI-Ideas/development
solved event notifications bug
2024-05-27 20:25:30 +05:30
rizwanisready
a9725dd0fc solved event notifications bug 2024-05-27 20:23:48 +05:30
rizwanisready
6a0cddffde Merge pull request #27 from WDI-Ideas/development
transaction filters only success and fail
2024-05-27 17:09:42 +05:30
rizwanisready
a3d31f8dd7 transaction filters only success and fail 2024-05-27 17:07:36 +05:30
rizwanisready
09e1702a46 Merge pull request #25 from WDI-Ideas/development
solved account deleted bug
2024-05-27 16:04:29 +05:30
rizwanisready
97bae9e940 solved account deleted bug 2024-05-27 16:03:41 +05:30
rizwanisready
2f6187df22 Merge pull request #24 from WDI-Ideas/development
added deleted in the principal form
2024-05-27 16:00:41 +05:30
rizwanisready
df59f0714e added deleted in the principal form 2024-05-27 16:00:02 +05:30
rizwanisready
5b404ecd7b Merge pull request #23 from WDI-Ideas/development
updating to event list serializer 2
2024-05-27 15:52:11 +05:30
rizwanisready
679abb78fe updating to event list serializer 2 2024-05-27 15:51:11 +05:30
rizwanisready
d74fbba0c5 Merge pull request #22 from WDI-Ideas/development
Development
2024-05-27 15:45:26 +05:30
rizwanisready
ff74aad3e5 optimized event api query and added events till 7 days 2024-05-27 15:44:00 +05:30
rizwanisready
411dd517a9 Merge branch 'main' of https://github.com/WDI-Ideas/goodtimes into development 2024-05-27 15:41:10 +05:30
rizwanisready
572143ca4f Merge pull request #21 from WDI-Ideas/development
updating to event list serializer
2024-05-27 12:52:34 +05:30
rizwanisready
496e8a3fba updating to event list serializer 2024-05-27 12:48:06 +05:30
rizwanisready
8cb8ab7e97 Merge pull request #20 from WDI-Ideas/development
existing subscription validation 9
2024-05-24 21:51:39 +05:30
rizwanisready
3cab0dbbc4 existing subscription validation 9 2024-05-24 21:51:07 +05:30
rizwanisready
08ae3d5cef Merge pull request #19 from WDI-Ideas/development
existing subscription validation 8
2024-05-24 21:49:47 +05:30
rizwanisready
685ee8a9fb existing subscription validation 8 2024-05-24 21:49:08 +05:30
rizwanisready
eacb7c5cff Merge pull request #18 from WDI-Ideas/development
existing subscription validation 8
2024-05-24 21:20:33 +05:30
rizwanisready
1afda70bf1 existing subscription validation 8 2024-05-24 21:19:14 +05:30
rizwanisready
0079a2f55f Merge pull request #17 from WDI-Ideas/development
existing subscription validation 7
2024-05-24 21:17:11 +05:30
rizwanisready
ca08d0aeb0 existing subscription validation 7 2024-05-24 21:16:30 +05:30
rizwanisready
946b8f016e Merge pull request #16 from WDI-Ideas/development
existing subscription validation 6
2024-05-24 21:09:18 +05:30
rizwanisready
d4a49fcf56 existing subscription validation 6 2024-05-24 21:08:36 +05:30
rizwanisready
fb34cac60f Merge pull request #15 from WDI-Ideas/development
existing subscription validation 5
2024-05-24 20:59:22 +05:30
rizwanisready
ed5d5253f0 existing subscription validation 5 2024-05-24 20:58:40 +05:30
rizwanisready
17a179d278 Merge pull request #14 from WDI-Ideas/development
existing subscription validation 4
2024-05-24 20:55:28 +05:30
rizwanisready
cb0b0a3e68 existing subscription validation 4 2024-05-24 20:54:35 +05:30
rizwanisready
2b170ee912 Merge pull request #13 from WDI-Ideas/development
existing subscription validation 3
2024-05-24 20:29:35 +05:30
rizwanisready
4040ddfd43 existing subscription validation 3 2024-05-24 20:28:51 +05:30
rizwanisready
d036dac763 Merge pull request #12 from WDI-Ideas/development
existing subscription validation 2
2024-05-24 20:11:20 +05:30
rizwanisready
c13f992e36 existing subscription validation 2 2024-05-24 20:10:34 +05:30
rizwanisready
33c9795920 Merge pull request #11 from WDI-Ideas/development
existing subscription validation
2024-05-24 19:53:18 +05:30
rizwanisready
899c2c1685 existing subscription validation 2024-05-24 19:51:55 +05:30
rizwanisready
98f7628854 Merge pull request #10 from WDI-Ideas/development
html web view
2024-05-24 17:45:31 +05:30
rizwanisready
a6979aba61 html web view 2024-05-24 17:44:38 +05:30
rizwanisready
e23576658b Merge pull request #9 from WDI-Ideas/development
html web view 3
2024-05-24 16:53:09 +05:30
rizwanisready
478783054b html web view 3 2024-05-24 16:51:50 +05:30
rizwanisready
8413b058a7 Merge pull request #8 from WDI-Ideas/development
html web view 2
2024-05-24 16:44:10 +05:30
rizwanisready
446535855b html web view 2 2024-05-24 16:43:02 +05:30
rizwanisready
b36f983de3 Merge pull request #7 from WDI-Ideas/development
html web view
2024-05-24 16:39:43 +05:30
rizwanisready
f35ffded81 html web view 2024-05-24 16:39:00 +05:30
rizwanisready
0db4639b6a Merge pull request #6 from WDI-Ideas/development
handling active subscription if exist 3
2024-05-24 16:23:37 +05:30
rizwanisready
ff433e4a76 handling active subscription if exist 3 2024-05-24 16:22:45 +05:30
rizwanisready
4067285d39 Merge pull request #5 from WDI-Ideas/development
handling active subscription if exist 2
2024-05-24 16:13:11 +05:30
rizwanisready
c5e3a8afd1 handling active subscription if exist 2 2024-05-24 16:12:27 +05:30
rizwanisready
8ba3b6d250 Merge pull request #4 from WDI-Ideas/development
Development
2024-05-24 16:01:15 +05:30
rizwanisready
5b98584c57 handling active subscription if exist 2024-05-24 15:59:39 +05:30
rizwanisready
a35f5f92d4 Merge pull request #3 from WDI-Ideas/development
Development
2024-05-24 12:35:14 +05:30
rizwanisready
150249cce3 Merge branch 'development' of https://github.com/WDI-Ideas/goodtimes into development 2024-05-24 12:29:27 +05:30
rizwanisready
348ed6e910 production url updated for stripe 2024-05-24 12:29:18 +05:30
BOBBY VISHWAKARMA
af0426c51c Merge pull request #2 from WDI-Ideas/development
refactor(setting): structure setting for different env
2024-05-24 12:23:34 +05:30
BOBBY VISHWAKARMA
507f147e51 Merge pull request #1 from WDI-Ideas/development
refactor(setting): structure setting for different env
2024-05-24 12:19:56 +05:30
bobbyvish
e5017899e3 refactor(setting): structure setting for different env 2024-05-24 12:18:58 +05:30
167 changed files with 10137 additions and 3611 deletions

View File

@@ -40,7 +40,7 @@ from .models import IAmPrincipalLocation
class IAmPrincipalLocationAdmin(admin.ModelAdmin):
list_display = ("id", "principal", "latitude", "longitude")
list_display = ("id", "principal", "latitude", "longitude", "created_on", "modified_on")
search_fields = (
"principal__first_name",
"principal__last_name",

View File

@@ -6,10 +6,12 @@ from rest_framework import serializers
from accounts.models import (
AppVersion,
IAmPrincipal,
IAmPrincipalExtendedData,
IAmPrincipalType,
# IAmPrincipalKYCDetails,
)
from manage_events.models import EventPrincipalInteraction, PrincipalPreference
from manage_events.models import EventInteractionType, EventPrincipalInteraction, FreeUsageFeatureLimit, PrincipalPreference
from manage_referrals.models import (
ReferralCode,
ReferralRecord,
@@ -136,17 +138,15 @@ class PasswordResetSerializer(BasePasswordSerializer, serializers.ModelSerialize
fields = ["password", "confirm_password"]
from phonenumbers import parse, phonenumberutil, NumberParseException
class ProfileSerializer(serializers.ModelSerializer):
profile_photo = serializers.ImageField(required=False)
email = serializers.CharField(read_only=True)
invite_count = serializers.SerializerMethodField(read_only=True)
principal_type_name = serializers.SerializerMethodField(read_only=True)
has_active_subscription = serializers.SerializerMethodField(read_only=True)
has_preferences = serializers.SerializerMethodField(read_only=True)
register_complete = serializers.BooleanField(read_only=True)
email = serializers.CharField(read_only=True)
is_active = serializers.BooleanField(read_only=True)
going_events_count = serializers.SerializerMethodField(read_only=True)
interested_events_count = serializers.SerializerMethodField(read_only=True)
phone_no = serializers.CharField(required=True)
class Meta:
model = IAmPrincipal
@@ -157,21 +157,29 @@ class ProfileSerializer(serializers.ModelSerializer):
"player_id",
"first_name",
"last_name",
'business_name',
"phone_no",
"email",
"invite_count",
"register_complete",
"has_active_subscription",
"has_preferences",
"linkedin_profile",
"youtube_profile",
"facebook_profile",
"instagram_profile",
"twitter_profile",
"website",
"is_active",
"going_events_count",
"interested_events_count",
]
# def validate_phone_no(self, value):
# try:
# # Parse the phone number
# phone_number = parse(value)
# # Check for validity
# if not phonenumberutil.is_valid_number(phone_number):
# raise serializers.ValidationError('Please enter a valid phone number.')
# return value
# except NumberParseException:
# raise serializers.ValidationError('The phone number format is invalid.')
def update(self, instance, validated_data):
instance.profile_photo = validated_data.get(
"profile_photo", instance.profile_photo
@@ -180,14 +188,53 @@ class ProfileSerializer(serializers.ModelSerializer):
instance.last_name = validated_data.get("last_name", instance.last_name)
return super().update(instance, validated_data)
def get_image_url(self, obj, field_name, request):
image_field = getattr(obj, field_name)
if image_field:
return request.build_absolute_uri(image_field.url)
return ""
def get_principal_type_name(self, obj):
return obj.principal_type.name if obj.principal_type else None
def to_representation(self, instance):
data = super().to_representation(instance)
request = self.context.get("request")
data["profile_photo"] = self.get_image_url(instance, "profile_photo", request)
return data
class ProfileExtendedDataSerializer(serializers.ModelSerializer):
invite_count = serializers.SerializerMethodField(read_only=True)
principal_type_name = serializers.SerializerMethodField(read_only=True)
has_active_subscription = serializers.SerializerMethodField(read_only=True)
preference = serializers.SerializerMethodField(read_only=True)
principal_preference_count = serializers.SerializerMethodField(read_only=True)
going_events_count = serializers.SerializerMethodField(read_only=True)
interested_events_count = serializers.SerializerMethodField(read_only=True)
feature_limit = serializers.SerializerMethodField(read_only=True)
class Meta:
model = IAmPrincipal
fields = [
"principal_type_name",
"invite_count",
"register_complete",
"has_active_subscription",
"preference",
"principal_preference_count",
"going_events_count",
"interested_events_count",
"feature_limit"
]
def get_going_events_count(self, obj):
return EventPrincipalInteraction.objects.filter(
principal=obj, status="going"
principal=obj, status=EventInteractionType.GOING
).count()
def get_interested_events_count(self, obj):
return EventPrincipalInteraction.objects.filter(
principal=obj, status="interested"
principal=obj, status=EventInteractionType.INTERESTED
).count()
def get_invite_count(self, obj):
@@ -198,38 +245,34 @@ class ProfileSerializer(serializers.ModelSerializer):
def get_principal_type_name(self, obj):
return obj.principal_type.name if obj.principal_type else None
def get_has_preferences(self, obj):
def get_preference(self, obj):
return PrincipalPreference.objects.filter(principal=obj).exists()
def get_image_url(self, obj, field_name, request):
image_field = getattr(obj, field_name)
if image_field:
return request.build_absolute_uri(image_field.url)
return ""
def get_principal_preference_count(self, obj):
principal_preference = PrincipalPreference.objects.filter(principal=obj).first()
if principal_preference:
categories = principal_preference.preferred_categories.all()
return categories.count()
return 0
def get_has_active_subscription(self, obj):
subscription_status = {
"has_active_subscription": False,
"in_grace_period": False,
"grace_period_end_date": None,
"subscription_id": None,
}
today = timezone.now().date()
# Attempt to find the active subscription with the furthest grace_period_end_date
latest_subscription = (
PrincipalSubscription.objects.filter(
principal=obj,
is_paid=True,
cancelled=False,
deleted=False,
active=True,
status=SubscriptionStatus.ACTIVE,
)
.order_by("-grace_period_end_date")
.first()
) # Order by descending grace_period_end_date and take the first
latest_subscription = PrincipalSubscription.get_principal_subscription(obj)
print(f"subscrition record {latest_subscription}")
if latest_subscription:
subscription_status["subscription_id"] = (
latest_subscription.stripe_subscription_id
)
# Check if we're within the grace period
if today <= latest_subscription.grace_period_end_date:
subscription_status["has_active_subscription"] = (
@@ -246,12 +289,10 @@ class ProfileSerializer(serializers.ModelSerializer):
return subscription_status
def to_representation(self, instance):
data = super().to_representation(instance)
request = self.context.get("request")
data["profile_photo"] = self.get_image_url(instance, "profile_photo", request)
return data
def get_feature_limit(self, obj):
from manage_events.api.serializers import FreeUsageFeatureLimitSerializer
obj = FreeUsageFeatureLimit.objects.first()
return FreeUsageFeatureLimitSerializer().to_representation(obj)
# class PrincipalKYCDetailsSerializer(serializers.ModelSerializer):
# aadhar_front_image = serializers.ImageField(required=False)
@@ -349,3 +390,9 @@ class AppVersionSerializer(serializers.ModelSerializer):
"force_upgrade",
"recommend_upgrade",
]
class IAmPrincipalExtendedDataSerializer(serializers.ModelSerializer):
class Meta:
model = IAmPrincipalExtendedData
fields = "__all__"

View File

@@ -17,7 +17,8 @@ urlpatterns = [
path('request-otp/', views.OtpRequestView.as_view(), name='send_otp'),
path('verify-otp/', views.OTPVerificationView.as_view(), name='send_otp'),
# path('profile/view/<str:principal_type>/', views.ProfileView.as_view(), name='profile_veiw'),
path('profile/extended-data/', views.ProfileExtendedView.as_view(), name='profile_veiw'),
path('profile/view/', views.ProfileView.as_view(), name='profile_veiw'),
path('profile/edit/', views.ProfileView.as_view(), name='profile_edit'),
path('profile/password-reset/', views.ProfilePasswordResetView.as_view(), name='password_reset'),
@@ -38,5 +39,6 @@ urlpatterns = [
name="delete_account",
),
path('version-check/', views.VersionCheck.as_view(), name='version_check'),
path('transfer-check/', views.AccountTransferCheckView.as_view(), name='transfer_check'),
]

View File

@@ -17,6 +17,7 @@ from django.views.decorators.http import require_http_methods
from accounts.models import (
AppVersion,
IAmPrincipal,
IAmPrincipalExtendedData,
IAmPrincipalOtp,
IAmPrincipalSource,
IAmPrincipalType,
@@ -41,7 +42,9 @@ from .serializers import (
# RegistrationPasswordSerializer,
# PhoneSerializer,
EmailSerializer,
IAmPrincipalExtendedDataSerializer,
PlayerIDSerializer,
ProfileExtendedDataSerializer,
RegistrationPasswordSerializer,
RegistrationSerializer,
ReferralCodeSerializer,
@@ -120,7 +123,7 @@ class RegistrationEmailView(APIView):
email_service = EmailService(
subject="Good Times - OTP",
to=[email],
from_email=settings.EMAIL_HOST_USER,
from_email=settings.DEFAULT_FROM_EMAIL,
)
email_service.load_template(
"otp/otp.html", context={"OTP": otp, "action": "Register"}
@@ -319,7 +322,7 @@ class OtpRequestView(APIView):
email_service = EmailService(
subject="Good Times - OTP",
to=[email],
from_email=settings.EMAIL_HOST_USER,
from_email=settings.DEFAULT_FROM_EMAIL,
)
email_service.load_template(
"otp/otp.html", context={"OTP": otp, "action": "Login"}
@@ -536,6 +539,21 @@ class ProfileView(APIView):
)
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
class ProfileExtendedView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
model = IAmPrincipal
serializer = ProfileExtendedDataSerializer
def get(self, request, *args, **kwargs):
serializer = self.serializer(instance=request.user)
success_response = {
"status": status.HTTP_200_OK,
"message": constants.SUCCESS,
"data": serializer.data,
}
return ApiResponse.success(**success_response)
class ProfilePasswordResetView(APIView):
authentication_classes = [JWTAuthentication]
@@ -937,7 +955,7 @@ class SoftDeletePrincipalAPIView(APIView):
def delete(self, request, format=None):
principal = request.user
if principal.deleted: # Check if already deleted
if not principal.is_active: # Check if already deleted
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Account already deleted.",
@@ -945,7 +963,6 @@ class SoftDeletePrincipalAPIView(APIView):
)
principal.is_active = False
principal.deleted = True
principal.save()
return ApiResponse.success(
status=status.HTTP_200_OK,
@@ -961,12 +978,74 @@ class VersionCheck(APIView):
def get(self, request, *args, **kwargs):
device_type = request.GET.get('type')
device_type = request.GET.get("type")
if not device_type:
return ApiResponse.error(message=constants.FAILURE, errors="device type is required")
return ApiResponse.error(
message=constants.FAILURE, errors="device type is required"
)
# Query the database to retrieve the upgrade flags based on the app version
version = self.model.objects.filter(app_type=device_type).last()
version_data = AppVersionSerializer(version)
return ApiResponse.success(message=constants.SUCCESS, data=version_data.data)
class AccountTransferCheckView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
model = IAmPrincipalExtendedData
serializer_class = IAmPrincipalExtendedDataSerializer
def get(self, request, *args, **kwargs):
print("request.user is ", request.user)
try:
obj = IAmPrincipalExtendedData.objects.get(principal=request.user)
except IAmPrincipalExtendedData.DoesNotExist:
# Create a dummy serializer record
obj = {
'principal': request.user.id,
'is_onboarded': False,
'is_transferred': False,
'transferred_on': None,
'pwd_changed_post_transfer': False
}
return ApiResponse.success(message=constants.SUCCESS, data=obj)
except Exception as e:
error_response = {
"status": status.HTTP_404_NOT_FOUND,
"message": constants.RECORD_NOT_FOUND,
"errors": str(e),
}
return ApiResponse.error(**error_response)
serializer = self.serializer_class(obj)
print("serializer data", serializer)
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
def post(self, request, *args, **kwargs):
try:
obj = IAmPrincipalExtendedData.objects.get(principal=request.user)
except IAmPrincipalExtendedData.DoesNotExist:
# Create a dummy serializer record
obj = {
'principal': request.user.id,
'is_onboarded': False,
'is_transferred': False,
'transferred_on': None,
'pwd_changed_post_transfer': False
}
return ApiResponse.success(message=constants.SUCCESS, data=obj)
except Exception as e:
error_response = {
"status": status.HTTP_404_NOT_FOUND,
"message": constants.RECORD_NOT_FOUND,
"errors": str(e),
}
return ApiResponse.error(**error_response)
obj.pwd_changed_post_transfer = True
obj.save()
serializer = self.serializer_class(obj)
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)

View File

@@ -44,6 +44,7 @@ def compute_resource_action_constants(request):
'RESOURCE_MANAGE_NOTIFICATIONS': resource_action.RESOURCE_MANAGE_NOTIFICATIONS,
'RESOURCE_MANAGE_REFERRALS': resource_action.RESOURCE_MANAGE_REFERRALS,
'RESOURCE_MANAGE_FEEDBACK': resource_action.RESOURCE_MANAGE_FEEDBACK,
'RESOURCE_MANAGE_COUPONS': resource_action.RESOURCE_MANAGE_COUPONS,
'RESOURCE_IAM_PRINCIPAL': resource_action.RESOURCE_IAM_PRINCIPAL,
'RESOURCE_IAM_PRINCIPAL_GROUP': resource_action.RESOURCE_IAM_PRINCIPAL_GROUP,
'RESOURCE_IAM_GROUP': resource_action.RESOURCE_IAM_GROUP,

View File

@@ -27,7 +27,8 @@ from accounts.resource_action import (
RESOURCE_MANAGE_REFERRALS,
RESOURCE_MANAGE_FEEDBACK,
RESOURCE_MANAGE_WITHDRAWALS,
RESOURCE_MANAGE_BANK_ACCOUNTS
RESOURCE_MANAGE_BANK_ACCOUNTS,
RESOURCE_MANAGE_COUPONS
)
# this variable store the data of model principaltype, action, resource
fixture_data = [
@@ -334,4 +335,16 @@ fixture_data = [
"action": [1, 2, 3, 4],
},
},
{
"model": "accounts.iamappresource",
"pk": 18,
"fields": {
"name": RESOURCE_MANAGE_COUPONS,
"label": RESOURCE_MANAGE_COUPONS,
"slug": RESOURCE_MANAGE_COUPONS,
"created_on": "2023-09-28T16:17:42.815",
"modified_on": "2023-09-28T16:17:42.815",
"action": [1, 2, 3, 4],
},
},
]

View File

@@ -386,5 +386,22 @@
4
]
}
},
{
"model": "accounts.iamappresource",
"pk": 18,
"fields": {
"name": "manage_coupons",
"label": "manage_coupons",
"slug": "manage_coupons",
"created_on": "2023-09-28T16:17:42.815",
"modified_on": "2023-09-28T16:17:42.815",
"action": [
1,
2,
3,
4
]
}
}
]

View File

@@ -6,6 +6,7 @@ from django.core import validators
from django.utils.translation import gettext_lazy as _
from goodtimes import constants
from manage_events.models import EventCategory
from . import models
# from .backend import EmailBackend
@@ -358,3 +359,91 @@ class IAmPrincipalResourceLinkForm(IAmPrincipalForm):
# Save the instance to the database
if commit:
principal.save()
class CreateCustomerForm(forms.Form):
profile_photo = forms.ImageField(label="Profile Image", widget=forms.ClearableFileInput(attrs={'class': 'filepond'}),)
first_name = forms.CharField(max_length=255, required=True, label='First Name')
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
email = forms.EmailField(required=True, label='Email')
phone_no = PhoneNumberField(
widget=forms.TextInput(),
label="Phone No"
)
preferences = forms.ModelMultipleChoiceField(
queryset=EventCategory.objects.all(),
widget=forms.widgets.SelectMultiple(
attrs={"class": "form_select js-example-basic-multiple"}
),
required=True,
label='Preferences'
)
free_start_date = forms.DateField(
required=True,
label=_('Free period start date'),
help_text=_('Enter the start date of the free period')
)
free_end_date = forms.DateField(
required=True,
label=_('Free period end date'),
help_text=_('Enter the end date of the free period')
)
address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}))
city = forms.CharField(max_length=200, required=False, label="Region")
country = forms.CharField(max_length=200, required=False, label="Country")
website = forms.URLField(max_length=255, required=False, label="Website")
linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn")
facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook")
instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram")
twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['preferences'].queryset = EventCategory.objects.all()
class UpdateCustomerForm(forms.Form):
profile_photo = forms.ImageField(label="Profile Image")
first_name = forms.CharField(max_length=255, required=True, label='First Name')
last_name = forms.CharField(max_length=255, required=True, label='Last Name')
business_name = forms.CharField(max_length=200, required=True, label="Business Name")
email = forms.EmailField(required=True, label='Email', widget=forms.TextInput(attrs={'readonly': 'readonly'}))
phone_no = PhoneNumberField(
widget=forms.TextInput(),
label="Phone No"
)
preferences = forms.ModelMultipleChoiceField(
queryset=EventCategory.objects.all(),
widget=forms.widgets.SelectMultiple(
attrs={"class": "form_select js-example-basic-multiple"}
),
required=True,
label='Preferences'
)
free_start_date = forms.DateField(
required=True,
label=_('Free period start date'),
help_text=_('Enter the start date of the free period')
)
free_end_date = forms.DateField(
required=True,
label=_('Free period end date'),
help_text=_('Enter the end date of the free period')
)
address_line1 = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 40}))
city = forms.CharField(max_length=200, required=False, label="Region")
country = forms.CharField(max_length=200, required=False, label="Country")
website = forms.URLField(max_length=255, required=False, label="Website")
linkedin_profile = forms.URLField(max_length=200, required=False, label="LinkedIn")
facebook_profile = forms.URLField(max_length=200, required=False, label="Facebook")
instagram_profile = forms.URLField(max_length=200, required=False, label="Instagram")
twitter_profile = forms.URLField(max_length=200, required=False, label="Twitter")
active = forms.BooleanField(required=False, label='Active', help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['preferences'].queryset = EventCategory.objects.all()
class UploadExcelForm(forms.Form):
file = forms.FileField()

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.0.2 on 2024-05-31 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0009_appversion_app_type"),
]
operations = [
migrations.AlterField(
model_name="appversion",
name="app_type",
field=models.CharField(
choices=[("android", "android"), ("ios", "ios")], max_length=10
),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2024-06-20 08:17
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0010_alter_appversion_app_type"),
]
operations = [
migrations.AlterField(
model_name="iamprincipallocation",
name="principal",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="principal_location",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.0.2 on 2024-06-25 07:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0011_alter_iamprincipallocation_principal'),
]
operations = [
migrations.CreateModel(
name='IAmPrincipalExtendedData',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_onboarded', models.BooleanField(default=False, help_text='Indicates whether the user was onboarded by an admin.')),
('is_transferred', models.BooleanField(default=False, help_text='Indicates whether the account has been transferred to the user.')),
('transferred_on', models.DateTimeField(blank=True, help_text='The date and time when the account was transferred to the user.', null=True)),
('principal', models.OneToOneField(help_text='The principal user to which this extended data is related.', on_delete=django.db.models.deletion.CASCADE, related_name='extended_data', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'iam_principal_extended_data',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-06-27 08:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0012_iamprincipalextendeddata'),
]
operations = [
migrations.AddField(
model_name='iamprincipalextendeddata',
name='pwd_changed_post_transfer',
field=models.BooleanField(default=False, help_text='Indicates if the user changed their password after the account was transferred.'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-08-11 16:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0013_iamprincipalextendeddata_pwd_changed_post_transfer'),
]
operations = [
migrations.AddField(
model_name='iamprincipal',
name='business_name',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Business Name'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-08-12 08:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0014_iamprincipal_business_name'),
]
operations = [
migrations.AddField(
model_name='iamprincipal',
name='twitter_profile',
field=models.URLField(blank=True, max_length=255, null=True, verbose_name='Principal Twitter'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-12-20 12:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0015_iamprincipal_twitter_profile'),
]
operations = [
migrations.AddField(
model_name='iamprincipalextendeddata',
name='encrypted_pass',
field=models.TextField(blank=True, null=True),
),
]

View File

@@ -13,6 +13,7 @@ from django.utils.text import slugify
from phonenumber_field.modelfields import PhoneNumberField
# from manage_subscriptions.models import Subscription
from goodtimes.utils import RandomGenerator
from .resource_action import (
PRINCIPAL_TYPE_EVENT_USER,
@@ -320,6 +321,8 @@ class IAmPrincipal(AbstractUser):
blank=True,
null=True,
)
business_name = models.CharField(verbose_name="Business Name", max_length=200, blank=True, null=True)
twitter_profile = models.URLField(verbose_name="Principal Twitter", max_length=255, null=True, blank=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
@@ -332,6 +335,52 @@ class IAmPrincipal(AbstractUser):
def __str__(self):
return f"{self.email}"
@staticmethod
def generate_random_password():
"""Generate a password in the format 'GoodTimes@xxxx'."""
random_number = random.randint(1000, 9999) # Generate a 4-digit random number
return f"GoodTimes@{random_number}"
class IAmPrincipalExtendedData(models.Model):
principal = models.OneToOneField(
IAmPrincipal,
related_name="extended_data",
on_delete=models.CASCADE,
help_text="The principal user to which this extended data is related."
)
is_onboarded = models.BooleanField(
default=False,
help_text="Indicates whether the user was onboarded by an admin."
)
is_transferred = models.BooleanField(
default=False,
help_text="Indicates whether the account has been transferred to the user."
)
transferred_on = models.DateTimeField(
null=True,
blank=True,
help_text="The date and time when the account was transferred to the user."
)
pwd_changed_post_transfer = models.BooleanField(default=False, help_text="Indicates if the user changed their password after the account was transferred.")
encrypted_pass = models.TextField(blank=True, null=True)
class Meta:
db_table = "iam_principal_extended_data"
def __str__(self):
return f"Extended Data for {self.principal}"
def save(self, *args, **kwargs):
if self.is_transferred and self.transferred_on is None:
self.transferred_on = datetime.datetime.now()
super().save(*args, **kwargs)
@property
def decrypted_field(self):
from goodtimes.services import Encryptor
encryptor = Encryptor()
return encryptor.decrypt(self.encrypted_pass)
class IAmPrincipalResourceLink(models.Model):
principal = models.ForeignKey(
@@ -419,7 +468,7 @@ class IAmPrincipalBiometric(BaseModel):
class IAmPrincipalLocation(BaseModel):
principal = models.ForeignKey(
principal = models.OneToOneField(
IAmPrincipal, related_name="principal_location", on_delete=models.CASCADE
)
latitude = models.DecimalField(max_digits=18, decimal_places=15)
@@ -432,76 +481,6 @@ class IAmPrincipalLocation(BaseModel):
return f"{self.principal.first_name}:{self.latitude}, {self.longitude}"
# Excluded in migrations
# class IAmPrincipalKYCDetails(models.Model):
# # the below is the table structure from Hritik Dhanawde for KYC
# kid =
# status =
# customer_identifier =
# reference_id =
# customer_name =
# reference_id =
# customer_name =
# workflow_name =
# template_id =
# kyc_created_at =
# access_token =
# # Regex pattern for Aadhar number with exactly 12 digits
# AADHAR_REGEX = r"^\d{12}$"
# # Regex pattern for PAN number in the format AAAAB1234C
# PAN_REGEX = r"^[A-Z]{5}[0-9]{4}[A-Z]$"
# # Regex pattern for IFSC code (11 alphanumeric characters)
# IFSC_REGEX = r"^[A-Za-z]{4}\d{7}$"
# principal = models.OneToOneField(
# IAmPrincipal, on_delete=models.CASCADE
# ) # Assuming IAmPrincipal is the user model.
# aadhar_front_image = models.ImageField(upload_to="kyc/", blank=True, null=True)
# aadhar_back_image = models.ImageField(upload_to="kyc/", blank=True, null=True)
# aadhar_number = models.CharField(
# max_length=12,
# blank=True,
# null=True,
# validators=[
# RegexValidator(AADHAR_REGEX, message="Aadhar number must be 12 digits.")
# ],
# )
# pan_image = models.ImageField(upload_to="kyc/", blank=True, null=True)
# pan_number = models.CharField(
# max_length=10,
# blank=True,
# null=True,
# validators=[
# RegexValidator(
# PAN_REGEX, message="PAN number must be in the format AAAAB1234C."
# )
# ],
# )
# is_aadhar_verified = models.BooleanField(default=False)
# is_pan_verified = models.BooleanField(default=False)
# account_no = models.CharField(max_length=20, blank=True, null=True)
# bank_name = models.CharField(max_length=100, blank=True, null=True)
# branch_name = models.CharField(max_length=100, blank=True, null=True)
# ifsc_code = models.CharField(
# max_length=11,
# blank=True,
# null=True,
# validators=[
# RegexValidator(
# IFSC_REGEX, message="IFSC code must be 11 alphanumeric characters."
# )
# ],
# )
# class Meta:
# db_table = "iam_principal_kyc_details"
# def __str__(self):
# return f"KYC Information for {self.principal.email}"
class AppType(models.TextChoices):
ANDROID = "android", "android"
IOS = "ios", "ios"

View File

@@ -28,6 +28,7 @@ RESOURCE_MANAGE_REFERRALS = "manage_referrals"
RESOURCE_MANAGE_NOTIFICATIONS = "manage_notifications"
RESOURCE_MANAGE_WITHDRAWALS = "manage_withdrawals"
RESOURCE_MANAGE_BANK_ACCOUNTS = "manage_bank_accounts"
RESOURCE_MANAGE_COUPONS = "manage_coupons"
# These constants are used solely for managing the active and inactive state of pages

View File

@@ -42,7 +42,15 @@ urlpatterns = [
path('principal/role/delete/<int:pk>/', views.AppRoleDeleteView.as_view(), name="role_delete"),
path('customer/', views.CustomerListView.as_view(), name="customer_list"),
path('customer/get-decrypted-password/<int:customer_id>/', views.GetDecryptedPasswordView.as_view(), name='get_decrypted_password'),
path('customer/add/', views.CustomerCreateView.as_view(), name="customer_add"),
path('customer/edit/<int:pk>/', views.CustomerUpdateView.as_view(), name="customer_edit"),
path('customer/detail/<int:pk>/', views.CustomerDetailView.as_view(), name="customer_detail"),
path('customer/transfer/<int:pk>/', views.CustomerTransferView.as_view(), name="customer_transfer"),
path('customer/check-email/', views.CustomerCheckEmail.as_view(), name="customer_check_email"),
path('customer/download-excel-template/', views.export_excel_template, name='download_excel_template'),
path('customer/import-customer-data/', views.CustomerImportView.as_view(), name='import_customer_data'),
path('customer/export-customer-data/', views.CustomerExportView.as_view(), name='export_customer_data'),
# ignore this to path this for example setup
path('principal/example/', TemplateView.as_view(template_name="accounts/iam_module/example_form.html"), name="example_add"),

View File

@@ -1,10 +1,12 @@
import logging
from django.conf import settings
from django.db.models import Count, Q
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.views import LogoutView
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.hashers import make_password
from django.contrib.auth.views import (
LoginView,
PasswordResetCompleteView,
@@ -13,16 +15,26 @@ from django.contrib.auth.views import (
PasswordResetView,
)
from django.core.exceptions import ValidationError
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse_lazy
from django.views import generic
from django.db import models, transaction, IntegrityError
from django.utils import timezone
import phonenumbers
from accounts import permission
from goodtimes import constants
from goodtimes.services import EmailService, Encryptor
from goodtimes.utils import JsonResponseUtil
from manage_events.models import EventCategory, PrincipalPreference
from manage_referrals.models import ReferralCode
from manage_subscriptions.models import PrincipalSubscription, Subscription
import datetime
from datetime import datetime, timedelta
from . import resource_action
from .forms import (
CreateCustomerForm,
CustomAuthenticationForm,
IAmPrincipalForm,
IAmPrincipalGroupRoleLinkForm,
@@ -30,9 +42,12 @@ from .forms import (
IAmPrincipalRoleAppResourceActionLinkForm,
IAmPrincipalGroupLinkForm,
ProfileEditForm,
UpdateCustomerForm,
UploadExcelForm,
)
from .models import (
IAmPrincipal,
IAmPrincipalExtendedData,
IAmPrincipalType,
IAmAppResourceActionLink,
IAmPrincipalGroup,
@@ -171,10 +186,6 @@ class PrincipalListView(LoginRequiredMixin, generic.ListView):
context["page_name"] = self.page_name
return context
import datetime
class PrincipalCreateOrUpdateView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_IAM_PRINCIPAL
model = IAmPrincipal
@@ -557,6 +568,266 @@ class AppRoleDeleteView(LoginRequiredMixin, generic.View):
"""Customer"""
class CustomerCheckEmail(generic.View):
model = IAmPrincipal
def post(self, request, *args, **kwargs):
email = request.POST.get('email')
print("check email is cllaed ", email)
if self.model.objects.filter(email=email).exists():
print("exist called")
return JsonResponse({'message': 'This email address is already in use.'}, status=400)
else:
print("email is valid")
return JsonResponse({'message': 'Email is available.'}, status=200)
class CustomerCreateView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
resource = resource_action.RESOURCE_MANAGE_CUSTOMER
model = IAmPrincipal
form_class = CreateCustomerForm
template_name = "accounts/customer/customer_add.html"
success_url = reverse_lazy("accounts:customer_list")
success_message = "Saved Successfully"
error_message = "An error occurred while saving the data."
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
"operation": "Add",
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def get(self, request, *args, **kwargs):
form = self.form_class()
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
def post(self, request, *args, **kwargs):
print(request.POST)
# return redirect(self.success_url)
form = self.form_class(request.POST, request.FILES)
context = self.get_context_data(form=form)
if not form.is_valid():
return render(request, self.template_name, context=context)
free_subscription = Subscription.objects.filter(is_free=True, active=True).first()
if not free_subscription:
messages.error(self.request, "Create a free subscription record for admin in manage subscription")
return render(request, self.template_name, context=context)
try:
with transaction.atomic():
random_password = IAmPrincipal.generate_random_password()
# Encrypt the password
encryptor = Encryptor()
encrypted_password = encryptor.encrypt(random_password)
# save principal data
principal_obj = IAmPrincipal.objects.create(
profile_photo = form.cleaned_data.get("profile_photo"),
email=form.cleaned_data.get('email'),
first_name=form.cleaned_data.get('first_name'),
last_name=form.cleaned_data.get('last_name'),
business_name=form.cleaned_data.get('business_name'),
phone_no=form.cleaned_data.get('phone_no'),
password=make_password(random_password),
username=form.cleaned_data.get("email"),
email_verified=True,
register_complete=True,
principal_type=IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER),
address_line1=form.cleaned_data.get("address_line1"),
city=form.cleaned_data.get("city"),
country=form.cleaned_data.get("country"),
website=form.cleaned_data.get("website"),
linkedin_profile=form.cleaned_data.get("linkedin_profile"),
facebook_profile=form.cleaned_data.get("facebook_profile"),
instagram_profile=form.cleaned_data.get("instagram_profile"),
twitter_profile=form.cleaned_data.get("twitter_profile"),
)
# generate referralcode of manager
ReferralCode.create_referral_code_for_user_manager(
principal=principal_obj, principal_type=principal_obj.principal_type
)
IAmPrincipalExtendedData.objects.create(
principal=principal_obj,
is_onboarded=True,
encrypted_pass=encrypted_password, # Save encrypted password
)
# save principal preferences record
principal_preference = PrincipalPreference.objects.create(principal=principal_obj)
principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
principal_subscription = PrincipalSubscription.objects.create(
start_date=form.cleaned_data.get("free_start_date"),
end_date=form.cleaned_data.get("free_end_date"),
principal=principal_obj,
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
is_paid=True,
subscription=free_subscription
)
messages.success(self.request, constants.REGISTRATION_SUCCESS)
return redirect(self.success_url)
except Exception as e:
print("errror is ", e)
messages.error(self.request, str(e))
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
class CustomerUpdateView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
resource = resource_action.RESOURCE_MANAGE_CUSTOMER
model = IAmPrincipal
form_class = UpdateCustomerForm
template_name = "accounts/customer/customer_edit.html"
success_url = reverse_lazy("accounts:customer_list")
success_message = "Updated Successfully"
error_message = "An error occurred while saving the data."
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
"operation": "Edit",
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def get(self, request, *args, **kwargs):
principal_id = kwargs.get("pk")
try:
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
except Exception as e:
messages.error(request, f"No Record of id {principal_id} is found")
return redirect(self.success_url)
print(f"principal address is {principal_obj.address_line1}")
initial_data = {
"profile_photo": principal_obj.profile_photo,
"first_name": principal_obj.first_name,
"last_name": principal_obj.last_name,
"email": principal_obj.email,
"business_name": principal_obj.business_name,
"phone_no": principal_obj.phone_no,
"address_line1": principal_obj.address_line1,
"city": principal_obj.city,
"country": principal_obj.country,
"website": principal_obj.website,
"linkedin_profile": principal_obj.linkedin_profile,
"facebook_profile": principal_obj.facebook_profile,
"instagram_profile": principal_obj.instagram_profile,
"twitter_profile": principal_obj.twitter_profile,
"active": principal_obj.is_active
}
try:
principal_preference = PrincipalPreference.objects.get(principal=principal_obj)
initial_data["preferences"] = list(principal_preference.preferred_categories.all().values_list("id", flat=True))
except PrincipalPreference.DoesNotExist:
initial_data["preferences"] = []
try:
subscription = PrincipalSubscription.objects.filter(principal=principal_obj).latest("created_on")
initial_data["free_start_date"] = subscription.start_date
initial_data["free_end_date"] = subscription.end_date
except PrincipalSubscription.DoesNotExist:
initial_data["free_start_date"] = None
initial_data["free_end_date"] = None
form = self.form_class(initial=initial_data)
context = self.get_context_data(form=form, principal_obj=principal_obj)
print("context dta is ", context)
return render(request, self.template_name, context=context)
def post(self, request, *args, **kwargs):
principal_id = kwargs.get("pk")
try:
principal_obj = IAmPrincipal.objects.get(pk=principal_id)
except Exception as e:
messages.error(request, f"No Record of customer id {principal_id} is found")
return redirect(self.success_url)
form = self.form_class(request.POST, request.FILES)
print(request.POST)
if not form.is_valid():
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
try:
with transaction.atomic():
# update principal data
principal_obj.profile_photo = form.cleaned_data.get('profile_photo')
principal_obj.first_name = form.cleaned_data.get('first_name')
principal_obj.last_name = form.cleaned_data.get('last_name')
principal_obj.business_name = form.cleaned_data.get("business_name")
principal_obj.phone_no = form.cleaned_data.get("phone_no")
principal_obj.address_line1 = form.cleaned_data.get("address_line1")
principal_obj.city = form.cleaned_data.get("city")
principal_obj.country = form.cleaned_data.get("country")
principal_obj.website = form.cleaned_data.get("website")
principal_obj.linkedin_profile = form.cleaned_data.get("linkedin_profile")
principal_obj.facebook_profile = form.cleaned_data.get("facebook_profile")
principal_obj.instagram_profile = form.cleaned_data.get("instagram_profile")
principal_obj.twitter_profile = form.cleaned_data.get("twitter_profile")
principal_obj.is_active = form.cleaned_data.get("active")
principal_obj.save()
# update principal preferences record
principal_preference, _ = PrincipalPreference.objects.get_or_create(principal=principal_obj)
principal_preference.preferred_categories.set(form.cleaned_data.get("preferences"))
# update principal subscription record
principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by('-end_date').first()
if principal_subscription:
principal_subscription.start_date = form.cleaned_data.get("free_start_date")
principal_subscription.end_date = form.cleaned_data.get("free_end_date")
principal_subscription.grace_period_end_date = form.cleaned_data.get("free_end_date") + timedelta(days=15)
principal_subscription.save()
else:
PrincipalSubscription.objects.create(
principal=principal_obj,
start_date=form.cleaned_data.get("free_start_date"),
end_date=form.cleaned_data.get("free_end_date"),
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(form.cleaned_data.get("free_end_date")),
is_paid=True,
subscription=Subscription.objects.filter().first() # Assuming you want to link a default subscription
)
messages.success(self.request, self.success_message)
return redirect(self.success_url)
except Exception as e:
messages.error(self.request, str(e))
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
class CustomerDetailView(LoginRequiredMixin, generic.DetailView):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
resource = resource_action.RESOURCE_MANAGE_CUSTOMER
action = resource_action.ACTION_READ
template_name = 'accounts/customer/customer_detail.html'
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def get(self, request, *args, **kwargs):
principal_obj = IAmPrincipal.objects.get(pk=kwargs.get("pk"))
try:
principal_preference = PrincipalPreference.objects.get(principal_id=principal_obj.id)
except Exception as e:
principal_preference = None
principal_subscription = PrincipalSubscription.objects.filter(principal=principal_obj).order_by("-start_date").first()
context = self.get_context_data(principal_obj=principal_obj,principal_preference=principal_preference,principal_subscription=principal_subscription)
return render(request, self.template_name, context=context)
class CustomerListView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
@@ -570,7 +841,7 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
queryset = (
super()
.get_queryset()
.select_related("principal_type", "principal_source")
.select_related("principal_type", "principal_source", "extended_data")
.filter(
models.Q(
principal_type__name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER
@@ -603,10 +874,457 @@ class CustomerListView(LoginRequiredMixin, generic.ListView):
return context
class GetDecryptedPasswordView(generic.View):
def get(self, request, customer_id):
try:
# Fetch the extended data for the customer
extended_data = IAmPrincipalExtendedData.objects.get(principal_id=customer_id)
if not extended_data.encrypted_pass:
return JsonResponse({"success": False, "message": "No password found."})
# Use Encryptor to decrypt the password
encryptor = Encryptor()
decrypted_password = encryptor.decrypt(extended_data.encrypted_pass)
return JsonResponse({"success": True, "decrypted_password": decrypted_password})
except IAmPrincipalExtendedData.DoesNotExist:
return JsonResponse({"success": False, "message": "Customer not found."})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)})
import pandas as pd
from openpyxl import Workbook, load_workbook
from openpyxl.worksheet.datavalidation import DataValidation
from openpyxl.styles import Font
from django.http import HttpResponse
# def export_excel_template(request):
# # Define the columns and create an empty DataFrame
# columns = ['First Name', 'Last Name', 'Email', 'Preferences', 'Free period start date', 'Free period end date']
# df = pd.DataFrame(columns=columns)
# # Create a workbook and select the active worksheet
# wb = Workbook()
# ws = wb.active
# ws.title = 'Customer Registration'
# # # Write the column headers
# # for col_num, column_title in enumerate(df.columns, 1):
# # cell = ws.cell(row=1, column=col_num, value=column_title)
# # cell.font = Font(bold=True)
# # # Create a hidden sheet for preferences
# # ws_prefs = wb.create_sheet(title="Preferences")
# # ws_prefs.sheet_state = 'hidden'
# # # Fetch preferences options from the EventCategory model
# # preferences_options = EventCategory.objects.values_list('title', flat=True)
# # # Write preferences to the hidden sheet
# # for row_num, preference in enumerate(preferences_options, 1):
# # ws_prefs.cell(row=row_num, column=1, value=preference)
# # # Define the range for preferences in the hidden sheet
# # preferences_range = f"Preferences!$A$1:$A${len(preferences_options)}"
# # # Add Data Validation for preferences (drop-down list)
# # dv_preferences = DataValidation(
# # type="list",
# # formula1=preferences_range,
# # allow_blank=True,
# # showDropDown=True
# # )
# # ws.add_data_validation(dv_preferences)
# # dv_preferences.add(f'D2:D1048576') # Apply to the whole column
# # # Add Data Validation for date comparison
# # dv_start_date = DataValidation(
# # type="date",
# # operator="greaterThan",
# # formula1='"1900-01-01"',
# # showErrorMessage=True
# # )
# # dv_end_date = DataValidation(
# # type="custom",
# # formula1="=AND(ISNUMBER(F2), F2>E2)",
# # showErrorMessage=True,
# # errorTitle="Invalid Date",
# # error="End date must be greater than start date."
# # )
# # ws.add_data_validation(dv_start_date)
# # ws.add_data_validation(dv_end_date)
# # dv_start_date.add(f'E2:E1048576')
# # dv_end_date.add(f'F2:F1048576')
# # Save the workbook to a bytes buffer
# response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
# response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
# wb.save(response)
# return response
# from openpyxl.styles import Font
def export_excel_template(request):
# Define the columns for the Customer Registration sheet
columns = [
'First Name',
'Last Name',
'Business Name',
'Email',
'Phone No (+919999999999)',
'Preferences (should be separated by comma)',
'Free period start date (DD-MM-YYYY)',
'Free period end date (DD-MM-YYYY)',
'Address',
'Region',
'Country',
'Website',
'LinkedIn',
'Facebook',
'Instagram',
'Twitter',
]
df = pd.DataFrame(columns=columns)
# Create a workbook and add the Customer Registration worksheet
wb = Workbook()
ws_customer = wb.active
ws_customer.title = 'Manager Onboarding'
# Write the column headers for the Customer Registration sheet
for col_num, column_title in enumerate(df.columns, 1):
cell = ws_customer.cell(row=1, column=col_num, value=column_title)
cell.font = Font(bold=True)
# Create the Readme worksheet
ws_readme = wb.create_sheet(title='Readme')
# Add information about each field to the Readme sheet
readme_data = [
['Field Name', 'Description'],
['First Name', 'The first name of the customer. This is a required field.'],
['Last Name', 'The last name of the customer. This is a required field.'],
['Business Name', 'The official name of the customer\'s business or organization.'],
['Email', 'The email address of the customer. This must be a unique email not already used in the system. This is a required Field'],
['Phone No', 'The business phone number. It should include the country code if applicable (+919999999999).'],
['Category', 'A comma-separated list of event categories the customer is interested in. These should match existing categories in the system. This is a required Field'],
['Free period start date', 'The start date of the customer\'s free trial period, formatted as DD-MM-YYYY.'],
['Free period end date', 'The end date of the customer\'s free trial period, formatted as DD-MM-YYYY. This date must be later than the start date.'],
['Address', 'The complete business address, including street, city, and postal code.'],
['Region', 'The geographic region where the business operates.'],
['Country', 'The country where the business is based.'],
['Website', 'The URL of the business\'s official website. Ensure it includes "http://" or "https://".'],
['LinkedIn', 'The LinkedIn profile URL of the business. Ensure it includes "http://" or "https://".'],
['Facebook', 'The Facebook page URL of the business. Ensure it includes "http://" or "https://".'],
['Instagram', 'The Instagram profile URL of the business. Ensure it includes "http://" or "https://".'],
['Twitter', 'The Twitter handle or profile URL of the business. Ensure it includes "http://" or "https://".'],
]
# Write the Readme data to the Readme worksheet
for row_num, row_data in enumerate(readme_data, 1):
for col_num, cell_value in enumerate(row_data, 1):
cell = ws_readme.cell(row=row_num, column=col_num, value=cell_value)
if row_num == 1: # Make the header bold
cell.font = Font(bold=True)
# Save the workbook to a bytes buffer
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = 'attachment; filename=customer_registration_template.xlsx'
wb.save(response)
return response
class CustomerTransferView(LoginRequiredMixin, generic.View):
model = IAmPrincipal
def get(self, request, *args, **kwargs):
try:
principal_obj = self.model.objects.get(pk=kwargs.get("pk"))
except self.model.DoesNotExist:
messages.error(request, "Something went wrong")
return redirect(reverse_lazy("accounts:customer_detail"))
email_service = EmailService(
subject="Your Exclusive Account Access Details with Good Times!",
to=principal_obj.email,
from_email=settings.DEFAULT_FROM_EMAIL,
)
# Send the email
try:
principal_preference = IAmPrincipalExtendedData.objects.get(principal=principal_obj)
# Use Encryptor to decrypt the password
encryptor = Encryptor()
temp_password = encryptor.decrypt(principal_preference.encrypted_pass)
# updating password
principal_obj.password = make_password(temp_password)
principal_obj.save()
principal_preference.is_transferred = True
principal_preference.save()
email_service.load_template(
"accounts/customer/account_transfer_email_template.html", locals()
)
email_service.send()
messages.success(request, "Account Transfer mail send successfully")
except Exception as e:
messages.error(request, f"{str(e)}")
return redirect(reverse_lazy("accounts:customer_detail", kwargs={"pk": kwargs.get("pk")}))
class CustomerImportView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_CUSTOMER
resource = resource_action.RESOURCE_MANAGE_CUSTOMER
action = resource_action.ACTION_READ
template_name = "accounts/customer/customer_bulk_template.html"
form_class = UploadExcelForm
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def validate_date(self, date_str, row_num, error_log, field_name):
"""function to validate the date format DD-MM-YYYY"""
# Check if the input is already a datetime object
if isinstance(date_str, datetime):
return date_str
# If it's a string, attempt to validate it
if isinstance(date_str, str):
try:
return datetime.strptime(date_str, '%d-%m-%Y')
except ValueError:
error_log.append(f"Row {row_num}: {field_name} '{date_str}' is not in the format DD-MM-YYYY.")
return None
# If it's neither a string nor a datetime object, log an error
error_log.append(f"Row {row_num}: {field_name} '{date_str}' is of invalid type.")
return None
def validate_phone(self, phone_str, row_num, error_log):
"""Helper function to validate phone number"""
try:
phone_number = phonenumbers.parse(phone_str, None)
if not phonenumbers.is_valid_number(phone_number):
error_log.append(f"Row {row_num}: Phone number '{phone_str}' is not valid.")
return None
return phone_number
except Exception as e:
error_log.append(f"Row {row_num}: Phone number '{phone_str}' could not be parsed.")
return None
def get(self, request, *args, **kwargs):
form = self.form_class()
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST, request.FILES)
context = self.get_context_data(form=form)
if not form.is_valid():
print(form.errors)
return render(request, self.template_name, context=context)
excel_file = request.FILES['file']
wb = load_workbook(filename=excel_file)
# Check if the specific sheet exists
if 'Manager Onboarding' not in wb.sheetnames:
form.add_error('file', 'The required sheet "Manager Onboarding" is not present in the uploaded file.')
return render(request, self.template_name, context=context)
# Load the "Manager Onboarding" worksheet
ws = wb['Manager Onboarding']
error_log = []
principals = []
preferences_list = []
subscriptions = []
principal_type = IAmPrincipalType.objects.get(name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
free_subscription = Subscription.objects.filter(is_free=True, active=True).first()
if not free_subscription:
messages.error(self.request, "Create a free subscription record for admin in manage subscription")
return render(request, self.template_name, context=context)
for idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
first_name, last_name, business_name, email, phone_no, preferences, start_date, end_date, address, region, country, website, linkedin, facebook, instagram, twitter = row
print(f"{first_name}, {last_name, email, preferences, start_date, end_date}")
# validate all data
if not first_name or not last_name or not email or not preferences or not start_date or not end_date:
error_log.append(f"Row {idx}: Missing data.")
continue
# validate email existence
if IAmPrincipal.objects.filter(email=email).exists():
error_log.append(f"Row {idx}: Email {email} already exists.")
continue
# Validate start_date and end_date formats
start_date = self.validate_date(start_date, idx, error_log, 'Start Date')
end_date = self.validate_date(end_date, idx, error_log, 'End Date')
if not start_date or not end_date:
continue # Skip if dates are invalid
# validate date rnage
if end_date < start_date:
error_log.append(f"Row {idx}: End date {end_date} must greater then start date {start_date}.")
continue
# Validate phone number
if phone_no:
phone_number = self.validate_phone(str(phone_no), idx, error_log)
if not phone_number:
continue # Skip if phone number is invalid
# validate preferences
preference_list = [pref.strip() for pref in preferences.split(',')]
event_categories = EventCategory.objects.filter(title__in=preference_list)
if len(event_categories) != len(preference_list):
error_log.append(f"Row {idx}: One or more preferences are invalid.")
continue
random_password = IAmPrincipal.generate_random_password()
# Encrypt the password
encryptor = Encryptor()
encrypted_password = encryptor.encrypt(random_password)
# collect the principals
principal = IAmPrincipal(
first_name=first_name.strip().capitalize(),
last_name=last_name.strip().capitalize(),
email=email.strip(),
password=make_password(random_password),
username=email.strip(),
email_verified=True,
register_complete=True,
principal_type=principal_type,
business_name=business_name,
phone_no=phone_no,
address_line1=address,
city=region,
country=country,
website=website,
linkedin_profile=linkedin,
facebook_profile=facebook,
instagram_profile=instagram,
twitter_profile=twitter
)
principals.append(principal)
# Collect preferences to be set later
preferences_list.append((principal, event_categories, start_date, end_date))
if error_log:
context = self.get_context_data(form=form, error_log=error_log)
messages.error(request, "No record is created check error log and fix the error in the file ")
return render(request, self.template_name, context=context)
# Use transaction.atomic to ensure all-or-nothing
with transaction.atomic():
# Bulk create principals
for principal in principals:
principal.save()
# Now we need to refresh principals from the DB to get their IDs
principals = IAmPrincipal.objects.filter(email__in=[p.email for p in principals])
# Create subscriptions and preferences
for principal, event_categories, start_date, end_date in preferences_list:
principal = principals.get(email=principal.email)
# Generate referral code for the manager
ReferralCode.create_referral_code_for_user_manager(principal=principal, principal_type=principal_type)
# Create IAmPrincipalExtendedData record
IAmPrincipalExtendedData.objects.create(principal=principal, is_onboarded=True,encrypted_pass=encrypted_password,)
# Create PrincipalSubscription record
subscription = PrincipalSubscription(
principal=principal,
start_date=start_date,
end_date=end_date,
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date),
is_paid=True,
subscription=free_subscription
)
subscriptions.append(subscription)
# Create PrincipalPreferences record
preference = PrincipalPreference(principal=principal)
preference.save()
preference.preferred_categories.set(event_categories)
# Bulk create subscriptions
PrincipalSubscription.objects.bulk_create(subscriptions)
messages.success(request, "Data imported successfully")
return render(request, self.template_name, context=context)
class CustomerExportView(LoginRequiredMixin, generic.View):
model = IAmPrincipal
def get(self, request, *args, **kwargs):
princiapls = IAmPrincipal.objects.select_related("extended_data").filter(principal_type__name=resource_action.PRINCIPAL_TYPE_EVENT_MANAGER)
# prepare data for excel file
data = []
for principal in princiapls:
data.append([
principal.email,
principal.first_name,
principal.last_name,
str(principal.phone_no) if principal.phone_no else "N/A",
principal.email_verified,
principal.is_active,
# principal.extended_data.is_onboarded if principal.extended_data else 'N/A',
# principal.extended_data.is_transferred if principal.extended_data else 'N/A',
# principal.created_on.replace(tzinfo=None) if principal.created_on else 'N/A'
])
# Define the columns for the Excel file
columns = ["Email", "First Name", "Last Name", "Phone Number", "Email Verified", "Active"]
# Create a workbook and select the active worksheet
wb = Workbook()
ws = wb.active
ws.title = "Event Managers List"
# Write the column headers
for col_num, column_title in enumerate(columns, 1):
cell = ws.cell(row=1, column=col_num, value=column_title)
cell.font = Font(bold=True)
# write the data rows
for row_num, row_data in enumerate(data, 2):
for col_num, cell_value in enumerate(row_data, 1):
ws.cell(row=row_num, column=col_num, value=cell_value)
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = 'attachment; filename=event_managers.xlsx'
wb.save(response)
return response
class DatatableListView(LoginRequiredMixin, generic.ListView):
pass
class PrincipalProfileView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_DASHBOARD
model = IAmPrincipal

View File

@@ -1,18 +1,10 @@
from channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer
from channels.generic.websocket import AsyncWebsocketConsumer
import json
import django
django.setup()
from accounts.models import IAmPrincipal
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync, sync_to_async
from django.utils import timezone
from chat.models import ChatGroup, ChatMessage
from channels.db import database_sync_to_async
from django.db import close_old_connections
from rest_framework_simplejwt.tokens import AccessToken
from rest_framework_simplejwt.authentication import JWTAuthentication
import threading
class ChatConsumer(AsyncWebsocketConsumer):
@@ -26,15 +18,6 @@ class ChatConsumer(AsyncWebsocketConsumer):
print("token_key: ", token_key)
self.user = await self.get_user_async(token_key)
print("self.user: ", self.user)
# Start the thread to get the user object
# user_thread = threading.Thread(target=self.get_user_async, args=(token_key,))
# user_thread.start()
# # Wait for the thread to finish and assign the user object to the scope
# user_thread.join()
# self.scope["user"] = self.user
# print("User: ", self.scope["user"])
# print("self.user: ", self.user)
# Join room group
await self.channel_layer.group_add(self.room_name, self.channel_name)

View File

@@ -23,6 +23,7 @@ EMAIL_PORT=
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_USE_TLS=
DEFAULT_FROM_EMAIL=
GOOGLE_MAPS_API_KEY=

View File

@@ -8,13 +8,15 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "goodtimes.settings")
django.setup()
from django.urls import path
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from chat.routing import websocket_urlpatterns
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "goodtimes.settings")
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter(
{

32
goodtimes/mixins.py Normal file
View File

@@ -0,0 +1,32 @@
from django.views import generic
from goodtimes.utils import JsonResponseUtil
class ActionMixin(generic.View):
model = None
def post(self, request, *args, **kwargs):
if self.model is None:
raise NotImplementedError("Subclasses of BaseActionView must define a 'model' attribute.")
action = request.POST.get('action') # 'archive', 'active', or 'unarchive'
ids = request.POST.getlist('ids[]') # List of IDs to perform action on
active = request.POST.get('active')
print(f"arhive action {action} and id is {ids} and active data is {active}")
if action == 'archive':
# Update 'deleted' field to True for the selected users
self.model.objects.filter(id__in=ids).update(deleted=True, active=False)
message = 'Record archived successfully.'
elif action == 'active':
# Update 'active' field to True for the selected users
self.model.objects.filter(id__in=ids).update(active=active.capitalize())
message = 'Record updated successfully.'
elif action == 'unarchive':
# Update 'deleted' field to False for the selected users
self.model.objects.filter(id__in=ids).update(deleted=False)
message = 'Record unarchived successfully.'
else:
return JsonResponseUtil.error(message="Invalid Action")
return JsonResponseUtil.success(message=message)

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,7 @@ LOCAL_APPS = [
"manage_referrals",
"manage_cms",
"manage_communications", # for contact us, and feedback
"manage_coupons",
"manage_notifications.apps.ManageNotificationsConfig",
"chat",
]
@@ -76,12 +77,14 @@ THIRD_PARTY_APPS = [
"taggit",
"django_quill",
"corsheaders",
'django_extensions',
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.apple",
"allauth.socialaccount.providers.google",
# "django_crontab",
"django_filters",
"django_crontab",
# "django_celery_results",
# "django_celery_beat",
]
@@ -209,6 +212,7 @@ TIME_FORMAT = "H:i p"
# otp expire time limit
OTP_EXPIRE_TIME = 1 # mins
DEFAULT_CHARSET = 'utf-8'
# Default primary key field type
@@ -237,6 +241,7 @@ EMAIL_HOST_USER = env.str("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env.str("EMAIL_HOST_PASSWORD")
EMAIL_PORT = env.str("EMAIL_PORT")
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL")
# LOGGING
@@ -301,8 +306,13 @@ SIMPLE_JWT = {
"JTI_CLAIM": "jti",
}
STRIPE_SECRET_KEY = env.str("STRIPE_SECRET_KEY")
STRIPE_PUBLISH_KEY = env.str("STRIPE_PUBLISH_KEY")
# https://dashboard.stripe.com/webhooks/create?endpoint_location=local
# This is your Stripe CLI webhook secret for testing your endpoint locally.
ENDPOINT_SECRET = "whsec_ccf1f87295603cdd1733995ee2d3c0d6f74c7ceaf28916ea45114a54b7ce1d0f"
ONE_SIGNAL_APP_ID = env.str("ONE_SIGNAL_APP_ID")
ONE_SIGNAL_API_KEY = env.str("ONE_SIGNAL_API_KEY")
@@ -328,7 +338,7 @@ CHANNEL_LAYERS = {
WEBSOCKET_TIMEOUT = 30
CRONJOBS = [
# ("0 9 * * 1-5", "manage_games.cron.update_game_status_live"),
# ('0 0 * * *', 'myapp.cron.daily_task >> /path/to/logfile.log 2>&1'),
]
GOOGLE_MAPS_API_KEY = env.str("GOOGLE_MAPS_API_KEY")
@@ -337,3 +347,19 @@ PLACES_MAPS_API_KEY = env.str("GOOGLE_MAPS_API_KEY")
PLACES_MAP_WIDGET_HEIGHT = 480
PLACES_MAP_OPTIONS = '{"center": { "lat": 38.971584, "lng": -95.235072 }, "zoom": 10}'
PLACES_MARKER_OPTIONS = '{"draggable": true}'
# twitter keys
TWITTER_API_KEY = env.str("TWITTER_API_KEY")
TWITTER_API_SECRET_KEY = env.str("TWITTER_API_SECRET_KEY")
TWITTER_ACCESS_TOKEN = env.str("TWITTER_ACCESS_TOKEN")
TWITTER_ACCESS_TOKEN_SECRET = env.str("TWITTER_ACCESS_TOKEN_SECRET")
# facebook keys
FACEBOOK_APP_ID = env.str("FACEBOOK_APP_ID")
FACEBOOK_APP_SECRET = env.str("FACEBOOK_APP_SECRET")
FACEBOOK_PAGE_ID = env.str("FACEBOOK_PAGE_ID")
FACEBOOK_GRAPH_VERSION_API = env.str("FACEBOOK_GRAPH_VERSION_API")
FACEBOOK_ACCESS_TOKEN = env.str("FACEBOOK_ACCESS_TOKEN")
# Instagram Key
INSTAGRAM_PAGE_ID = env.str('INSTAGRAM_PAGE_ID')

View File

@@ -26,17 +26,17 @@ CORS_ORIGIN_ALLOW_ALL = True
CORS_ORIGIN_WHITELIST = ("http://localhost:3000",)
if DEBUG:
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
INSTALLED_APPS += [
"debug_toolbar",
"django_extensions",
]
INTERNAL_IPS = [
"127.0.0.1",
]
# if DEBUG:
# MIDDLEWARE += [
# "debug_toolbar.middleware.DebugToolbarMiddleware",
# ]
# INSTALLED_APPS += [
# "debug_toolbar",
# "django_extensions",
# ]
# INTERNAL_IPS = [
# "127.0.0.1",
# ]
BASE_DOMAIN = "http://192.168.29.219:8000"
@@ -44,15 +44,14 @@ BASE_DOMAIN = "http://192.168.29.219:8000"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
STRIPE_CHECKOUT_URL = "http://localhost:8000/subscriptions/stripe-subscription/"
STRIPE_FINAL_URL = "http://localhost:8000/subscriptions/create-checkout-session/"
STRIPE_CHECKOUT_URL = "https://deciding-firmly-fly.ngrok-free.app/subscriptions/create-checkout-session/"
COUPON_VALIDITY_CHECK_URL = "https://deciding-firmly-fly.ngrok-free.app/subscriptions/coupon-validity-check/"
LOGO_PATH = "static"

View File

@@ -6,7 +6,7 @@ from logging.handlers import TimedRotatingFileHandler
DEBUG = False
ALLOWED_HOSTS = ["goodtimes.betadelivery.com", "154.41.254.33"]
ALLOWED_HOSTS = ["admin.goodtimesltd.co.uk", "77.68.29.148"]
LOGGING_DIR = os.path.join(
@@ -61,7 +61,7 @@ LOGGING = {
}
BASE_DOMAIN = "https://goodtimes.betadelivery.com"
BASE_DOMAIN = "https://admin.goodtimesltd.co.uk"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
@@ -77,8 +77,8 @@ STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
STRIPE_CHECKOUT_URL = (
"https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/"
)
STRIPE_FINAL_URL = (
"https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/"
"https://admin.goodtimesltd.co.uk/subscriptions/create-checkout-session/"
)
COUPON_VALIDITY_CHECK_URL = "https://admin.goodtimesltd.co.uk/subscriptions/coupon-validity-check/"
LOGO_PATH = "/var/www/goodtimes_prod/goodtimes/static"

View File

@@ -6,61 +6,61 @@ import colorlog
# from logging.handlers import TimedRotatingFileHandler
DEBUG = False
ALLOWED_HOSTS = ["127.0.0.1", "77.68.8.229", "staging.goodtimesltd.co.uk"]
ALLOWED_HOSTS = ["staging.goodtimesltd.co.uk", "77.68.8.229",".staging.goodtimesltd.co.uk"]
# LOGGING_DIR = os.path.join(
# BASE_DIR, "logs"
# ) # Define the directory where log files will be stored
LOGGING_DIR = os.path.join(
BASE_DIR, "logs"
) # Define the directory where log files will be stored
# Ensure the directory exists; create it if it doesn't
# if not os.path.exists(LOGGING_DIR):
# os.makedirs(LOGGING_DIR)
if not os.path.exists(LOGGING_DIR):
os.makedirs(LOGGING_DIR)
# LOGGING_LEVEL = env.str(
# "LOG_LEVEL", "INFO"
# ) # Set your desired log level (e.g., DEBUG, INFO, WARNING, ERROR) in the env file
LOGGING_LEVEL = env.str(
"LOG_LEVEL", "INFO"
) # Set your desired log level (e.g., DEBUG, INFO, WARNING, ERROR)
# LOGGING = {
# "version": 1,
# "disable_existing_loggers": False,
# "formatters": {
# "verbose": {
# "()": colorlog.ColoredFormatter,
# "format": "%(cyan)s%(asctime)s%(reset)s | %(red)s[%(levelname)8s]%(reset)s | [ %(yellow)s%(name)s.%(module)s:%(white)s%(lineno)d%(reset)s - %(green)s%(funcName)10s()%(reset)s ] --> %(message)s",
# "datefmt": "%Y-%m-%d %H:%M:%S",
# "log_colors": {
# "DEBUG": "white",
# "INFO": "green",
# "WARNING": "yellow",
# "ERROR": "red",
# "CRITICAL": "bold_red",
# },
# "secondary_log_colors": {},
# "style": "%",
# }
# },
# "handlers": {
# "logfile": {
# "level": LOGGING_LEVEL,
# "class": "logging.handlers.RotatingFileHandler",
# "filename": os.path.join(LOGGING_DIR, "goodtimes_staging_error.log"),
# 'maxBytes': 5242880, # 5*1024*1024 bytes (5MB)
# "backupCount": 10, # Number of log files to keep (15 days' worth of logs)
# "formatter": "verbose",
# },
# },
# "loggers": {
# "django": {
# "handlers": ["logfile"],
# "level": LOGGING_LEVEL,
# "propagate": False,
# },
# },
# }
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"()": colorlog.ColoredFormatter,
"format": "%(cyan)s%(asctime)s%(reset)s | %(red)s[%(levelname)8s]%(reset)s | [ %(yellow)s%(name)s.%(module)s:%(white)s%(lineno)d%(reset)s - %(green)s%(funcName)10s()%(reset)s ] --> %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
"log_colors": {
"DEBUG": "white",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "bold_red",
},
"secondary_log_colors": {},
"style": "%",
}
},
"handlers": {
"logfile": {
"level": LOGGING_LEVEL,
"class": "logging.handlers.RotatingFileHandler",
"filename": os.path.join(LOGGING_DIR, "godtimes_pro_error.log"),
"maxBytes": 5242880, # 5*1024*1024 bytes (5MB)
"backupCount": 10, # Number of log files to keep (15 days' worth of logs)
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["logfile"],
"level": LOGGING_LEVEL,
"propagate": False,
},
},
}
# BASE_DOMAIN = "https://goodtimes.betadelivery.com"
BASE_DOMAIN = "https://staging.goodtimesltd.co.uk"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
@@ -75,9 +75,10 @@ STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
STRIPE_CHECKOUT_URL = (
"https://staging.goodtimesltd.co.uk/subscriptions/stripe-subscription/"
)
STRIPE_FINAL_URL = (
"https://staging.goodtimesltd.co.uk/subscriptions/create-checkout-session/"
)
COUPON_VALIDITY_CHECK_URL = "https://staging.goodtimesltd.co.uk/subscriptions/coupon-validity-check/"
LOGO_PATH = "/var/www/goodtimes/static"

View File

@@ -5,7 +5,7 @@ import colorlog
# from logging.handlers import TimedRotatingFileHandler
DEBUG = False
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "goodtimes.betadelivery.com", "154.41.254.33"]
@@ -60,7 +60,7 @@ ALLOWED_HOSTS = ["127.0.0.1", "goodtimes.betadelivery.com", "154.41.254.33"]
# },
# }
# BASE_DOMAIN = "https://goodtimes.betadelivery.com"
BASE_DOMAIN = "https://goodtimes.betadelivery.com"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
@@ -76,8 +76,13 @@ STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR.joinpath("static")]
STRIPE_CHECKOUT_URL = (
"https://goodtimes.betadelivery.com/subscriptions/stripe-subscription/"
)
STRIPE_FINAL_URL = (
"https://goodtimes.betadelivery.com/subscriptions/create-checkout-session/"
)
COUPON_VALIDITY_CHECK_URL = (
"https://goodtimes.betadelivery.com/subscriptions/coupon-validity-check/"
)
LOGO_PATH = "/var/www/goodtimes/static"
STRIPE_TEST_MODE_SECRET_KEY = env.str("STRIPE_TEST_MODE_SECRET_KEY")
STRIPE_TEST_MODE_PUBLISH_KEY = env.str("STRIPE_TEST_MODE_PUBLISH_KEY")

View File

@@ -53,6 +53,9 @@ urlpatterns = [
path("subscriptions/", include("manage_subscriptions.urls")),
path("api/subscriptions/", include("manage_subscriptions.api.urls")),
path("coupons/", include("manage_coupons.urls")),
# path("api/coupons/", include("manage_coupons.api.urls")),
path("notifications/", include("manage_notifications.urls")),
path("api/notifications/", include("manage_notifications.api.urls")),
@@ -60,8 +63,9 @@ urlpatterns = [
# path('api/', include("accounts.api.urls")),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
import debug_toolbar
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]

View File

@@ -1,3 +1,4 @@
from django.http import JsonResponse
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework import status
@@ -40,6 +41,24 @@ class ApiResponse:
# return ApiResponse.error("Validation error", errors, status_code)
class JsonResponseUtil:
"""
A utility class for creating JSON responses with a standardized format.
"""
@staticmethod
def success(message, data=None, status=200):
response_data = {"success": True, "status": status, "message": message}
if data is not None:
response_data["data"] = data
return JsonResponse(response_data, status=status)
@staticmethod
def error(message, errors=None, status=403):
response_data = {"success": False, "status": status, "message": message}
if errors is not None:
response_data["errors"] = errors
return JsonResponse(response_data, status=status)
class RandomGenerator:
@staticmethod
def number(start, end):

View File

@@ -1,285 +0,0 @@
from django.conf import settings
from django.db import transaction
import requests
from datetime import timedelta
from django.utils import timezone
from onesignal_sdk.client import Client as OneSignalClient
from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType
from manage_notifications.models import InAppNotification, NotificationCategoryChoices
from manage_referrals.models import (
GoodTimeCoins,
ReferralRecord,
ReferralRecordReward,
ReferralTracking,
)
from django.core.exceptions import ObjectDoesNotExist
from manage_subscriptions.models import PrincipalSubscription, Subscription
from manage_wallets.models import (
TransactionStatus,
TransactionType,
Wallet,
Transaction,
)
import logging
logger = logging.getLogger(__name__)
class NotificationService:
def __init__(self):
self.client = OneSignalClient(
app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY
)
def send_notification(self, title, message, player_id):
if player_id is None:
print("Player ID is None, skipping notification")
return
notification_payload = {
"headings": {"en": title},
"contents": {"en": message},
"include_player_ids": [player_id],
}
response = self.client.send_notification(notification_payload)
return response
def save_notification(self, principal, title, message, notification_category):
InAppNotification.objects.create(
principal=principal,
title=title,
message=message,
notification_category=notification_category,
)
def payment_success_notification(
self, principal, subscription, principal_subscription, amount
):
print("payment_success_notification: ", principal.player_id)
title = "Payment Successful"
end_date = principal_subscription.end_date
message = f"Your payment for {subscription} of ${amount} was successfully processed. Your subscription is valid till {end_date}"
self.send_notification(title, message, principal.player_id)
self.save_notification(principal, title, message, NotificationCategoryChoices.TRANSACTION)
def referral_received_notification(self, principal, amount, email):
print("referral_received_notification: ", principal.player_id)
title = "Congratulations! You got a referral G-Token."
message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)"
self.send_notification(title, message, principal.player_id)
self.save_notification(principal, title, message, NotificationCategoryChoices.REFERRAL)
def payment_failed_notification(self, principal, subscription, amount):
print("payment_failed_notification: ", principal.player_id)
title = "Payment Failed!"
message = f"Your payment for {subscription} of ${amount} was failed."
self.send_notification(title, message, principal.player_id)
self.save_notification(principal, title, message, NotificationCategoryChoices.TRANSACTION)
class WebhookService:
def __init__(self, webhook_data):
self.webhook_data = webhook_data
self.event_type = webhook_data["type"]
self.charge_data = webhook_data["data"]["object"]
def get_event_type(self):
return self.event_type
def get_principal(self):
principal_id = self.charge_data["metadata"]["principal"]
try:
return IAmPrincipal.objects.get(id=int(principal_id))
except (ObjectDoesNotExist, ValueError):
logger.error(f"Invalid principal ID: {principal_id}")
raise ValueError(f"Invalid principal ID: {principal_id}")
def get_transaction(self):
transaction_id = self.charge_data["metadata"]["transaction_id"]
try:
return Transaction.objects.get(id=int(transaction_id))
except (ObjectDoesNotExist, ValueError):
logger.error(f"Invalid transaction ID: {transaction_id}")
raise ValueError(f"Invalid transaction ID: {transaction_id}")
def get_subscription(self):
subscription_id = self.charge_data["metadata"]["subscription_id"]
try:
return Subscription.objects.get(id=int(subscription_id))
except (ObjectDoesNotExist, ValueError):
logger.error(f"Invalid subscription ID: {subscription_id}")
raise ValueError(f"Invalid subscription ID: {subscription_id}")
def get_order_id(self):
return self.charge_data["metadata"]["order_id"]
class ReferralRewardService:
def __init__(self, principal, principal_subscription, subscription):
self.notification_service = NotificationService()
self.principal = principal
self.principal_subscription = principal_subscription
self.subscription = subscription
def _fetch_referral_record(self):
return ReferralRecord.objects.filter(
referred_principal=self.principal,
is_completed=True,
active=True, # Assuming 'active' is a field determining if the record is currently relevant
deleted=False, # Assuming logical deletion is handled by a 'deleted' field
).first()
def _check_active_subscription(self, referrer_principal):
today = timezone.now().date()
return (
PrincipalSubscription.objects.filter(
principal=referrer_principal,
is_paid=True,
end_date__gte=today,
cancelled=False,
deleted=False,
)
.order_by("-end_date")
.first()
)
def _credit_reward(self, referral_record, subscription):
amount = subscription.referral_percentage * subscription.amount / 100
ReferralRecordReward.objects.create(
referral_record=referral_record,
subscription=subscription,
coins=1, # This value could be dynamically calculated or configured elsewhere
value=amount,
)
self._credit_transaction(referral_record.referrer_principal, amount)
self.notification_service.referral_received_notification(
referral_record.referrer_principal, amount, self.principal.email
)
def _credit_transaction(self, referrer_principal, amount):
print("_credit_transaction: ", referrer_principal)
Transaction.objects.create(
principal=referrer_principal,
transaction_type=TransactionType.CREDIT,
payment_method="",
transaction_status=TransactionStatus.SUCCESS,
amount=amount,
coins=1,
comment="Referral reward",
# Populate other fields as necessary, such as `order_id`, `product_id`, or `reference_id` if applicable
)
def _update_reward_status(self, referral_record, active_subscription):
# Check if the referrer has an active subscription and get its ID if it exists
referrer_subscription_id = (
active_subscription.id if active_subscription else None
)
# Create a new subscription for the referred principal
referred_subscription_id = self.principal_subscription.id
is_referrer_subscribed = bool(active_subscription)
ReferralTracking.objects.create(
referral_record=referral_record,
referrer_subscription_id=referrer_subscription_id,
referred_subscription_id=referred_subscription_id,
is_referrer_subscribed=is_referrer_subscribed,
)
def credit_referral_reward_if_applicable(self):
referral_record = self._fetch_referral_record()
if referral_record:
active_subscription = self._check_active_subscription(
referral_record.referrer_principal
)
if active_subscription:
print("active_subscription: ", active_subscription)
if self.subscription:
print("self.subscription: ", self.subscription)
self._credit_reward(referral_record, self.subscription)
self._update_reward_status(referral_record, active_subscription)
class SubscriptionService:
def __init__(self):
self.principal_subscription = None
def create_principal_subscription(self, principal, subscription, order_id):
subscription_days = subscription.plan.days
today = timezone.now().date()
last_date = today + timedelta(days=subscription_days)
principal_subscription = PrincipalSubscription.objects.create(
principal=principal,
subscription=subscription,
is_paid=True,
order_id=order_id,
start_date=today,
end_date=last_date,
grace_period_end_date=last_date + timedelta(days=15),
)
self.principal_subscription = principal_subscription
return principal_subscription
def update_transaction_success(self, principal_transaction, principal_subscription):
principal_transaction.transaction_status = TransactionStatus.SUCCESS
principal_transaction.principal_subscription = principal_subscription
principal_transaction.save()
def update_transaction_failure(self, principal_transaction):
principal_transaction.transaction_status = TransactionStatus.FAIL
principal_transaction.save()
class PaymentProcessingService:
def __init__(self, webhook_data):
self.webhook_service = WebhookService(webhook_data)
self.notification_service = NotificationService()
# Retrieve objects
self.principal = self.webhook_service.get_principal()
self.transaction = self.webhook_service.get_transaction()
self.subscription = self.webhook_service.get_subscription()
self.order_id = self.webhook_service.get_order_id()
self.subscription_service = SubscriptionService()
self.principal_subscription = None
def process_event(self):
if self.webhook_service.get_event_type() == "checkout.session.completed":
self.handle_success()
else:
self.handle_failure()
def handle_success(self):
with transaction.atomic():
# Create or update the principal subscription
self.principal_subscription = (
self.subscription_service.create_principal_subscription(
self.principal, self.subscription, self.order_id
)
)
print("First Part Done....!!!!!")
# Update transaction status to success
self.subscription_service.update_transaction_success(
self.transaction, self.principal_subscription
)
print("Second Part Done....!!!!!")
# Now handle referral rewards, if applicable
referral_service = ReferralRewardService(
self.principal, self.principal_subscription, self.subscription
)
print("Above Third Part...!!!!!!!!!!!")
referral_service.credit_referral_reward_if_applicable()
print("Third Part Done....!!!!!")
self.notification_service.payment_success_notification(
self.principal,
self.subscription,
self.principal_subscription,
self.transaction.amount,
)
def handle_failure(self):
self.subscription_service.update_transaction_failure(self.transaction)
# self.notification_service.payment_failed_notification(
# self.principal, self.subscription, self.transaction.amount
# )

View File

View File

@@ -0,0 +1,80 @@
from onesignal_sdk.client import Client as OneSignalClient
import logging
from manage_notifications.models import (
IAmPrincipalNotificationSettings,
InAppNotification,
NotificationCategoryChoices,
)
from django.shortcuts import get_object_or_404
from django.conf import settings
logger = logging.getLogger(__name__)
class NotificationService:
def __init__(self):
self.client = OneSignalClient(
app_id=settings.ONE_SIGNAL_APP_ID, rest_api_key=settings.ONE_SIGNAL_API_KEY
)
def send_notification(self, title, message, player_id):
if player_id is None:
print("Player ID is None, skipping notification")
return
notification_payload = {
"headings": {"en": title},
"contents": {"en": message},
"include_player_ids": [player_id],
}
response = self.client.send_notification(notification_payload)
return response
def save_notification(self, principal, title, message, notification_category):
InAppNotification.objects.create(
principal=principal,
title=title,
message=message,
notification_category=notification_category,
)
def payment_success_notification(
self, principal, subscription, principal_subscription, amount
):
print("payment_success_notification: ", principal.player_id)
title = "Payment Successful"
end_date = principal_subscription.end_date
message = f"Your payment for {subscription} of ${amount} was successfully processed. Your subscription is valid till {end_date}"
self.send_notification(title, message, principal.player_id)
self.save_notification(
principal, title, message, NotificationCategoryChoices.TRANSACTION
)
def referral_received_notification(self, principal, amount, email):
print("referral_received_notification: ", principal.player_id)
title = "Congratulations! You got a referral G-Token."
message = f"Your referral {email} has subscribed to GoodTimesApp. You have received {amount} (£)"
self.save_notification(
principal, title, message, NotificationCategoryChoices.REFERRAL
)
if not self.should_send_referral_notification(principal):
print("Referral notifications are disabled for this user")
return
self.send_notification(title, message, principal.player_id)
def payment_failed_notification(self, principal, subscription, amount):
print("payment_failed_notification: ", principal.player_id)
title = "Payment Failed!"
message = f"Your payment for {subscription} of ${amount} was failed."
self.send_notification(title, message, principal.player_id)
self.save_notification(
principal, title, message, NotificationCategoryChoices.TRANSACTION
)
def should_send_referral_notification(self, principal):
notification_settings = get_object_or_404(
IAmPrincipalNotificationSettings,
principal=principal,
notification_category=NotificationCategoryChoices.REFERRAL,
)
return notification_settings.is_enabled

View File

@@ -0,0 +1,180 @@
from venv import logger
from django.db import transaction
from manage_wallets.models import (
PaymentMethod,
Transaction,
TransactionStatus,
TransactionType,
)
from .notification_service import NotificationService
from .referral_reward_service import ReferralRewardService
from .subscription_service import SubscriptionService
from .webhook_service import WebhookService
class PaymentProcessingService:
def __init__(
self,
webhook_data,
stripe_subscription,
current_period_start,
current_period_end,
):
self.webhook_service = WebhookService(webhook_data)
self._order_id = None
self.notification_service = NotificationService()
self.subscription_service = SubscriptionService()
self.stripe_subscription = stripe_subscription
self.current_period_start = current_period_start
self.current_period_end = current_period_end
@property
def charge_data(self):
"""Return charge data from the webhook service."""
return self.webhook_service.charge_data
@property
def principal(self):
"""Return the principal from the webhook service."""
return self.webhook_service.get_principal()
@property
def subscription(self):
"""Return the subscription from the webhook service."""
return self.webhook_service.get_subscription()
@property
def order_id(self):
"""Return the order ID from the created transaction."""
return self._order_id
@order_id.setter
def order_id(self, value):
"""Set the order ID."""
self._order_id = value
@property
def coupon(self):
"""Return the coupon from the webhook service."""
return self.webhook_service.get_coupon()
@property
def amount(self):
"""Return the final amount from the webhook service."""
return self.webhook_service.get_final_amount()
def create_transaction(self):
"""Create a transaction based on webhook data."""
transaction = Transaction.objects.create(
principal=self.principal,
principal_subscription=None,
transaction_type=TransactionType.PAYMENT,
payment_method=PaymentMethod.CARD,
transaction_status=TransactionStatus.INITIATE,
amount=self.amount,
# order_id=self.order_id,
comment="Principal Subscription Initiated",
)
# Save the transaction to auto-generate the order_id
transaction.save()
# Step 1: Update the order_id in PaymentProcessingService
self.order_id = transaction.order_id
return transaction
def process_event(self):
"""Process the webhook event."""
try:
with transaction.atomic():
event_type = self.webhook_service.event_type
if event_type == "invoice.payment_succeeded" and self.charge_data.get("billing_reason") == "subscription_create":
logger.info(f"Skipping event {event_type} with billing reason 'subscription_create'")
return
txn = self.create_transaction()
if event_type in ["checkout.session.completed", "invoice.payment_succeeded"]:
self.handle_success(txn)
elif event_type in ["checkout.session.expired", "invoice.payment_failed"]:
self.handle_failure(txn)
else:
logger.warning(f"Unknown event type {event_type}. Skipping.")
return
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise
def handle_success(self, transaction):
"""Handle a successful payment."""
try:
self.create_principal_subscription(transaction)
self.process_referral_rewards()
self.send_success_notification(transaction)
self.update_transaction_status(
transaction,
TransactionStatus.SUCCESS,
self.subscription_service.principal_subscription,
)
except Exception as e:
self.handle_failure(transaction, error_message=str(e))
logger.error(f"Transaction Error: {str(e)}")
raise e
def create_principal_subscription(self, transaction):
"""Create or update the principal subscription."""
self.subscription_service.principal_subscription = (
self.subscription_service.create_principal_subscription(
principal=self.principal,
subscription=self.subscription,
stripe_subscription=self.stripe_subscription,
order_id=transaction.order_id,
current_period_start=self.current_period_start,
current_period_end=self.current_period_end,
coupon=self.coupon,
)
)
print("Principal Subscription Created")
def process_referral_rewards(self):
"""Handle referral rewards."""
referral_service = ReferralRewardService(
self.principal,
self.subscription_service.principal_subscription,
self.subscription,
)
referral_service.credit_referral_reward_if_applicable()
print("Referral Rewards Processed")
def send_success_notification(self, transaction):
"""Send a payment success notification."""
self.notification_service.payment_success_notification(
self.principal,
self.subscription,
self.subscription_service.principal_subscription,
transaction.amount,
)
print("Payment Success Notification Sent")
def handle_failure(self, transaction, error_message=None):
"""Handle a failed payment."""
self.update_transaction_status(
transaction, TransactionStatus.FAIL, error_message=error_message
)
self.notification_service.payment_failed_notification(
self.principal, self.subscription, transaction.amount
)
print("Payment Failure Notification Sent")
def update_transaction_status(
self, transaction, status, principal_subscription=None, error_message=None
):
"""Update the transaction status and associate with a subscription if provided."""
transaction.transaction_status = status
if principal_subscription:
transaction.principal_subscription = principal_subscription
if error_message:
transaction.error_message = error_message
transaction.save()

View File

@@ -0,0 +1,110 @@
from .notification_service import NotificationService
from manage_referrals.models import (
ReferralRecord,
ReferralRecordReward,
ReferralTracking,
)
from manage_wallets.models import Transaction, TransactionType, TransactionStatus
from django.utils import timezone
from manage_subscriptions.models import PrincipalSubscription
class ReferralRewardService:
def __init__(self, principal, principal_subscription, subscription):
self._notification_service = NotificationService()
self._principal = principal
self._principal_subscription = principal_subscription
self._subscription = subscription
@property
def principal(self):
return self._principal
@property
def principal_subscription(self):
return self._principal_subscription
@property
def subscription(self):
return self._subscription
@staticmethod
def _fetch_referral_record(principal):
"""Fetch the referral record for the given principal."""
return ReferralRecord.objects.filter(
referred_principal=principal,
is_completed=True,
active=True,
deleted=False,
).first()
@staticmethod
def _check_active_subscription(referrer_principal):
"""Check if the referrer principal has an active subscription."""
today = timezone.now().date()
return (
PrincipalSubscription.objects.filter(
principal=referrer_principal,
is_paid=True,
end_date__gte=today,
active=True,
)
.order_by("-end_date")
.first()
)
def _credit_reward(self, referral_record, subscription):
amount = subscription.referral_percentage * subscription.amount / 100
ReferralRecordReward.objects.create(
referral_record=referral_record,
subscription=subscription,
coins=1, # This value could be dynamically calculated or configured elsewhere
value=amount,
)
self._credit_transaction(referral_record.referrer_principal, amount)
self._notification_service.referral_received_notification(
referral_record.referrer_principal, amount, self.principal.email
)
def _credit_transaction(self, referrer_principal, amount):
"""Create a transaction record for the referral reward."""
print("referrer_principal: ", referrer_principal)
Transaction.objects.create(
principal=referrer_principal,
transaction_type=TransactionType.CREDIT,
payment_method="",
transaction_status=TransactionStatus.SUCCESS,
amount=amount,
coins=1,
comment="Referral reward",
)
def _update_reward_status(self, referral_record, active_subscription):
"""Update the status of the referral reward."""
referrer_subscription_id = (
active_subscription.id if active_subscription else None
)
# Create a new subscription for the referred principal
referred_subscription_id = self.principal_subscription.id
is_referrer_subscribed = bool(active_subscription)
ReferralTracking.objects.create(
referral_record=referral_record,
referrer_subscription_id=referrer_subscription_id,
referred_subscription_id=referred_subscription_id,
is_referrer_subscribed=is_referrer_subscribed,
)
def credit_referral_reward_if_applicable(self):
"""Credit referral reward if applicable based on the referral record."""
referral_record = self._fetch_referral_record(self.principal)
if referral_record:
active_subscription = self._check_active_subscription(
referral_record.referrer_principal
)
if active_subscription and self.subscription:
self._credit_reward(referral_record, self.subscription)
self._update_reward_status(referral_record, active_subscription)

View File

@@ -0,0 +1,78 @@
from datetime import timedelta
from django.utils import timezone
import datetime
from manage_subscriptions.models import PrincipalSubscription, SubscriptionStatus
class SubscriptionService:
def __init__(self):
self._principal_subscription = None
@property
def principal_subscription(self):
"""Get the current principal subscription."""
return self._principal_subscription
@principal_subscription.setter
def principal_subscription(self, value):
"""Set the current principal subscription."""
self._principal_subscription = value
def create_principal_subscription(
self,
principal,
subscription,
stripe_subscription,
order_id,
current_period_start,
current_period_end,
coupon=None,
):
"""Create a principal subscription and return it."""
start_date, end_date = self._calculate_dates(
current_period_start, current_period_end, subscription.calculate_days()
)
PrincipalSubscription.objects.filter(principal=principal, status=SubscriptionStatus.ACTIVE).update(status=SubscriptionStatus.EXPIRED, active=False)
principal_subscription = PrincipalSubscription.objects.create(
principal=principal,
subscription=subscription,
stripe_subscription_id=stripe_subscription,
is_paid=True,
auto_renew=bool(stripe_subscription),
order_id=order_id,
start_date=start_date,
end_date=end_date,
grace_period_end_date=PrincipalSubscription.generate_grace_period_end_date(end_date),
coupon_code=coupon.coupon_code if coupon else None,
)
if coupon:
self._update_coupon(coupon)
self.principal_subscription = principal_subscription
return principal_subscription
def _calculate_dates(
self, current_period_start, current_period_end, subscription_days
):
"""Calculate subscription start and end dates."""
today = timezone.now().date()
start_date = (
datetime.datetime.fromtimestamp(current_period_start).date()
if current_period_start
else today
)
end_date = (
datetime.datetime.fromtimestamp(current_period_end).date()
if current_period_end
else (today + timedelta(days=subscription_days))
)
return start_date, end_date
def _update_coupon(self, coupon):
"""Update coupon usage count."""
coupon.no_of_redeems += 1
coupon.save()
print("Coupon Saved Successfully!!!")

View File

@@ -0,0 +1,90 @@
from decimal import Decimal
from django.conf import settings
import stripe
from django.core.exceptions import ObjectDoesNotExist
from accounts.models import IAmPrincipal
from manage_coupons.models import Coupon
from manage_subscriptions.models import Subscription
import logging
logger = logging.getLogger(__name__)
stripe.api_key = settings.STRIPE_SECRET_KEY
class WebhookService:
def __init__(self, webhook_data):
self._webhook_data = webhook_data
self._event_type = webhook_data["type"]
self._charge_data = webhook_data["data"]["object"]
self._metadata = self._fetch_metadata()
def _fetch_metadata(self):
"""Fetch metadata based on the event type."""
if self._event_type in ["checkout.session.expired", "checkout.session.completed"]:
return self._charge_data.get("metadata", {})
elif self._event_type == "invoice.payment_succeeded":
subscription_id = self._charge_data.get("subscription")
if subscription_id:
subscription = stripe.Subscription.retrieve(subscription_id)
return subscription.get("metadata", {})
return {}
@property
def event_type(self):
return self._event_type
@property
def charge_data(self):
return self._charge_data
def _get_object_from_metadata(self, model, id_key):
"""Retrieve object from metadata."""
obj_id = self._metadata.get(id_key)
if obj_id:
try:
return model.objects.get(id=int(obj_id))
except (ObjectDoesNotExist, ValueError) as e:
logger.error(f"Invalid {model.__name__} ID: {obj_id}")
raise ValueError(f"Invalid {model.__name__} ID: {obj_id}") from e
return None
def get_event_type(self):
return self.event_type
def get_principal(self):
"""Retrieve principal from metadata."""
return self._get_object_from_metadata(IAmPrincipal, "principal")
def get_subscription(self):
"""Retrieve subscription from metadata."""
return self._get_object_from_metadata(Subscription, "subscription_id")
def get_coupon(self):
"""Retrieve coupon from metadata."""
coupon_code = self._metadata.get("couponCode")
print("get_coupon:coupon_code: ", coupon_code)
if coupon_code:
try:
return Coupon.objects.get(coupon_code=coupon_code)
except Coupon.DoesNotExist:
logger.error(f"Invalid coupon code: {coupon_code}")
raise ValueError(f"Invalid coupon code: {coupon_code}")
return None
def get_final_amount(self):
"""Retrieve Amount after coupon discount from either stripe event or metadata."""
if self.event_type == "checkout.session.completed":
return (
Decimal(self._charge_data.get("amount_total", 0)) / 100
)
elif self.event_type == "invoice.payment_succeeded":
return (
Decimal(self._charge_data.get("amount_paid", 0)) / 100
)
# Fallback: Try to get the amount from metadata
return (
Decimal(self._metadata.get("metadata", {}).get("finalAmount", 0)) / 100
)

View File

@@ -55,6 +55,13 @@ class OrganizationSerializer(serializers.ModelSerializer):
"subscription_agreement",
"license_agreement_user",
"license_agreement_merchant",
"contact_us_email",
"instagram_handle",
"facebook_handle",
"linkedin_handle",
"website_url",
"address",
"contact_us_phone",
]
class EducationVideoSerializer(serializers.ModelSerializer):

View File

@@ -22,26 +22,31 @@ class OrganizationForm(forms.ModelForm):
fields = [
"title",
"contact_us_email",
"contact_us_phone",
"address",
"website_url",
"instagram_handle",
"facebook_handle",
"linkedin_handle",
"twitter_handle",
"logo_image",
"favicon_image",
"website_url",
]
labels = {
"title": "Organization Title",
"contact_us_email": "Contact Email",
"contact_us_phone": "Contact Phone No",
"Address": "Contact Address",
"website_url": "Website URL",
"instagram_handle": "Instagram URL",
"facebook_handle": "Facebook URL",
"linkedin_handle": "LinkedIn URL",
"Twitter_handle": "Twitter URL",
"logo_image": "Organization Logo",
"favicon_image": "Favicon",
"website_url": "Website URL",
}
class NewsAndArticleCategoryForm(forms.ModelForm):
class Meta:
model = NewsAndArticlesCategory

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0.2 on 2024-08-16 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_cms', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='organization',
name='address',
field=models.TextField(default='this is address'),
preserve_default=False,
),
migrations.AddField(
model_name='organization',
name='contact_us_phone',
field=models.CharField(default='+911212121212', max_length=16),
preserve_default=False,
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.0.2 on 2024-08-16 09:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_cms', '0002_organization_address_organization_contact_us_phone'),
]
operations = [
migrations.AddField(
model_name='organization',
name='twitter_handle',
field=models.URLField(blank=True, null=True),
),
migrations.AlterField(
model_name='organization',
name='address',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='organization',
name='contact_us_phone',
field=models.CharField(blank=True, max_length=16, null=True),
),
]

View File

@@ -89,6 +89,7 @@ class Organization(BaseModel):
instagram_handle = models.URLField(blank=True, null=True)
facebook_handle = models.URLField(blank=True, null=True)
linkedin_handle = models.URLField(blank=True, null=True)
twitter_handle = models.URLField(blank=True, null=True)
logo_image = models.ImageField(blank=True, null=True, upload_to="organization/logo")
favicon_image = models.ImageField(
blank=True, null=True, upload_to="organization/favicon"
@@ -104,6 +105,8 @@ class Organization(BaseModel):
subscription_agreement = QuillField()
license_agreement_user = QuillField()
license_agreement_merchant = QuillField()
address = models.TextField(blank=True, null=True)
contact_us_phone = models.CharField(max_length=16, blank=True, null=True)
class Meta:
db_table = "organization"

View File

@@ -2,7 +2,8 @@ import logging
from threading import Thread
# Configure logging at the beginning of your application
logging.basicConfig(level=logging.INFO, filename='app.log', filemode='a', format='%(name)s - %(levelname)s - %(message)s')
# logging.basicConfig(level=logging.INFO, filename='app.log', filemode='a', format='%(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def send_email_async(email_service):
try:
@@ -11,6 +12,6 @@ def send_email_async(email_service):
except Exception as e:
# Log the exception
print(f"Failed to send email: {e}")
logging.error(f"Failed to send email: {e}")
logger.error(f"Failed to send email: {e}")
# Optionally, you could use other means to notify you of the failure,
# such as sending an alert to an admin email, or using a monitoring service.

View File

@@ -56,7 +56,7 @@ class ContactUsReplyView(LoginRequiredMixin, generic.View):
to=[
email,
],
from_email=settings.EMAIL_HOST_USER,
from_email=settings.DEFAULT_FROM_EMAIL,
)
print("email_service: ", email_service)
email_service.load_template(

View File

40
manage_coupons/admin.py Normal file
View File

@@ -0,0 +1,40 @@
from django.contrib import admin
from .models import Coupon
class CouponAdmin(admin.ModelAdmin):
list_display = (
"id",
"title",
"coupon_id",
"coupon_code",
"discount_amount",
"discount_percentage",
"valid_from",
"valid_to",
"max_redeems",
"no_of_redeems",
"is_active",
)
search_fields = ("title", "coupon_code")
list_filter = ("valid_from", "valid_to", "max_redeems")
readonly_fields = ("no_of_redeems",)
fieldsets = (
(
None,
{"fields": ("title", "coupon_code", "coupon_id", "description", "image")},
),
(
"Discount Information",
{"fields": ("discount_amount", "discount_percentage")},
),
("Validity", {"fields": ("valid_from", "valid_to")}),
("Redemption", {"fields": ("max_redeems", "no_of_redeems")}),
)
def is_active(self, obj):
return obj.is_valid()
admin.site.register(Coupon, CouponAdmin)

View File

View File

View File

6
manage_coupons/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ManageCouponsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "manage_coupons"

24
manage_coupons/forms.py Normal file
View File

@@ -0,0 +1,24 @@
from django import forms
from django.core.exceptions import ValidationError
from manage_coupons.models import Coupon
class CouponForm(forms.ModelForm):
class Meta:
model = Coupon
fields = [
"title",
"description",
# "image",
"discount_amount",
"discount_percentage",
"valid_from",
"valid_to",
"max_redeems",
]
widgets = {
"valid_from": forms.DateTimeInput(attrs={"type": "datetime-local"}),
"valid_to": forms.DateTimeInput(attrs={"type": "datetime-local"}),
# "discount_amount": forms.NumberInput(attrs={'step': '0.01'}),
# "discount_percentage": forms.NumberInput(attrs={'step': '0.01'}),
}

View File

@@ -0,0 +1,81 @@
# Generated by Django 5.0.2 on 2024-07-22 12:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Coupon",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("active", models.BooleanField(default=True)),
("deleted", models.BooleanField(default=False)),
("created_on", models.DateTimeField(auto_now_add=True)),
("modified_on", models.DateTimeField(auto_now=True)),
("title", models.CharField(max_length=255)),
("coupon_code", models.CharField(max_length=50, unique=True)),
("no_of_redeems", models.IntegerField(default=0)),
("description", models.TextField(blank=True, null=True)),
(
"image",
models.ImageField(blank=True, null=True, upload_to="coupon_img"),
),
(
"discount_amount",
models.DecimalField(
blank=True, decimal_places=2, max_digits=10, null=True
),
),
(
"discount_percentage",
models.DecimalField(
blank=True, decimal_places=2, max_digits=5, null=True
),
),
("valid_from", models.DateTimeField()),
("valid_to", models.DateTimeField()),
("max_redeems", models.IntegerField(default=0)),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"modified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_modified",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"db_table": "coupon",
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-07-31 07:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_coupons", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="coupon",
name="coupon_id",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.0.2 on 2024-08-21 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_coupons', '0002_coupon_coupon_id'),
]
operations = [
migrations.AlterField(
model_name='coupon',
name='discount_amount',
field=models.DecimalField(blank=True, decimal_places=2, help_text='Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)', max_digits=10, null=True),
),
migrations.AlterField(
model_name='coupon',
name='discount_percentage',
field=models.DecimalField(blank=True, decimal_places=2, help_text='A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed).', max_digits=5, null=True),
),
migrations.AlterField(
model_name='coupon',
name='max_redeems',
field=models.IntegerField(default=1),
),
migrations.AlterField(
model_name='coupon',
name='valid_to',
field=models.DateTimeField(help_text='Datetime for the last redeemable date. After this, the coupon is invalid for new customers.'),
),
]

View File

104
manage_coupons/models.py Normal file
View File

@@ -0,0 +1,104 @@
from decimal import Decimal
from django.db import models
from django.utils import timezone
from accounts.models import BaseModel, IAmPrincipalType
from django.core.exceptions import ValidationError
class Coupon(BaseModel):
title = models.CharField(max_length=255)
coupon_code = models.CharField(max_length=50, unique=True)
coupon_id = models.CharField(max_length=255, blank=True, null=True)
no_of_redeems = models.IntegerField(default=0)
description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to="coupon_img", null=True, blank=True)
discount_amount = models.DecimalField(
max_digits=10, decimal_places=2, null=True, blank=True, help_text="Representing the amount to subtract from an invoice total (required if discount_percentage is not passed)"
)
discount_percentage = models.DecimalField(
max_digits=5, decimal_places=2, null=True, blank=True, help_text="A positive float larger than 0, and smaller or equal to 100, that represents the discount the coupon will apply (required if discount_amount is not passed)."
)
valid_from = models.DateTimeField()
valid_to = models.DateTimeField(help_text="Datetime for the last redeemable date. After this, the coupon is invalid for new customers.")
max_redeems = models.IntegerField(default=1)
class Meta:
db_table = "coupon"
def __str__(self):
return self.coupon_code
def clean(self):
"""
Validate the Coupon instance. Ensure that the `max_redeems` is greater than 0,
that either `discount_amount` or `discount_percentage` is set, and that
`valid_from` is earlier than `valid_to`.
"""
if self.max_redeems < 1:
raise ValidationError({"max_redeems": "Redeems must be more than 1."})
# Ensure discount_amount is non-negative
if self.discount_amount is not None and self.discount_amount < 1:
raise ValidationError(
{"discount_amount": "Discount amount must be more than 1."}
)
# Ensure discount_percentage is non-negative
if self.discount_percentage is not None and self.discount_percentage < 1:
raise ValidationError(
{"discount_percentage": "Discount percentage must be more than 1."}
)
if self.discount_amount and self.discount_percentage:
raise ValidationError(
"You can only set either a discount amount or a discount percentage, not both."
)
if not self.discount_amount and not self.discount_percentage:
raise ValidationError(
"You must set either a discount amount or a discount percentage."
)
if self.valid_from and self.valid_to and self.valid_from >= self.valid_to:
raise ValidationError(
"The valid_from date must be earlier than the valid_to date."
)
def save(self, *args, **kwargs):
from goodtimes.services import StripeService
if not self.delete:
self.clean() # Call clean before saving to ensure validation
if not self.pk and not self.coupon_id:
amount_off = int(self.discount_amount * Decimal(100)) if self.discount_amount else None
percent_off = float(self.discount_percentage) if self.discount_percentage else None
result = StripeService.create_coupon(
amount_off=amount_off,
percent_off=percent_off,
duration="once",
name=self.title,
redeem_by=int(self.valid_to.timestamp()),
max_redemptions=self.max_redeems,
currency='gbp',
metadata={"local_id": self.id}
)
if not result["success"]:
raise ValueError(f"Failed to create Stripe coupon: {result['message']}")
self.coupon_code = result['data'].id
self.coupon_id = result["data"].id
super().save(*args, **kwargs)
# If max_redeems is 0, it means that we are allowing unlimited redeems
# def is_valid(self):
# now = timezone.now()
# return (
# self.active
# and not self.deleted
# and self.valid_from <= now <= self.valid_to
# and (self.max_redeems == 0 or self.no_of_redeems < self.max_redeems)
# )

3
manage_coupons/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

23
manage_coupons/urls.py Normal file
View File

@@ -0,0 +1,23 @@
from django.urls import path
from . import views
app_name = "manage_coupons"
urlpatterns = [
path("coupon/list/", views.CouponView.as_view(), name="coupon_list"),
path(
"coupon/add/",
views.CouponCreateOrUpdateView.as_view(),
name="coupon_add",
),
# path(
# "coupon/edit/<int:pk>/",
# views.CouponCreateOrUpdateView.as_view(),
# name="coupon_edit",
# ),
path(
"coupon/delete/<int:pk>/",
views.CouponDeleteView.as_view(),
name="coupon_delete",
),
]

47
manage_coupons/utils.py Normal file
View File

@@ -0,0 +1,47 @@
import stripe
from decimal import Decimal
def handle_stripe_coupon(coupon_instance, stripe_secret_key):
"""
Handles the creation or updating of a Stripe coupon.
Returns True if successful, otherwise returns False.
"""
try:
stripe.api_key = stripe_secret_key
# Prepare coupon data without setting the ID
coupon_data = {
"name": coupon_instance.title,
"metadata": {
"local_id": coupon_instance.id,
},
"redeem_by": int(coupon_instance.valid_to.timestamp()),
"max_redemptions": (
coupon_instance.max_redeems if coupon_instance.max_redeems > 0 else None
),
"duration": "once",
}
if coupon_instance.discount_amount:
coupon_data["amount_off"] = int(
coupon_instance.discount_amount * Decimal(100)
) # Amount in cents/fils
coupon_data["currency"] = "gbp"
elif coupon_instance.discount_percentage:
coupon_data["percent_off"] = float(coupon_instance.discount_percentage)
# Creating a new Stripe coupon
stripe_coupon = stripe.Coupon.create(**coupon_data)
# Using the Stripe-generated ID for coupon_code and coupon_id
coupon_instance.coupon_code = stripe_coupon.id
coupon_instance.coupon_id = stripe_coupon.id
# Saving the coupon instance after successful Stripe operation
coupon_instance.save()
return True, "Coupon successfully created."
except Exception as e:
error_message = f"Error creating Stripe coupon: {e}"
print(error_message)
return False, error_message

129
manage_coupons/views.py Normal file
View File

@@ -0,0 +1,129 @@
from django.conf import settings
from django.shortcuts import get_object_or_404, render, redirect
from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin
import stripe
from accounts import resource_action
from django.urls import reverse_lazy
from django.contrib import messages
from goodtimes import constants
from manage_coupons.forms import CouponForm
from manage_coupons.models import Coupon
from manage_coupons.utils import handle_stripe_coupon
# Create your views here.
class CouponView(LoginRequiredMixin, generic.ListView):
page_name = resource_action.RESOURCE_MANAGE_COUPONS
resource = resource_action.RESOURCE_MANAGE_COUPONS
action = resource_action.ACTION_READ
model = Coupon
template_name = "manage_coupons/coupon_list.html"
context_object_name = "coupon_obj"
def get_queryset(self):
queryset = super().get_queryset().filter(deleted=False, active=True)
return queryset.order_by("-created_on")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_name"] = self.page_name
return context
class CouponCreateOrUpdateView(LoginRequiredMixin, generic.View):
# Set the page_name and resource
page_name = resource_action.RESOURCE_MANAGE_COUPONS
resource = resource_action.RESOURCE_MANAGE_COUPONS
# Initialize the action as ACTION_CREATE (can change based on logic)
action = resource_action.ACTION_CREATE
template_name = "manage_coupons/coupon_add.html"
model = Coupon
form_class = CouponForm
success_url = reverse_lazy("manage_coupons:coupon_list")
error_message = "An error occurred while saving the data."
# Determine the success message dynamically based on whether it's an update or create
def get_success_message(self):
self.success_message = (
constants.RECORD_CREATED if not self.object else constants.RECORD_UPDATED
)
return self.success_message
# Get the object (if exists) based on URL parameter 'pk'
def get_object(self):
pk = self.kwargs.get("pk")
return get_object_or_404(self.model, pk=pk) if pk else None
# Add page_name and operation to the context
def get_context_data(self, **kwargs):
context = {
"page_name": self.page_name,
"operation": "Add" if not self.object else "Edit",
}
context.update(kwargs) # Include any additional context data passed to the view
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
self.action = resource_action.ACTION_UPDATE
form = self.form_class(instance=self.object)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
self.action = resource_action.ACTION_UPDATE
form = self.form_class(request.POST, request.FILES, instance=self.object)
if not form.is_valid():
print(form.errors)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
form.save()
messages.success(request, self.get_success_message)
return redirect(self.success_url)
class CouponDeleteView(LoginRequiredMixin, generic.View):
page_name = resource_action.RESOURCE_MANAGE_COUPONS
resource = resource_action.RESOURCE_MANAGE_COUPONS
action = resource_action.ACTION_DELETE
model = Coupon
success_url = reverse_lazy("manage_coupons:coupon_list")
success_message = constants.RECORD_DELETED
error_message = constants.RECORD_NOT_FOUND
def get(self, request, pk):
try:
type_obj = self.model.objects.get(id=pk)
if type_obj.coupon_id:
stripe.api_key = settings.STRIPE_SECRET_KEY
try:
stripe.Coupon.delete(type_obj.coupon_id)
except stripe.error.StripeError as e:
# Handle Stripe errors
error_message = f"Stripe error: {e.user_message or e}"
messages.error(request, error_message)
return redirect(self.success_url)
type_obj.deleted = True
type_obj.active = False
type_obj.save()
messages.success(request, self.success_message)
except self.model.DoesNotExist:
messages.warning(request, self.error_message)
return redirect(self.success_url)

View File

@@ -1,5 +1,16 @@
from django.contrib import admin
from .models import EventCategory, Venue, EventMaster, Event, EventPrincipalInteraction
from .models import (
EventCategory,
EventShare,
EventView,
Favorites,
FreeUsageFeatureLimit,
Venue,
EventMaster,
Event,
EventPrincipalInteraction,
AgeGroups
)
# Register your models here.
@@ -78,7 +89,7 @@ class EventAdmin(admin.ModelAdmin):
},
),
)
filter_horizontal = () # Use this if there are many-to-many fields
filter_horizontal = () # if there are many-to-many fields
raw_id_fields = ("venue", "category", "event_master")
@@ -88,11 +99,37 @@ class EventPrincipalInteractionAdmin(admin.ModelAdmin):
search_fields = (
"principal__name",
"event__title",
) # Adjust these field lookups according to your models.
)
class EventViewAdmin(admin.ModelAdmin):
list_display = ("id", "event", "principal", "view_date", "location")
search_fields = ("event__title", "principal__email", "location")
list_filter = ("id", "view_date", "location", "event__title", "principal__email")
ordering = ("-view_date",)
readonly_fields = ("id",)
class EventShareAdmin(admin.ModelAdmin):
list_display = ("id", "event", "principal", "created_on")
search_fields = ("event__title", "principal__username")
list_filter = ("id", "event", "principal", "created_on")
class FavoritesAdmin(admin.ModelAdmin):
list_display = ("id", "principal", "event")
search_fields = ("principal__username", "event__title")
list_filter = ("principal", "event")
ordering = ("id",)
admin.site.register(Favorites, FavoritesAdmin)
admin.site.register(EventShare, EventShareAdmin)
admin.site.register(EventView, EventViewAdmin)
admin.site.register(EventPrincipalInteraction, EventPrincipalInteractionAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(EventCategory, EventCategoryAdmin)
admin.site.register(Venue, VenueAdmin)
admin.site.register(EventMaster, EventMasterAdmin)
admin.site.register(AgeGroups)
admin.site.register(FreeUsageFeatureLimit)

View File

@@ -0,0 +1,52 @@
from django_filters import rest_framework as filters
from django.db.models import Count, Q
from ..models import Event, EventInteractionType
class EventFilter(filters.FilterSet):
"""
FilterSet for Event model.
"""
title = filters.CharFilter(method="filter_title")
location = filters.CharFilter(field_name="venue__address", lookup_expr="icontains")
category = filters.CharFilter(method="filter_category")
start_date = filters.DateFilter(field_name="start_date", lookup_expr="gte")
# end_date = filters.DateFilter(field_name="end_date", lookup_expr="lte")
price_from = filters.NumberFilter(field_name="entry_fee", lookup_expr="gte")
price_to = filters.NumberFilter(field_name="entry_fee", lookup_expr="lte")
age_group = filters.CharFilter(method="filter_age_group")
class Meta:
model = Event
fields = [
'title',
'location',
'category',
'start_date',
# 'end_date',
'price_from',
'price_to',
'age_group',
]
def filter_title(self, queryset, name, value):
if value:
return queryset.filter(
Q(title__icontains=value) | Q(tags__name__icontains=value)
).distinct()
return queryset
def filter_category(self, queryset, name, value):
category = value.split(',')
return queryset.filter(category__title__in=category)
def filter_age_group(self, queryset, name, value):
age_group = value.split(',')
return queryset.filter(age_group__in=age_group)
# def filter_queryset(self, queryset):
# queryset = super().filter_queryset(queryset)
# if 'price_from' in self.data or 'price_to' in self.data:
# queryset = queryset.order_by('entry_fee')
# return queryset

View File

@@ -6,6 +6,7 @@ from taggit.models import Tag
from manage_events.utils import get_location_info
from accounts.api.serializers import ProfileSerializer
from manage_events.models import (
AgeGroups,
EventMaster,
Event,
EventCategory,
@@ -13,11 +14,18 @@ from manage_events.models import (
EventPrincipalInteraction,
EventReview,
Favorites,
FreeUsageFeatureLimit,
Venue,
PrincipalPreference,
)
class FreeUsageFeatureLimitSerializer(serializers.ModelSerializer):
class Meta:
model = FreeUsageFeatureLimit
fields = ['id', 'category_limit']
class EventImageSerializer(serializers.ModelSerializer):
class Meta:
model = EventImage
@@ -38,18 +46,39 @@ class VenueSerializer(serializers.ModelSerializer):
fields = "__all__"
read_only_fields = ("created_by",)
class VenueShortSerializer(serializers.ModelSerializer):
class Meta:
model = Venue
fields = ["id", "title"]
class EventCategorySerializer(serializers.ModelSerializer):
class Meta:
model = EventCategory
fields = ["id", "title", "image", "description", "video_url"]
def get_image_url(self, obj, field_name, request):
image_field = getattr(obj, field_name)
if image_field:
return request.build_absolute_uri(image_field.url)
return ""
def to_representation(self, instance):
data = super().to_representation(instance)
request = self.context.get("request")
data["image"] = self.get_image_url(instance, "image", request)
return data
class AgeGroupsSerializer(serializers.ModelSerializer):
class Meta:
model = AgeGroups
fields = ['id', 'name']
class EventListSerializer(serializers.ModelSerializer):
category = EventCategorySerializer(read_only=True)
venue = VenueSerializer(read_only=True)
draft = serializers.BooleanField(read_only=True)
tags = TagSerializer(many=True, read_only=True)
# venue = VenueSerializer(read_only=True)
# draft = serializers.BooleanField(read_only=True)
# tags = TagSerializer(many=True, read_only=True)
class Meta:
model = Event
@@ -62,22 +91,29 @@ class EventListSerializer(serializers.ModelSerializer):
"from_time",
"to_time",
"category",
"venue",
"venue_capacity",
# "venue",
# "venue_capacity",
"image",
# "video_url",
"entry_type",
# "entry_type",
"entry_fee",
"key_guest",
"age_group",
"link",
# "images",
# "is_favorited",
# "reviews",
"tags",
# "tags",
# "principal_interaction",
"draft",
# "draft",
]
# def to_representation(self, instance):
# """Customize the representation of the instance."""
# representation = super().to_representation(instance)
# representation['key_guest'] = instance.get_key_guests() # Return as a list
# return representation
class EventDetailSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
@@ -107,6 +143,9 @@ class EventDetailSerializer(serializers.ModelSerializer):
"entry_type",
"entry_fee",
"key_guest",
"coupon_code",
"coupon_description",
"link",
"age_group",
"images",
"is_favorited",
@@ -148,6 +187,12 @@ class EventDetailSerializer(serializers.ModelSerializer):
}
return None
# def to_representation(self, instance):
# """Customize the representation of the instance."""
# representation = super().to_representation(instance)
# representation['key_guest'] = instance.get_key_guests() # Return as a list
# return representation
class CreateEventSerializer(serializers.ModelSerializer):
tags = TagListSerializerField(required=False)
@@ -177,11 +222,21 @@ class CreateEventSerializer(serializers.ModelSerializer):
"draft",
"venue",
"tags",
"coupon_code",
"coupon_description",
"link"
]
def validate_key_guest(self, value):
if value and not isinstance(value, str):
raise serializers.ValidationError("key_guest must be a string")
return value
def create(self, validated_data):
tags = validated_data.pop("tags", None)
images_data = validated_data.pop("images", None)
key_guest = validated_data.pop("key_guest", None)
event = Event.objects.create(**validated_data)
if tags:
@@ -191,11 +246,15 @@ class CreateEventSerializer(serializers.ModelSerializer):
for image_data in images_data:
EventImage.objects.create(event=event, image=image_data)
if key_guest:
event.set_key_guests(key_guest)
event.save()
return event
def update(self, instance, validated_data):
tags = validated_data.pop("tags", None)
images_data = validated_data.pop("images", None)
key_guest = validated_data.pop("key_guest", None)
# Update fields if there is any change.
if tags is not None:
@@ -204,11 +263,13 @@ class CreateEventSerializer(serializers.ModelSerializer):
instance.tags.add(*tags)
if images_data is not None:
# Assuming you want to add new images without deleting the old ones
# If you want to replace them, you should delete the old images first
EventImage.objects.filter(event=instance).delete()
for image_data in images_data:
EventImage.objects.create(event=instance, image=image_data)
if key_guest is not None:
instance.set_key_guests(key_guest)
# Update other fields
for attr, value in validated_data.items():
setattr(instance, attr, value)
@@ -216,6 +277,12 @@ class CreateEventSerializer(serializers.ModelSerializer):
return instance
# def to_representation(self, instance):
# """Customize the representation of the instance."""
# representation = super().to_representation(instance)
# representation['key_guest'] = instance.get_key_guests() # Return as a list
# return representation
class CreateVenueSerializer(serializers.ModelSerializer):
class Meta:
@@ -236,32 +303,6 @@ class IAmPrincipalLocationSerializer(serializers.ModelSerializer):
model = IAmPrincipalLocation
fields = ["latitude", "longitude"]
def create(self, validated_data):
principal = self.context["request"].user
latitude = validated_data.get("latitude")
longitude = validated_data.get("longitude")
location = get_location_info(latitude=latitude, longitude=longitude)
print("location: ", location)
city = location.get("city")
state = location.get("state")
country = location.get("country")
country_code = location.get("country_code")
if hasattr(principal, "city"):
principal.city = city or state # Use state as city if city is not found
if hasattr(principal, "state"):
principal.state = state
if hasattr(principal, "country"):
principal.country = country
if hasattr(principal, "address_line1"):
principal.address_line1 = country_code
# save the principal object after making changes
principal.save()
return IAmPrincipalLocation.objects.create(
principal=principal, **validated_data
)
class PrincipalPreferenceSerializer(serializers.ModelSerializer):
preferred_categories = serializers.PrimaryKeyRelatedField(

View File

@@ -4,13 +4,14 @@ from . import views
app_name = "manage_events_api"
urlpatterns = [
path('free/feature-limit/', views.FreeUsageFeatureLimitView.as_view(), name='feature-limit'),
path(
"add-event/",
views.CreateEventApi.as_view(),
name="add_event",
),
path("edit-event/<int:pk>/", views.EventEditAPIView.as_view(), name="event-edit"),
path("get-events/", views.EventsAPIView.as_view(), name="events"),
path(
"event/<int:pk>/",
views.EventDetailAPIView.as_view(),
@@ -56,6 +57,11 @@ urlpatterns = [
views.PrincipalPreferenceDetailView.as_view(),
name="principal-preferences",
),
path(
"preferences/",
views.EventPreferencesView.as_view(),
name="preferences",
),
# Principal Location
path(
"add-location/",
@@ -111,4 +117,32 @@ urlpatterns = [
name="principal-events",
),
path("tags/", views.TagListView.as_view(), name="tag-list"),
# For counting event views
path(
"event/<int:pk>/view/",
views.CaptureEventViewAPIView.as_view(),
name="capture_event_view",
),
# For counting event shares
path(
"event/<int:pk>/share/",
views.EventShareView.as_view(),
name="capture_event_share",
),
path(
"age-groups/", views.AgeGroupListView.as_view(),
name="age_group_list"
),
path("get-events/calendar/", views.EventsCalenderAPIView.as_view(), name="events-calendar"),
# event list with filter
path(
"events/",
views.EventListView.as_view(),
name="event_filter",
),
path("post-to-social-media/<int:id>/", views.SocialMediaPostView.as_view(), name="social_media_post")
]

View File

@@ -1,19 +1,25 @@
import datetime
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
import googlemaps
from rest_framework import status, generics, mixins
from rest_framework.views import APIView
from django.conf import settings
from accounts import resource_action
from accounts.models import IAmPrincipalLocation
from goodtimes import constants
from django.db.models import Q
from django.db.models import Q, Count
from taggit.models import Tag
from django.utils.dateparse import parse_date
from goodtimes import services
from goodtimes.services import FacebookAPI, FacebookPoster, GoogleMapsservice, InstagramAPI, InstagramPoster, TwitterAPI, TwitterPoster
from goodtimes.utils import ApiResponse, CapacityError
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
from manage_cms.api.serializers import TagSerializer
from manage_events.api.serializers import (
AgeGroupsSerializer,
EventDateRangeSerializer,
EventMasterSearchSerializer,
EventMasterSerializer,
@@ -22,26 +28,47 @@ from manage_events.api.serializers import (
EventCategorySerializer,
EventDetailSerializer,
EventReviewSerializer,
FreeUsageFeatureLimitSerializer,
IAmPrincipalLocationSerializer,
PrincipalPreferenceSerializer,
VenueSerializer,
EventListSerializer,
)
from manage_events.models import (
AgeGroups,
EventInteractionType,
EventMaster,
Event,
EventCategory,
EventPrincipalInteraction,
EventReview,
EventShare,
EventView,
Favorites,
FreeUsageFeatureLimit,
PrincipalPreference,
Venue,
)
import requests
from manage_events.utils import haversine_one
from manage_events.utils import haversine_one, update_principal_location
from manage_subscriptions.models import PrincipalSubscription
from .filters import EventFilter
class FreeUsageFeatureLimitView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
model = FreeUsageFeatureLimit
serializer_class = FreeUsageFeatureLimitSerializer
def get(self, request):
obj = self.model.objects.first()
serializer = self.serializer_class(obj)
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
data=serializer.data,
)
class CreateEventApi(APIView):
authentication_classes = [JWTAuthentication]
@@ -52,7 +79,7 @@ class CreateEventApi(APIView):
serializer = CreateEventSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(created_by=self.request.user)
serializer.save(created_by=self.request.user, principal=self.request.user)
# Add additional logic for handling other relationships (e.g., Venue)
return ApiResponse.success(
@@ -110,10 +137,20 @@ class CreateVenueApi(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = VenueSerializer(data=request.data, context={"request": request})
data = request.data.copy()
print("prindata is ", data)
# Convert latitude and longitude to float and round to 8 decimal places
data["latitude"] = round(float(data["latitude"]), 8)
data["longitude"] = round(float(data["longitude"]), 8)
serializer = VenueSerializer(data=data, context={"request": request})
serializer.is_valid(raise_exception=True)
serializer.save(created_by=self.request.user, active=True)
serializer.save(
created_by=self.request.user, principal=self.request.user, active=True
)
# Add additional logic for handling other relationships (e.g., Venue)
return ApiResponse.success(
@@ -123,6 +160,20 @@ class CreateVenueApi(APIView):
)
# # Prepare the email
# subject = f"Your Event Report for {start_date.month} {start_date.year}."
# body = f"Please find attached the event report for {start_date.month} {start_date.month}."
# email_service = EmailService(
# subject="Good Times - Report",
# to=[user.email],
# from_email=settings.DEFAULT_FROM_EMAIL,
# )
# email_service.attach(filename, buffer.getvalue(), "application/pdf")
# # Send the email
# email_service.send()
class VenueDeleteAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -149,67 +200,6 @@ class VenueDeleteAPIView(APIView):
)
class EventsAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
filter = request.query_params.get("filter", None)
query = request.query_params.get("query", None)
category_id = request.query_params.get("category_id", None)
params = [
"expensive",
"cheap",
"preference",
"today",
"tomorrow",
"category",
"key_guest",
"tags",
]
if filter not in params:
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message=constants.FAILURE,
errors="No filter found",
)
try:
if filter == "today":
events = services.EventFilterService.filter_events_for_today()
elif filter == "tomorrow":
events = services.EventFilterService.filter_events_for_tomorrow()
elif filter == "key_guest":
events = services.EventFilterService.filter_events_by_search(
search_query=query
)
elif filter == "category" and category_id is not None:
events = services.EventFilterService.filter_events_by_category(
int(category_id)
)
else:
events = services.EventFilterService.filter_events(
filter_type=filter, principal=request.user
)
serializer = EventDetailSerializer(
events, context={"request": request}, many=True
)
# serializer = EventListSerializer(
# events, context={"request": request}, many=True
# )
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
data=serializer.data,
)
except Exception as e:
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message=constants.FAILURE,
errors=str(e),
)
class MyEventsAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -248,6 +238,9 @@ class MyEventsAPIView(APIView):
serializer = EventDetailSerializer(
events, context={"request": request}, many=True
)
# serializer = EventListSerializer(
# events, context={"request": request}, many=True
# )
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
@@ -445,21 +438,57 @@ class IAmPrincipalLocationAPIView(APIView):
)
def post(self, request, *args, **kwargs):
data = request.data.copy()
# Convert latitude and longitude to float and round to 8 decimal places
latitude = round(float(data["latitude"]), 15)
longitude = round(float(data["longitude"]), 15)
try:
principal = request.user
location = IAmPrincipalLocation.objects.get(principal=principal)
# Update existing location
location.latitude = latitude
location.longitude = longitude
location.save()
# Update principal fields using the utility function
update_principal_location(principal, latitude, longitude)
serializer = IAmPrincipalLocationSerializer(
data=request.data, context={"request": request}
location, context={"request": request}
)
print("serializer: ", serializer)
if serializer.is_valid():
serializer.save()
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
data=serializer.data,
)
except IAmPrincipalLocation.DoesNotExist:
# Create a new location object
location = IAmPrincipalLocation.objects.create(
principal=principal, latitude=latitude, longitude=longitude
)
# Update principal fields using the utility function
update_principal_location(principal, latitude, longitude)
serializer = IAmPrincipalLocationSerializer(
location, context={"request": request}
)
return ApiResponse.success(
status=status.HTTP_201_CREATED,
message=constants.SUCCESS,
data=serializer.data,
)
except Exception as e:
print(f"Error occurred while saving location: {e}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message=constants.FAILURE,
errors=serializer.errors,
errors=str(e),
)
@@ -468,6 +497,27 @@ class PrincipalPreferenceView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
principal = request.user
# Check if the principal has a subscription
if not PrincipalSubscription.has_principal_subscription(principal):
# Get the preferred categories from the request data
preferred_categories = request.data.get("preferred_categories", [])
# Get the category limit for free usage
category_limit = FreeUsageFeatureLimit.get_category_limit()
# Check if the principal is an event user and has exceeded the category limit
if principal.principal_type.name == resource_action.PRINCIPAL_TYPE_EVENT_USER and len(preferred_categories) > category_limit:
# Create an error message indicating that a paid subscription is required
error_message = f"Upgrade to paid subscription to select more than {category_limit} categories."
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message=error_message,
errors=error_message,
)
serializer = PrincipalPreferenceSerializer(
data=request.data, context={"request": request}
)
@@ -506,6 +556,21 @@ class PrincipalPreferenceDetailView(generics.RetrieveAPIView):
)
class EventPreferencesView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
model = EventCategory
serializer_class = EventCategorySerializer
def get(self, request, *args, **kwargs):
"""Get all event categories for the authenticated user."""
obj = self.model.objects.filter(active=True, deleted=False)
serializer = self.serializer_class(obj, many=True, context={"request": request})
return ApiResponse.success(
data=serializer.data, message=constants.SUCCESS, status=status.HTTP_200_OK
)
class EventMasterSearchAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
@@ -626,9 +691,12 @@ class EventFilterByLocationAPIView(APIView):
)
max_distance_km = 10 # Set your desired maximum distance
current_and_future_events_query = Q(active=True, deleted=False, draft=False) & (
Q(end_date__gte=today)
)
current_and_future_events_query = Q(
active=True,
deleted=False,
draft=False,
created_by__is_active=True,
) & (Q(end_date__gte=today))
# Get the queryset based on the filter conditions
events_queryset = Event.objects.filter(current_and_future_events_query)
@@ -652,7 +720,7 @@ class EventFilterByLocationAPIView(APIView):
venues_within_range.append(venue.id)
print("venues_within_range: ", venues_within_range)
# venues_data = [venue_to_dict(venue) for venue in venues_within_range]
events = Event.objects.filter(venue__id__in=venues_within_range)
events = events_queryset.filter(venue__id__in=venues_within_range)
# Serialize and return the filtered events
serializer = EventDetailSerializer(
@@ -846,3 +914,270 @@ class TagListView(generics.ListAPIView):
data=serializer.data,
status=status.HTTP_200_OK,
)
class CaptureEventViewAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, pk):
try:
event = Event.objects.get(pk=pk)
user = request.user
location_parts = [user.city, user.state, user.country]
location = " ".join(part for part in location_parts if part)
EventView.objects.create(event=event, principal=user, location=location)
return ApiResponse.success(
message=constants.SUCCESS,
data="Event view recorded successfully.",
status=status.HTTP_200_OK,
)
except Event.DoesNotExist:
return ApiResponse.error(
message=constants.FAILURE,
errors="Event not found.",
status=status.HTTP_400_BAD_REQUEST,
)
class EventShareView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, pk):
try:
event = Event.objects.get(id=pk)
except Event.DoesNotExist:
return ApiResponse.error(
message=constants.FAILURE,
errors="Event not found.",
status=status.HTTP_400_BAD_REQUEST,
)
# Incrementing the social media shares count
event.increment_shares()
user = request.user # Assuming the user is authenticated
EventShare.objects.create(principal=user, event=event)
return ApiResponse.success(
message=constants.SUCCESS,
data="Event shared successfully.",
status=status.HTTP_200_OK,
)
class AgeGroupListView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = AgeGroupsSerializer
model = AgeGroups
def get(self, request):
queryset = self.model.objects.filter(active=True)
serializer = self.serializer_class(queryset, many=True)
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
class EventsCalenderAPIView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
try:
principal = request.user
queryset = Event.objects.filter(
active=True,
draft=False,
deleted=False,
end_date__gte=timezone.now().date()
)
# queryset = Event.objects.filter(
# ((Q(active=True) & Q(draft=False) & Q(deleted=False) & Q(end_date__gte=timezone.now().date())))
# )
preferences = PrincipalPreference.objects.get(principal=principal)
preferred_categories_ids = preferences.preferred_categories.values_list("id", flat=True)
# Filter the queryset to only include events in the user's preferred categories
queryset = queryset.filter(Q(category__in=preferred_categories_ids) | Q(principal=principal))
serializer = EventListSerializer(
queryset, context={"request": request}, many=True
)
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
data=serializer.data,
)
except Exception as e:
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message=constants.FAILURE,
errors=str(e),
)
class EventListView(generics.ListAPIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = EventListSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = EventFilter
def get_queryset(self):
"""
Returns a queryset of events filtered by the user's preferences and subscription status.
"""
principal = self.request.user
# Filter the base queryset to only include active, non-draft, non-deleted events with an end date in the future
queryset = Event.objects.filter(
active=True,
draft=False,
deleted=False,
end_date__gte=timezone.now().date()
)
# If no filter is applied and the user does not have a subscription,
# only show events that match the user's preferred categories
if not self.request.query_params or not PrincipalSubscription.has_principal_subscription(principal):
# Get the user's preferred categories
preferences = PrincipalPreference.objects.get(principal=principal)
preferred_categories_ids = preferences.preferred_categories.values_list("id", flat=True)
# Filter the queryset to only include events in the user's preferred categories
queryset = queryset.filter(category__in=preferred_categories_ids).order_by("start_date")
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# Get query parameters
latitude = self.request.query_params.get("latitude", "")
longitude = self.request.query_params.get("longitude", "")
ordering = self.request.query_params.get("sort", "")
# Handle nearest location sorting
if latitude and longitude and "nearest" in ordering:
queryset = self.apply_nearest_filter(queryset, latitude, longitude)
# Handle popularity and latest sorting
queryset = self.apply_sorting(queryset, ordering)
return queryset
def apply_nearest_filter(self, queryset, latitude, longitude):
gmaps_service = GoogleMapsservice()
return gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude))
def apply_sorting(self, queryset, ordering):
# Split ordering fields and process each field
ordering_fields = [field.lstrip("-") for field in ordering.split(",")]
# Remove 'nearest' from ordering as it's handled separately
if "nearest" in ordering_fields:
ordering_fields.remove("nearest")
ordering = ordering.replace("nearest", "")
# Annotate with popularity and order it if requested
if "popularity" in ordering_fields:
queryset = queryset.annotate(popularity=Count("interaction_event")).order_by("-popularity")
# order latest record and by default sorting
if "latest" in ordering_fields:
queryset = queryset.order_by("start_date")
if "price" in ordering_fields:
queryset = queryset.order_by('entry_fee')
return queryset
def get(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)
from rest_framework.response import Response
class SocialMediaPostView(APIView):
def get(self, request, *args, **kwargs):
platform = request.query_params.get("platform", "")
event_id = kwargs.get("id")
print(platform, event_id)
errors = []
success_messages = []
try:
event = Event.objects.get(id=event_id)
except Event.DoesNotExist:
errors.append("Event does not exist")
return Response({
'message': "Error in posting to social media",
'errors': errors,
'success_messages': success_messages
}, status=400)
if not event.active:
errors.append("Event is not active")
return Response({
'message': "Error in posting to social media",
'errors': errors,
'success_messages': success_messages
}, status=400)
caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}"
if platform in ['instagram', 'facebook', 'twitter', 'all']:
if platform in ['twitter', 'all']:
image_url = event.image.path
twitter_api = TwitterAPI()
twitter_poster = TwitterPoster(twitter_api)
result = twitter_poster.post_image_with_caption(image_url, caption)
if result['success']:
success_messages.append("Posted to Twitter successfully")
else:
errors.append("Fail to post on Twitter")
image_url = request.build_absolute_uri(event.image.url)
if platform in ['facebook', 'all']:
facebook_api = FacebookAPI()
facebook_poster = FacebookPoster(facebook_api)
result = facebook_poster.post_photo(image_url, caption)
if result["success"]:
success_messages.append("Posted to Facebook successfully")
else:
errors.append("Fail to post on Facebook")
if platform in ['instagram', 'all']:
instagram_api = InstagramAPI()
instagram_poster = InstagramPoster(instagram_api)
result = instagram_poster.post_image_with_caption(image_url, caption)
if result["success"]:
success_messages.append("Posted to Instagram successfully")
else:
errors.append("Fail to post on Instagram")
if not errors:
return Response({'message': 'Post Successful', 'errors': errors, 'success_messages': success_messages})
if errors and success_messages:
return Response({
'message': 'Some posts succeeded while others failed',
'errors': errors,
'success_messages': success_messages
}, status=200)
return Response({
'message': 'Error in posting to social media',
'errors': errors,
'success_messages': success_messages
}, status=400)

View File

@@ -1,5 +1,6 @@
from django import forms
from manage_events.models import EventMaster, Event, EventCategory, Venue
from accounts.models import IAmPrincipal, IAmPrincipalExtendedData
from manage_events.models import AgeGroups, EventImage, EventMaster, Event, EventCategory, Venue
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@@ -16,83 +17,121 @@ class EventCategoryForm(forms.ModelForm):
class EventForm(forms.ModelForm):
principal = forms.ModelChoiceField(
queryset=IAmPrincipal.objects.select_related("extended_data").filter(
extended_data__is_onboarded=True,
extended_data__is_transferred=False
),
label="Non-transfer user list",
required=True
)
venue = forms.ModelChoiceField(
queryset=Venue.objects.none(),
label="venue",
required=True
)
image = forms.ImageField(label="Thumbnail")
event_images = forms.ImageField(label="Event Images")
age_group = forms.ChoiceField(
choices=[],
label="Age Group",
required=True
)
class Meta:
model = Event
fields = [
"principal",
"venue",
"title",
# "event_master",
"description",
"link",
"image",
"status",
"event_images",
# "status",
"start_date",
"end_date",
"from_time",
"to_time",
"category",
"venue",
"venue_capacity",
"video_url",
# "video_url",
"entry_type",
"entry_fee",
"key_guest",
"age_group",
"coupon_code",
"coupon_description",
"tags",
"draft",
"active",
"deleted",
]
widgets = {
"title": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
"status": forms.Select(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"rows": 4}),
"start_date": forms.DateInput(
attrs={"class": "form-control", "type": "date"}
attrs={"type": "date"}
),
"end_date": forms.DateInput(
attrs={"class": "form-control", "type": "date"}
attrs={"type": "date"}
),
"from_time": forms.TimeInput(
attrs={"class": "form-control", "type": "time"}
attrs={"type": "time"}
),
"to_time": forms.TimeInput(attrs={"class": "form-control", "type": "time"}),
"venue_capacity": forms.NumberInput(attrs={"class": "form-control"}),
"video_url": forms.URLInput(attrs={"class": "form-control"}),
"entry_type": forms.TextInput(attrs={"class": "form-control"}),
"entry_fee": forms.NumberInput(attrs={"class": "form-control"}),
"key_guest": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"age_group": forms.TextInput(attrs={"class": "form-control"}),
"draft": forms.CheckboxInput(attrs={"class": "form-check-input"}),
# For the 'image' field, you might not need to specify a widget since the default is appropriate.
# However, if you want to add specific classes or attributes, you can do it like this:
"image": forms.FileInput(attrs={"class": "form-control-file"}),
# For ForeignKey fields like 'EventMaster' and 'venue', Django uses a select widget by default.
# You can customize it further if needed:
# "event_master": forms.Select(attrs={"class": "form-control"}),
"venue": forms.Select(attrs={"class": "form-control"}),
"category": forms.Select(attrs={"class": "form-control"}),
"to_time": forms.TimeInput(attrs={"type": "time"}),
"key_guest": forms.Textarea(attrs={"rows": 3}),
"coupon_description": forms.Textarea(attrs={"rows": 3}),
}
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance')
principal_id = kwargs.pop('principal_id', None)
super().__init__(*args, **kwargs)
if instance:
event_images = EventImage.objects.filter(event=instance)
if event_images.exists():
self.fields['event_images'].initial = [image.image.url for image in event_images]
# Set the initial value for age_group if instance is provided
print(f"age group is {self.instance.age_group}")
age_groups = [(age_group.name, age_group.name) for age_group in AgeGroups.objects.filter(active=True)]
self.fields['age_group'].choices = age_groups
if self.instance:
self.fields['age_group'].initial = self.instance.age_group
if principal_id:
self.fields['venue'].queryset = Venue.objects.filter(principal_id=principal_id, active=True)
else:
self.fields['venue'].queryset = Venue.objects.none()
def clean(self):
cleaned_data = super().clean()
# Get the start and end dates from cleaned_data
start_date = cleaned_data.get("start_date")
end_date = cleaned_data.get("end_date")
# Validation 1: end_date should not be less than start_date
if end_date and start_date and end_date < start_date:
self.add_error("end_date", _("End date cannot be before the start date."))
# Get the from and to times from cleaned_data
from_time = cleaned_data.get("from_time")
to_time = cleaned_data.get("to_time")
# Validation 2: to_time should not be less than or equal to from_time
# Validation 1: end_date should not be less than start_date
if start_date and end_date and end_date < start_date:
self.add_error("end_date", _("End date cannot be before the start date."))
if end_date == start_date:
if to_time and from_time and to_time <= from_time:
self.add_error("to_time", _("End time must be after the start time."))
return cleaned_data
class EventImageForm(forms.ModelForm):
class Meta:
model = EventImage
fields = ['image']
class EventMasterForm(forms.ModelForm):
class Meta:
@@ -101,23 +140,35 @@ class EventMasterForm(forms.ModelForm):
class VenueForm(forms.ModelForm):
principal = forms.ModelChoiceField(
queryset=IAmPrincipal.objects.select_related("extended_data").filter(
extended_data__is_onboarded=True,
extended_data__is_transferred=False
),
label="Non-transfer user list",
required=True
)
image = forms.ImageField(required=True)
postcode = forms.CharField(required=True, max_length=10)
latitude = forms.DecimalField(
widget=forms.NumberInput()
)
longitude = forms.DecimalField(
widget=forms.NumberInput()
)
class Meta:
model = Venue
fields = [
"principal",
"title",
"description",
"address",
"postcode",
"image",
"url",
"latitude",
"longitude",
]
widgets = {
"title": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
"address": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"image": forms.FileInput(attrs={"class": "form-control"}),
"url": forms.URLInput(attrs={"class": "form-control"}),
"latitude": forms.NumberInput(attrs={"class": "form-control"}),
"longitude": forms.NumberInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"rows": 4}),
"address": forms.Textarea(attrs={"rows": 3}),
}

View File

@@ -0,0 +1,60 @@
from django.core.mail import EmailMessage
from django.conf import settings
from django.core.management.base import BaseCommand
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from django.utils.timezone import now
from django.contrib.auth import get_user_model
import calendar
from manage_events.report import generate_event_report, generate_event_report_pdf_three
class Command(BaseCommand):
help = 'Getting event reports of specific event managers'
def add_arguments(self, parser):
parser.add_argument('month', type=int, help='Month number (1-12)')
parser.add_argument('email', type=str, help='User email address')
parser.add_argument('mail_send_on', type=str, help='Email to send the report')
def handle(self, *args, **kwargs):
month = kwargs['month']
email = kwargs['email']
mail_send_on = kwargs['mail_send_on']
# Validate the month
if month < 1 or month > 12:
self.stdout.write(self.style.ERROR("Invalid month. Must be between 1 and 12."))
return
User = get_user_model()
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
self.stdout.write(self.style.ERROR(f"User with email {email} does not exist."))
return
# Calculate start and end dates of the month
year = now().year # Assuming the current year
start_date = datetime(year, month, 1)
last_day = calendar.monthrange(year, month)[1]
end_date = datetime(year, month, last_day)
report_data = generate_event_report(user.id, start_date, end_date)
if report_data:
pdf_data, filename = generate_event_report_pdf_three(user, report_data, start_date)
self.send_email_with_attachment(mail_send_on, pdf_data, filename)
def send_email_with_attachment(self, email, pdf_data, filename):
try:
email_message = EmailMessage(
subject="Monthly Event Report",
body="Please find the attached report for the last month.",
to=[email],
from_email=settings.DEFAULT_FROM_EMAIL,
)
email_message.attach(filename, pdf_data, "application/pdf")
email_message.send()
self.stdout.write(self.style.SUCCESS(f"Email successfully sent to {email} with attachment {filename}."))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Failed to send email to {email}. Error: {str(e)}"))

View File

@@ -0,0 +1,33 @@
from django.core.management.base import BaseCommand
from django.core.mail import EmailMessage
from django.conf import settings
from manage_events.report import (
get_previous_month_date_range,
event_managers,
generate_event_report,
generate_event_report_pdf_three,
)
class Command(BaseCommand):
help = "Send monthly event reports to event managers"
def handle(self, *args, **kwargs):
start_date, end_date = get_previous_month_date_range()
users = event_managers()
for user in users:
report_data = generate_event_report(user.id, start_date, end_date)
if report_data:
pdf_data, filename = generate_event_report_pdf_three(user, report_data)
self.send_email_with_attachment(user.email, pdf_data, filename)
def send_email_with_attachment(self, email, pdf_data, filename):
email_message = EmailMessage(
subject="Monthly Event Report",
body="Please find the attached report for the last month.",
to=[email],
from_email=settings.DEFAULT_FROM_EMAIL,
)
email_message.attach(filename, pdf_data, "application/pdf")
email_message.send()

View File

@@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand
from ...models import AgeGroups, Event
import random
class Command(BaseCommand):
help = 'Populate the AgeGroup model with predefined age groups'
def handle(self, *args, **kwargs):
age_groups = ["18-21", "21-30", "30-40", "40-50", "50+"]
for age in age_groups:
age_group, created = AgeGroups.objects.get_or_create(name=age)
if created:
self.stdout.write(self.style.SUCCESS(f'Age group "{age}" created.'))
else:
self.stdout.write(self.style.WARNING(f'Age group "{age}" already exists.'))
# Update all Event objects with a random age group
for event in Event.objects.all():
event.age_group = random.choice(age_groups)
event.save()

View File

@@ -0,0 +1,30 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from goodtimes.services import FacebookAPI, FacebookPoster
from ...models import Event
class Command(BaseCommand):
help = 'Test facebook posting functionality'
def handle(self, *args, **kwargs):
event = Event.objects.get(id=20)
if not event:
self.stdout.write(self.style.ERROR("No event found."))
if not event.image:
self.stdout.write(self.style.ERROR("No image found."))
image_path = f"{settings.BASE_DOMAIN}{event.image.url}"
print(f"complete path of image {image_path}")
caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}"
facebook_api = FacebookAPI()
facebook_poster = FacebookPoster(facebook_api)
response = facebook_poster.post_photo(image_path, caption)
if response['success']:
self.stdout.write(self.style.SUCCESS(response['message']))
else:
self.stdout.write(self.style.ERROR(response['message']))

View File

@@ -0,0 +1,33 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand
from goodtimes.services import InstagramAPI, InstagramPoster
from ...models import Event
import urllib.request
class Command(BaseCommand):
help = 'Test Instagram posting functionality'
def handle(self, *args, **kwargs):
event = Event.objects.get(id=20)
if not event:
self.stdout.write(self.style.ERROR("No event found."))
if not event.image:
self.stdout.write(self.style.ERROR("No image found."))
image_path = f"{settings.BASE_DOMAIN}{event.image.url}"
# image_path = event.image.url
print(f"complete path of image {image_path}")
caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}"
instagram_api = InstagramAPI()
instagram_poster = InstagramPoster(instagram_api)
response = instagram_poster.post_image_with_caption(image_path, caption)
if response['success']:
self.stdout.write(self.style.SUCCESS(response['message']))
else:
self.stdout.write(self.style.ERROR(response['message']))

View File

@@ -0,0 +1,28 @@
import os
from django.core.management.base import BaseCommand
from goodtimes.services import TwitterAPI, TwitterPoster
from ...models import Event
class Command(BaseCommand):
help = 'Test Twitter posting functionality'
def handle(self, *args, **kwargs):
event = Event.objects.get(id=19)
if not event:
self.stdout.write(self.style.ERROR("No event found."))
if not event.image:
self.stdout.write(self.style.ERROR("No image found."))
image_path = event.image.path
caption = f"{event.title}\nDuration: {event.start_date} to {event.end_date}\nAddress: {event.venue.address}"
twitter_api = TwitterAPI()
twitter_poster = TwitterPoster(twitter_api)
response = twitter_poster.post_image_with_caption(image_path, caption)
if response['success']:
self.stdout.write(self.style.SUCCESS(response['message']))
else:
self.stdout.write(self.style.ERROR(response['message']))

View File

@@ -0,0 +1,103 @@
import os
from django.conf import settings
import requests
from dotenv import load_dotenv
from django.core.management.base import BaseCommand
# Load .env variables
load_dotenv()
class Command(BaseCommand):
help = 'Update Facebook long-lived access tokens and page access token'
def __init__(self):
super().__init__()
self.app_id = settings.FACEBOOK_APP_ID
self.app_secret = settings.FACEBOOK_APP_SECRET
self.page_id = settings.FACEBOOK_PAGE_ID
self.graph_api_version = settings.FACEBOOK_GRAPH_VERSION_API
self.page_access_token = settings.FACEBOOK_ACCESS_TOKEN
self.long_lived_token = settings.FACEBOOK_ACCESS_TOKEN
def handle(self, *args, **kwargs):
"""Handle the token refresh and update .env file."""
if self.refresh_access_tokens():
self.stdout.write(self.style.SUCCESS("Successfully refreshed Facebook tokens."))
else:
self.stdout.write(self.style.ERROR("Failed to refresh Facebook tokens."))
def _exchange_short_to_long_lived_token(self, short_lived_token):
"""Exchange short-lived token for long-lived token."""
try:
url = f"https://graph.facebook.com/{self.graph_api_version}/oauth/access_token"
params = {
"grant_type": "fb_exchange_token",
"client_id": self.app_id,
"client_secret": self.app_secret,
"fb_exchange_token": short_lived_token,
}
response = requests.get(url, params=params)
response.raise_for_status()
long_lived_token = response.json().get("access_token")
self.stdout.write(self.style.SUCCESS("Successfully exchanged for long-lived user access token."))
return long_lived_token
except requests.exceptions.RequestException as e:
self.stdout.write(self.style.ERROR(f"Error exchanging short-lived token: {e}"))
return None
def _get_page_access_token(self, user_token):
"""Retrieve Page Access Token."""
try:
url = f"https://graph.facebook.com/{self.graph_api_version}/{self.page_id}"
params = {
"fields": "access_token",
"access_token": user_token,
}
response = requests.get(url, params=params)
response.raise_for_status()
page_access_token = response.json().get("access_token")
self.stdout.write(self.style.SUCCESS("Successfully obtained page access token."))
return page_access_token
except requests.exceptions.RequestException as e:
self.stdout.write(self.style.ERROR(f"Error retrieving page access token: {e}"))
return None
def _update_env_variable(self, key, value):
"""Update a variable in the .env file."""
with open('.env', 'r') as file:
lines = file.readlines()
with open('.env', 'w') as file:
updated = False
for line in lines:
if line.startswith(key):
file.write(f"{key}={value}\n")
updated = True
else:
file.write(line)
if not updated:
file.write(f"{key}={value}\n")
def refresh_access_tokens(self):
"""Refresh long-lived user access token and page access token."""
if not self.long_lived_token:
self.stdout.write(self.style.ERROR("No valid long-lived user access token found."))
return False
# Refresh long-lived user token (optional, based on expiry)
refreshed_user_token = self._exchange_short_to_long_lived_token(self.long_lived_token)
if refreshed_user_token:
self.long_lived_token = refreshed_user_token
# Refresh page access token
# page_access_token = self._get_page_access_token(self.long_lived_token)
# if page_access_token:
# self.page_access_token = page_access_token
# # Update tokens in .env file
self._update_env_variable("FACEBOOK_ACCESS_TOKEN", self.long_lived_token)
# self._update_env_variable("FACEBOOK_PAGE_ACCESS_TOKEN", self.page_access_token)
return True
self.stdout.write(self.style.ERROR("Failed to refresh page access token."))
return False

View File

@@ -0,0 +1,75 @@
# Generated by Django 5.0.2 on 2024-05-31 11:31
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_events", "0008_alter_eventprincipalinteraction_event_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="EventView",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("active", models.BooleanField(default=True)),
("deleted", models.BooleanField(default=False)),
("created_on", models.DateTimeField(auto_now_add=True)),
("modified_on", models.DateTimeField(auto_now=True)),
("view_date", models.DateTimeField(auto_now_add=True)),
("location", models.CharField(blank=True, max_length=255, null=True)),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="views",
to="manage_events.event",
),
),
(
"modified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_modified",
to=settings.AUTH_USER_MODEL,
),
),
(
"principal",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="event_views",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]

View File

@@ -0,0 +1,78 @@
# Generated by Django 5.0.2 on 2024-06-01 15:06
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_events", "0009_eventview"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="event",
name="social_media_shares_count",
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name="EventShare",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("active", models.BooleanField(default=True)),
("deleted", models.BooleanField(default=False)),
("created_on", models.DateTimeField(auto_now_add=True)),
("modified_on", models.DateTimeField(auto_now=True)),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_created",
to=settings.AUTH_USER_MODEL,
),
),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="social_media_shares",
to="manage_events.event",
),
),
(
"modified_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="%(class)s_modified",
to=settings.AUTH_USER_MODEL,
),
),
(
"principal",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="event_shares",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.0.2 on 2024-06-04 10:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_events", "0010_event_social_media_shares_count_eventshare"),
]
operations = [
migrations.AlterField(
model_name="event",
name="entry_type",
field=models.CharField(
choices=[("free", "Free"), ("paid", "Paid")], max_length=10
),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.2 on 2024-06-20 06:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_events", "0011_alter_event_entry_type"),
]
operations = [
migrations.AddField(
model_name="event",
name="coupon_code",
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name="event",
name="coupon_description",
field=models.TextField(blank=True, null=True),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.0.2 on 2024-06-25 17:04
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_events', '0012_event_coupon_code_event_coupon_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='venue',
name='principal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='venues_principal', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.0.2 on 2024-06-25 17:09
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_events', '0013_venue_principal'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='event',
name='principal',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events_principal', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.0.2 on 2024-07-17 06:36
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_events', '0014_event_principal'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AgeGroups',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=True)),
('deleted', models.BooleanField(default=False)),
('created_on', models.DateTimeField(auto_now_add=True)),
('modified_on', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=10, unique=True)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_created', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modified', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'age_group',
},
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0.2 on 2024-08-05 10:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_events', '0015_agegroups'),
]
operations = [
migrations.CreateModel(
name='FreeUsageFeatureLimit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('category_limit', models.PositiveIntegerField(default=3, help_text='The maximum number of categories that free app users can select.')),
],
options={
'verbose_name': 'Free Usage Feature Limit',
'verbose_name_plural': 'Free Usage Feature Limits',
'db_table': 'free_usage_feature_limit',
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-12-20 09:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_events', '0016_freeusagefeaturelimit'),
]
operations = [
migrations.AddField(
model_name='venue',
name='postcode',
field=models.CharField(blank=True, max_length=20, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-12-24 11:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_events', '0017_venue_postcode'),
]
operations = [
migrations.AddField(
model_name='event',
name='link',
field=models.URLField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,12 +1,33 @@
from django.db import models
from django.core.exceptions import ValidationError
from accounts.models import BaseModel, IAmPrincipal
from django.db import transaction
from taggit.managers import TaggableManager
# from django.contrib.gis.db import models as gis_models
class FreeUsageFeatureLimit(models.Model):
category_limit = models.PositiveIntegerField(
default=3,
help_text="The maximum number of categories that free app users can select."
)
class Meta:
db_table = "free_usage_feature_limit"
verbose_name = "Free Usage Feature Limit"
verbose_name_plural = "Free Usage Feature Limits"
def __str__(self):
return f"Free usage limit: {self.category_limit} categories"
def save(self, *args, **kwargs):
if not self.pk and FreeUsageFeatureLimit.objects.exists():
raise ValidationError("There can only be one FreeUsageFeatureLimit instance.")
return super().save(*args, **kwargs)
@classmethod
def get_category_limit(cls):
return cls.objects.values_list('category_limit', flat=True).first()
# Create your models here.
class EventCategory(BaseModel):
title = models.CharField(max_length=255)
image = models.ImageField(upload_to="event_category", null=True, blank=True)
@@ -18,6 +39,7 @@ class EventCategory(BaseModel):
class Venue(BaseModel):
principal = models.ForeignKey(IAmPrincipal, related_name="venues_principal", on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
address = models.TextField(null=True, blank=True)
@@ -29,10 +51,20 @@ class Venue(BaseModel):
longitude = models.DecimalField(
max_digits=14, decimal_places=8, blank=True, null=True
)
postcode = models.CharField(max_length=20, blank=True, null=True)
def __str__(self):
return self.title
class AgeGroups(BaseModel):
name = models.CharField(max_length=10, unique=True)
class Meta:
db_table = "age_group"
def __str__(self):
return self.name
class EventStatus(models.TextChoices):
UPCOMING = "upcoming", "Upcoming"
@@ -55,6 +87,11 @@ class EventMaster(BaseModel):
class Event(BaseModel):
ENTRY_TYPE_CHOICES = [
("free", "Free"),
("paid", "Paid"),
]
principal = models.ForeignKey(IAmPrincipal, related_name="events_principal", on_delete=models.CASCADE, null=True)
title = models.CharField(max_length=255)
category = models.ForeignKey(EventCategory, on_delete=models.CASCADE)
event_master = models.ForeignKey(
@@ -75,15 +112,37 @@ class Event(BaseModel):
video_url = models.URLField(max_length=200, blank=True, null=True)
entry_type = models.CharField(
max_length=100
) # Assuming entry type is a string (e.g., Free, Ticketed)
max_length=10,
choices=ENTRY_TYPE_CHOICES,
)
entry_fee = models.DecimalField(
max_digits=14, decimal_places=2, default=0.00
) # Assuming it's an integer. Use DecimalField if you need to handle cents.
)
key_guest = models.TextField(blank=True, null=True)
tags = TaggableManager(blank=True)
age_group = models.CharField(max_length=100, blank=True, null=True)
draft = models.BooleanField(default=False)
social_media_shares_count = models.IntegerField(default=0)
coupon_code = models.CharField(max_length=255, blank=True, null=True)
coupon_description = models.TextField(blank=True, null=True)
link = models.URLField(max_length=255, blank=True, null=True)
def increment_shares(self):
self.social_media_shares_count += 1
self.save()
def set_key_guests(self, guests):
"""Set the key guests as a comma-seperated string."""
if isinstance(guests, list):
self.key_guest = ",".join(guests)
elif isinstance(guests, str):
self.key_guest = guests
else:
raise ValueError("Guests must be a comma-seperated string")
def get_key_guests(self):
"""Return the key guests as a list of strings."""
return self.key_guest.split(",") if self.key_guest else []
def __str__(self):
return self.title
@@ -132,7 +191,7 @@ class PrincipalPreference(BaseModel):
)
def __str__(self):
return str(self.preferred_categories)
return str(self.preferred_categories.name)
class Meta:
db_table = "user_preference"
@@ -173,3 +232,26 @@ class EventReview(BaseModel):
def __str__(self):
return f"Review by {self.principal} on {self.event}"
class EventView(BaseModel):
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="views")
principal = models.ForeignKey(
IAmPrincipal, on_delete=models.CASCADE, related_name="event_views"
)
view_date = models.DateTimeField(auto_now_add=True)
location = models.CharField(
max_length=255, blank=True, null=True
) # Or use a more complex field for location data
def __str__(self):
return f"{self.principal.email} viewed {self.event.title} from {self.location}"
class EventShare(BaseModel):
event = models.ForeignKey(
Event, on_delete=models.CASCADE, related_name="social_media_shares"
)
principal = models.ForeignKey(
IAmPrincipal, on_delete=models.CASCADE, related_name="event_shares"
)

378
manage_events/report.py Normal file
View File

@@ -0,0 +1,378 @@
from django.db.models import Count, Q
from django.utils import timezone
from datetime import timedelta
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import mm
from reportlab.graphics.shapes import Drawing, Rect
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.graphics.charts.piecharts import Pie
from reportlab.platypus import (
SimpleDocTemplate,
Table,
TableStyle,
Paragraph,
Spacer,
PageBreak,
Image,
)
from io import BytesIO
from django.conf import settings
from collections import defaultdict
from reportlab.graphics import renderPDF
from django.contrib.auth import get_user_model
from accounts.models import IAmPrincipalType
from manage_events.models import Event, EventInteractionType, EventShare, EventView
import os
User = get_user_model()
def generate_filename(email, date):
# Extract the username from the email address
username = email.split("@")[0]
# Get the full month name from the date
month_name = date.strftime("%B")
# Create the filename
filename = f"{username}_{month_name}_report.pdf"
return filename
def event_managers():
principal_type = IAmPrincipalType.objects.filter(name="event_manager").first()
return User.objects.filter(principal_type=principal_type, is_active=True)
def get_previous_month_date_range():
today = timezone.now()
first_day_of_current_month = today.replace(day=1)
last_day_of_previous_month = first_day_of_current_month - timedelta(days=1)
first_day_of_previous_month = last_day_of_previous_month.replace(day=1)
return first_day_of_previous_month, last_day_of_previous_month
def generate_event_report(user_id, start_date, end_date):
# start_date, end_date = get_previous_month_date_range()
user = User.objects.get(id=user_id)
# events = Event.objects.filter(
# created_by=user, start_date__gte=start_date, start_date__lte=end_date
# ).annotate(
# favorites_count=Count("favorites"),
# interested_count=Count(
# "interaction_event",
# filter=Q(interaction_event__status=EventInteractionType.INTERESTED),
# ),
# going_count=Count(
# "interaction_event",
# filter=Q(interaction_event__status=EventInteractionType.GOING),
# ),
# reviews_count=Count(
# "reviews", filter=Q(reviews__active=True, reviews__deleted=False)
# ),
# views_count=Count("views"),
# )
# # print("events: ", events)
# report_data = []
# for event in events:
# views = EventView.objects.filter(event=event)
# locations = defaultdict(int)
# for view in views:
# locations[view.location] += 1
# shares = (
# EventShare.objects.filter(event=event)
# .values("principal")
# .annotate(share_count=Count("principal"))
# )
# shares_data = {
# User.objects.get(id=share["principal"]).get_full_name(): share[
# "share_count"
# ]
# for share in shares
# }
# report_data.append(
# {
# "event_name": event.title,
# "event_type": event.category.title,
# "event_date": str(event.start_date),
# "favorites_count": event.favorites_count,
# "interested_count": event.interested_count,
# "going_count": event.going_count,
# "reviews_count": event.reviews_count,
# "views_count": event.views_count,
# "locations": dict(locations),
# "social_media_shares": event.social_media_shares_count,
# "shares_data": shares_data,
# }
# )
# # print("report_data: ", report_data)
# return report_data
events = Event.objects.filter(
created_by=user, start_date__gte=start_date, start_date__lte=end_date
)
report_data = []
for event in events:
# Collecting individual counts for each event
favorites_count = event.favorites.count()
interested_count = event.interaction_event.filter(
status=EventInteractionType.INTERESTED
).count()
going_count = event.interaction_event.filter(
status=EventInteractionType.GOING
).count()
reviews_count = event.reviews.filter(active=True, deleted=False).count()
views_count = event.views.count()
# Collecting views and locations
views = EventView.objects.filter(event=event)
locations = defaultdict(int)
for view in views:
locations[view.location] += 1
# Collecting shares data
shares = (
EventShare.objects.filter(event=event)
.values("principal")
.annotate(share_count=Count("principal"))
)
shares_data = {
User.objects.get(id=share["principal"]).get_full_name(): share[
"share_count"
]
for share in shares
}
# Appending event data to report
report_data.append(
{
"event_name": event.title,
"event_type": event.category.title,
"event_date": str(event.start_date),
"favorites_count": favorites_count,
"interested_count": interested_count,
"going_count": going_count,
"reviews_count": reviews_count,
"views_count": views_count,
"locations": dict(locations),
"social_media_shares": event.social_media_shares_count,
"shares_data": shares_data,
}
)
return report_data
def generate_event_report_pdf_three(user, report_data, start_date):
# start_date, _ = get_previous_month_date_range()
filename = generate_filename(user.email, start_date)
buffer = BytesIO()
# pdf = canvas.Canvas(buffer, pagesize=letter)
pdf = SimpleDocTemplate(buffer, pagesize=letter)
width, height = letter
elements = []
styles = getSampleStyleSheet()
custom_style = ParagraphStyle(
name="Custom",
parent=styles["Normal"],
fontName="Helvetica",
fontSize=14,
leading=18,
spaceAfter=12,
)
def add_page_number(canvas, doc):
page_num_text = f"Page {doc.page}"
canvas.drawRightString(200 * mm, 10 * mm, page_num_text)
# Header Section
title = Paragraph("Good Times Ltd. Monthly Report", styles["Title"])
report_for_month = Paragraph(
f"Report for the month of - {start_date.strftime('%B %Y')}", styles["Title"]
)
organiser_name = Paragraph(
f"Name of the Organiser - {user.get_full_name()}", styles["Title"]
)
contact_email = Paragraph(f"Contact Email - {user.email}", styles["Title"])
elements.extend(
[
title,
Spacer(1, 12),
report_for_month,
Spacer(1, 12),
organiser_name,
Spacer(1, 12),
contact_email,
Spacer(1, 72), # Add space before the logo
]
)
# Insert company logo
logo_path = settings.LOGO_PATH
print("logo_path: ", logo_path)
logo_path = os.path.join(str(logo_path), "images/icon.png") # Path to the logo
logo = Image(logo_path)
logo.drawWidth = 200 # Adjust the width as needed
logo.drawHeight = 300 # Adjust the height as needed
logo.hAlign = "CENTER" # Center the logo
elements.append(logo)
elements.append(Spacer(1, 12)) # Add space after the logo
# Page break after header and logo
elements.append(PageBreak())
# Summary Section
summary_text = (
f"Number of Events added in {start_date.strftime('%B %Y')} - {len(report_data)}"
)
elements.append(Paragraph(summary_text, styles["Title"]))
elements.append(Spacer(1, 24))
data = [["Sr No.", "Name of the Event", "Event Type", "Date"]]
for idx, event in enumerate(report_data, start=1):
data.append(
[
idx,
event["event_name"],
event["event_type"],
event["event_date"],
]
)
table = Table(data, colWidths=[50, 250, 150, 100])
style = TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
("GRID", (0, 0), (-1, -1), 1, colors.black),
]
)
table.setStyle(style)
elements.append(table)
elements.append(PageBreak())
# Traffic Details Section
traffic_header = "Traffic Details for profile - Event Organisers London Ltd."
elements.append(Paragraph(traffic_header, styles["Title"]))
elements.append(Spacer(1, 12))
views_count = sum(event["views_count"] for event in report_data)
favorites_count = sum(event["favorites_count"] for event in report_data)
social_media_shares = sum(event["social_media_shares"] for event in report_data)
elements.append(Paragraph(f"Number of Event Views - {views_count}", custom_style))
elements.append(Spacer(1, 12))
elements.append(
Paragraph(f"Number of Event Favorites - {favorites_count}", custom_style)
)
elements.append(Spacer(1, 12))
elements.append(
Paragraph(f"Social Media Shares - {social_media_shares}", custom_style)
)
# elements.append(PageBreak())
elements.append(Spacer(1, 60))
# Top 5 Locations and Top 5 Viewed Events
all_locations = defaultdict(int)
for event in report_data:
for location, count in event["locations"].items():
all_locations[location] += count
# top 5 events by location
top_locations = sorted(all_locations.items(), key=lambda x: x[1], reverse=True)[:5]
# top 5 events overall by views
top_events_by_views = sorted(
report_data, key=lambda x: x["views_count"], reverse=True
)[:5]
data = [
["Top 5 Locations Viewed From", "Top 5 Viewed Events"],
]
for i in range(5):
location = top_locations[i][0] if i < len(top_locations) else ""
event = (
top_events_by_views[i]["event_name"] if i < len(top_events_by_views) else ""
)
data.append([location, event])
table = Table(data, colWidths=[200, 200])
style = TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
("GRID", (0, 0), (-1, -1), 1, colors.black),
]
)
table.setStyle(style)
elements.append(table)
elements.append(PageBreak())
# Detailed Review of Each Event
for event in report_data:
elements.append(
Paragraph(f"Event Name - {event['event_name']}", styles["Heading1"])
)
elements.append(Spacer(1, 12))
elements.append(Paragraph(f"Event Type - {event['event_type']}", custom_style))
elements.append(Paragraph(f"Event Date - {event['event_date']}", custom_style))
views = f"Number of Views - {event['views_count']}"
favorites = f"Favorites Count - {event['favorites_count']}"
interested = f"Interested in Going - {event['interested_count']}"
going = f"Going - {event['going_count']}"
reviews = f"Reviews - {event['reviews_count']}"
shares = f"Social Media Shares - {event['social_media_shares']}"
elements.append(Paragraph(views, custom_style))
elements.append(Paragraph(shares, custom_style))
elements.append(Paragraph(favorites, custom_style))
elements.append(Paragraph(interested, custom_style))
elements.append(Paragraph(going, custom_style))
elements.append(Paragraph(reviews, custom_style))
elements.append(Spacer(1, 48))
pie_data = [
event["views_count"],
event["social_media_shares"],
event["favorites_count"],
event["interested_count"],
event["going_count"],
]
pie_labels = ["Views", "Shares", "Favorites", "Interested", "Going"]
drawing = Drawing(300, 200)
pie = Pie()
pie.data = pie_data
pie.labels = pie_labels
pie.width = 150
pie.height = 150
pie.x = 75
pie.y = 25
drawing.add(pie)
elements.append(drawing)
elements.append(PageBreak())
pdf.build(elements, onFirstPage=add_page_number, onLaterPages=add_page_number)
buffer.seek(0)
pdf_data = buffer.read()
buffer.close()
return pdf_data, filename

View File

@@ -89,4 +89,16 @@ urlpatterns = [
views.VenueDeleteView.as_view(),
name="venue_delete",
),
path(
"venue/customer/",
views.CustomerVenueFilterView.as_view(),
name="venue_customer_filter",
),
path(
"generate-event-report/<int:user_id>/",
views.GenerateEventReportView.as_view(),
name="generate_event_report",
),
path("post-to-social-media/<int:id>/<str:platform>/", views.SocialMediaPostView.as_view(), name="social_media_post")
]

View File

@@ -1,4 +1,5 @@
import math
from accounts.models import IAmPrincipal
from manage_events.models import Event, Venue
from django.utils.timezone import now
import googlemaps
@@ -104,3 +105,23 @@ def get_location_info(latitude, longitude):
}
else:
return {}
def update_principal_location(principal, latitude, longitude):
location_data = get_location_info(latitude=latitude, longitude=longitude)
city = location_data.get("city")
state = location_data.get("state")
country = location_data.get("country")
country_code = location_data.get("country_code")
if hasattr(principal, "city"):
principal.city = city or state
if hasattr(principal, "state"):
principal.state = state
if hasattr(principal, "country"):
principal.country = country
if hasattr(principal, "address_line1"):
principal.address_line1 = country_code
principal.save()

View File

@@ -1,5 +1,8 @@
from django.shortcuts import get_object_or_404, redirect, render
from accounts import resource_action
from goodtimes.services import FacebookAPI, FacebookPoster, InstagramAPI, InstagramPoster, TwitterAPI, TwitterPoster
from goodtimes.utils import JsonResponseUtil
from manage_events.api.serializers import VenueSerializer, VenueShortSerializer
from manage_events.forms import (
EventMasterForm,
EventCategoryForm,
@@ -7,12 +10,14 @@ from manage_events.forms import (
VenueForm,
)
from django.core.paginator import Paginator
from .models import EventMaster, Event, EventCategory, EventPrincipalInteraction, Venue
from .models import EventImage, EventMaster, Event, EventCategory, EventPrincipalInteraction, Venue
from django.views import generic
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.contrib import messages
from goodtimes import constants
from django.contrib.auth import get_user_model
from datetime import date
# Create your views here.
@@ -221,7 +226,7 @@ class EventMasterDeleteView(LoginRequiredMixin, generic.View):
return redirect(self.success_url)
from django.core.files.storage import default_storage
class EventCreateOrUpdateView(LoginRequiredMixin, generic.View):
# Set the page_name and resource
page_name = resource_action.RESOURCE_MANAGE_EVENTS
@@ -257,6 +262,9 @@ class EventCreateOrUpdateView(LoginRequiredMixin, generic.View):
context.update(kwargs) # Include any additional context data passed to the view
return context
def get_event_images(self):
return [image.image.url for image in EventImage.objects.filter(event=self.object)]
def get(self, request, *args, **kwargs):
self.object = self.get_object()
@@ -265,23 +273,46 @@ class EventCreateOrUpdateView(LoginRequiredMixin, generic.View):
self.action = resource_action.ACTION_UPDATE
form = self.form_class(instance=self.object)
context = self.get_context_data(form=form)
context = self.get_context_data(form=form, event_images_urls=self.get_event_images())
return render(request, self.template_name, context=context)
def post(self, request, *args, **kwargs):
print(request.POST)
print(request.FILES)
self.object = self.get_object()
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
self.action = resource_action.ACTION_UPDATE
form = self.form_class(request.POST, request.FILES, instance=self.object)
principal_id = request.POST.get('principal')
form = self.form_class(request.POST, request.FILES, instance=self.object, principal_id=principal_id)
if not form.is_valid():
print(form.errors)
context = self.get_context_data(form=form)
print(f"form error is {form.errors}")
context = self.get_context_data(form=form, event_images_urls=self.get_event_images())
return render(request, self.template_name, context=context)
form.save()
instance = form.save()
instance.created_by = form.cleaned_data.get("principal")
instance.save()
# Delete old images from storage
old_images = EventImage.objects.filter(event=instance)
for old_image in old_images:
if default_storage.exists(old_image.image.name):
default_storage.delete(old_image.image.name)
# Delete old images from database
old_images.delete()
event_images = request.FILES.getlist("event_images")
event_image_objects = [EventImage(event=instance, image=image) for image in event_images]
EventImage.objects.bulk_create(event_image_objects)
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
@@ -332,24 +363,26 @@ class EventDetailView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_name"] = self.page_name
event_id = self.object.id # Get the current event's ID
event = self.object # Get the current event's ID
# Separate count for interested and going
interested_count = EventPrincipalInteraction.objects.filter(
event_id=event_id, status="interested"
event=event, status="interested"
).count()
going_count = EventPrincipalInteraction.objects.filter(
event_id=event_id, status="going"
event=event, status="going"
).count()
context["interested_count"] = interested_count
context["going_count"] = going_count
today = date.today()
context["publish"] = not event.draft and event.active and event.end_date >= today
# Reviews for the event
context["reviews"] = self.object.reviews.all()
context["reviews"] = event.reviews.all()
# Images of the event
context["images"] = self.object.event_images.all()
context["images"] = event.event_images.all()
return context
@@ -413,6 +446,7 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
print(f"self.object is {self.object}")
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
@@ -424,6 +458,7 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
print(f"form data is {request.POST} and self.object is {self.object}")
# If an object is found, change action to ACTION_UPDATE
if self.object is not None:
@@ -434,7 +469,10 @@ class VenueCreateOrUpdateView(LoginRequiredMixin, generic.View):
print(form.errors)
context = self.get_context_data(form=form)
return render(request, self.template_name, context=context)
form.save()
instance = form.save()
instance.created_by = form.cleaned_data.get("principal")
instance.save()
messages.success(self.request, self.get_success_message())
return redirect(self.success_url)
@@ -477,3 +515,124 @@ class VenueDeleteView(LoginRequiredMixin, generic.View):
messages.success(request, self.error_message)
return redirect(self.success_url)
class CustomerVenueFilterView(LoginRequiredMixin, generic.View):
model = Venue
serializer_class = VenueShortSerializer
def get(self, request, *args, **kwargs):
pk = request.GET.get("pk", None)
if not pk:
return JsonResponseUtil.error(message="Non transfer user list field is required")
obj = self.model.objects.filter(principal=pk, active=True)
if not obj.exists():
return JsonResponseUtil.error(message="No venue found for the given user.")
serializer = self.serializer_class(obj, many=True)
return JsonResponseUtil.success(message=constants.SUCCESS, data=serializer.data)
User = get_user_model()
from .report import generate_event_report, generate_event_report_pdf_three
from django.http import HttpResponse, JsonResponse
class GenerateEventReportView(generic.View):
def get(self, request, user_id):
print("INside GET GenerateEventReportView")
# Generate the event report
report_data = generate_event_report(user_id)
# Get the user
user = get_object_or_404(User, id=user_id)
# Generate the PDF
pdf_data, filename = generate_event_report_pdf_three(user, report_data)
# Create the HttpResponse object with the PDF data
response = HttpResponse(pdf_data, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
class SocialMediaPostView(generic.View):
def get(self, request, *args, **kwargs):
platform = kwargs.get("platform")
event_id = kwargs.get("id")
print(platform, event_id)
errors = []
success_messages = []
try:
event = Event.objects.get(id=event_id)
except Event.DoesNotExist:
errors.append("Event does not exist")
return JsonResponse({
'message': "Error in posting to social media",
'errors': errors,
'success_messages': success_messages
}, status=400)
if not event.active:
errors.append("Event is not active")
return JsonResponse({
'message': "Error in posting to social media",
'errors': errors,
'success_messages': success_messages
}, status=400)
caption = f"Venue: {event.venue.title} \n Event: {event.title}\n Description: {event.description} \n Date: {event.start_date} to {event.end_date}\n Time: {event.from_time} - {event.to_time} \n Address: {event.venue.address}"
if platform in ['instagram', 'facebook', 'twitter', 'all']:
if platform in ['twitter', 'all']:
image_url = event.image.path
twitter_api = TwitterAPI()
twitter_poster = TwitterPoster(twitter_api)
result = twitter_poster.post_image_with_caption(image_url, caption)
if result['success']:
success_messages.append("Posted to Twitter successfully")
else:
errors.append("Fail to post on Twitter")
image_url = request.build_absolute_uri(event.image.url)
if platform in ['facebook', 'all']:
try:
print("facebook is called")
facebook_api = FacebookAPI()
facebook_poster = FacebookPoster(facebook_api)
result = facebook_poster.post_photo(image_url, caption)
if result["success"]:
success_messages.append("Posted to Facebook successfully")
else:
errors.append("Fail to post on Facebook")
except Exception as e:
print(f"facebook error {e}")
errors.append("Fail to post on Facebook")
if platform in ['instagram', 'all']:
instagram_api = InstagramAPI()
instagram_poster = InstagramPoster(instagram_api)
result = instagram_poster.post_image_with_caption(image_url, caption)
if result["success"]:
success_messages.append("Posted to Instagram successfully")
else:
errors.append("Fail to post on Instagram")
if not errors:
return JsonResponse({'message': 'Post Successful', 'errors': errors, 'success_messages': success_messages})
if errors and success_messages:
return JsonResponse({
'message': 'Some posts succeeded while others failed',
'errors': errors,
'success_messages': success_messages
}, status=200)
return JsonResponse({
'message': 'Error in posting to social media',
'errors': errors,
'success_messages': success_messages
}, status=400)

View File

@@ -29,21 +29,7 @@ class Command(BaseCommand):
principal_interaction.event.title
) # Accessing event title correctly
message = f"{event_title} is going live tomorrow."
try:
player_id = (
principal_interaction.principal.player_id
) # Correctly access player_id from principal
except AttributeError:
continue
notification_title = "Event Reminder"
notification_payload = {
"headings": {"en": notification_title},
"contents": {"en": message},
"include_player_ids": [player_id],
}
response = client.send_notification(notification_payload)
in_app_notification = InAppNotification(
principal=principal_interaction.principal,
@@ -53,6 +39,28 @@ class Command(BaseCommand):
)
in_app_notification.save()
notification_settings = IAmPrincipalNotificationSettings.objects.filter(
principal=principal_interaction.principal,
notification_category=NotificationCategoryChoices.EVENT,
is_enabled=True,
).exists()
if notification_settings:
try:
player_id = (
principal_interaction.principal.player_id
) # Correctly access player_id from principal
if player_id:
notification_payload = {
"headings": {"en": notification_title},
"contents": {"en": message},
"include_player_ids": [player_id],
}
response = client.send_notification(notification_payload)
except AttributeError:
# Handle the case where player_id does not exist
continue
self.stdout.write(self.style.SUCCESS("Event reminders sent successfully"))
def eligible_event_interactions(self):
@@ -62,8 +70,8 @@ class Command(BaseCommand):
event__deleted=False,
event__active=True,
event__created_by__is_active=True,
event__created_by__deleted=False,
principal__is_active=True,
principal__deleted=False,
status__in=[EventInteractionType.GOING, EventInteractionType.INTERESTED],
# principal__notifications_principal__notification_category=NotificationCategoryChoices.EVENT,
# principal__notifications_principal__is_enabled=True,
).select_related("principal", "event")

View File

@@ -75,12 +75,20 @@ class Command(BaseCommand):
message = f"Your subscription is going to expire in {days_before} days."
for principal in eligible_principals:
notification_title = "Subscription Expiry Reminder"
in_app_notification = InAppNotification(
principal=principal.principal,
title=notification_title,
message=message,
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
)
if principal.is_enabled:
try:
player_id = principal.principal.player_id
except AttributeError:
continue
notification_title = "Subscription Expiry Reminder"
notification_payload = {
"headings": {"en": notification_title},
"contents": {"en": message},
@@ -88,13 +96,9 @@ class Command(BaseCommand):
}
response = client.send_notification(notification_payload)
in_app_notification = InAppNotification(
principal=principal.principal,
title=notification_title,
message=message,
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
)
# Save the notification in the database
in_app_notification.save()
self.stdout.write(
self.style.SUCCESS(
f"Notifications for {days_before} days sent successfully"
@@ -106,8 +110,7 @@ class Command(BaseCommand):
return IAmPrincipalNotificationSettings.objects.filter(
principal__principal_subscription__end_date=target_date,
principal__principal_subscription__status=SubscriptionStatus.ACTIVE,
principal__principal_subscription__cancelled=False,
principal__principal_subscription__deleted=False,
notification_category=NotificationCategoryChoices.SUBSCRIPTION,
is_enabled=True,
# is_enabled=True,
).select_related("principal")

View File

@@ -50,4 +50,5 @@ class ReferralRecordRewardSerializer(serializers.ModelSerializer):
"coins",
"unique_token",
"value",
"created_on",
]

View File

@@ -24,4 +24,9 @@ urlpatterns = [
views.RedeemRewardView.as_view(),
name="redeem_reward",
),
path(
"redeem-selected-rewards/",
views.RedeemSelectedRewardsView.as_view(),
name="redeem_selected_rewards",
),
]

View File

@@ -62,7 +62,9 @@ class RewardListView(APIView):
def get(self, request):
# Filter rewards based on specified conditions
current_principal = request.user # Adjust based on how user is linked to principal
current_principal = (
request.user
) # Adjust based on how user is linked to principal
# Filter rewards based on the authenticated referrer
rewards_query = ReferralRecordReward.objects.filter(
@@ -126,3 +128,68 @@ class RedeemRewardView(APIView):
message=constants.SUCCESS,
data="Token sold successfully.",
)
class RedeemSelectedRewardsView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def patch(self, request):
# Extract the number of tokens from the request data
num_tokens_to_sell = request.data.get("num_tokens", None)
if num_tokens_to_sell is None:
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message=constants.FAILURE,
errors="Number of tokens to sell is required.",
)
try:
num_tokens_to_sell = int(num_tokens_to_sell)
except ValueError:
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message=constants.FAILURE,
errors="Invalid number of tokens.",
)
# Retrieve the rewards for the authenticated user
rewards = ReferralRecordReward.objects.filter(
referral_record__referrer_principal=request.user, sell=False
).order_by(
"id"
) # FIFO method
if rewards.count() < num_tokens_to_sell:
return ApiResponse.error(
status=status.HTTP_404_NOT_FOUND,
message=constants.FAILURE,
errors="Not enough unsold rewards available.",
)
# Select the required number of rewards
rewards_to_sell = rewards[:num_tokens_to_sell]
total_value = sum(reward.value for reward in rewards_to_sell)
total_coins = sum(reward.coins for reward in rewards_to_sell)
tokens = ",".join(str(reward.unique_token) for reward in rewards_to_sell)
with transaction.atomic():
# Create a new withdrawal request
withdrawal_request = WithdrawalRequest.objects.create(
principal=request.user,
coins=total_coins,
amount=total_value,
token=tokens,
)
# Update each reward to mark it as sold
for reward in rewards_to_sell:
reward.sell = True
reward.save()
return ApiResponse.success(
status=status.HTTP_200_OK,
message=constants.SUCCESS,
data="Selected tokens sold successfully.",
)

View File

@@ -1,31 +1,19 @@
from django.contrib import admin
from .models import (
Plan,
PrincipalSubscription,
Subscription,
WebhookEvent,
) # Update this with the correct import path for your models
# Plan ModelAdmin
class PlanAdmin(admin.ModelAdmin):
list_display = ("id", "title", "days") # Include 'id' field here
search_fields = ("title",) # Add search functionality by title
# Register Plan with the admin site
admin.site.register(Plan, PlanAdmin)
# Subscription ModelAdmin
class SubscriptionAdmin(admin.ModelAdmin):
list_display = ("id", "title", "plan", "amount") # Include 'id' field here
list_select_related = ("plan",) # Optimizes queries for the plan field
list_display = ("id", "title", "interval", "amount") # Include 'id' field here
list_select_related = ("interval",) # Optimizes queries for the interval field
search_fields = (
"title",
"plan__title",
) # Add search functionality by title and plan's title
raw_id_fields = ("plan",) # Use a raw ID widget for the plan ForeignKey field
"interval",
) # Add search functionality by title and interval's title
# Register Subscription with the admin site
@@ -47,7 +35,7 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
"is_paid",
"auto_renew",
"status",
"cancelled",
# "cancelled",
) # Enable filtering by these fields
search_fields = (
"subscription__title",
@@ -63,7 +51,6 @@ class PrincipalSubscriptionAdmin(admin.ModelAdmin):
admin.site.register(PrincipalSubscription, PrincipalSubscriptionAdmin)
@admin.register(WebhookEvent)
class WebhookEventAdmin(admin.ModelAdmin):
list_display = ("event_id", "received_at", "event_type", "processed_at", "status")

View File

@@ -1,8 +1,5 @@
from django.urls import path
from . import views
from rest_framework_simplejwt.views import (
TokenRefreshView,
)
urlpatterns = [

View File

@@ -1,5 +1,6 @@
from datetime import timedelta
import datetime
import json
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils import timezone
@@ -7,9 +8,7 @@ from rest_framework import status
from rest_framework.views import APIView
from django.conf import settings
import stripe
from accounts.models import IAmPrincipal
import json
from goodtimes import constants, services
from goodtimes import constants
from manage_subscriptions.models import (
Subscription,
PrincipalSubscription,
@@ -35,7 +34,11 @@ from .serializers import PrincipalSubscriptionSerializer
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from rest_framework.response import Response
from goodtimes.webhook import PaymentProcessingService
from goodtimes.webhook.payment_processing_service import PaymentProcessingService
import logging
logger = logging.getLogger(__name__)
class CreatePrincipalSubscriptionApi(APIView):
@@ -118,48 +121,6 @@ class CreatePrincipalSubscriptionApi(APIView):
return ApiResponse.error(**fail_response)
# class CreatePrincipalSubscriptionApi(APIView):
# authentication_classes = [JWTAuthentication]
# permission_classes = [IsAuthenticated]
# def post(self, request):
# serializer = PrincipalSubscriptionSerializer(data=request.data)
# if serializer.is_valid():
# subscription_id = serializer.validated_data.get("subscription").id
# try:
# subscription = Subscription.objects.get(id=subscription_id)
# except Subscription.DoesNotExist:
# return ApiResponse.error(
# status=status.HTTP_404_NOT_FOUND, message="Subscription not found."
# )
# start_date = timezone.localtime().date()
# end_date = start_date + timedelta(days=subscription.plan.days)
# grace_period_end_date = end_date + timedelta(days=15)
# # You can directly pass the additional fields as save method arguments
# instance = serializer.save(
# start_date=start_date,
# end_date=end_date,
# grace_period_end_date=grace_period_end_date,
# created_by=request.user, # Assuming your model has this field and you want to track who created the subscription
# )
# success_response = {
# "status": status.HTTP_201_CREATED, # Use 201 for successful resource creation
# "message": "Success",
# "data": serializer.data,
# }
# return ApiResponse.success(**success_response)
# else:
# fail_response = {
# "status": status.HTTP_400_BAD_REQUEST,
# "message": "Validation Failed",
# "errors": serializer.errors,
# }
# return ApiResponse.error(**fail_response)
@method_decorator(csrf_exempt, name="dispatch")
@@ -172,83 +133,115 @@ class StripeWebhookTest(APIView):
stripe.api_key = settings.STRIPE_SECRET_KEY
payload = request.body
sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
endpoint_secret = "whsec_ccf1f87295603cdd1733995ee2d3c0d6f74c7ceaf28916ea45114a54b7ce1d0f" # Make sure to retrieve this from your settings
# This is your Stripe CLI webhook secret for testing your endpoint locally.
endpoint_secret = settings.ENDPOINT_SECRET
event = None
webhook_event = None
try:
# Construct Stripe event
event = stripe.Event.construct_from(json.loads(payload), stripe.api_key)
event_id = event["id"]
event_type = event["type"]
principal_id = event["data"]["object"]["metadata"]["principal"]
stripe_subscription_id = event["data"]["object"].get("subscription")
# Retrieve subscription details if available
stripe_subscription = (
stripe.Subscription.retrieve(stripe_subscription_id)
if stripe_subscription_id
else None
)
current_period_start = stripe_subscription["current_period_start"] if stripe_subscription else None
current_period_end = stripe_subscription["current_period_end"] if stripe_subscription else None
# Log received event details
logger.info(f"Received event {event_type} with ID {event_id}")
# Get or create WebhookEvent in DB
webhook_event, created = WebhookEvent.objects.get_or_create(
event_id=event_id,
defaults={
"event_type": event_type,
"event_payload": json.loads(payload),
"event_payload": event,
},
)
if not created and webhook_event.status == "processed":
logger.info(f"Event {event_id} already processed.")
return ApiResponse.success(
status=status.HTTP_208_ALREADY_REPORTED,
message="Event already processed",
message="Event already processed.",
)
# Check if there is an active principal subscription
if self._has_active_principal_subscription(principal_id):
return ApiResponse.success(
status=status.HTTP_208_ALREADY_REPORTED,
message="Active principal subscription already exists",
# Process the event
payment_service = PaymentProcessingService(
webhook_data=event,
stripe_subscription=stripe_subscription_id,
current_period_start=current_period_start,
current_period_end=current_period_end,
)
# payment_service = services.PaymentProcessingService(webhook_data=event)
payment_service = PaymentProcessingService(webhook_data=event)
payment_service.process_event()
webhook_event = WebhookEvent.objects.get(event_id=event_id)
# Mark event as successfully processed
webhook_event.status = "processed"
webhook_event.processed_at = timezone.now() # Make sure to import timezone
webhook_event.processed_at = timezone.now()
webhook_event.save()
logger.info(f"Event {event_id} processed successfully.")
return ApiResponse.success(
status=status.HTTP_200_OK, message="Event processed successfully"
status=status.HTTP_200_OK, message="Event processed successfully."
)
except stripe.error.SignatureVerificationError as e:
logger.error(f"Invalid Stripe signature for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid signature.",
errors=str(e),
)
except ValueError as e:
# Invalid payload
logger.error(f"Invalid payload for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid payload",
errors=str(e),
)
except stripe.error.SignatureVerificationError as e:
# Invalid signature
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid signature",
errors=str(e),
)
except Transaction.DoesNotExist:
# Handle case where the transaction does not exist
return ApiResponse.error(
status=status.HTTP_404_NOT_FOUND, message="Transaction not found"
)
except Exception as e:
webhook_event.status = "failed"
webhook_event.error_message = str(e)
webhook_event.save()
return ApiResponse.error(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="Error processing event",
message="Invalid payload.",
errors=str(e),
)
def _has_active_principal_subscription(self, principal_id):
return PrincipalSubscription.objects.filter(
principal__id=principal_id,
active=True,
deleted=False,
is_paid=True,
end_date__gte=timezone.now().date(),
).exists()
except stripe.error.InvalidRequestError as e:
logger.error(f"Invalid request for event: {str(e)}")
return ApiResponse.error(
status=status.HTTP_400_BAD_REQUEST,
message="Invalid request to Stripe.",
errors=str(e),
)
except stripe.error.StripeError as e:
logger.error(f"General Stripe error: {str(e)}")
return ApiResponse.error(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="Stripe error occurred.",
errors=str(e),
)
except Exception as e:
logger.error(f"Unexpected error processing event {event_id}: {str(e)}")
if "webhook_event" in locals():
webhook_event.status = "failed"
webhook_event.error_message = str(e)
webhook_event.save()
return ApiResponse.error(
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
message="Unexpected error processing event.",
errors=str(e),
)
finally:
print(f"finally is runn")
webhook_event.status = "processed"
webhook_event.processed_at = timezone.now()
webhook_event.save()
class LastActiveSubscriptionView(APIView):
@@ -281,3 +274,47 @@ class LastActiveSubscriptionView(APIView):
message="No Active Subscription Found",
errors="No Active Subscription Found",
)
class CancelSubscription(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
data = json.loads(request.body)
subscription_id = data.get("subscription_id")
try:
subscription = PrincipalSubscription.objects.get(
id=subscription_id, principal=request.user
)
except PrincipalSubscription.DoesNotExist:
return ApiResponse(
message=constants.FAILURE,
errors="Subscription not found.",
status=status.HTTP_404_NOT_FOUND,
)
with transaction.atomic():
if subscription.stripe_subscription_id:
# Cancel Stripe subscription
try:
stripe.Subscription.modify(subscription.stripe_subscription_id, cancel_at_period_end=True)
except stripe.error.InvalidRequestError as e:
return ApiResponse(
message=constants.FAILURE,
errors=f"Stripe error: {str(e)}",
status=status.HTTP_400_BAD_REQUEST,
)
# Updating subscription status in the local database
subscription.status = SubscriptionStatus.INACTIVE
# subscription.cancelled = True
subscription.cancelled_date_time = timezone.now()
subscription.save()
return ApiResponse(
message=constants.SUCCESS,
data="Subscription cancelled successfully.",
status=status.HTTP_200_OK,
)

View File

@@ -1,43 +1,64 @@
from django import forms
from manage_subscriptions.models import PrincipalSubscription, Subscription, Plan
class PlanForm(forms.ModelForm):
class Meta:
model = Plan
fields = ["title", "days"] # Include all fields you want from the model
# You can add custom validation for Plan fields here if needed
# Example:
# def clean_title(self):
# title = self.cleaned_data.get('title')
# # Add your validation logic here
# return title
from accounts.models import IAmPrincipalType
from manage_subscriptions.models import (
PrincipalSubscription,
Subscription,
)
class SubscriptionForm(forms.ModelForm):
class Meta:
model = Subscription
fields = [
"title",
"plan",
"short_description",
"long_description",
"interval",
"interval_count",
"high_amount",
"amount",
"short_description",
# "long_description",
# "image",
"principal_types",
"referral_percentage",
] # Include all fields you want from the model
"active",
"is_free",
]
def __init__(self, *args, **kwargs):
super(SubscriptionForm, self).__init__(*args, **kwargs)
event_user = IAmPrincipalType.objects.get(name="event_user")
event_manager = IAmPrincipalType.objects.get(name="event_manager")
self.fields["principal_types"].queryset = IAmPrincipalType.objects.filter(
id__in=[event_user.id, event_manager.id]
)
class SubscriptionUpdateForm(forms.ModelForm):
class Meta:
model = Subscription
fields = [
"title",
"short_description",
"long_description",
"referral_percentage",
"active",
"is_free",
]
class PrincipalSubscriptionForm(forms.ModelForm):
class Meta:
model = PrincipalSubscription
fields = "__all__" # Includes all fields from the model
fields = [
"subscription",
"principal",
"status",
"start_date",
"end_date",
"grace_period_end_date",
"comments",
"coupon_code"
] # Includes all fields from the model
widgets = {
"start_date": forms.DateInput(attrs={"type": "date"}),
"end_date": forms.DateInput(attrs={"type": "date"}),
"grace_period_end_date": forms.DateInput(attrs={"type": "date"}),
"cancelled_date_time": forms.DateTimeInput(attrs={"type": "datetime"}),
}

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-06-25 07:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manage_subscriptions', '0007_alter_subscription_referral_percentage'),
]
operations = [
migrations.AddField(
model_name='subscription',
name='is_free',
field=models.BooleanField(default=False, help_text='Indicates whether this subscription is free and only accessible by administrators, not visible to regular users.'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-07-22 12:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("manage_subscriptions", "0008_subscription_is_free"),
]
operations = [
migrations.AddField(
model_name="principalsubscription",
name="coupon_code",
field=models.CharField(blank=True, max_length=255, null=True),
),
]

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