Compare commits
483 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66538f7335 | |||
| 1bfb710ee6 | |||
|
|
2c674572c9 | ||
| b1a4ba6f0e | |||
|
|
e9cb303b09 | ||
|
|
bdf9f53648 | ||
|
|
8d6f2ed4ae |
||
|
|
062e81408d | ||
|
|
3a489f5be0 | ||
|
|
961bf2e46f |
||
|
|
c36554b4d1 | ||
|
|
270c610111 | ||
|
|
3ebf932422 | ||
|
|
814163e58e | ||
|
|
0d767e74a9 | ||
|
|
5a52ae1a1d | ||
|
|
6612a7debd |
||
|
|
8929a26714 | ||
|
|
0e9f8ce906 | ||
|
|
b7cc7b08ce | ||
|
|
26970ece92 | ||
|
|
42d6f38f0c | ||
|
|
27ee5ca5ac | ||
|
|
733fb9fc43 |
||
|
|
5770c231ee | ||
|
|
5985ded36f | ||
|
|
2b7d4bbef5 | ||
|
|
10dab1350a | ||
|
|
806726614e | ||
|
|
5d9b2ee41a | ||
|
|
2fbee916cc | ||
|
|
1dafa592a2 | ||
|
|
e557a777c3 |
||
|
|
f4579595c3 | ||
|
|
12eabc5f6c | ||
|
|
e3ec67d32c | ||
|
|
8758cd1cd8 | ||
|
|
1100d61b5d | ||
|
|
b3e3af1c1a | ||
|
|
ec70cd1c83 | ||
|
|
74ec39498e | ||
|
|
e4bfdec0b6 | ||
|
|
193b87bbb5 | ||
|
|
ca18004819 | ||
|
|
e1c91d886b | ||
|
|
27a92780a6 | ||
|
|
8a2734fa0e | ||
|
|
97693f0bb3 | ||
|
|
495ac0c6d6 | ||
|
|
530bbcd5f6 | ||
|
|
1cdc9ea657 | ||
|
|
a4065c7e24 | ||
|
|
e3bd963600 | ||
|
|
e47f4f05b4 | ||
|
|
3bad37c605 | ||
|
|
930333357e | ||
|
|
dd9e7dde35 | ||
|
|
f2f95c8559 | ||
|
|
a93c900109 | ||
|
|
c2dbbf0424 | ||
|
|
f85ef714ab | ||
|
|
24d719e4f1 | ||
|
|
5452c1c478 | ||
|
|
8743853a7b | ||
|
|
737d890a7c | ||
|
|
db20e3cbe7 | ||
|
|
32767fed68 |
||
|
|
e1ce017ec8 | ||
|
|
b047ccdef1 | ||
|
|
56460ac8f0 | ||
|
|
d93861ca32 | ||
|
|
a02c3c6973 | ||
|
|
768f3532f7 | ||
|
|
21084cdc9f | ||
|
|
12f139976d | ||
|
|
da21699212 | ||
|
|
3075cffd77 | ||
|
|
232022aaaf | ||
|
|
b7929a16e2 | ||
|
|
6c03e3f712 | ||
|
|
72c16713a7 | ||
|
|
52d048a555 | ||
|
|
3148dbccf8 | ||
|
|
84056a5b36 | ||
|
|
fcc113e9d9 | ||
|
|
1041284866 | ||
|
|
737681136f | ||
|
|
efae2b1d9a | ||
|
|
31e8467f20 |
||
|
|
4feeec23d4 | ||
|
|
ca578ecf56 | ||
|
|
b021a8ed6e | ||
|
|
99b11f013f | ||
|
|
1f990b1ab7 | ||
|
|
41cba9daa3 | ||
|
|
4575ff60ec | ||
|
|
508360472a | ||
|
|
48ba6a6166 | ||
|
|
10e8f0a820 | ||
|
|
5df2080f92 | ||
|
|
9ff20491bd | ||
|
|
ce2ac4524c | ||
|
|
5d5408eb8c |
||
|
|
c9ac959ff6 | ||
|
|
d54bf84b1e | ||
|
|
50e5fea339 | ||
|
|
67231275c7 | ||
|
|
1228d3dbc6 | ||
|
|
69a1d2df71 | ||
|
|
b63a572231 | ||
|
|
51c5fa98dd | ||
|
|
ed7ffb355f | ||
|
|
1988020006 | ||
|
|
b348c93fee | ||
|
|
481f13d20c | ||
|
|
114dbd8242 | ||
|
|
a90bec98ec | ||
|
|
d99271f71d | ||
|
|
ff993e32db | ||
|
|
2a694295ad | ||
|
|
fcfc56e132 | ||
|
|
d6e4a86724 | ||
|
|
dd82bdc9da | ||
|
|
7f3b916c58 | ||
|
|
d23624c525 | ||
|
|
9ec05e7df4 | ||
|
|
bc3eaaa7eb | ||
|
|
bce47032ab | ||
|
|
e94ecfe52c | ||
|
|
332e7d6624 | ||
|
|
c7edcdc8b1 | ||
|
|
730492089b | ||
|
|
429dd10b75 | ||
|
|
a7fa52490c | ||
|
|
76efc35324 | ||
|
|
d7be223fcb | ||
|
|
304feb4f7b | ||
|
|
e376f38baa | ||
|
|
84dae63968 | ||
|
|
4914280868 | ||
|
|
ce2d34350f | ||
|
|
743ce4ed70 |
||
|
|
a7afbec5b4 | ||
|
|
ff6df8cd58 | ||
|
|
7d4cf5c3c2 | ||
|
|
8e7789462e | ||
|
|
d601b987d2 | ||
|
|
c6ec2c062c | ||
|
|
c6612153e8 |
||
|
|
fc8f9993af | ||
|
|
b03cb073c2 | ||
|
|
fdffe2389b | ||
|
|
d8a532e7b0 | ||
|
|
642153345c | ||
|
|
8a30100488 | ||
|
|
5f19a85a28 | ||
|
|
362a93a97f |
||
|
|
4d4472f0d3 | ||
|
|
c891694dc0 | ||
|
|
23eb054422 | ||
|
|
144a780105 | ||
|
|
3796dabe11 | ||
|
|
3de172adf2 |
||
|
|
d7f171d710 | ||
|
|
4f745b607d | ||
|
|
9a5b9c7af5 | ||
|
|
4a16251a69 | ||
|
|
fbfa6c8d21 |
||
|
|
c458b56f71 | ||
|
|
a45eef0409 | ||
|
|
83a6e53fae | ||
|
|
feceb14aec | ||
|
|
f1cc3c5892 |
||
|
|
e1263ce9b3 | ||
|
|
4baa3a7095 | ||
|
|
4a1434c514 | ||
|
|
f9584b4c82 | ||
|
|
78e44332b5 | ||
|
|
af78631ec8 | ||
|
|
29172a9df7 | ||
|
|
4425aa7c88 | ||
|
|
b7ff519624 | ||
|
|
07837c8752 | ||
|
|
1e5cf08273 | ||
|
|
7e790e7027 | ||
|
|
929e7ead1c | ||
|
|
ab074d9bc2 | ||
|
|
05d48ec4ff |
||
|
|
9c5363ef55 | ||
|
|
fb4591ef6a | ||
|
|
438a6ab4e4 |
||
|
|
432b54a86d | ||
|
|
e66e8202db | ||
|
|
13876c24c1 | ||
|
|
f56933021f | ||
|
|
2ff7eaea65 | ||
|
|
893c8ed08b | ||
|
|
3b07687d3e | ||
|
|
a55587dbf3 | ||
|
|
91021d7612 | ||
|
|
cda6c60e02 | ||
|
|
873f44c6e5 |
||
|
|
b69abf3edd | ||
|
|
8bf6440110 | ||
|
|
768b8ca820 | ||
|
|
770b5e080b | ||
|
|
9381327a12 | ||
|
|
e5fc87a971 | ||
|
|
7b8301f143 | ||
|
|
051408e134 | ||
|
|
29c4cc4454 | ||
|
|
895961868b | ||
|
|
ec2cef7ec5 |
||
|
|
e27317aeab | ||
|
|
a8bf22a5b9 | ||
|
|
5366745188 | ||
|
|
8bce9c82a9 |
||
|
|
e16aab6951 |
||
|
|
48936ace51 | ||
|
|
0a3f51361f | ||
|
|
0ddbdb045f | ||
|
|
93d489334f | ||
|
|
6546814d62 | ||
|
|
07c7a64c5a | ||
|
|
df10e84418 | ||
|
|
a5cdfab306 | ||
|
|
59b020c0d4 | ||
|
|
15db1c88d7 | ||
|
|
be2831818d | ||
|
|
a51064448a |
||
|
|
62d23b8a5c | ||
|
|
a062c0091f |
||
|
|
1291b49ec3 | ||
|
|
4a19bd1971 | ||
|
|
d6a404d49d | ||
|
|
13e33cbb7a | ||
|
|
15e435d220 | ||
|
|
098a9065f1 | ||
|
|
c97e2c55f3 | ||
|
|
a5929b3e86 | ||
|
|
aec92d4c80 |
||
|
|
b3cdbbcae2 | ||
|
|
5c6528aa03 |
||
|
|
28cac31a93 | ||
|
|
b580ac24f6 | ||
|
|
3ccefbdb74 | ||
|
|
d9bcdf22b7 | ||
|
|
2ada3ccc6f | ||
|
|
0caf3da3bd |
||
|
|
33bd2e1760 | ||
|
|
34ed51a643 | ||
|
|
0f26917f35 | ||
|
|
678167978c | ||
|
|
ebcbb26276 | ||
|
|
e18b8a527a | ||
|
|
e60b93d126 | ||
|
|
8d42ca3200 | ||
|
|
4c06a9e730 | ||
|
|
1f2743a65d | ||
|
|
0db4a113e6 | ||
|
|
5cd62abc70 | ||
|
|
1e214f7b21 | ||
|
|
6b663d82a1 | ||
|
|
baa2817f57 | ||
|
|
9035f98060 | ||
|
|
faa0604fae | ||
|
|
8d7b01d7e2 | ||
|
|
e9ac699be8 | ||
|
|
ec0216790f | ||
|
|
9bf8992ff4 | ||
|
|
dc28186fe9 | ||
|
|
8e742852a5 | ||
|
|
3d8237a34a | ||
|
|
c118e86230 | ||
|
|
d98a683b2a | ||
|
|
1c5ff1f9dd | ||
|
|
9904a71d38 | ||
|
|
c9d01ba95f | ||
|
|
6d2b011925 | ||
|
|
cb911e05c5 | ||
|
|
1fa260aaf5 |
||
|
|
44900f6a48 | ||
|
|
00cb1de75d | ||
|
|
7f57ace92d | ||
|
|
f48005166e | ||
|
|
900f014d92 | ||
|
|
5851277d9a | ||
|
|
43b3a63958 | ||
|
|
081921e846 | ||
|
|
6593983f04 | ||
|
|
ba286eb053 |
||
|
|
ef8e380ab7 | ||
|
|
ae0d4c0841 | ||
|
|
c13af95017 | ||
|
|
8993a7bde1 | ||
|
|
52e53d479e | ||
|
|
68a65b7bc7 | ||
|
|
5eff54cffe | ||
|
|
f1e021e1e9 | ||
|
|
88e6d9d216 | ||
|
|
2cd73b313a | ||
|
|
a3db7f2e1a | ||
|
|
88f0d73336 | ||
|
|
a56b2d02c8 |
||
|
|
60260ccb08 | ||
|
|
4a5c5f7942 | ||
|
|
1ec7cb8761 | ||
|
|
6c2eabbe6a | ||
|
|
a5c42b9c44 | ||
|
|
e816f65114 | ||
|
|
1522701a5e |
||
|
|
bf45bf1b4d | ||
|
|
549e882ebe | ||
|
|
78bda89b16 |
||
|
|
fb9000de90 | ||
|
|
70cac38f81 | ||
|
|
79e83b4480 | ||
|
|
dcbb0c2d64 | ||
|
|
b872777bda | ||
|
|
94f520be35 | ||
|
|
5748eecedb | ||
|
|
fae9fce5c6 | ||
|
|
6db38d7e29 | ||
|
|
86f0526773 | ||
|
|
23630d4473 | ||
|
|
7494116468 | ||
|
|
57eda62586 | ||
|
|
cf00ff6bd8 | ||
|
|
ae911c8c21 |
||
|
|
2d1805f11d | ||
|
|
a4ca17e2ed | ||
|
|
cba53e0fe3 |
||
|
|
4e971c23a0 |
||
|
|
8fb0d9a48a | ||
|
|
3446dcc469 | ||
|
|
d40977c5da |
||
|
|
b09604d30f | ||
|
|
f39f95e1f7 | ||
|
|
20f1df8a70 | ||
|
|
39f7898259 | ||
|
|
55889499df | ||
|
|
8044e0c2a0 | ||
|
|
30deae5a20 | ||
|
|
a14407182f | ||
|
|
73e3dce8d4 | ||
|
|
f3ffbd96e5 | ||
|
|
b351cb9aa0 | ||
|
|
55cbe3244a | ||
|
|
0fdb88b8aa | ||
|
|
3d2ce27954 | ||
|
|
eeed9b2e72 | ||
|
|
2ff8c25034 | ||
|
|
144d885fbd | ||
|
|
7a72cc02ab | ||
|
|
f8dc2c6bbe | ||
|
|
25ef657c62 |
||
|
|
4d2d337651 |
||
|
|
ccf55acbaf |
||
|
|
6ff03d8f48 |
||
|
|
d6db984156 |
||
|
|
35fb872dc1 |
||
|
|
86188b1fb0 |
||
|
|
6bb65d8e8d |
||
|
|
34df86fb90 | ||
|
|
89ed869780 | ||
|
|
0c4c945ec3 | ||
|
|
14548b2f01 | ||
|
|
91a65e88ec | ||
|
|
f66d768ecb | ||
|
|
3b3b73a2ce | ||
|
|
3bf064a017 | ||
|
|
91f1c1ef06 |
||
|
|
3b6c2b9d4e | ||
|
|
a5bd8347e8 | ||
|
|
736253feda | ||
|
|
3debf34118 | ||
|
|
a25bcc807f | ||
|
|
fae1c7fbeb | ||
|
|
80c3ac5346 | ||
|
|
a3a8227007 | ||
|
|
791f48513a | ||
|
|
8a659c153e | ||
|
|
1a7412f8ff | ||
|
|
bd875ffe7d | ||
|
|
c24d81042b | ||
|
|
272e14f712 | ||
|
|
4a30438704 | ||
|
|
c438c0d8cb | ||
|
|
f9bd849333 | ||
|
|
d8ce0f95c5 | ||
|
|
3e08760e04 | ||
|
|
a2a35a9475 | ||
|
|
24d904288f | ||
|
|
16b6ecb38c | ||
|
|
83dbae74e6 | ||
|
|
4be2796270 | ||
|
|
62f30bf03c | ||
|
|
edac806c11 | ||
|
|
3d8f81339b | ||
|
|
618d0004f2 | ||
|
|
af1690b846 | ||
|
|
63eb7fc0e2 | ||
|
|
abe8c9efa5 | ||
|
|
bea3477d84 | ||
|
|
23e7edf7c2 | ||
|
|
82ad9ac337 | ||
|
|
2ffaee2d5b | ||
|
|
248283b369 | ||
|
|
bafb4e7b68 | ||
|
|
db8dd9af54 | ||
|
|
7e5cab2cc4 | ||
|
|
2a8f02a197 | ||
|
|
cac00d4b9e | ||
|
|
dc8ea8d253 | ||
|
|
c47b5cdc72 | ||
|
|
175180e193 | ||
|
|
2a59a3336b | ||
|
|
d2ae94327a | ||
|
|
06a5cba50e | ||
|
|
4be105a0a9 | ||
|
|
a17a5f66bc | ||
|
|
b6d1e8df6b | ||
|
|
6212c9df50 | ||
|
|
d14a643171 | ||
|
|
8759e2a4b5 | ||
|
|
73169e825d | ||
|
|
38168e8f8f | ||
|
|
bd7db30633 | ||
|
|
1374eaf1a2 | ||
|
|
a6d28bff86 | ||
|
|
4dd407da67 | ||
|
|
780fa6cb60 | ||
|
|
85d19c004b | ||
|
|
692f82cba4 | ||
|
|
ef9dc446db | ||
|
|
8d2c120b43 | ||
|
|
de5035d12e | ||
|
|
21bb336166 | ||
|
|
8351b1bf8b | ||
|
|
bf91bf3822 | ||
|
|
7f4993c3f0 | ||
|
|
303eb4112d | ||
|
|
14c7d6ac0e | ||
|
|
8df72620d6 | ||
|
|
1e08ae5426 | ||
|
|
fb2056bf95 | ||
|
|
600b549704 | ||
|
|
75e90dbacd | ||
|
|
be8181ec42 | ||
|
|
70b6bbdf2f | ||
|
|
76b3785adc | ||
|
|
c664b44f2c | ||
|
|
255c8a1d80 | ||
|
|
51039a5cb3 | ||
|
|
60cfc24319 | ||
|
|
68a34762b9 | ||
|
|
1839a1c27c | ||
|
|
2a71be354b | ||
|
|
c939106a35 | ||
|
|
bff37d6246 | ||
|
|
3d50868c6a | ||
|
|
de275c23ac | ||
|
|
619f37829e | ||
|
|
52791f5e48 | ||
|
|
3f9c1a68d1 | ||
|
|
74a1f82c30 | ||
|
|
a3ce43fd53 | ||
|
|
83363f4701 | ||
|
|
7d69d8d5d4 | ||
|
|
167e3589d4 | ||
|
|
a1d7b07e0c | ||
|
|
9a9a764023 | ||
|
|
f3ca9110e1 | ||
|
|
6420a9869b | ||
|
|
f71c8e553d | ||
|
|
1a25bbf11e | ||
|
|
a4b63e220a | ||
|
|
707f1a8768 | ||
|
|
e6f92d9ae4 | ||
|
|
cc2e08aff5 | ||
|
|
ba8eaa2937 | ||
|
|
bd3c59eb3e | ||
|
|
6536991209 |
83 changed files with 3974 additions and 1460 deletions
61
Changelog
61
Changelog
|
|
@ -1,5 +1,64 @@
|
|||
Next:
|
||||
* bugfix: Use correct version of django-multisite (MR #676)
|
||||
2.4.1: 2018-10-18
|
||||
* bugfix: Update pycryptodome module from 3.4 to 3.6.6 (PR #674)
|
||||
2.4: 2018-10-18
|
||||
* #5681: [hosting,dcl] Allow admin to lower minimum RAM to 512 MB (PR #672)
|
||||
2.3.1: 2018-10-17
|
||||
* bugfix: [hosting, dcl] Show VAT percent rounded to 2 decimal places in the order confirmation page (PR #673)
|
||||
2.3: 2018-10-08
|
||||
* #5690: Generic payment page - allow admin to add a onetime/monthly product and the frontend for user to pay for this product (PR #666)
|
||||
2.2.2: 2018-09-28
|
||||
* #5721: Set calculator OS list in alphabetical order and set `Devuan Ascii` as the default (PR #668)
|
||||
* bugfix: Fix some typos and correct DE translations (PR #667)
|
||||
2.2.1: 2018-09-25
|
||||
* feature: Change DCLNavbarPlugin to show login option only if set (PR #665)
|
||||
* bugfix: Log opennebula errors and send proper message when vm terminate is not completed in the stipulated time (PR #648)
|
||||
2.2: 2018-09-06
|
||||
* bugfix: Include price in the Stripe plan name to make it distinct and to correct pricing since version 1.9
|
||||
2.1.2: 2018-08-30
|
||||
* bugfix: [blog, comic] Set blog rss feed for all blog templates
|
||||
2.1.1: 2018-08-24
|
||||
* #5487: [hosting] Add explicit warning message for teminating VM (PR #656)
|
||||
* bugfix: [dg] Send email to admin on dg subscription and increase cc_brand field to 128 characters (PR #652)
|
||||
* #5458: [admin] Make hostingorder more readable (PR #657)
|
||||
* bugfix: [CMS templates] Set description meta field of ungleich template (was missing before) and set ungleich glarus ag uniformly as author of various CMS pages (PR #653)
|
||||
* #5473: Ping a VM before saving ssh key of the user (PR #655)
|
||||
2.1: 2018-08-21
|
||||
* Bugfix: Increase CC brand name fields from 10 to 128 characters (PR #654)
|
||||
2.0.5: 2018-08-08
|
||||
* Fix IPv6 VM name in the billing invoice
|
||||
2.0.4: 2018-08-07
|
||||
* Add RSS feed link to the footer of the blog template (PR #651)
|
||||
* #5308: [ipv6only] Fix - when creating a VM, the name begins with v6only (PR #649)
|
||||
* #5293: Use `terminate-hard` action instead of `terminate` in the opennebula call to terminate a vm (PR #650)
|
||||
2.0.3: 2018-07-18
|
||||
* Remove unused /comic url (PR #644)
|
||||
* #5126: Allow dynamicweb sites to be iframed on other by setting `X_FRAME_OPTIONS_ALLOW_FROM_URI` (PR #645)
|
||||
2.0.2: 2018-07-14
|
||||
* bugfix: [blog] Add missing content block in the blog_ungleich.html template file
|
||||
2.0.1: 2018-07-14
|
||||
* bugfix: [blog] Enable content/structure mode in blog page
|
||||
2.0: 2018-07-07
|
||||
* #3747: [dcl,hosting] Add multiple cards support (PR #530)
|
||||
* #3934: [dcl,hosting] Create HostingOrder outside celery task and add and associate OrderDetail with HostingOrder (PR #624)
|
||||
* #4890: [hosting] Manage SSH keys using IPv6 of the VM (PR #640)
|
||||
* bugfix: Fix flake8 error that was ignored in release 1.9.1
|
||||
1.9.1: 2018-06-24
|
||||
* #4799: [dcl] Show selected vm templates only in calculator (PR #638)
|
||||
* #4847: [comic] Add google analytics code for comic.ungleich.ch (PR #639)
|
||||
* feature: add vm_type option to vm_template and dcl calculator to distinguish between public and ipv6only templates (PR #635)
|
||||
1.9: 2018-05-16
|
||||
* #4559: [cms] enable discount on cms calculator
|
||||
1.8: 2018-05-01
|
||||
* #4527: [hosting] cms calculator on non-cms pages for the hosting app
|
||||
* bgfix: [dcl] navbar dropdown target fix
|
||||
* bgfix: [hosting] login/signup pages footer link fix
|
||||
1.7.2: 2018-04-30
|
||||
* bgfix: [cms] add favicon extension to ungleich cms pages
|
||||
* #4474: [cms] reduce heading slider side padding
|
||||
1.7.1: 2018-04-21
|
||||
* #4481: [digitalglarus] Make /blog available on all domains
|
||||
* #4481: [blog] fix de blog pages 500 error
|
||||
* #4370: [comic] new url /comic to show only comic blogs
|
||||
1.7: 2018-04-20
|
||||
* bgfix: [all] Make /blog available on all domains
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||
from cms.admin.placeholderadmin import PlaceholderAdminMixin
|
||||
from cms.extensions import PageExtensionAdmin
|
||||
from .cms_models import CMSIntegration, CMSFaviconExtension
|
||||
from .models import VMPricing
|
||||
from .models import VMPricing, VMTemplate
|
||||
|
||||
|
||||
class CMSIntegrationAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
|
||||
|
|
@ -16,3 +16,4 @@ class CMSFaviconExtensionAdmin(PageExtensionAdmin):
|
|||
admin.site.register(CMSIntegration, CMSIntegrationAdmin)
|
||||
admin.site.register(CMSFaviconExtension, CMSFaviconExtensionAdmin)
|
||||
admin.site.register(VMPricing)
|
||||
admin.site.register(VMTemplate)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ from cms.extensions import PageExtension
|
|||
from cms.extensions.extension_pool import extension_pool
|
||||
from cms.models.fields import PlaceholderField
|
||||
from cms.models.pluginmodel import CMSPlugin
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.contrib.sites.models import Site
|
||||
from django.db import models
|
||||
from django.utils.safestring import mark_safe
|
||||
|
|
@ -9,7 +12,7 @@ from djangocms_text_ckeditor.fields import HTMLField
|
|||
from filer.fields.file import FilerFileField
|
||||
from filer.fields.image import FilerImageField
|
||||
|
||||
from datacenterlight.models import VMPricing
|
||||
from datacenterlight.models import VMPricing, VMTemplate
|
||||
|
||||
|
||||
class CMSIntegration(models.Model):
|
||||
|
|
@ -26,6 +29,10 @@ class CMSIntegration(models.Model):
|
|||
navbar_placeholder = PlaceholderField(
|
||||
'datacenterlight_navbar', related_name='dcl-navbar-placeholder+'
|
||||
)
|
||||
calculator_placeholder = PlaceholderField(
|
||||
'datacenterlight_calculator',
|
||||
related_name='dcl-calculator-placeholder+'
|
||||
)
|
||||
domain = models.ForeignKey(Site, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -173,6 +180,10 @@ class DCLNavbarPluginModel(CMSPlugin):
|
|||
default=True,
|
||||
help_text='Select to include the language selection dropdown.'
|
||||
)
|
||||
show_login_option = models.BooleanField(
|
||||
default=True,
|
||||
help_text='Uncheck this if you do not want to show login/dashboard.'
|
||||
)
|
||||
|
||||
def get_logo_dark(self):
|
||||
# used only if atleast one logo exists
|
||||
|
|
@ -288,10 +299,66 @@ class DCLSectionPromoPluginModel(CMSPlugin):
|
|||
return extra_classes
|
||||
|
||||
|
||||
class DCLCustomPricingModel(CMSPlugin):
|
||||
class MultipleChoiceArrayField(ArrayField):
|
||||
"""
|
||||
A field that allows us to store an array of choices.
|
||||
Uses Django's Postgres ArrayField
|
||||
and a MultipleChoiceField for its formfield.
|
||||
"""
|
||||
VMTemplateChoices = []
|
||||
if settings.OPENNEBULA_DOMAIN != 'test_domain':
|
||||
VMTemplateChoices = list(
|
||||
(
|
||||
str(obj.opennebula_vm_template_id),
|
||||
(obj.name + ' - ' + VMTemplate.IPV6.title()
|
||||
if obj.vm_type == VMTemplate.IPV6 else obj.name
|
||||
)
|
||||
)
|
||||
for obj in VMTemplate.objects.all()
|
||||
)
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'form_class': forms.MultipleChoiceField,
|
||||
'choices': self.VMTemplateChoices,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
# Skip our parent's formfield implementation completely as we don't
|
||||
# care for it.
|
||||
# pylint:disable=bad-super-call
|
||||
return super(ArrayField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class DCLCalculatorPluginModel(CMSPlugin):
|
||||
pricing = models.ForeignKey(
|
||||
VMPricing,
|
||||
related_name="dcl_custom_pricing_vm_pricing",
|
||||
help_text='Choose a pricing that will be associated with this '
|
||||
'Calculator'
|
||||
)
|
||||
vm_type = models.CharField(
|
||||
max_length=50, choices=VMTemplate.VM_TYPE_CHOICES,
|
||||
default=VMTemplate.PUBLIC
|
||||
)
|
||||
vm_templates_to_show = MultipleChoiceArrayField(
|
||||
base_field=models.CharField(
|
||||
blank=True,
|
||||
max_length=256,
|
||||
),
|
||||
default=list,
|
||||
blank=True,
|
||||
help_text="Recommended: If you wish to show all templates of the "
|
||||
"corresponding VM Type (public/ipv6only), please do not "
|
||||
"select any of the items in the above field. "
|
||||
"This will allow any new template(s) added "
|
||||
"in the backend to be automatically listed in this "
|
||||
"calculator instance."
|
||||
)
|
||||
default_selected_template = models.CharField(
|
||||
default="Devuan Ascii",
|
||||
null=True,
|
||||
max_length=128,
|
||||
help_text="Write the name of the template that you need selected as"
|
||||
" default when the calculator loads"
|
||||
)
|
||||
enable_512mb_ram = models.BooleanField(default=False)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ from .cms_models import (
|
|||
DCLFooterPluginModel, DCLLinkPluginModel, DCLNavbarDropdownPluginModel,
|
||||
DCLSectionIconPluginModel, DCLSectionImagePluginModel,
|
||||
DCLSectionPluginModel, DCLNavbarPluginModel,
|
||||
DCLSectionPromoPluginModel, DCLCustomPricingModel
|
||||
DCLSectionPromoPluginModel, DCLCalculatorPluginModel
|
||||
)
|
||||
from .models import VMTemplate, VMPricing
|
||||
from .models import VMTemplate
|
||||
from datacenterlight.utils import clear_all_session_vars
|
||||
|
||||
|
||||
@plugin_pool.register_plugin
|
||||
|
|
@ -21,7 +22,7 @@ class DCLSectionPlugin(CMSPluginBase):
|
|||
allow_children = True
|
||||
child_classes = [
|
||||
'DCLSectionIconPlugin', 'DCLSectionImagePlugin',
|
||||
'DCLSectionPromoPlugin', 'UngleichHTMLPlugin'
|
||||
'DCLSectionPromoPlugin', 'UngleichHTMLPlugin', 'DCLCalculatorPlugin'
|
||||
]
|
||||
|
||||
def render(self, context, instance, placeholder):
|
||||
|
|
@ -30,14 +31,17 @@ class DCLSectionPlugin(CMSPluginBase):
|
|||
)
|
||||
context['children_to_side'] = []
|
||||
context['children_to_content'] = []
|
||||
context['children_calculator'] = []
|
||||
if instance.child_plugin_instances is not None:
|
||||
right_children = [
|
||||
'DCLSectionImagePluginModel',
|
||||
'DCLSectionIconPluginModel'
|
||||
'DCLSectionIconPluginModel',
|
||||
]
|
||||
for child in instance.child_plugin_instances:
|
||||
if child.__class__.__name__ in right_children:
|
||||
context['children_to_side'].append(child)
|
||||
elif child.plugin_type == 'DCLCalculatorPlugin':
|
||||
context['children_calculator'].append(child)
|
||||
else:
|
||||
context['children_to_content'].append(child)
|
||||
return context
|
||||
|
|
@ -75,52 +79,31 @@ class DCLSectionPromoPlugin(CMSPluginBase):
|
|||
@plugin_pool.register_plugin
|
||||
class DCLCalculatorPlugin(CMSPluginBase):
|
||||
module = "Datacenterlight"
|
||||
name = "DCL Calculator Section Plugin"
|
||||
model = DCLSectionPluginModel
|
||||
name = "DCL Calculator Plugin"
|
||||
model = DCLCalculatorPluginModel
|
||||
render_template = "datacenterlight/cms/calculator.html"
|
||||
cache = False
|
||||
allow_children = True
|
||||
child_classes = [
|
||||
'DCLSectionPromoPlugin', 'UngleichHTMLPlugin', 'DCLCustomPricingPlugin'
|
||||
]
|
||||
require_parent = True
|
||||
|
||||
def render(self, context, instance, placeholder):
|
||||
clear_all_session_vars(context['request'])
|
||||
context = super(DCLCalculatorPlugin, self).render(
|
||||
context, instance, placeholder
|
||||
)
|
||||
context['templates'] = VMTemplate.objects.all()
|
||||
context['children_to_content'] = []
|
||||
pricing_plugin_model = None
|
||||
if instance.child_plugin_instances is not None:
|
||||
context['children_to_content'].extend(
|
||||
instance.child_plugin_instances
|
||||
)
|
||||
for child in instance.child_plugin_instances:
|
||||
if child.__class__.__name__ == 'DCLCustomPricingModel':
|
||||
# The second clause is just to make sure we pick up the
|
||||
# most recent CustomPricing, if more than one is present
|
||||
if (pricing_plugin_model is None or child.pricing_id >
|
||||
pricing_plugin_model.model.pricing_id):
|
||||
pricing_plugin_model = child
|
||||
|
||||
if pricing_plugin_model:
|
||||
context['vm_pricing'] = VMPricing.get_vm_pricing_by_name(
|
||||
name=pricing_plugin_model.pricing.name
|
||||
)
|
||||
ids = instance.vm_templates_to_show
|
||||
if ids:
|
||||
context['templates'] = VMTemplate.objects.filter(
|
||||
vm_type=instance.vm_type
|
||||
).filter(opennebula_vm_template_id__in=ids).order_by('name')
|
||||
else:
|
||||
context['vm_pricing'] = VMPricing.get_default_pricing()
|
||||
|
||||
context['templates'] = VMTemplate.objects.filter(
|
||||
vm_type=instance.vm_type
|
||||
).order_by('name')
|
||||
context['instance'] = instance
|
||||
context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1
|
||||
return context
|
||||
|
||||
|
||||
@plugin_pool.register_plugin
|
||||
class DCLCustomPricingPlugin(CMSPluginBase):
|
||||
module = "Datacenterlight"
|
||||
name = "DCL Custom Pricing Plugin"
|
||||
model = DCLCustomPricingModel
|
||||
render_plugin = False
|
||||
|
||||
|
||||
@plugin_pool.register_plugin
|
||||
class DCLBannerListPlugin(CMSPluginBase):
|
||||
module = "Datacenterlight"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-04-17 19:26+0000\n"
|
||||
"POT-Creation-Date: 2018-09-26 20:44+0000\n"
|
||||
"PO-Revision-Date: 2018-03-30 23:22+0000\n"
|
||||
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -19,6 +19,9 @@ msgstr ""
|
|||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Translated-Using: django-rosetta 0.8.1\n"
|
||||
|
||||
msgid "CMS Favicon"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Your New VM %(vm_name)s at Data Center Light"
|
||||
msgstr "Deine neue VM %(vm_name)s bei Data Center Light"
|
||||
|
|
@ -140,6 +143,9 @@ msgstr "Monat"
|
|||
msgid "VAT included"
|
||||
msgstr "MwSt. inklusive"
|
||||
|
||||
msgid "You save"
|
||||
msgstr "Du sparst"
|
||||
|
||||
msgid "Hosted in Switzerland"
|
||||
msgstr "Standort: Schweiz"
|
||||
|
||||
|
|
@ -287,6 +293,9 @@ msgstr "Registrieren"
|
|||
msgid "Billing Address"
|
||||
msgstr "Rechnungsadresse"
|
||||
|
||||
msgid "Make a payment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your Order"
|
||||
msgstr "Deine Bestellung"
|
||||
|
||||
|
|
@ -314,9 +323,26 @@ msgstr "exkl. Mehrwertsteuer"
|
|||
msgid "Month"
|
||||
msgstr "Monat"
|
||||
|
||||
msgid "Discount"
|
||||
msgstr "Rabatt"
|
||||
|
||||
msgid "Will be applied at checkout"
|
||||
msgstr "wird an der Kasse angewendet"
|
||||
|
||||
msgid "Credit Card"
|
||||
msgstr "Kreditkarte"
|
||||
|
||||
msgid ""
|
||||
"Please select one of the cards that you used before or fill in your credit "
|
||||
"card information below. We are using <a href=\"https://stripe.com\" target="
|
||||
"\"_blank\">Stripe</a> for payment and do not store your information in our "
|
||||
"database."
|
||||
msgstr ""
|
||||
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
|
||||
"Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
|
||||
"stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
|
||||
"Kreditkartendetails nicht in unserer Datenbank."
|
||||
|
||||
msgid ""
|
||||
"Please fill in your credit card information below. We are using <a href="
|
||||
"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not "
|
||||
|
|
@ -326,31 +352,23 @@ msgstr ""
|
|||
"\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und "
|
||||
"speichern keine Informationen in unserer Datenbank."
|
||||
|
||||
msgid ""
|
||||
"You are not making any payment yet. After submitting your card information, "
|
||||
"you will be taken to the Confirm Order Page."
|
||||
msgstr ""
|
||||
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
|
||||
"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
|
||||
msgid "Last"
|
||||
msgstr "Letzten"
|
||||
|
||||
msgid "Card Number"
|
||||
msgstr "Kreditkartennummer"
|
||||
msgid "Type"
|
||||
msgstr "Typ"
|
||||
|
||||
msgid "Expiry Date"
|
||||
msgstr "Ablaufdatum"
|
||||
msgid "SELECT"
|
||||
msgstr "AUSWÄHLEN"
|
||||
|
||||
msgid "CVC"
|
||||
msgstr ""
|
||||
msgid "Add a new credit card"
|
||||
msgstr "Eine neue Kreditkarte hinzufügen"
|
||||
|
||||
msgid "Card Type"
|
||||
msgstr "Kartentyp"
|
||||
msgid "NEW CARD"
|
||||
msgstr "NEUE KARTE"
|
||||
|
||||
msgid ""
|
||||
"You are not making any payment yet. After placing your order, you will be "
|
||||
"taken to the Submit Payment Page."
|
||||
msgstr ""
|
||||
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
|
||||
"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
|
||||
msgid "New Credit Card"
|
||||
msgstr "Neue Kreditkarte"
|
||||
|
||||
msgid "Processing"
|
||||
msgstr "Weiter"
|
||||
|
|
@ -380,6 +398,15 @@ msgstr "Bestellungsübersicht"
|
|||
msgid "Product"
|
||||
msgstr "Produkt"
|
||||
|
||||
msgid "Amount"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recurring"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subtotal"
|
||||
msgstr "Zwischensumme"
|
||||
|
||||
|
|
@ -388,7 +415,22 @@ msgstr "Mehrwertsteuer"
|
|||
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this plan will charge your credit card account "
|
||||
"with the fee of %(vm_total_price)s CHF/month"
|
||||
"with %(total_price)s CHF/month"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
"%(vm_total_price)s CHF pro Monat belastet"
|
||||
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this payment will charge your credit card "
|
||||
"account with a one time amount of %(total_price)s CHF"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
"%(vm_total_price)s CHF pro Monat belastet"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this plan will charge your credit card account "
|
||||
"with %(vm_total_price)s CHF/month"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
"%(vm_total_price)s CHF pro Monat belastet"
|
||||
|
|
@ -503,6 +545,13 @@ msgstr "Ungültige Speicher-Grösse"
|
|||
msgid "Incorrect pricing name. Please contact support{support_email}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{user} does not have permission to access the card"
|
||||
msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
|
||||
|
||||
msgid "An error occurred. Details: {}"
|
||||
msgstr "Ein Fehler ist aufgetreten. Details: {}"
|
||||
|
||||
msgid "Confirm Order"
|
||||
msgstr "Bestellung Bestätigen"
|
||||
|
||||
|
|
@ -516,6 +565,36 @@ msgstr ""
|
|||
"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom "
|
||||
"Popup zur Bezahlseite weitergeleitet."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "An error occurred while associating the card. Details: {details}"
|
||||
msgstr ""
|
||||
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
|
||||
|
||||
msgid "Confirmation of your payment"
|
||||
msgstr ""
|
||||
|
||||
msgid " This is a monthly recurring plan."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Hi {name},\n"
|
||||
"\n"
|
||||
"thank you for your order!\n"
|
||||
"We have just received a payment of CHF {amount:.2f} from you.{recurring}\n"
|
||||
"\n"
|
||||
"Cheers,\n"
|
||||
"Your Data Center Light team"
|
||||
msgstr ""
|
||||
|
||||
msgid "Thank you for the payment."
|
||||
msgstr "Danke für Deine Bestellung."
|
||||
|
||||
msgid ""
|
||||
"You will soon receive a confirmation email of the payment. You can always "
|
||||
"contact us at info@ungleich.ch for any question that you may have."
|
||||
msgstr ""
|
||||
|
||||
msgid "Thank you for the order."
|
||||
msgstr "Danke für Deine Bestellung."
|
||||
|
||||
|
|
@ -526,6 +605,28 @@ msgstr ""
|
|||
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
|
||||
"auf sie zugreifen kannst."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "You are not making any payment yet. After submitting your card "
|
||||
#~ "information, you will be taken to the Confirm Order Page."
|
||||
#~ msgstr ""
|
||||
#~ "Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst "
|
||||
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
|
||||
#~ "hast."
|
||||
|
||||
#~ msgid "Card Number"
|
||||
#~ msgstr "Kreditkartennummer"
|
||||
|
||||
#~ msgid "Expiry Date"
|
||||
#~ msgstr "Ablaufdatum"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "You are not making any payment yet. After placing your order, you will be "
|
||||
#~ "taken to the Submit Payment Page."
|
||||
#~ msgstr ""
|
||||
#~ "Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst "
|
||||
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
|
||||
#~ "hast."
|
||||
|
||||
#~ msgid "Pricing"
|
||||
#~ msgstr "Preise"
|
||||
|
||||
|
|
|
|||
|
|
@ -10,16 +10,28 @@ class Command(BaseCommand):
|
|||
help = '''Fetches the VM templates from OpenNebula and populates the dcl
|
||||
VMTemplate model'''
|
||||
|
||||
def get_templates(self, manager, prefix):
|
||||
templates = manager.get_templates('%s-' % prefix)
|
||||
dcl_vm_templates = []
|
||||
for template in templates:
|
||||
template_name = template.name.lstrip('%s-' % prefix)
|
||||
template_id = template.id
|
||||
dcl_vm_template = VMTemplate.create(
|
||||
template_name, template_id, prefix
|
||||
)
|
||||
dcl_vm_templates.append(dcl_vm_template)
|
||||
return dcl_vm_templates
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
manager = OpenNebulaManager()
|
||||
templates = manager.get_templates()
|
||||
dcl_vm_templates = []
|
||||
for template in templates:
|
||||
template_name = template.name.lstrip('public-')
|
||||
template_id = template.id
|
||||
dcl_vm_template = VMTemplate.create(template_name, template_id)
|
||||
dcl_vm_templates.append(dcl_vm_template)
|
||||
dcl_vm_templates.extend(
|
||||
self.get_templates(manager, VMTemplate.PUBLIC)
|
||||
)
|
||||
dcl_vm_templates.extend(
|
||||
self.get_templates(manager, VMTemplate.IPV6)
|
||||
)
|
||||
|
||||
old_vm_templates = VMTemplate.objects.all()
|
||||
old_vm_templates.delete()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-04-25 09:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import cms.models.fields
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0020_merge'),
|
||||
('cms', '0014_auto_20160404_1908'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cmsintegration',
|
||||
name='calculator_placeholder',
|
||||
field=cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='dcl-calculator-placeholder+', slotname='datacenterlight_calculator', to='cms.Placeholder'),
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name='DCLCustomPricingModel',
|
||||
new_name='DCLCalculatorPluginModel',
|
||||
),
|
||||
]
|
||||
26
datacenterlight/migrations/0022_auto_20180506_1950.py
Normal file
26
datacenterlight/migrations/0022_auto_20180506_1950.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-05-07 02:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0021_cmsintegration_calculator_placeholder'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vmpricing',
|
||||
name='discount_amount',
|
||||
field=models.DecimalField(
|
||||
decimal_places=2, default=0, max_digits=6),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vmpricing',
|
||||
name='discount_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
25
datacenterlight/migrations/0023_auto_20180524_0349.py
Normal file
25
datacenterlight/migrations/0023_auto_20180524_0349.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-05-23 22:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0022_auto_20180506_1950'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dclcalculatorpluginmodel',
|
||||
name='vm_type',
|
||||
field=models.CharField(choices=[('public', 'Public'), ('ipv6only', 'Ipv6Only')], default='public', max_length=50),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='vmtemplate',
|
||||
name='vm_type',
|
||||
field=models.CharField(choices=[('public', 'Public'), ('ipv6only', 'Ipv6Only')], default='public', max_length=50),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-06-24 08:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datacenterlight.cms_models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0023_auto_20180524_0349'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dclcalculatorpluginmodel',
|
||||
name='vm_templates_to_show',
|
||||
field=datacenterlight.cms_models.MultipleChoiceArrayField(base_field=models.CharField(blank=True, max_length=256), blank=True, default=list, help_text='Recommended: If you wish to show all templates of the corresponding VM Type (public/ipv6only), please do not select any of the items in the above field. This will allow any new template(s) added in the backend to be automatically listed in this calculator instance.', size=None),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-09-25 20:27
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0024_dclcalculatorpluginmodel_vm_templates_to_show'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dclnavbarpluginmodel',
|
||||
name='show_login_option',
|
||||
field=models.BooleanField(default=True, help_text='Uncheck this if you do not want to show login/dashboard.'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-09-27 20:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0025_dclnavbarpluginmodel_show_login_option'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dclcalculatorpluginmodel',
|
||||
name='default_selected_template',
|
||||
field=models.CharField(default='Devuan Ascii', help_text='Write the name of the template that you need selected as default when the calculator loads', max_length=128, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-09-29 05:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0026_dclcalculatorpluginmodel_default_selected_template'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dclcalculatorpluginmodel',
|
||||
name='enable_512mb_ram',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
|
|
@ -6,13 +6,29 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class VMTemplate(models.Model):
|
||||
PUBLIC = 'public'
|
||||
IPV6 = 'ipv6only'
|
||||
VM_TYPE_CHOICES = (
|
||||
(PUBLIC, PUBLIC.title()),
|
||||
(IPV6, IPV6.title()),
|
||||
)
|
||||
name = models.CharField(max_length=50)
|
||||
opennebula_vm_template_id = models.IntegerField()
|
||||
vm_type = models.CharField(
|
||||
max_length=50, choices=VM_TYPE_CHOICES, default=PUBLIC
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '%s - %s - %s' % (
|
||||
self.opennebula_vm_template_id, self.vm_type, self.name
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, opennebula_vm_template_id):
|
||||
def create(cls, name, opennebula_vm_template_id, vm_type):
|
||||
vm_template = cls(
|
||||
name=name, opennebula_vm_template_id=opennebula_vm_template_id)
|
||||
name=name, opennebula_vm_template_id=opennebula_vm_template_id,
|
||||
vm_type=vm_type
|
||||
)
|
||||
return vm_template
|
||||
|
||||
|
||||
|
|
@ -34,16 +50,29 @@ class VMPricing(models.Model):
|
|||
hdd_unit_price = models.DecimalField(
|
||||
max_digits=7, decimal_places=6, default=0
|
||||
)
|
||||
discount_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
discount_amount = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name + ' => ' + ' - '.join([
|
||||
display_str = self.name + ' => ' + ' - '.join([
|
||||
'{}/Core'.format(self.cores_unit_price.normalize()),
|
||||
'{}/GB RAM'.format(self.ram_unit_price.normalize()),
|
||||
'{}/GB SSD'.format(self.ssd_unit_price.normalize()),
|
||||
'{}/GB HDD'.format(self.hdd_unit_price.normalize()),
|
||||
'{}% VAT'.format(self.vat_percentage.normalize())
|
||||
if not self.vat_inclusive else 'VAT-Incl', ]
|
||||
)
|
||||
if not self.vat_inclusive else 'VAT-Incl',
|
||||
])
|
||||
if self.discount_amount:
|
||||
display_str = ' - '.join([
|
||||
display_str,
|
||||
'{} {}'.format(
|
||||
self.discount_amount,
|
||||
self.discount_name if self.discount_name else 'Discount'
|
||||
)
|
||||
])
|
||||
return display_str
|
||||
|
||||
@classmethod
|
||||
def get_vm_pricing_by_name(cls, name):
|
||||
|
|
|
|||
|
|
@ -150,3 +150,39 @@ footer .dcl-link-separator::before {
|
|||
border-radius: 100%;
|
||||
background: #777;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.thin-hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.payment-container .credit-card-info {
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.credit-card-info {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.credit-card-info .align-bottom {
|
||||
align-self: flex-end;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.new-card-head {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.new-card-button-margin button{
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.input-no-border {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
resize: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.header_slider > .carousel .item .container {
|
||||
.header_slider > .carousel .item .container-fluid {
|
||||
overflow: auto;
|
||||
padding: 50px 20px 60px;
|
||||
height: 100%;
|
||||
|
|
@ -104,9 +104,9 @@
|
|||
.header_slider .carousel-control .fa {
|
||||
font-size: 4em;
|
||||
}
|
||||
.header_slider > .carousel .item .container {
|
||||
.header_slider > .carousel .item .container-fluid {
|
||||
overflow: auto;
|
||||
padding: 75px 50px;
|
||||
padding: 75px;
|
||||
}
|
||||
.header_slider .btn-trans {
|
||||
padding: 8px 15px;
|
||||
|
|
@ -120,11 +120,6 @@
|
|||
.header_slider .intro-cap {
|
||||
font-size: 3.25em;
|
||||
}
|
||||
|
||||
.header_slider > .carousel .item .container {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header_slider .intro_lead {
|
||||
|
|
|
|||
|
|
@ -482,6 +482,7 @@
|
|||
margin: 100px auto 40px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 30px 30px 20px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container .dashboard-title-thin {
|
||||
|
|
@ -503,10 +504,6 @@
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.order-detail-container .order-details strong {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
|
@ -515,13 +512,28 @@
|
|||
|
||||
.order-detail-container p {
|
||||
margin-bottom: 5px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container hr {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.order-detail-container .thin-hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.order-detail-container .subtotal-price {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.order-detail-container .subtotal-price .text-primary {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.order-detail-container .total-price {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.order-detail-container {
|
||||
padding: 15px;
|
||||
|
|
|
|||
|
|
@ -776,7 +776,7 @@ textarea {
|
|||
width: 100%;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 6px rgba(0, 0, 0, 0.15);
|
||||
padding-bottom: 40px;
|
||||
border-radius: 7px;
|
||||
text-align: center;
|
||||
|
|
@ -929,7 +929,7 @@ textarea {
|
|||
}
|
||||
|
||||
|
||||
@media(max-width:991px) {
|
||||
@media(max-width:767px) {
|
||||
.section-sm-center .split-text,
|
||||
.section-sm-center .space {
|
||||
text-align: center !important;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
/* ---------------------------------------------
|
||||
Scripts initialization
|
||||
--------------------------------------------- */
|
||||
var minRam = 1;
|
||||
if(window.minRam){
|
||||
minRam = window.minRam;
|
||||
}
|
||||
var cardPricing = {
|
||||
'cpu': {
|
||||
'id': 'coreValue',
|
||||
|
|
@ -16,7 +20,7 @@
|
|||
'ram': {
|
||||
'id': 'ramValue',
|
||||
'value': 2,
|
||||
'min': 1,
|
||||
'min': minRam,
|
||||
'max': 200,
|
||||
'interval': 1
|
||||
},
|
||||
|
|
@ -40,6 +44,7 @@
|
|||
_initNavUrl();
|
||||
_initPricing();
|
||||
ajaxForms();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
});
|
||||
|
||||
$(window).resize(function() {
|
||||
|
|
@ -144,21 +149,54 @@
|
|||
var data = $(this).data('minus');
|
||||
|
||||
if (cardPricing[data].value > cardPricing[data].min) {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval;
|
||||
if(data === 'ram' && String(cardPricing[data].value) === "1" && minRam === 0.5){
|
||||
cardPricing[data].value = 0.5;
|
||||
$('#ramValue').val('0.5');
|
||||
$("#ramValue").attr('step', 0.5);
|
||||
} else {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval;
|
||||
}
|
||||
}
|
||||
_fetchPricing();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
});
|
||||
$('.fa-plus-circle.right').click(function(event) {
|
||||
var data = $(this).data('plus');
|
||||
if (cardPricing[data].value < cardPricing[data].max) {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval;
|
||||
if(data === 'ram' && String(cardPricing[data].value) === "0.5" && minRam === 0.5){
|
||||
cardPricing[data].value = 1;
|
||||
$('#ramValue').val('1');
|
||||
$("#ramValue").attr('step', 1);
|
||||
} else {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval;
|
||||
}
|
||||
}
|
||||
_fetchPricing();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
});
|
||||
|
||||
$('.input-price').change(function() {
|
||||
var data = $(this).attr("name");
|
||||
cardPricing[data].value = $('input[name=' + data + ']').val();
|
||||
var input = $('input[name=' + data + ']');
|
||||
var inputValue = input.val();
|
||||
|
||||
if(data === 'ram') {
|
||||
var ramInput = $('#ramValue');
|
||||
if ($('#ramValue').data('old-value') < $('#ramValue').val()) {
|
||||
if($('#ramValue').val() === '1' && minRam === 0.5) {
|
||||
$("#ramValue").attr('step', 1);
|
||||
$('#ramValue').val('1');
|
||||
}
|
||||
} else {
|
||||
if($('#ramValue').val() === '0' && minRam === 0.5) {
|
||||
$("#ramValue").attr('step', 0.5);
|
||||
$('#ramValue').val('0.5');
|
||||
}
|
||||
}
|
||||
inputValue = $('#ramValue').val();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
}
|
||||
cardPricing[data].value = inputValue;
|
||||
_fetchPricing();
|
||||
});
|
||||
}
|
||||
|
|
@ -175,14 +213,18 @@
|
|||
window.coresUnitPrice = 5;
|
||||
}
|
||||
if(typeof window.ramUnitPrice === 'undefined'){
|
||||
window.coresUnitPrice = 2;
|
||||
window.ramUnitPrice = 2;
|
||||
}
|
||||
if(typeof window.ssdUnitPrice === 'undefined'){
|
||||
window.ssdUnitPrice = 0.6;
|
||||
}
|
||||
if(typeof window.discountAmount === 'undefined'){
|
||||
window.discountAmount = 0;
|
||||
}
|
||||
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
|
||||
(cardPricing['ram'].value * window.ramUnitPrice) +
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice);
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice) -
|
||||
window.discountAmount;
|
||||
total = parseFloat(total.toFixed(2));
|
||||
$("#total").text(total);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
from datetime import datetime
|
||||
|
||||
from celery import current_task
|
||||
from celery.exceptions import MaxRetriesExceededError
|
||||
from celery.utils.log import get_task_logger
|
||||
from celery import current_task
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMessage
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from time import sleep
|
||||
|
||||
from dynamicweb.celery import app
|
||||
from hosting.models import HostingOrder, HostingBill
|
||||
from membership.models import StripeCustomer, CustomUser
|
||||
from hosting.models import HostingOrder
|
||||
from membership.models import CustomUser
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from opennebula_api.serializers import VirtualMachineSerializer
|
||||
from utils.hosting_utils import get_all_public_keys, get_or_create_vm_detail
|
||||
from utils.forms import UserBillingAddressForm
|
||||
from utils.hosting_utils import (
|
||||
get_all_public_keys, get_or_create_vm_detail, ping_ok
|
||||
)
|
||||
from utils.mailer import BaseEmail
|
||||
from utils.models import BillingAddress
|
||||
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from .models import VMPricing
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
|
@ -51,24 +52,15 @@ def retry_task(task, exception=None):
|
|||
|
||||
|
||||
@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
|
||||
def create_vm_task(self, vm_template_id, user, specs, template,
|
||||
stripe_customer_id, billing_address_data,
|
||||
stripe_subscription_id, cc_details):
|
||||
def create_vm_task(self, vm_template_id, user, specs, template, order_id):
|
||||
logger.debug(
|
||||
"Running create_vm_task on {}".format(current_task.request.hostname))
|
||||
vm_id = None
|
||||
try:
|
||||
final_price = (specs.get('total_price') if 'total_price' in specs
|
||||
else specs.get('price'))
|
||||
billing_address = BillingAddress(
|
||||
cardholder_name=billing_address_data['cardholder_name'],
|
||||
street_address=billing_address_data['street_address'],
|
||||
city=billing_address_data['city'],
|
||||
postal_code=billing_address_data['postal_code'],
|
||||
country=billing_address_data['country']
|
||||
final_price = (
|
||||
specs.get('total_price') if 'total_price' in specs
|
||||
else specs.get('price')
|
||||
)
|
||||
billing_address.save()
|
||||
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
|
||||
|
||||
if 'pass' in user:
|
||||
on_user = user.get('email')
|
||||
|
|
@ -97,38 +89,43 @@ def create_vm_task(self, vm_template_id, user, specs, template,
|
|||
if vm_id is None:
|
||||
raise Exception("Could not create VM")
|
||||
|
||||
vm_pricing = VMPricing.get_vm_pricing_by_name(
|
||||
name=specs['pricing_name']
|
||||
) if 'pricing_name' in specs else VMPricing.get_default_pricing()
|
||||
# Create a Hosting Order
|
||||
order = HostingOrder.create(
|
||||
price=final_price,
|
||||
vm_id=vm_id,
|
||||
customer=customer,
|
||||
billing_address=billing_address,
|
||||
vm_pricing=vm_pricing
|
||||
# Update HostingOrder with the created vm_id
|
||||
hosting_order = HostingOrder.objects.filter(id=order_id).first()
|
||||
error_msg = None
|
||||
|
||||
try:
|
||||
hosting_order.vm_id = vm_id
|
||||
hosting_order.save()
|
||||
logger.debug(
|
||||
"Updated hosting_order {} with vm_id={}".format(
|
||||
hosting_order.id, vm_id
|
||||
)
|
||||
)
|
||||
except Exception as ex:
|
||||
error_msg = (
|
||||
"HostingOrder with id {order_id} not found. This means that "
|
||||
"the hosting order was not created and/or it is/was not "
|
||||
"associated with VM with id {vm_id}. Details {details}".format(
|
||||
order_id=order_id, vm_id=vm_id, details=str(ex)
|
||||
)
|
||||
)
|
||||
logger.error(error_msg)
|
||||
|
||||
stripe_utils = StripeUtils()
|
||||
result = stripe_utils.set_subscription_metadata(
|
||||
subscription_id=hosting_order.subscription_id,
|
||||
metadata={"VM_ID": str(vm_id)}
|
||||
)
|
||||
|
||||
# Create a Hosting Bill
|
||||
HostingBill.create(
|
||||
customer=customer, billing_address=billing_address
|
||||
)
|
||||
|
||||
# Create Billing Address for User if he does not have one
|
||||
if not customer.user.billing_addresses.count():
|
||||
billing_address_data.update({
|
||||
'user': customer.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
billing_address_data)
|
||||
billing_address_user_form.is_valid()
|
||||
billing_address_user_form.save()
|
||||
|
||||
# Associate an order with a stripe subscription
|
||||
order.set_subscription_id(stripe_subscription_id, cc_details)
|
||||
|
||||
# If the Stripe payment succeeds, set order status approved
|
||||
order.set_approved()
|
||||
if result.get('error') is not None:
|
||||
emsg = "Could not update subscription metadata for {sub}".format(
|
||||
sub=hosting_order.subscription_id
|
||||
)
|
||||
logger.error(emsg)
|
||||
if error_msg:
|
||||
error_msg += ". " + emsg
|
||||
else:
|
||||
error_msg = emsg
|
||||
|
||||
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
|
||||
|
||||
|
|
@ -142,8 +139,11 @@ def create_vm_task(self, vm_template_id, user, specs, template,
|
|||
'template': template.get('name'),
|
||||
'vm_name': vm.get('name'),
|
||||
'vm_id': vm['vm_id'],
|
||||
'order_id': order.id
|
||||
'order_id': order_id
|
||||
}
|
||||
|
||||
if error_msg:
|
||||
context['errors'] = error_msg
|
||||
if 'pricing_name' in specs:
|
||||
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
|
||||
name=specs['pricing_name']
|
||||
|
|
@ -171,7 +171,7 @@ def create_vm_task(self, vm_template_id, user, specs, template,
|
|||
'base_url': "{0}://{1}".format(user.get('request_scheme'),
|
||||
user.get('request_host')),
|
||||
'order_url': reverse('hosting:orders',
|
||||
kwargs={'pk': order.id}),
|
||||
kwargs={'pk': order_id}),
|
||||
'page_header': _(
|
||||
'Your New VM %(vm_name)s at Data Center Light') % {
|
||||
'vm_name': vm.get('name')},
|
||||
|
|
@ -188,11 +188,11 @@ def create_vm_task(self, vm_template_id, user, specs, template,
|
|||
email = BaseEmail(**email_data)
|
||||
email.send()
|
||||
|
||||
# try to see if we have the IP and that if the ssh keys can
|
||||
# be configured
|
||||
new_host = manager.get_primary_ipv4(vm_id)
|
||||
# try to see if we have the IPv6 of the new vm and that if the ssh
|
||||
# keys can be configured
|
||||
vm_ipv6 = manager.get_ipv6(vm_id)
|
||||
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
|
||||
if new_host is not None:
|
||||
if vm_ipv6 is not None:
|
||||
custom_user = CustomUser.objects.get(email=user.get('email'))
|
||||
get_or_create_vm_detail(custom_user, manager, vm_id)
|
||||
if custom_user is not None:
|
||||
|
|
@ -203,13 +203,48 @@ def create_vm_task(self, vm_template_id, user, specs, template,
|
|||
logger.debug(
|
||||
"Calling configure on {host} for "
|
||||
"{num_keys} keys".format(
|
||||
host=new_host, num_keys=len(keys)))
|
||||
# Let's delay the task by 75 seconds to be sure
|
||||
# that we run the cdist configure after the host
|
||||
# is up
|
||||
manager.manage_public_key(keys,
|
||||
hosts=[new_host],
|
||||
countdown=75)
|
||||
host=vm_ipv6, num_keys=len(keys)
|
||||
)
|
||||
)
|
||||
# Let's wait until the IP responds to ping before we
|
||||
# run the cdist configure on the host
|
||||
did_manage_public_key = False
|
||||
for i in range(0, 15):
|
||||
if ping_ok(vm_ipv6):
|
||||
logger.debug(
|
||||
"{} is pingable. Doing a "
|
||||
"manage_public_key".format(vm_ipv6)
|
||||
)
|
||||
sleep(10)
|
||||
manager.manage_public_key(
|
||||
keys, hosts=[vm_ipv6]
|
||||
)
|
||||
did_manage_public_key = True
|
||||
break
|
||||
else:
|
||||
logger.debug(
|
||||
"Can't ping {}. Wait 5 secs".format(
|
||||
vm_ipv6
|
||||
)
|
||||
)
|
||||
sleep(5)
|
||||
if not did_manage_public_key:
|
||||
emsg = ("Waited for over 75 seconds for {} to be "
|
||||
"pingable. But the VM was not reachable. "
|
||||
"So, gave up manage_public_key. Please do "
|
||||
"this manually".format(vm_ipv6))
|
||||
logger.error(emsg)
|
||||
email_data = {
|
||||
'subject': '{} CELERY TASK INCOMPLETE: {} not '
|
||||
'pingable for 75 seconds'.format(
|
||||
settings.DCL_TEXT, vm_ipv6
|
||||
),
|
||||
'from_email': current_task.request.hostname,
|
||||
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
|
||||
'body': emsg
|
||||
}
|
||||
email = EmailMessage(**email_data)
|
||||
email.send()
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{% page_attribute 'meta_description' %}">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
<meta name="description" content="{% page_attribute 'meta_description' %}">
|
||||
<title>{% page_attribute "page_title" %}</title>
|
||||
|
||||
<!-- Vendor CSS -->
|
||||
|
|
@ -61,6 +61,7 @@
|
|||
</div>
|
||||
{% endplaceholder %}
|
||||
|
||||
{% url 'datacenterlight:index' as calculator_form_url %}
|
||||
{% placeholder 'Datacenterlight Content' %}
|
||||
|
||||
{% placeholder 'datacenterlight_footer'%}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,5 @@
|
|||
<div class="split-section {{ instance.get_extra_classes }}" id="{{ instance.html_id }}">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 {% if instance.text_direction == 'right' %}col-sm-push-6{% endif %} split-text">
|
||||
{% include "datacenterlight/cms/includes/_section_split_content.html" %}
|
||||
</div>
|
||||
<div class="col-sm-6 {% if instance.text_direction == 'right' %}col-sm-pull-6{% endif %}">
|
||||
<div class="price-calc-section">
|
||||
<div class="card">
|
||||
{% include "datacenterlight/includes/_calculator_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-calc-section">
|
||||
<div class="card">
|
||||
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -35,14 +35,16 @@
|
|||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if not request.user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'hosting:login' %}">{% trans "Login" %} <span class="fa fa-sign-in"></span></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{% url 'hosting:dashboard' %}">{% trans "Dashboard" %}</a>
|
||||
</li>
|
||||
{% if instance.show_login_option %}
|
||||
{% if not request.user.is_authenticated %}
|
||||
<li>
|
||||
<a href="{% url 'hosting:login' %}">{% trans "Login" %} <span class="fa fa-sign-in"></span></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<a href="{% url 'hosting:dashboard' %}">{% trans "Dashboard" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% comment %}
|
||||
<!-- to be used when more than one option for language -->
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{% load cms_tags %}
|
||||
|
||||
<div class="dropdown highlights-dropdown">
|
||||
<a class="dropdown-toggle url-init dcl-link" href="{{ instance.url|default:'#' }}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ instance.text }} <span class="caret"></span></a>
|
||||
<a class="dropdown-toggle url-init dcl-link" href="{{ instance.target|default:'#' }}" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ instance.text }} <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for plugin in instance.child_plugin_instances %}
|
||||
{% render_plugin plugin %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,24 @@
|
|||
|
||||
<section class="split-section {{ instance.get_extra_classes }}" id="{{ instance.html_id }}">
|
||||
<div class="container">
|
||||
{% if children_to_side|length %}
|
||||
{% if children_to_side|length or children_calculator|length %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6 {% if instance.text_direction == 'right' %}col-sm-push-6{% endif %} split-text">
|
||||
{% include "datacenterlight/cms/includes/_section_split_content.html" %}
|
||||
</div>
|
||||
<div class="col-sm-6 {% if instance.text_direction == 'right' %}col-sm-pull-6{% endif %} split-figure">
|
||||
<div class="section-figure">
|
||||
{% for plugin in children_to_side %}
|
||||
{% if children_calculator|length %}
|
||||
{% for plugin in children_calculator %}
|
||||
{% render_plugin plugin %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if children_to_side %}
|
||||
<div class="section-figure">
|
||||
{% for plugin in children_to_side %}
|
||||
{% render_plugin plugin %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,29 @@
|
|||
window.ramUnitPrice = {{vm_pricing.ram_unit_price|default:0}};
|
||||
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
|
||||
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
|
||||
window.discountAmount = {{vm_pricing.discount_amount|default:0}};
|
||||
window.minRam = {{min_ram}};
|
||||
window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}';
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<form id="order_form" method="POST" action="{% url 'datacenterlight:index' %}" data-toggle="validator" role="form">
|
||||
<form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="pid" value="{{instance.id}}">
|
||||
<div class="title">
|
||||
<h3>{% trans "VM hosting" %} </h3>
|
||||
</div>
|
||||
<div class="price">
|
||||
<span id="total">15</span>
|
||||
<span id="total"></span>
|
||||
<span>CHF/{% trans "month" %}</span>
|
||||
{% if vm_pricing.vat_inclusive %}
|
||||
<div class="price-text">
|
||||
<p>{% trans "VAT included" %}</p>
|
||||
<p>
|
||||
{% if vm_pricing.vat_inclusive %}{% trans "VAT included" %} <br>{% endif %}
|
||||
{% if vm_pricing.discount_amount %}
|
||||
{% trans "You save" %} {{ vm_pricing.discount_amount }} CHF
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="descriptions">
|
||||
<div class="description form-group">
|
||||
|
|
@ -50,8 +57,8 @@
|
|||
<div class="form-group">
|
||||
<div class="description input">
|
||||
<i class="fa fa-minus-circle left" data-minus="ram" aria-hidden="true"></i>
|
||||
<input id="ramValue" class="input-price select-number" type="number" min="1" max="200" name="ram"
|
||||
data-error="{% trans 'Please enter a value in range 1 - 200.' %}" required>
|
||||
<input id="ramValue" class="input-price select-number" type="number" min="{% if min_ram == 0.5 %}0{% else %}1{% endif %}" max="200" name="ram"
|
||||
data-error="{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}" required step="1">
|
||||
<span> GB RAM</span>
|
||||
<i class="fa fa-plus-circle right" data-plus="ram" aria-hidden="true"></i>
|
||||
</div>
|
||||
|
|
@ -87,7 +94,8 @@
|
|||
<label for="config">OS</label>
|
||||
<select name="config">
|
||||
{% for template in templates %}
|
||||
<option value="{{template.opennebula_vm_template_id}}">{{template.name}}</option>
|
||||
|
||||
<option value="{{template.opennebula_vm_template_id}}" {% if template.name|lower == instance.default_selected_template|lower %}selected="selected"{% endif %}>{{template.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,110 +67,102 @@
|
|||
</div>
|
||||
<div class="dcl-payment-box">
|
||||
<div class="dcl-payment-section">
|
||||
<h3>{%trans "Your Order" %}</h3>
|
||||
<hr class="top-hr">
|
||||
<div class="dcl-payment-order">
|
||||
<p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p>
|
||||
<hr>
|
||||
<p class="last-p"><strong>{%trans "Total" %}</strong> <small>({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %})</small> <strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong></p>
|
||||
</div>
|
||||
{% if generic_payment_form %}
|
||||
<h3>{%trans "Make a payment" %}</h3>
|
||||
<hr class="top-hr">
|
||||
<form role="form" id="generic-payment-form" method="post" action="" novalidate>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="product" value="1" />
|
||||
{% for field in generic_payment_form %}
|
||||
{% bootstrap_field field type='fields'%}
|
||||
{% endfor %}
|
||||
<p class="text-danger">{{generic_payment_form.non_field_errors|striptags}}</p>
|
||||
</form>
|
||||
{% else %}
|
||||
<h3>{%trans "Your Order" %}</h3>
|
||||
<hr class="top-hr">
|
||||
<div class="dcl-payment-order">
|
||||
<p>{% trans "Cores"%} <strong class="pull-right">{{request.session.specs.cpu|floatformat}}</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Memory"%} <strong class="pull-right">{{request.session.specs.memory|floatformat}} GB</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Disk space"%} <strong class="pull-right">{{request.session.specs.disk_size|floatformat}} GB</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Configuration"%} <strong class="pull-right">{{request.session.template.name}}</strong></p>
|
||||
<hr>
|
||||
<p>
|
||||
<strong>{%trans "Total" %}</strong>
|
||||
<small>
|
||||
({% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %})
|
||||
</small>
|
||||
<strong class="pull-right">{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}</strong>
|
||||
</p>
|
||||
<hr>
|
||||
{% if vm_pricing.discount_amount %}
|
||||
<p class="mb-0">
|
||||
{%trans "Discount" as discount_name %}
|
||||
<strong>{{ vm_pricing.discount_name|default:discount_name }}</strong>
|
||||
<strong class="pull-right text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</strong>
|
||||
</p>
|
||||
<p>
|
||||
({% trans "Will be applied at checkout" %})
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dcl-payment-box">
|
||||
<div class="dcl-payment-section">
|
||||
{% with card_list_len=cards_list|length %}
|
||||
<h3><b>{%trans "Credit Card"%}</b></h3>
|
||||
<hr class="top-hr">
|
||||
<p>
|
||||
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
</p>
|
||||
<div>
|
||||
{% if credit_card_data.last4 %}
|
||||
<form role="form" id="payment-form-with-creditcard" novalidate>
|
||||
<h5 class="billing-head">Credit Card</h5>
|
||||
<h5 class="membership-lead">Last 4: *****{{credit_card_data.last4}}</h5>
|
||||
<h5 class="membership-lead">Type: {{credit_card_data.cc_brand}}</h5>
|
||||
<input type="hidden" name="credit_card_needed" value="false"/>
|
||||
</form>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-addtional-margin">
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button id="payment_button_with_creditcard" class="btn btn-vm-contact" type="submit">{%trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
{% if card_list_len > 0 %}
|
||||
{% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% else %}
|
||||
<form action="" id="payment-form-new" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<div class="group">
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-5 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div>
|
||||
{% for card in cards_list %}
|
||||
<div class="credit-card-info">
|
||||
<div class="col-xs-6 no-padding">
|
||||
<h5 class="billing-head">{% trans "Credit Card" %}</h5>
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right align-bottom">
|
||||
<a class="btn choice-btn choice-btn-faded" href="#" data-id_card="{{card.id}}">{% trans "SELECT" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-errors"></div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if card_list_len > 0 %}
|
||||
<div class="new-card-head">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>{% trans "Add a new credit card" %}</h4>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right new-card-button-margin">
|
||||
<button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
|
||||
<span class="fa fa-plus"></span> {% trans "NEW CARD" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-vm-contact btn-wide" type="submit">{%trans "SUBMIT" %}</button>
|
||||
<div id="newcard" class="collapse">
|
||||
<hr class="thick-hr">
|
||||
<div class="card-details-box">
|
||||
<h3>{%trans "New Credit Card" %}</h3>
|
||||
<hr>
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else%}
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -190,13 +182,4 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
window.hasCreditcard = true;
|
||||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{%endblock%}
|
||||
|
|
|
|||
|
|
@ -47,48 +47,104 @@
|
|||
<hr>
|
||||
<div>
|
||||
<h4>{% trans "Order summary" %}</h4>
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{{ request.session.template.name }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
{% if generic_payment_details %}
|
||||
<p>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
<span class="pull-right">{{vm.cpu|floatformat}}</span>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{{ generic_payment_details.product_name }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<span>{% trans "Amount" %}: </span>
|
||||
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
|
||||
</p>
|
||||
{% if generic_payment_details.description %}
|
||||
<p>
|
||||
<span>{% trans "Description" %}: </span>
|
||||
<strong class="pull-right">{{generic_payment_details.description}}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if generic_payment_details.recurring %}
|
||||
<p>
|
||||
<span>{% trans "Recurring" %}: </span>
|
||||
<strong class="pull-right">Yes</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<span>{% trans "Memory" %}: </span>
|
||||
<span class="pull-right">{{vm.memory|intcomma}} GB</span>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{{ request.session.template.name }}
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Disk space" %}: </span>
|
||||
<span class="pull-right">{{vm.disk_size|intcomma}} GB</span>
|
||||
</p>
|
||||
{% if vm.vat > 0 %}
|
||||
<p>
|
||||
<strong>{% trans "Subtotal" %}: </strong>
|
||||
<span class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%): </span>
|
||||
<span class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{% trans "Total" %}</strong>
|
||||
<span class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Memory" %}: </span>
|
||||
<strong class="pull-right">{{vm.memory|intcomma}} GB</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Disk space" %}: </span>
|
||||
<strong class="pull-right">{{vm.disk_size|intcomma}} GB</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% if vm.vat > 0 or vm.discount.amount > 0 %}
|
||||
<div class="col-sm-6">
|
||||
<div class="subtotal-price">
|
||||
{% if vm.vat > 0 %}
|
||||
<p>
|
||||
<strong class="text-lg">{% trans "Subtotal" %} </strong>
|
||||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
|
||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if vm.discount.amount > 0 %}
|
||||
<p class="text-primary">
|
||||
{%trans "Discount" as discount_name %}
|
||||
<strong>{{ vm.discount.name|default:discount_name }} </strong>
|
||||
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
<p class="total-price">
|
||||
<strong>{% trans "Total" %} </strong>
|
||||
<strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<form id="virtual_machine_create_form" action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with the fee of {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
|
||||
{% if generic_payment_details %}
|
||||
{% if generic_payment_details.recurring %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.</div>
|
||||
{% else %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4 order-confirm-btn text-right">
|
||||
<button class="btn choice-btn" id="btn-create-vm" data-toggle="modal" data-target="#createvm-modal">
|
||||
|
|
@ -130,16 +186,5 @@
|
|||
<script type="text/javascript">
|
||||
{% trans "Some problem encountered. Please try again later." as err_msg %}
|
||||
var create_vm_error_message = '{{err_msg|safe}}';
|
||||
window.onload = function () {
|
||||
var locale_dates = document.getElementsByClassName("locale_date");
|
||||
var formats = ['YYYY-MM-DD hh:mm a']
|
||||
var i;
|
||||
for (i = 0; i < locale_dates.length; i++) {
|
||||
var oldDate = moment.utc(locale_dates[i].textContent, formats);
|
||||
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
|
||||
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
|
||||
locale_dates[i].className += ' done';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{%endblock%}
|
||||
|
|
@ -12,9 +12,11 @@ from unittest import skipIf
|
|||
|
||||
from datacenterlight.models import VMTemplate
|
||||
from datacenterlight.tasks import create_vm_task
|
||||
from hosting.models import HostingOrder
|
||||
from membership.models import StripeCustomer
|
||||
from opennebula_api.serializers import VMTemplateSerializer
|
||||
from utils.hosting_utils import get_vm_price
|
||||
from utils.models import BillingAddress
|
||||
from utils.stripe_utils import StripeUtils
|
||||
|
||||
|
||||
|
|
@ -81,11 +83,13 @@ class CeleryTaskTestCase(TestCase):
|
|||
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=self.customer_email,
|
||||
token=self.token)
|
||||
token=self.token
|
||||
)
|
||||
card_details = self.stripe_utils.get_card_details(
|
||||
stripe_customer.stripe_id,
|
||||
self.token)
|
||||
card_details_dict = card_details.get('response_object')
|
||||
stripe_customer.stripe_id
|
||||
)
|
||||
card_details_dict = card_details.get('error')
|
||||
self.assertEquals(card_details_dict, None)
|
||||
billing_address_data = {'cardholder_name': self.customer_name,
|
||||
'postal_code': '1231',
|
||||
'country': 'CH',
|
||||
|
|
@ -101,7 +105,8 @@ class CeleryTaskTestCase(TestCase):
|
|||
disk_size=disk_size)
|
||||
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size)
|
||||
disk_size=disk_size,
|
||||
price=amount_to_be_charged)
|
||||
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
|
||||
ram=memory,
|
||||
ssd=disk_size,
|
||||
|
|
@ -122,10 +127,24 @@ class CeleryTaskTestCase(TestCase):
|
|||
msg = subscription_result.get('error')
|
||||
raise Exception("Creating subscription failed: {}".format(msg))
|
||||
|
||||
billing_address = BillingAddress(
|
||||
cardholder_name=billing_address_data['cardholder_name'],
|
||||
street_address=billing_address_data['street_address'],
|
||||
city=billing_address_data['city'],
|
||||
postal_code=billing_address_data['postal_code'],
|
||||
country=billing_address_data['country']
|
||||
)
|
||||
billing_address.save()
|
||||
|
||||
order = HostingOrder.create(
|
||||
price=specs['price'],
|
||||
vm_id=0,
|
||||
customer=stripe_customer,
|
||||
billing_address=billing_address
|
||||
)
|
||||
|
||||
async_task = create_vm_task.delay(
|
||||
vm_template_id, self.user, specs, template_data,
|
||||
stripe_customer.id, billing_address_data,
|
||||
stripe_subscription_obj.id, card_details_dict
|
||||
vm_template_id, self.user, specs, template_data, order.id
|
||||
)
|
||||
new_vm_id = 0
|
||||
res = None
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import logging
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
from datacenterlight.tasks import create_vm_task
|
||||
from hosting.models import HostingOrder, HostingBill, OrderDetail
|
||||
from membership.models import StripeCustomer
|
||||
from utils.forms import UserBillingAddressForm
|
||||
from utils.models import BillingAddress
|
||||
from .cms_models import CMSIntegration
|
||||
from .models import VMPricing, VMTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_cms_integration(name):
|
||||
|
|
@ -12,3 +21,82 @@ def get_cms_integration(name):
|
|||
except CMSIntegration.DoesNotExist:
|
||||
cms_integration = CMSIntegration.objects.get(name=name, domain=None)
|
||||
return cms_integration
|
||||
|
||||
|
||||
def create_vm(billing_address_data, stripe_customer_id, specs,
|
||||
stripe_subscription_obj, card_details_dict, request,
|
||||
vm_template_id, template, user):
|
||||
billing_address = BillingAddress(
|
||||
cardholder_name=billing_address_data['cardholder_name'],
|
||||
street_address=billing_address_data['street_address'],
|
||||
city=billing_address_data['city'],
|
||||
postal_code=billing_address_data['postal_code'],
|
||||
country=billing_address_data['country']
|
||||
)
|
||||
billing_address.save()
|
||||
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
|
||||
vm_pricing = (
|
||||
VMPricing.get_vm_pricing_by_name(name=specs['pricing_name'])
|
||||
if 'pricing_name' in specs else
|
||||
VMPricing.get_default_pricing()
|
||||
)
|
||||
|
||||
final_price = (
|
||||
specs.get('total_price')
|
||||
if 'total_price' in specs
|
||||
else specs.get('price')
|
||||
)
|
||||
|
||||
# Create a Hosting Order with vm_id = 0, we shall set it later in
|
||||
# celery task once the VM instance is up and running
|
||||
order = HostingOrder.create(
|
||||
price=final_price,
|
||||
customer=customer,
|
||||
billing_address=billing_address,
|
||||
vm_pricing=vm_pricing
|
||||
)
|
||||
|
||||
order_detail_obj, obj_created = OrderDetail.objects.get_or_create(
|
||||
vm_template=VMTemplate.objects.get(
|
||||
opennebula_vm_template_id=vm_template_id
|
||||
),
|
||||
cores=specs['cpu'], memory=specs['memory'], ssd_size=specs['disk_size']
|
||||
)
|
||||
order.order_detail = order_detail_obj
|
||||
order.save()
|
||||
|
||||
# Create a Hosting Bill
|
||||
HostingBill.create(customer=customer, billing_address=billing_address)
|
||||
|
||||
# Create Billing Address for User if he does not have one
|
||||
if not customer.user.billing_addresses.count():
|
||||
billing_address_data.update({
|
||||
'user': customer.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
billing_address_data
|
||||
)
|
||||
billing_address_user_form.is_valid()
|
||||
billing_address_user_form.save()
|
||||
|
||||
# Associate the given stripe subscription with the order
|
||||
order.set_subscription_id(
|
||||
stripe_subscription_obj.id, card_details_dict
|
||||
)
|
||||
|
||||
# Set order status approved
|
||||
order.set_approved()
|
||||
|
||||
create_vm_task.delay(vm_template_id, user, specs, template, order.id)
|
||||
|
||||
clear_all_session_vars(request)
|
||||
|
||||
|
||||
def clear_all_session_vars(request):
|
||||
if request.session is not None:
|
||||
for session_var in ['specs', 'template', 'billing_address',
|
||||
'billing_address_data', 'card_id',
|
||||
'token', 'customer', 'generic_payment_type',
|
||||
'generic_payment_details', 'product_id']:
|
||||
if session_var in request.session:
|
||||
del request.session[session_var]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
|
|
@ -7,24 +6,31 @@ from django.contrib import messages
|
|||
from django.contrib.auth import login, authenticate
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.http import HttpResponseRedirect, JsonResponse, Http404
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.generic import FormView, CreateView, DetailView
|
||||
|
||||
from datacenterlight.tasks import create_vm_task
|
||||
from hosting.forms import HostingUserLoginForm
|
||||
from hosting.models import HostingOrder
|
||||
from hosting.forms import (
|
||||
HostingUserLoginForm, GenericPaymentForm, ProductPaymentForm
|
||||
)
|
||||
from hosting.models import (
|
||||
HostingBill, HostingOrder, UserCardDetail, GenericProduct
|
||||
)
|
||||
from membership.models import CustomUser, StripeCustomer
|
||||
from opennebula_api.serializers import VMTemplateSerializer
|
||||
from utils.forms import BillingAddressForm, BillingAddressFormSignup
|
||||
from utils.forms import (
|
||||
BillingAddressForm, BillingAddressFormSignup, UserBillingAddressForm,
|
||||
BillingAddress
|
||||
)
|
||||
from utils.hosting_utils import get_vm_price_with_vat
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from utils.tasks import send_plain_email_task
|
||||
from .cms_models import DCLCalculatorPluginModel
|
||||
from .forms import ContactForm
|
||||
from .models import VMTemplate, VMPricing
|
||||
from .utils import get_cms_integration
|
||||
from .utils import get_cms_integration, create_vm, clear_all_session_vars
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -58,7 +64,7 @@ class ContactUsView(FormView):
|
|||
sender=form.cleaned_data.get('email')
|
||||
),
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': [from_emails.get(from_page, 'info@ungleich.ch')],
|
||||
'to': [from_emails.get(from_page, 'support@ungleich.ch')],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in form.cleaned_data.items()]),
|
||||
'reply_to': [form.cleaned_data.get('email')],
|
||||
|
|
@ -84,7 +90,29 @@ class IndexView(CreateView):
|
|||
raise ValidationError(_('Invalid number of cores'))
|
||||
|
||||
def validate_memory(self, value):
|
||||
if (value > 200) or (value < 1):
|
||||
if 'pid' in self.request.POST:
|
||||
try:
|
||||
plugin = DCLCalculatorPluginModel.objects.get(
|
||||
id=self.request.POST['pid']
|
||||
)
|
||||
except DCLCalculatorPluginModel.DoesNotExist as dne:
|
||||
logger.error(
|
||||
str(dne) + " plugin_id: " + self.request.POST['pid']
|
||||
)
|
||||
raise ValidationError(_('Invalid calculator properties'))
|
||||
if plugin.enable_512mb_ram:
|
||||
if value % 1 == 0 or value == 0.5:
|
||||
logger.debug(
|
||||
"Given ram {value} is either 0.5 or a"
|
||||
" whole number".format(value=value)
|
||||
)
|
||||
if (value > 200) or (value < 0.5):
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
else:
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
elif (value > 200) or (value < 1) or (value % 1 != 0):
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
else:
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
|
||||
def validate_storage(self, value):
|
||||
|
|
@ -93,17 +121,14 @@ class IndexView(CreateView):
|
|||
|
||||
@cache_control(no_cache=True, must_revalidate=True, no_store=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
for session_var in ['specs', 'user', 'billing_address_data',
|
||||
'pricing_name']:
|
||||
if session_var in request.session:
|
||||
del request.session[session_var]
|
||||
clear_all_session_vars(request)
|
||||
return HttpResponseRedirect(reverse('datacenterlight:cms_index'))
|
||||
|
||||
def post(self, request):
|
||||
cores = request.POST.get('cpu')
|
||||
cores_field = forms.IntegerField(validators=[self.validate_cores])
|
||||
memory = request.POST.get('ram')
|
||||
memory_field = forms.IntegerField(validators=[self.validate_memory])
|
||||
memory_field = forms.FloatField(validators=[self.validate_memory])
|
||||
storage = request.POST.get('storage')
|
||||
storage_field = forms.IntegerField(validators=[self.validate_storage])
|
||||
template_id = int(request.POST.get('config'))
|
||||
|
|
@ -158,7 +183,7 @@ class IndexView(CreateView):
|
|||
)
|
||||
return HttpResponseRedirect(referer_url + "#order_form")
|
||||
|
||||
price, vat, vat_percent = get_vm_price_with_vat(
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=cores,
|
||||
memory=memory,
|
||||
ssd_size=storage,
|
||||
|
|
@ -171,7 +196,8 @@ class IndexView(CreateView):
|
|||
'price': price,
|
||||
'vat': vat,
|
||||
'vat_percent': vat_percent,
|
||||
'total_price': price + vat,
|
||||
'discount': discount,
|
||||
'total_price': round(price + vat - discount['amount'], 2),
|
||||
'pricing_name': vm_pricing_name
|
||||
}
|
||||
request.session['specs'] = specs
|
||||
|
|
@ -223,19 +249,15 @@ class PaymentOrderView(FormView):
|
|||
billing_address_form = BillingAddressForm(
|
||||
instance=self.request.user.billing_addresses.first()
|
||||
)
|
||||
# Get user last order
|
||||
last_hosting_order = HostingOrder.objects.filter(
|
||||
customer__user=self.request.user
|
||||
).last()
|
||||
|
||||
# If user has already an hosting order, get the credit card
|
||||
# data from it
|
||||
if last_hosting_order:
|
||||
credit_card_data = last_hosting_order.get_cc_data()
|
||||
if credit_card_data:
|
||||
context['credit_card_data'] = credit_card_data
|
||||
else:
|
||||
context['credit_card_data'] = None
|
||||
user = self.request.user
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
stripe_customer = user.stripecustomer
|
||||
else:
|
||||
stripe_customer = None
|
||||
cards_list = UserCardDetail.get_all_cards_list(
|
||||
stripe_customer=stripe_customer
|
||||
)
|
||||
context.update({'cards_list': cards_list})
|
||||
else:
|
||||
billing_address_form = BillingAddressFormSignup(
|
||||
initial=billing_address_data
|
||||
|
|
@ -247,19 +269,93 @@ class PaymentOrderView(FormView):
|
|||
'login_form': HostingUserLoginForm(prefix='login_form'),
|
||||
'billing_address_form': billing_address_form,
|
||||
'cms_integration': get_cms_integration('default'),
|
||||
'vm_pricing': VMPricing.get_vm_pricing_by_name(
|
||||
self.request.session['specs']['pricing_name']
|
||||
)
|
||||
})
|
||||
|
||||
if ('generic_payment_type' in self.request.session and
|
||||
self.request.session['generic_payment_type'] == 'generic'):
|
||||
if 'product_id' in self.request.session:
|
||||
product = GenericProduct.objects.get(
|
||||
id=self.request.session['product_id']
|
||||
)
|
||||
context.update({'generic_payment_form': ProductPaymentForm(
|
||||
prefix='generic_payment_form',
|
||||
initial={'product_name': product.product_name,
|
||||
'amount': float(product.get_actual_price()),
|
||||
'recurring': product.product_is_subscription,
|
||||
'description': product.product_description,
|
||||
},
|
||||
product_id=product.id
|
||||
), })
|
||||
else:
|
||||
context.update({'generic_payment_form': GenericPaymentForm(
|
||||
prefix='generic_payment_form',
|
||||
), })
|
||||
else:
|
||||
context.update({
|
||||
'vm_pricing': VMPricing.get_vm_pricing_by_name(
|
||||
self.request.session['specs']['pricing_name']
|
||||
)
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
@cache_control(no_cache=True, must_revalidate=True, no_store=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'specs' not in request.session:
|
||||
if (('type' in request.GET and request.GET['type'] == 'generic')
|
||||
or 'product_slug' in kwargs):
|
||||
request.session['generic_payment_type'] = 'generic'
|
||||
if 'generic_payment_details' in request.session:
|
||||
request.session.pop('generic_payment_details')
|
||||
request.session.pop('product_id')
|
||||
if 'product_slug' in kwargs:
|
||||
logger.debug("Product slug is " + kwargs['product_slug'])
|
||||
try:
|
||||
product = GenericProduct.objects.get(
|
||||
product_slug=kwargs['product_slug']
|
||||
)
|
||||
except GenericProduct.DoesNotExist as dne:
|
||||
logger.error(
|
||||
"Product '{}' does "
|
||||
"not exist".format(kwargs['product_slug'])
|
||||
)
|
||||
raise Http404()
|
||||
request.session['product_id'] = product.id
|
||||
elif 'specs' not in request.session:
|
||||
return HttpResponseRedirect(reverse('datacenterlight:index'))
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'product' in request.POST:
|
||||
# query for the supplied product
|
||||
product = None
|
||||
try:
|
||||
product = GenericProduct.objects.get(
|
||||
id=request.POST['generic_payment_form-product_name']
|
||||
)
|
||||
except GenericProduct.DoesNotExist as dne:
|
||||
logger.error(
|
||||
"The requested product '{}' does not exist".format(
|
||||
request.POST['generic_payment_form-product_name']
|
||||
)
|
||||
)
|
||||
except GenericProduct.MultipleObjectsReturned as mpe:
|
||||
logger.error(
|
||||
"There seem to be more than one product with "
|
||||
"the name {}".format(
|
||||
request.POST['generic_payment_form-product_name']
|
||||
)
|
||||
)
|
||||
product = GenericProduct.objects.all(
|
||||
product_name=request.
|
||||
POST['generic_payment_form-product_name']
|
||||
).first()
|
||||
if product is None:
|
||||
return JsonResponse({})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'amount': product.get_actual_price(),
|
||||
'isSubscription': product.product_is_subscription
|
||||
})
|
||||
if 'login_form' in request.POST:
|
||||
login_form = HostingUserLoginForm(
|
||||
data=request.POST, prefix='login_form'
|
||||
|
|
@ -270,6 +366,13 @@ class PaymentOrderView(FormView):
|
|||
auth_user = authenticate(email=email, password=password)
|
||||
if auth_user:
|
||||
login(self.request, auth_user)
|
||||
if 'product_slug' in kwargs:
|
||||
return HttpResponseRedirect(
|
||||
reverse('show_product',
|
||||
kwargs={
|
||||
'product_slug': kwargs['product_slug']}
|
||||
)
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:payment')
|
||||
)
|
||||
|
|
@ -286,15 +389,87 @@ class PaymentOrderView(FormView):
|
|||
data=request.POST,
|
||||
)
|
||||
if address_form.is_valid():
|
||||
# Check if we are in a generic payment case and handle the generic
|
||||
# payment details form before we go on to verify payment
|
||||
if ('generic_payment_type' in request.session and
|
||||
self.request.session['generic_payment_type'] == 'generic'):
|
||||
if 'product_id' in request.session:
|
||||
generic_payment_form = ProductPaymentForm(
|
||||
data=request.POST, prefix='generic_payment_form',
|
||||
product_id=request.session['product_id']
|
||||
)
|
||||
else:
|
||||
generic_payment_form = GenericPaymentForm(
|
||||
data=request.POST, prefix='generic_payment_form'
|
||||
)
|
||||
if generic_payment_form.is_valid():
|
||||
logger.debug("Generic payment form is valid.")
|
||||
if 'product_id' in request.session:
|
||||
product = generic_payment_form.product
|
||||
else:
|
||||
product = generic_payment_form.cleaned_data.get(
|
||||
'product_name'
|
||||
)
|
||||
gp_details = {
|
||||
"product_name": product.product_name,
|
||||
"amount": generic_payment_form.cleaned_data.get(
|
||||
'amount'
|
||||
),
|
||||
"recurring": generic_payment_form.cleaned_data.get(
|
||||
'recurring'
|
||||
),
|
||||
"description": generic_payment_form.cleaned_data.get(
|
||||
'description'
|
||||
),
|
||||
"product_id": product.id,
|
||||
"product_slug": product.product_slug
|
||||
}
|
||||
request.session["generic_payment_details"] = (
|
||||
gp_details
|
||||
)
|
||||
else:
|
||||
logger.debug("Generic payment form invalid")
|
||||
context = self.get_context_data()
|
||||
context['generic_payment_form'] = generic_payment_form
|
||||
context['billing_address_form'] = address_form
|
||||
return self.render_to_response(context)
|
||||
token = address_form.cleaned_data.get('token')
|
||||
if token is '':
|
||||
card_id = address_form.cleaned_data.get('card')
|
||||
try:
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
if not request.user.has_perm(
|
||||
'view_usercarddetail', user_card_detail
|
||||
):
|
||||
raise UserCardDetail.DoesNotExist(
|
||||
_("{user} does not have permission to access the "
|
||||
"card").format(user=request.user.email)
|
||||
)
|
||||
except UserCardDetail.DoesNotExist as e:
|
||||
ex = str(e)
|
||||
logger.error("Card Id: {card_id}, Exception: {ex}".format(
|
||||
card_id=card_id, ex=ex
|
||||
)
|
||||
)
|
||||
msg = _("An error occurred. Details: {}".format(ex))
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error'
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:payment') + '#payment_error'
|
||||
)
|
||||
request.session['card_id'] = user_card_detail.id
|
||||
else:
|
||||
request.session['token'] = token
|
||||
if request.user.is_authenticated():
|
||||
this_user = {
|
||||
'email': request.user.email,
|
||||
'name': request.user.name
|
||||
}
|
||||
customer = StripeCustomer.get_or_create(
|
||||
email=this_user.get('email'),
|
||||
token=token)
|
||||
email=this_user.get('email'), token=token
|
||||
)
|
||||
else:
|
||||
user_email = address_form.cleaned_data.get('email')
|
||||
user_name = address_form.cleaned_data.get('name')
|
||||
|
|
@ -342,7 +517,6 @@ class PaymentOrderView(FormView):
|
|||
billing_address_form=address_form
|
||||
)
|
||||
)
|
||||
request.session['token'] = token
|
||||
if type(customer) is StripeCustomer:
|
||||
request.session['customer'] = customer.stripe_id
|
||||
else:
|
||||
|
|
@ -363,48 +537,137 @@ class OrderConfirmationView(DetailView):
|
|||
|
||||
@cache_control(no_cache=True, must_revalidate=True, no_store=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'specs' not in request.session or 'user' not in request.session:
|
||||
context = {}
|
||||
if (('specs' not in request.session or 'user' not in request.session)
|
||||
and 'generic_payment_type' not in request.session):
|
||||
return HttpResponseRedirect(reverse('datacenterlight:index'))
|
||||
if 'token' not in request.session:
|
||||
return HttpResponseRedirect(reverse('datacenterlight:payment'))
|
||||
stripe_api_cus_id = request.session.get('customer')
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_card_details(stripe_api_cus_id,
|
||||
request.session.get(
|
||||
'token'))
|
||||
if not card_details.get('response_object'):
|
||||
msg = card_details.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:payment') + '#payment_error')
|
||||
context = {
|
||||
if 'token' in self.request.session:
|
||||
token = self.request.session['token']
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
token
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
card_details_response = card_details['response_object']
|
||||
context['cc_last4'] = card_details_response['last4']
|
||||
context['cc_brand'] = card_details_response['brand']
|
||||
else:
|
||||
card_id = self.request.session.get('card_id')
|
||||
card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
context['cc_last4'] = card_detail.last4
|
||||
context['cc_brand'] = card_detail.brand
|
||||
|
||||
if ('generic_payment_type' in request.session and
|
||||
self.request.session['generic_payment_type'] == 'generic'):
|
||||
context.update({
|
||||
'generic_payment_details':
|
||||
request.session['generic_payment_details'],
|
||||
})
|
||||
else:
|
||||
context.update({
|
||||
'vm': request.session.get('specs'),
|
||||
})
|
||||
context.update({
|
||||
'site_url': reverse('datacenterlight:index'),
|
||||
'cc_last4': card_details.get('response_object').get('last4'),
|
||||
'cc_brand': card_details.get('response_object').get('brand'),
|
||||
'vm': request.session.get('specs'),
|
||||
'page_header_text': _('Confirm Order'),
|
||||
'billing_address_data': (
|
||||
request.session.get('billing_address_data')
|
||||
),
|
||||
'cms_integration': get_cms_integration('default')
|
||||
}
|
||||
'cms_integration': get_cms_integration('default'),
|
||||
})
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
template = request.session.get('template')
|
||||
specs = request.session.get('specs')
|
||||
user = request.session.get('user')
|
||||
stripe_api_cus_id = request.session.get('customer')
|
||||
vm_template_id = template.get('id', 1)
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_card_details(stripe_api_cus_id,
|
||||
request.session.get(
|
||||
'token'))
|
||||
if not card_details.get('response_object'):
|
||||
msg = card_details.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
|
||||
if 'token' in request.session:
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
request.session.get('token')
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
msg = card_details.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=(reverse(
|
||||
'show_product',
|
||||
kwargs={'product_slug':
|
||||
request.session['generic_payment_details']
|
||||
['product_slug']}
|
||||
) if 'generic_payment_details' in request.session else
|
||||
reverse('datacenterlight:payment')
|
||||
),
|
||||
section='payment_error'),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be'
|
||||
' redirected back to the payment page.')
|
||||
)
|
||||
}
|
||||
return JsonResponse(response)
|
||||
card_details_response = card_details['response_object']
|
||||
card_details_dict = {
|
||||
'last4': card_details_response['last4'],
|
||||
'brand': card_details_response['brand'],
|
||||
'card_id': card_details_response['card_id']
|
||||
}
|
||||
stripe_customer_obj = StripeCustomer.objects.filter(
|
||||
stripe_id=stripe_api_cus_id).first()
|
||||
if stripe_customer_obj:
|
||||
ucd = UserCardDetail.get_user_card_details(
|
||||
stripe_customer_obj, card_details_response
|
||||
)
|
||||
if not ucd:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
stripe_api_cus_id, request.session['token'],
|
||||
set_as_default=True
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=(reverse(
|
||||
'show_product',
|
||||
kwargs={'product_slug':
|
||||
request.session
|
||||
['generic_payment_details']
|
||||
['product_slug']}
|
||||
) if 'generic_payment_details' in
|
||||
request.session else
|
||||
reverse('datacenterlight:payment')
|
||||
),
|
||||
section='payment_error'),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected'
|
||||
' back to the payment page.')
|
||||
)
|
||||
}
|
||||
return JsonResponse(response)
|
||||
elif 'card_id' in request.session:
|
||||
card_id = request.session.get('card_id')
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
card_details_dict = {
|
||||
'last4': user_card_detail.last4,
|
||||
'brand': user_card_detail.brand,
|
||||
'card_id': user_card_detail.card_id
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
|
|
@ -416,49 +679,124 @@ class OrderConfirmationView(DetailView):
|
|||
' On close of this popup, you will be redirected back to'
|
||||
' the payment page.'))
|
||||
}
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
card_details_dict = card_details.get('response_object')
|
||||
cpu = specs.get('cpu')
|
||||
memory = specs.get('memory')
|
||||
disk_size = specs.get('disk_size')
|
||||
amount_to_be_charged = specs.get('total_price')
|
||||
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size)
|
||||
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
|
||||
ram=memory,
|
||||
ssd=disk_size,
|
||||
version=1,
|
||||
app='dcl')
|
||||
stripe_plan = stripe_utils.get_or_create_stripe_plan(
|
||||
amount=amount_to_be_charged,
|
||||
name=plan_name,
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
subscription_result = stripe_utils.subscribe_customer_to_plan(
|
||||
stripe_api_cus_id,
|
||||
[{"plan": stripe_plan.get(
|
||||
'response_object').stripe_plan_id}])
|
||||
stripe_subscription_obj = subscription_result.get('response_object')
|
||||
# Check if the subscription was approved and is active
|
||||
if (stripe_subscription_obj is None
|
||||
or stripe_subscription_obj.status != 'active'):
|
||||
msg = subscription_result.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=reverse('datacenterlight:payment'),
|
||||
section='payment_error'),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected back to'
|
||||
' the payment page.'))
|
||||
}
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
return JsonResponse(response)
|
||||
|
||||
if ('generic_payment_type' in request.session and
|
||||
self.request.session['generic_payment_type'] == 'generic'):
|
||||
gp_details = self.request.session['generic_payment_details']
|
||||
if gp_details['recurring']:
|
||||
# generic recurring payment
|
||||
logger.debug("Commencing a generic recurring payment")
|
||||
else:
|
||||
# generic one time payment
|
||||
logger.debug("Commencing a one time payment")
|
||||
charge_response = stripe_utils.make_charge(
|
||||
amount=gp_details['amount'],
|
||||
customer=stripe_api_cus_id
|
||||
)
|
||||
stripe_onetime_charge = charge_response.get('response_object')
|
||||
|
||||
# Check if the payment was approved
|
||||
if not stripe_onetime_charge:
|
||||
msg = charge_response.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=(reverse('show_product', kwargs={
|
||||
'product_slug': gp_details['product_slug']}
|
||||
) if 'generic_payment_details' in
|
||||
request.session else
|
||||
reverse('datacenterlight:payment')
|
||||
),
|
||||
section='payment_error'),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected'
|
||||
' back to the payment page.'))
|
||||
}
|
||||
return JsonResponse(response)
|
||||
|
||||
if ('generic_payment_type' not in request.session or
|
||||
(request.session['generic_payment_details']['recurring'])):
|
||||
if 'generic_payment_details' in request.session:
|
||||
amount_to_be_charged = (
|
||||
round(
|
||||
request.session['generic_payment_details']['amount'],
|
||||
2
|
||||
)
|
||||
)
|
||||
plan_name = "generic-{0}-{1:.2f}".format(
|
||||
request.session['generic_payment_details']['product_id'],
|
||||
amount_to_be_charged
|
||||
)
|
||||
stripe_plan_id = plan_name
|
||||
else:
|
||||
template = request.session.get('template')
|
||||
specs = request.session.get('specs')
|
||||
vm_template_id = template.get('id', 1)
|
||||
|
||||
cpu = specs.get('cpu')
|
||||
memory = specs.get('memory')
|
||||
disk_size = specs.get('disk_size')
|
||||
amount_to_be_charged = specs.get('total_price')
|
||||
plan_name = StripeUtils.get_stripe_plan_name(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
price=amount_to_be_charged
|
||||
)
|
||||
stripe_plan_id = StripeUtils.get_stripe_plan_id(
|
||||
cpu=cpu,
|
||||
ram=memory,
|
||||
ssd=disk_size,
|
||||
version=1,
|
||||
app='dcl',
|
||||
price=amount_to_be_charged
|
||||
)
|
||||
stripe_plan = stripe_utils.get_or_create_stripe_plan(
|
||||
amount=amount_to_be_charged,
|
||||
name=plan_name,
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
subscription_result = stripe_utils.subscribe_customer_to_plan(
|
||||
stripe_api_cus_id,
|
||||
[{"plan": stripe_plan.get(
|
||||
'response_object').stripe_plan_id}])
|
||||
stripe_subscription_obj = subscription_result.get('response_object')
|
||||
# Check if the subscription was approved and is active
|
||||
if (stripe_subscription_obj is None
|
||||
or stripe_subscription_obj.status != 'active'):
|
||||
# At this point, we have created a Stripe API card and
|
||||
# associated it with the customer; but the transaction failed
|
||||
# due to some reason. So, we would want to dissociate this card
|
||||
# here.
|
||||
# ...
|
||||
|
||||
msg = subscription_result.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=(reverse(
|
||||
'show_product',
|
||||
kwargs={'product_slug':
|
||||
request.session['generic_payment_details']
|
||||
['product_slug']}
|
||||
) if 'generic_payment_details' in request.session else
|
||||
reverse('datacenterlight:payment')
|
||||
),
|
||||
section='payment_error'
|
||||
),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected back to'
|
||||
' the payment page.'))
|
||||
}
|
||||
return JsonResponse(response)
|
||||
|
||||
# Create user if the user is not logged in and if he is not already
|
||||
# registered
|
||||
|
|
@ -498,12 +836,148 @@ class OrderConfirmationView(DetailView):
|
|||
stripe_customer_id = request.user.stripecustomer.id
|
||||
custom_user = request.user
|
||||
|
||||
if 'token' in request.session:
|
||||
ucd = UserCardDetail.get_or_create_user_card_detail(
|
||||
stripe_customer=self.request.user.stripecustomer,
|
||||
card_details=card_details_response
|
||||
)
|
||||
UserCardDetail.save_default_card_local(
|
||||
self.request.user.stripecustomer.stripe_id,
|
||||
ucd.card_id
|
||||
)
|
||||
else:
|
||||
card_id = request.session.get('card_id')
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
card_details_dict = {
|
||||
'last4': user_card_detail.last4,
|
||||
'brand': user_card_detail.brand,
|
||||
'card_id': user_card_detail.card_id
|
||||
}
|
||||
if not user_card_detail.preferred:
|
||||
UserCardDetail.set_default_card(
|
||||
stripe_api_cus_id=stripe_api_cus_id,
|
||||
stripe_source_id=user_card_detail.card_id
|
||||
)
|
||||
|
||||
# Save billing address
|
||||
billing_address_data = request.session.get('billing_address_data')
|
||||
logger.debug('billing_address_data is {}'.format(billing_address_data))
|
||||
billing_address_data.update({
|
||||
'user': custom_user.id
|
||||
})
|
||||
|
||||
if 'generic_payment_type' in request.session:
|
||||
stripe_cus = StripeCustomer.objects.filter(
|
||||
stripe_id=stripe_api_cus_id
|
||||
).first()
|
||||
billing_address = BillingAddress(
|
||||
cardholder_name=billing_address_data['cardholder_name'],
|
||||
street_address=billing_address_data['street_address'],
|
||||
city=billing_address_data['city'],
|
||||
postal_code=billing_address_data['postal_code'],
|
||||
country=billing_address_data['country']
|
||||
)
|
||||
billing_address.save()
|
||||
|
||||
order = HostingOrder.create(
|
||||
price=self.request
|
||||
.session['generic_payment_details']['amount'],
|
||||
customer=stripe_cus,
|
||||
billing_address=billing_address,
|
||||
vm_pricing=VMPricing.get_default_pricing()
|
||||
)
|
||||
|
||||
# Create a Hosting Bill
|
||||
HostingBill.create(customer=stripe_cus,
|
||||
billing_address=billing_address)
|
||||
|
||||
# Create Billing Address for User if he does not have one
|
||||
if not stripe_cus.user.billing_addresses.count():
|
||||
billing_address_data.update({
|
||||
'user': stripe_cus.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
billing_address_data
|
||||
)
|
||||
billing_address_user_form.is_valid()
|
||||
billing_address_user_form.save()
|
||||
|
||||
if self.request.session['generic_payment_details']['recurring']:
|
||||
# Associate the given stripe subscription with the order
|
||||
order.set_subscription_id(
|
||||
stripe_subscription_obj.id, card_details_dict
|
||||
)
|
||||
else:
|
||||
# Associate the given stripe charge id with the order
|
||||
order.set_stripe_charge(stripe_onetime_charge)
|
||||
|
||||
# Set order status approved
|
||||
order.set_approved()
|
||||
order.generic_payment_description = gp_details["description"]
|
||||
order.generic_product_id = gp_details["product_id"]
|
||||
order.save()
|
||||
# send emails
|
||||
context = {
|
||||
'name': user.get('name'),
|
||||
'email': user.get('email'),
|
||||
'amount': gp_details['amount'],
|
||||
'description': gp_details['description'],
|
||||
'recurring': gp_details['recurring'],
|
||||
'product_name': gp_details['product_name'],
|
||||
'product_id': gp_details['product_id'],
|
||||
'order_id': order.id
|
||||
}
|
||||
|
||||
email_data = {
|
||||
'subject': (settings.DCL_TEXT +
|
||||
" Payment received from %s" % context['email']),
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': ['info@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in context.items()]),
|
||||
'reply_to': [context['email']],
|
||||
}
|
||||
send_plain_email_task.delay(email_data)
|
||||
|
||||
email_data = {
|
||||
'subject': _("Confirmation of your payment"),
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': [user.get('email')],
|
||||
'body': _("Hi {name},\n\n"
|
||||
"thank you for your order!\n"
|
||||
"We have just received a payment of CHF {amount:.2f}"
|
||||
" from you.{recurring}\n\n"
|
||||
"Cheers,\nYour Data Center Light team".format(
|
||||
name=user.get('name'),
|
||||
amount=gp_details['amount'],
|
||||
recurring=(
|
||||
_(' This is a monthly recurring plan.')
|
||||
if gp_details['recurring'] else ''
|
||||
)
|
||||
)
|
||||
),
|
||||
'reply_to': ['info@ungleich.ch'],
|
||||
}
|
||||
send_plain_email_task.delay(email_data)
|
||||
|
||||
response = {
|
||||
'status': True,
|
||||
'redirect': (
|
||||
reverse('hosting:orders')
|
||||
if request.user.is_authenticated()
|
||||
else reverse('datacenterlight:index')
|
||||
),
|
||||
'msg_title': str(_('Thank you for the payment.')),
|
||||
'msg_body': str(
|
||||
_('You will soon receive a confirmation email of the '
|
||||
'payment. You can always contact us at '
|
||||
'info@ungleich.ch for any question that you may have.')
|
||||
)
|
||||
}
|
||||
clear_all_session_vars(request)
|
||||
|
||||
return JsonResponse(response)
|
||||
|
||||
user = {
|
||||
'name': custom_user.name,
|
||||
'email': custom_user.email,
|
||||
|
|
@ -513,14 +987,11 @@ class OrderConfirmationView(DetailView):
|
|||
'language': get_language(),
|
||||
}
|
||||
|
||||
create_vm_task.delay(vm_template_id, user, specs, template,
|
||||
stripe_customer_id, billing_address_data,
|
||||
stripe_subscription_obj.id, card_details_dict)
|
||||
for session_var in ['specs', 'template', 'billing_address',
|
||||
'billing_address_data',
|
||||
'token', 'customer', 'pricing_name']:
|
||||
if session_var in request.session:
|
||||
del request.session[session_var]
|
||||
create_vm(
|
||||
billing_address_data, stripe_customer_id, specs,
|
||||
stripe_subscription_obj, card_details_dict, request,
|
||||
vm_template_id, template, user
|
||||
)
|
||||
|
||||
response = {
|
||||
'status': True,
|
||||
|
|
@ -536,5 +1007,4 @@ class OrderConfirmationView(DetailView):
|
|||
' it is ready.'))
|
||||
}
|
||||
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
return JsonResponse(response)
|
||||
|
|
|
|||
25
digitalglarus/migrations/0026_auto_20180824_0739.py
Normal file
25
digitalglarus/migrations/0026_auto_20180824_0739.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-08-24 07:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('digitalglarus', '0025_membershiporder_stripe_subscription_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='bookingorder',
|
||||
name='cc_brand',
|
||||
field=models.CharField(blank=True, max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='membershiporder',
|
||||
name='cc_brand',
|
||||
field=models.CharField(blank=True, max_length=128),
|
||||
),
|
||||
]
|
||||
|
|
@ -39,7 +39,7 @@ class Ordereable(models.Model):
|
|||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
approved = models.BooleanField(default=False)
|
||||
last4 = models.CharField(max_length=4, blank=True)
|
||||
cc_brand = models.CharField(max_length=10, blank=True)
|
||||
cc_brand = models.CharField(max_length=128, blank=True)
|
||||
stripe_charge_id = models.CharField(max_length=100, null=True)
|
||||
|
||||
class Meta:
|
||||
|
|
|
|||
|
|
@ -492,6 +492,18 @@ class MembershipPaymentView(LoginRequiredMixin, IsNotMemberMixin, FormView):
|
|||
'membership_dates': membership.type.first_month_formated_range
|
||||
})
|
||||
|
||||
email_to_admin_data = {
|
||||
'subject': "New Digital Glarus subscription: {user}".format(
|
||||
user=self.request.user.email
|
||||
),
|
||||
'from_email': 'info@digitalglarus.ch',
|
||||
'to': ['info@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in
|
||||
order_data.items()]),
|
||||
}
|
||||
send_plain_email_task.delay(email_to_admin_data)
|
||||
|
||||
context = {
|
||||
'membership': membership,
|
||||
'order': membership_order,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
Copyright 2015 ungleich.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
# -*- coding: utf-8 -*-
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# dotenv
|
||||
import dotenv
|
||||
import logging
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -56,6 +55,7 @@ PROJECT_DIR = os.path.abspath(
|
|||
dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR))
|
||||
|
||||
from multisite import SiteID
|
||||
|
||||
SITE_ID = SiteID(default=1)
|
||||
|
||||
APP_ROOT_ENDPOINT = "/"
|
||||
|
|
@ -179,9 +179,7 @@ ROOT_URLCONF = 'dynamicweb.urls'
|
|||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(PROJECT_DIR, 'cms_templates/'),
|
||||
os.path.join(PROJECT_DIR, 'cms_templates/djangocms_blog/'),
|
||||
os.path.join(PROJECT_DIR, 'membership'),
|
||||
'DIRS': [os.path.join(PROJECT_DIR, 'membership'),
|
||||
os.path.join(PROJECT_DIR, 'hosting/templates/'),
|
||||
os.path.join(PROJECT_DIR, 'nosystemd/templates/'),
|
||||
os.path.join(PROJECT_DIR,
|
||||
|
|
@ -192,6 +190,8 @@ TEMPLATES = [
|
|||
os.path.join(PROJECT_DIR,
|
||||
'ungleich_page/templates/ungleich_page'),
|
||||
os.path.join(PROJECT_DIR, 'templates/analytics'),
|
||||
os.path.join(PROJECT_DIR, 'cms_templates/'),
|
||||
os.path.join(PROJECT_DIR, 'cms_templates/djangocms_blog/'),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
|
|
@ -267,6 +267,10 @@ LANGUAGES = (
|
|||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
LOCALE_PATHS = [
|
||||
os.path.join(PROJECT_DIR, 'digitalglarus/locale'),
|
||||
]
|
||||
|
||||
CMS_PLACEHOLDER_CONF = {
|
||||
'logo_image': {
|
||||
'name': 'Logo Image',
|
||||
|
|
@ -352,6 +356,18 @@ CMS_PLACEHOLDER_CONF = {
|
|||
},
|
||||
]
|
||||
},
|
||||
'datacenterlight_calculator': {
|
||||
'name': _('Datacenterlight Calculator'),
|
||||
'plugins': ['DCLCalculatorPlugin'],
|
||||
'default_plugins': [
|
||||
{
|
||||
'plugin_type': 'DCLCalculatorPlugin',
|
||||
'values': {
|
||||
'pricing_id': 1
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
CMS_PERMISSION = True
|
||||
|
|
@ -564,7 +580,6 @@ MULTISITE_FALLBACK_KWARGS = {
|
|||
|
||||
FILER_ENABLE_PERMISSIONS = True
|
||||
|
||||
|
||||
#############################################
|
||||
# configurations for opennebula-integration #
|
||||
#############################################
|
||||
|
|
@ -614,6 +629,7 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = {
|
|||
'ipv6onlyhosting.ch': 'UA-62285904-10',
|
||||
'ipv6onlyhosting.net': 'UA-62285904-10',
|
||||
'ipv6onlyhosting.com': 'UA-62285904-10',
|
||||
'comic.ungleich.ch': 'UA-62285904-13',
|
||||
'127.0.0.1:8000': 'localhost',
|
||||
'dynamicweb-development.ungleich.ch': 'development',
|
||||
'dynamicweb-staging.ungleich.ch': 'staging'
|
||||
|
|
@ -685,6 +701,12 @@ if ENABLE_LOGGING:
|
|||
TEST_MANAGE_SSH_KEY_PUBKEY = env('TEST_MANAGE_SSH_KEY_PUBKEY')
|
||||
TEST_MANAGE_SSH_KEY_HOST = env('TEST_MANAGE_SSH_KEY_HOST')
|
||||
|
||||
X_FRAME_OPTIONS_ALLOW_FROM_URI = env('X_FRAME_OPTIONS_ALLOW_FROM_URI')
|
||||
X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else
|
||||
'ALLOW-FROM {}'.format(
|
||||
X_FRAME_OPTIONS_ALLOW_FROM_URI.strip()
|
||||
))
|
||||
|
||||
DEBUG = bool_env('DEBUG')
|
||||
|
||||
if DEBUG:
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ from django.conf import settings
|
|||
from hosting.views import (
|
||||
RailsHostingView, DjangoHostingView, NodeJSHostingView
|
||||
)
|
||||
from datacenterlight.views import PaymentOrderView
|
||||
from membership import urls as membership_urls
|
||||
from ungleich import views as ungleich_views
|
||||
from ungleich_page.views import LandingView
|
||||
from django.views.generic import RedirectView
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
|
|
@ -30,6 +30,9 @@ urlpatterns = [
|
|||
url(r'^nosystemd/', include('nosystemd.urls', namespace="nosystemd")),
|
||||
url(r'^taggit_autosuggest/', include('taggit_autosuggest.urls')),
|
||||
url(r'^jsi18n/(?P<packages>\S+?)/$', i18n.javascript_catalog),
|
||||
url(r'^product/(?P<product_slug>[\w-]+)/$',
|
||||
PaymentOrderView.as_view(),
|
||||
name='show_product'),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
|
|
@ -57,9 +60,6 @@ urlpatterns += i18n_patterns(
|
|||
url(r'^blog/$',
|
||||
RedirectView.as_view(url=reverse_lazy('ungleich:post-list')),
|
||||
name='blog_list_view'),
|
||||
url(r'^comic/$',
|
||||
ungleich_views.PostListViewUngleich.as_view(category='comic'),
|
||||
name='comic_post_list_view'),
|
||||
url(r'^cms/', include('cms.urls')),
|
||||
url(r'^blog/', include('djangocms_blog.urls', namespace='djangocms_blog')),
|
||||
url(r'^$', RedirectView.as_view(url='/cms') if REDIRECT_TO_CMS
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import HostingOrder, HostingBill, HostingPlan
|
||||
|
||||
from .models import HostingOrder, HostingBill, HostingPlan, GenericProduct
|
||||
|
||||
admin.site.register(HostingOrder)
|
||||
admin.site.register(HostingBill)
|
||||
admin.site.register(HostingPlan)
|
||||
admin.site.register(GenericProduct)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from membership.models import CustomUser
|
||||
from utils.hosting_utils import get_all_public_keys
|
||||
from .models import UserHostingKey
|
||||
from .models import UserHostingKey, GenericProduct
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -52,6 +52,93 @@ class HostingUserLoginForm(forms.Form):
|
|||
raise forms.ValidationError(_("User does not exist"))
|
||||
|
||||
|
||||
class ProductModelChoiceField(forms.ModelChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return obj.product_name
|
||||
|
||||
|
||||
class GenericPaymentForm(forms.Form):
|
||||
product_name = ProductModelChoiceField(
|
||||
queryset=GenericProduct.objects.all().order_by('product_name'),
|
||||
empty_label=_("Choose a product"),
|
||||
)
|
||||
amount = forms.FloatField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': _('Amount in CHF'),
|
||||
'readonly': 'readonly', }
|
||||
),
|
||||
max_value=999999,
|
||||
min_value=1,
|
||||
label=_('Amount in CHF')
|
||||
)
|
||||
recurring = forms.BooleanField(required=False,
|
||||
label=_("Recurring monthly"), )
|
||||
description = forms.CharField(
|
||||
widget=forms.Textarea(attrs={'style': "height: 60px;"}),
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = GenericProduct
|
||||
fields = ['product_name', 'amount', 'recurring', 'description']
|
||||
|
||||
def clean_amount(self):
|
||||
amount = self.cleaned_data.get('amount')
|
||||
if (float(self.cleaned_data.get('product_name').get_actual_price()) !=
|
||||
amount):
|
||||
raise forms.ValidationError(_("Amount field does not match"))
|
||||
return amount
|
||||
|
||||
def clean_recurring(self):
|
||||
recurring = self.cleaned_data.get('recurring')
|
||||
if (self.cleaned_data.get('product_name').product_is_subscription !=
|
||||
(True if recurring else False)):
|
||||
raise forms.ValidationError(_("Recurring field does not match"))
|
||||
return recurring
|
||||
|
||||
|
||||
class ProductPaymentForm(GenericPaymentForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
product_id = kwargs.pop('product_id', None)
|
||||
if product_id is not None:
|
||||
self.product = GenericProduct.objects.get(id=product_id)
|
||||
super(ProductPaymentForm, self).__init__(*args, **kwargs)
|
||||
self.fields['product_name'] = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={'placeholder': _('Product name'),
|
||||
'readonly': 'readonly'}
|
||||
)
|
||||
)
|
||||
if self.product.product_is_subscription:
|
||||
self.fields['amount'].label = "{amt} ({payment_type})".format(
|
||||
amt=_('Amount in CHF'),
|
||||
payment_type=_('Monthly subscription')
|
||||
)
|
||||
else:
|
||||
self.fields['amount'].label = "{amt} ({payment_type})".format(
|
||||
amt=_('Amount in CHF'),
|
||||
payment_type=_('One time payment')
|
||||
)
|
||||
self.fields['recurring'].widget = forms.HiddenInput()
|
||||
self.fields['product_name'].widget.attrs['class'] = 'input-no-border'
|
||||
self.fields['amount'].widget.attrs['class'] = 'input-no-border'
|
||||
self.fields['description'].widget.attrs['class'] = 'input-no-border'
|
||||
|
||||
def clean_amount(self):
|
||||
amount = self.cleaned_data.get('amount')
|
||||
if (self.product is None or
|
||||
float(self.product.get_actual_price()) != amount):
|
||||
raise forms.ValidationError(_("Amount field does not match"))
|
||||
return amount
|
||||
|
||||
def clean_recurring(self):
|
||||
recurring = self.cleaned_data.get('recurring')
|
||||
if (self.product.product_is_subscription !=
|
||||
(True if recurring else False)):
|
||||
raise forms.ValidationError(_("Recurring field does not match"))
|
||||
return recurring
|
||||
|
||||
|
||||
class HostingUserSignupForm(forms.ModelForm):
|
||||
confirm_password = forms.CharField(label=_("Confirm Password"),
|
||||
widget=forms.PasswordInput())
|
||||
|
|
@ -111,7 +198,7 @@ class UserHostingKeyForm(forms.ModelForm):
|
|||
public_key=openssh_pubkey_str).first().name
|
||||
KEY_EXISTS_MESSAGE = _(
|
||||
"This key exists already with the name \"%(name)s\"") % {
|
||||
'name': key_name}
|
||||
'name': key_name}
|
||||
raise forms.ValidationError(KEY_EXISTS_MESSAGE)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-12-21 00:23+0000\n"
|
||||
"POT-Creation-Date: 2018-09-08 08:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -209,7 +209,7 @@ msgstr "Du hast eine neue virtuelle Maschine bestellt!"
|
|||
|
||||
#, python-format
|
||||
msgid "Your order of <strong>%(vm_name)s</strong> has been charged."
|
||||
msgstr "Deine Bestellung von <strong>%(vm_name)s</strong> wurde erhoben."
|
||||
msgstr "Deine Bestellung von <strong>%(vm_name)s</strong> wurde entgegengenommen."
|
||||
|
||||
msgid "You can view your VM detail by clicking the button below."
|
||||
msgstr "Um die Rechnung zu sehen, klicke auf den Button unten."
|
||||
|
|
@ -222,7 +222,7 @@ msgstr "Dein Data Center Light Team"
|
|||
|
||||
#, python-format
|
||||
msgid "Your order of %(vm_name)s has been charged."
|
||||
msgstr "Deine Bestellung von %(vm_name)s wurde erhoben."
|
||||
msgstr "Deine Bestellung von %(vm_name)s wurde entgegengenommen."
|
||||
|
||||
msgid "You can view your VM detail by following the link below."
|
||||
msgstr "Um die Rechnung zu sehen, klicke auf den Link unten."
|
||||
|
|
@ -249,7 +249,7 @@ msgstr "VM Kündigung"
|
|||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You are receiving this email because your virutal machine <strong>"
|
||||
"You are receiving this email because your virtual machine <strong>"
|
||||
"%(vm_name)s</strong> has been cancelled."
|
||||
msgstr ""
|
||||
"Du erhälst diese E-Mail, da deine virtuelle Maschine <strong>%(vm_name)s</"
|
||||
|
|
@ -265,7 +265,7 @@ msgstr "NEUE VM"
|
|||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You are receiving this email because your virutal machine %(vm_name)s has "
|
||||
"You are receiving this email because your virtual machine %(vm_name)s has "
|
||||
"been cancelled."
|
||||
msgstr ""
|
||||
"Du erhälst diese E-Mail, da deine virtuelle Maschine %(vm_name)s gekündigt "
|
||||
|
|
@ -274,6 +274,28 @@ msgstr ""
|
|||
msgid "You can always order a new VM by following the link below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Card Number"
|
||||
msgstr "Kreditkartennummer"
|
||||
|
||||
msgid "Expiry Date"
|
||||
msgstr "Ablaufdatum"
|
||||
|
||||
msgid "CVC"
|
||||
msgstr ""
|
||||
|
||||
msgid "Card Type"
|
||||
msgstr "Kartentyp"
|
||||
|
||||
msgid ""
|
||||
"You are not making any payment yet. After placing your order, you will be "
|
||||
"taken to the Submit Payment Page."
|
||||
msgstr ""
|
||||
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
|
||||
"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
|
||||
|
||||
msgid "SUBMIT"
|
||||
msgstr "ABSENDEN"
|
||||
|
||||
msgid "Toggle navigation"
|
||||
msgstr "Umschalten"
|
||||
|
||||
|
|
@ -365,15 +387,24 @@ msgstr "Arbeitsspeicher"
|
|||
msgid "Disk space"
|
||||
msgstr "Festplattenkapazität"
|
||||
|
||||
msgid "Subtotal"
|
||||
msgstr "Zwischensumme"
|
||||
|
||||
msgid "VAT"
|
||||
msgstr "Mehrwertsteuer"
|
||||
|
||||
msgid "Discount"
|
||||
msgstr "Rabatt"
|
||||
|
||||
msgid "Total"
|
||||
msgstr "Gesamt"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this plan will charge your credit card account "
|
||||
"with the fee of %(vm_price)sCHF/month"
|
||||
"with %(vm_price)s CHF/month"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)sCHF "
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)s CHF "
|
||||
"pro Monat belastet"
|
||||
|
||||
msgid "Place order"
|
||||
|
|
@ -421,9 +452,26 @@ msgstr "Konfiguration"
|
|||
msgid "including VAT"
|
||||
msgstr "inkl. Mehrwertsteuer"
|
||||
|
||||
msgid "excluding VAT"
|
||||
msgstr "exkl. Mehrwertsteuer"
|
||||
|
||||
msgid "Will be applied at checkout"
|
||||
msgstr "wird an der Kasse angewendet"
|
||||
|
||||
msgid "Billing Address"
|
||||
msgstr "Rechnungsadresse"
|
||||
|
||||
msgid ""
|
||||
"Please select one of the cards that you used before or fill in your credit "
|
||||
"card information below. We are using <a href=\"https://stripe.com\" target="
|
||||
"\"_blank\">Stripe</a> for payment and do not store your information in our "
|
||||
"database."
|
||||
msgstr ""
|
||||
"Bitte wähle eine der zuvor genutzten Kreditkarten oder gib Deine "
|
||||
"Kreditkartendetails unten an. Die Bezahlung wird über <a href=\"https://"
|
||||
"stripe.com\" target=\"_blank\">Stripe</a> abgewickelt. Wir speichern Deine "
|
||||
"Kreditkartendetails nicht in unserer Datenbank."
|
||||
|
||||
msgid ""
|
||||
"Please fill in your credit card information below. We are using <a href="
|
||||
"\"https://stripe.com\" target=\"_blank\">Stripe</a> for payment and do not "
|
||||
|
|
@ -433,28 +481,24 @@ msgstr ""
|
|||
"\"https://stripe.com\" target=\"_blank\">Stripe</a> für die Bezahlung und "
|
||||
"speichern keine Informationen in unserer Datenbank."
|
||||
|
||||
msgid ""
|
||||
"You are not making any payment yet. After submitting your card information, "
|
||||
"you will be taken to the Confirm Order Page."
|
||||
msgstr ""
|
||||
"Es wird noch keine Bezahlung vorgenommen. Die Bezahlung wird erst ausgelöst, "
|
||||
"nachdem Du die Bestellung auf der nächsten Seite bestätigt hast."
|
||||
msgid "Last"
|
||||
msgstr "Letzten"
|
||||
|
||||
msgid "SUBMIT"
|
||||
msgstr "ABSENDEN"
|
||||
|
||||
msgid "Card Number"
|
||||
msgstr "Kreditkartennummer"
|
||||
|
||||
msgid "Expiry Date"
|
||||
msgstr "Ablaufdatum"
|
||||
|
||||
msgid "CVC"
|
||||
msgstr ""
|
||||
|
||||
msgid "Card Type"
|
||||
msgid "Type"
|
||||
msgstr "Kartentyp"
|
||||
|
||||
msgid "SELECT"
|
||||
msgstr "AUSWÄHLEN"
|
||||
|
||||
msgid "Add a new credit card"
|
||||
msgstr "Eine neue Kreditkarte hinzufügen"
|
||||
|
||||
msgid "NEW CARD"
|
||||
msgstr "BEARBEITEN"
|
||||
|
||||
msgid "New Credit Card"
|
||||
msgstr "Neue Kreditkarte"
|
||||
|
||||
msgid "Processing"
|
||||
msgstr "Weiter"
|
||||
|
||||
|
|
@ -468,13 +512,22 @@ msgid "Password reset"
|
|||
msgstr "Passwort zurücksetzen"
|
||||
|
||||
msgid "UPDATE"
|
||||
msgstr ""
|
||||
msgstr "AKTUALISIEREN"
|
||||
|
||||
msgid "Last"
|
||||
msgstr ""
|
||||
msgid "REMOVE CARD"
|
||||
msgstr "KARTE ENTFERNEN"
|
||||
|
||||
msgid "Type"
|
||||
msgstr "Kartentyp"
|
||||
msgid "Remove Card"
|
||||
msgstr "Karte entfernen"
|
||||
|
||||
msgid "Do you want to remove this associated card?"
|
||||
msgstr "Möchtest Du den Schlüssel löschen?"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
msgid "DEFAULT"
|
||||
msgstr "STANDARD"
|
||||
|
||||
msgid "No Credit Cards Added"
|
||||
msgstr "Es wurde keine Kreditkarte hinzugefügt"
|
||||
|
|
@ -519,10 +572,7 @@ msgid "Public Key"
|
|||
msgstr ""
|
||||
|
||||
msgid "Private Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
msgstr "Privater Schlüssel"
|
||||
|
||||
msgid "Delete SSH Key"
|
||||
msgstr "SSH Key löschen"
|
||||
|
|
@ -580,6 +630,12 @@ msgstr ""
|
|||
"Bitte entschuldige, es scheint ein unerwarteter Fehler aufgetreten zu sein. "
|
||||
"Versuche es doch bitte noch einmal."
|
||||
|
||||
msgid "Attention:"
|
||||
msgstr "Achtung:"
|
||||
|
||||
msgid "terminating VM can not be reverted."
|
||||
msgstr "Das Beenden kann nicht rückgängig gemacht werden."
|
||||
|
||||
msgid "Something doesn't work?"
|
||||
msgstr "Etwas funktioniert nicht?"
|
||||
|
||||
|
|
@ -592,8 +648,12 @@ msgstr "KONTAKT"
|
|||
msgid "Terminate your Virtual Machine"
|
||||
msgstr "Deine Virtuelle Maschine beenden"
|
||||
|
||||
msgid "Do you want to cancel your Virtual Machine"
|
||||
msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
|
||||
msgid ""
|
||||
"Terminated VMs can not be revived and will not be refunded. Do you want to "
|
||||
"terminate your VM?"
|
||||
msgstr ""
|
||||
"Beendete VMs können nicht wiederhergestellt oder erstattet werden. Möchtest "
|
||||
"du die VM beenden?"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
|
@ -655,6 +715,36 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden."
|
|||
msgid "The reset password link is no longer valid."
|
||||
msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig."
|
||||
|
||||
msgid "Card deassociation successful"
|
||||
msgstr "Die Verbindung mit der Karte wurde erfolgreich aufgehoben"
|
||||
|
||||
msgid "You are not permitted to do this operation"
|
||||
msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen"
|
||||
|
||||
msgid "The selected card does not exist"
|
||||
msgstr "Die ausgewählte Karte existiert nicht"
|
||||
|
||||
msgid "Billing address updated successfully"
|
||||
msgstr "Die Rechnungsadresse wurde erfolgreich aktualisiert"
|
||||
|
||||
msgid "You seem to have already added this card"
|
||||
msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "An error occurred while associating the card. Details: {details}"
|
||||
msgstr ""
|
||||
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
|
||||
|
||||
msgid "Successfully associated the card with your account"
|
||||
msgstr "Die Karte wurde erfolgreich mit deinem Konto verbunden"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{user} does not have permission to access the card"
|
||||
msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
|
||||
|
||||
msgid "An error occurred. Details: {}"
|
||||
msgstr "Ein Fehler ist aufgetreten. Details: {}"
|
||||
|
||||
msgid "Invalid credit card"
|
||||
msgstr "Ungültige Kreditkarte"
|
||||
|
||||
|
|
@ -699,6 +789,10 @@ msgstr "Ungültige RAM-Grösse"
|
|||
msgid "Invalid storage size"
|
||||
msgstr "Ungültige Speicher-Grösse"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Incorrect pricing name. Please contact support{support_email}"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"We could not find the requested VM. Please "
|
||||
"contact Data Center Light Support."
|
||||
|
|
@ -713,6 +807,11 @@ msgstr ""
|
|||
msgid "Error terminating VM"
|
||||
msgstr "Fehler beenden VM"
|
||||
|
||||
msgid ""
|
||||
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
|
||||
"further information."
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Virtual Machine %(vm_name)s Cancelled"
|
||||
msgstr "Virtuelle Maschine %(vm_name)s Kündigung"
|
||||
|
|
@ -722,6 +821,9 @@ msgstr ""
|
|||
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
|
||||
"noch einmal."
|
||||
|
||||
#~ msgid "Do you want to cancel your Virtual Machine"
|
||||
#~ msgstr "Bist Du sicher, dass Du Deine virtuelle Maschine beenden willst"
|
||||
|
||||
#~ msgid "Reset your password"
|
||||
#~ msgstr "Passwort zurücksetzen"
|
||||
|
||||
|
|
@ -788,15 +890,6 @@ msgstr ""
|
|||
#~ msgid "Notifications "
|
||||
#~ msgstr "Benachrichtigungen"
|
||||
|
||||
#~ msgid "REMOVE CARD"
|
||||
#~ msgstr "KARTE ENTFERNEN"
|
||||
|
||||
#~ msgid "EDIT CARD"
|
||||
#~ msgstr "BEARBEITEN"
|
||||
|
||||
#~ msgid "Add a new Card."
|
||||
#~ msgstr "Neue Kreditkarte hinzufügen."
|
||||
|
||||
#~ msgid "You are not making any payment here."
|
||||
#~ msgstr "Es wird noch keine Bezahlung vorgenommen"
|
||||
|
||||
|
|
|
|||
45
hosting/management/commands/import_usercarddetails.py
Normal file
45
hosting/management/commands/import_usercarddetails.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from hosting.models import UserCardDetail
|
||||
from membership.models import CustomUser
|
||||
from utils.stripe_utils import StripeUtils
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Imports the usercard details of all customers. Created just for
|
||||
multiple card support.'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
stripe_utils = StripeUtils()
|
||||
for user in CustomUser.objects.all():
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
if user.stripecustomer:
|
||||
card_details_resp = stripe_utils.get_card_details(
|
||||
user.stripecustomer.stripe_id
|
||||
)
|
||||
card_details = card_details_resp['response_object']
|
||||
if card_details:
|
||||
ucd = UserCardDetail.get_or_create_user_card_detail(
|
||||
stripe_customer=user.stripecustomer,
|
||||
card_details=card_details
|
||||
)
|
||||
UserCardDetail.save_default_card_local(
|
||||
user.stripecustomer.stripe_id,
|
||||
ucd.card_id
|
||||
)
|
||||
print("Saved user card details for {}".format(
|
||||
user.email
|
||||
))
|
||||
else:
|
||||
print(" --- Could not get card details for "
|
||||
"{}".format(user.email))
|
||||
print(" --- Error: {}".format(
|
||||
card_details_resp['error']
|
||||
))
|
||||
else:
|
||||
print(" === {} does not have a StripeCustomer object".format(
|
||||
user.email
|
||||
))
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
||||
35
hosting/migrations/0045_auto_20180701_2028.py
Normal file
35
hosting/migrations/0045_auto_20180701_2028.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-07-01 20:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0024_dclcalculatorpluginmodel_vm_templates_to_show'),
|
||||
('hosting', '0044_hostingorder_vm_pricing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrderDetail',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cores', models.IntegerField(default=0)),
|
||||
('memory', models.IntegerField(default=0)),
|
||||
('hdd_size', models.IntegerField(default=0)),
|
||||
('ssd_size', models.IntegerField(default=0)),
|
||||
('vm_template', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='datacenterlight.VMTemplate')),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='order_detail',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.OrderDetail'),
|
||||
),
|
||||
]
|
||||
36
hosting/migrations/0046_usercarddetail.py
Normal file
36
hosting/migrations/0046_usercarddetail.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-07-03 20:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('membership', '0007_auto_20180213_0128'),
|
||||
('hosting', '0045_auto_20180701_2028'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserCardDetail',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last4', models.CharField(max_length=4)),
|
||||
('brand', models.CharField(max_length=10)),
|
||||
('card_id', models.CharField(blank=True, default='', max_length=100)),
|
||||
('fingerprint', models.CharField(max_length=100)),
|
||||
('exp_month', models.IntegerField()),
|
||||
('exp_year', models.IntegerField()),
|
||||
('preferred', models.BooleanField(default=False)),
|
||||
('stripe_customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.StripeCustomer')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('view_usercarddetail', 'View User Card'),),
|
||||
},
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
]
|
||||
25
hosting/migrations/0047_auto_20180821_1240.py
Normal file
25
hosting/migrations/0047_auto_20180821_1240.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-08-21 12:40
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0046_usercarddetail'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='hostingorder',
|
||||
name='cc_brand',
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usercarddetail',
|
||||
name='brand',
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
]
|
||||
41
hosting/migrations/0048_auto_20181003_0757.py
Normal file
41
hosting/migrations/0048_auto_20181003_0757.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-10-03 07:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0047_auto_20180821_1240'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GenericProduct',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('product_name', models.CharField(default='', max_length=128)),
|
||||
('product_slug', models.SlugField(help_text='An optional html id for the Section. Required to set as target of a link on page', unique=True)),
|
||||
('product_description', models.CharField(default='', max_length=500)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('product_price', models.DecimalField(decimal_places=2, max_digits=6)),
|
||||
('product_vat', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
|
||||
('product_is_subscription', models.BooleanField(default=True)),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='generic_payment_description',
|
||||
field=models.CharField(max_length=500, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hostingorder',
|
||||
name='generic_product',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hosting.GenericProduct'),
|
||||
),
|
||||
]
|
||||
20
hosting/migrations/0049_auto_20181005_0736.py
Normal file
20
hosting/migrations/0049_auto_20181005_0736.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2018-10-05 07:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0048_auto_20181003_0757'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='genericproduct',
|
||||
name='product_slug',
|
||||
field=models.SlugField(help_text='An mandatory unique slug for the product', unique=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import os
|
||||
import logging
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import os
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from datacenterlight.models import VMPricing
|
||||
from datacenterlight.models import VMPricing, VMTemplate
|
||||
from membership.models import StripeCustomer, CustomUser
|
||||
from utils.models import BillingAddress
|
||||
from utils.mixins import AssignPermissionsMixin
|
||||
from utils.models import BillingAddress
|
||||
from utils.stripe_utils import StripeUtils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -41,6 +42,49 @@ class HostingPlan(models.Model):
|
|||
return price
|
||||
|
||||
|
||||
class OrderDetail(AssignPermissionsMixin, models.Model):
|
||||
vm_template = models.ForeignKey(
|
||||
VMTemplate, blank=True, null=True, default=None,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
cores = models.IntegerField(default=0)
|
||||
memory = models.IntegerField(default=0)
|
||||
hdd_size = models.IntegerField(default=0)
|
||||
ssd_size = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return "Not available" if self.vm_template is None else (
|
||||
"%s - %s, %s cores, %s GB RAM, %s GB SSD" % (
|
||||
self.vm_template.name, self.vm_template.vm_type, self.cores,
|
||||
self.memory, self.ssd_size
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class GenericProduct(AssignPermissionsMixin, models.Model):
|
||||
permissions = ('view_genericproduct',)
|
||||
product_name = models.CharField(max_length=128, default="")
|
||||
product_slug = models.SlugField(
|
||||
unique=True,
|
||||
help_text=(
|
||||
'An mandatory unique slug for the product'
|
||||
)
|
||||
)
|
||||
product_description = models.CharField(max_length=500, default="")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
product_price = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0)
|
||||
product_is_subscription = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
def get_actual_price(self):
|
||||
return round(
|
||||
self.product_price + (self.product_price * self.product_vat), 2
|
||||
)
|
||||
|
||||
|
||||
class HostingOrder(AssignPermissionsMixin, models.Model):
|
||||
ORDER_APPROVED_STATUS = 'Approved'
|
||||
ORDER_DECLINED_STATUS = 'Declined'
|
||||
|
|
@ -51,12 +95,22 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
|
|||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
approved = models.BooleanField(default=False)
|
||||
last4 = models.CharField(max_length=4)
|
||||
cc_brand = models.CharField(max_length=10)
|
||||
cc_brand = models.CharField(max_length=128)
|
||||
stripe_charge_id = models.CharField(max_length=100, null=True)
|
||||
price = models.FloatField()
|
||||
subscription_id = models.CharField(max_length=100, null=True)
|
||||
vm_pricing = models.ForeignKey(VMPricing)
|
||||
|
||||
order_detail = models.ForeignKey(
|
||||
OrderDetail, null=True, blank=True, default=None,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
generic_product = models.ForeignKey(
|
||||
GenericProduct, null=True, blank=True, default=None,
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
generic_payment_description = models.CharField(
|
||||
max_length=500, null=True
|
||||
)
|
||||
permissions = ('view_hostingorder',)
|
||||
|
||||
class Meta:
|
||||
|
|
@ -65,14 +119,25 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % (self.id)
|
||||
hosting_order_str = ("Order Nr: #{} - VM_ID: {} - {} - {} - "
|
||||
"Specs: {} - Price: {}").format(
|
||||
self.id, self.vm_id, self.customer.user.email, self.created_at,
|
||||
self.order_detail, self.price
|
||||
)
|
||||
if self.generic_product_id is not None:
|
||||
hosting_order_str += " - Generic Payment"
|
||||
if self.stripe_charge_id is not None:
|
||||
hosting_order_str += " - One time charge"
|
||||
else:
|
||||
hosting_order_str += " - Recurring"
|
||||
return hosting_order_str
|
||||
|
||||
@cached_property
|
||||
def status(self):
|
||||
return self.ORDER_APPROVED_STATUS if self.approved else self.ORDER_DECLINED_STATUS
|
||||
|
||||
@classmethod
|
||||
def create(cls, price=None, vm_id=None, customer=None,
|
||||
def create(cls, price=None, vm_id=0, customer=None,
|
||||
billing_address=None, vm_pricing=None):
|
||||
instance = cls.objects.create(
|
||||
price=price,
|
||||
|
|
@ -184,3 +249,156 @@ class VMDetail(models.Model):
|
|||
months = relativedelta(end_date, self.created_at).months or 1
|
||||
end_date = self.created_at + relativedelta(months=months, days=-1)
|
||||
return end_date
|
||||
|
||||
|
||||
class UserCardDetail(AssignPermissionsMixin, models.Model):
|
||||
permissions = ('view_usercarddetail',)
|
||||
stripe_customer = models.ForeignKey(StripeCustomer)
|
||||
last4 = models.CharField(max_length=4)
|
||||
brand = models.CharField(max_length=128)
|
||||
card_id = models.CharField(max_length=100, blank=True, default='')
|
||||
fingerprint = models.CharField(max_length=100)
|
||||
exp_month = models.IntegerField(null=False)
|
||||
exp_year = models.IntegerField(null=False)
|
||||
preferred = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
('view_usercarddetail', 'View User Card'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, stripe_customer=None, last4=None, brand=None,
|
||||
fingerprint=None, exp_month=None, exp_year=None, card_id=None,
|
||||
preferred=False):
|
||||
instance = cls.objects.create(
|
||||
stripe_customer=stripe_customer, last4=last4, brand=brand,
|
||||
fingerprint=fingerprint, exp_month=exp_month, exp_year=exp_year,
|
||||
card_id=card_id, preferred=preferred
|
||||
)
|
||||
instance.assign_permissions(stripe_customer.user)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_all_cards_list(cls, stripe_customer):
|
||||
"""
|
||||
Get all the cards of the given customer as a list
|
||||
|
||||
:param stripe_customer: The StripeCustomer object
|
||||
:return: A list of all cards; an empty list if the customer object is
|
||||
None
|
||||
"""
|
||||
cards_list = []
|
||||
if stripe_customer is None:
|
||||
return cards_list
|
||||
user_card_details = UserCardDetail.objects.filter(
|
||||
stripe_customer_id=stripe_customer.id
|
||||
).order_by('-preferred', 'id')
|
||||
for card in user_card_details:
|
||||
cards_list.append({
|
||||
'last4': card.last4, 'brand': card.brand, 'id': card.id,
|
||||
'preferred': card.preferred
|
||||
})
|
||||
return cards_list
|
||||
|
||||
@classmethod
|
||||
def get_or_create_user_card_detail(cls, stripe_customer, card_details):
|
||||
"""
|
||||
A method that checks if a UserCardDetail object exists already
|
||||
based upon the given card_details and creates it for the given
|
||||
customer if it does not exist. It returns the UserCardDetail object
|
||||
matching the given card_details if it exists.
|
||||
|
||||
:param stripe_customer: The given StripeCustomer object to whom the
|
||||
card object should belong to
|
||||
:param card_details: A dictionary identifying a given card
|
||||
:return: UserCardDetail object
|
||||
"""
|
||||
try:
|
||||
if ('fingerprint' in card_details and 'exp_month' in card_details
|
||||
and 'exp_year' in card_details):
|
||||
card_detail = UserCardDetail.objects.get(
|
||||
stripe_customer=stripe_customer,
|
||||
fingerprint=card_details['fingerprint'],
|
||||
exp_month=card_details['exp_month'],
|
||||
exp_year=card_details['exp_year']
|
||||
)
|
||||
else:
|
||||
raise UserCardDetail.DoesNotExist()
|
||||
except UserCardDetail.DoesNotExist:
|
||||
preferred = False
|
||||
if 'preferred' in card_details:
|
||||
preferred = card_details['preferred']
|
||||
card_detail = UserCardDetail.create(
|
||||
stripe_customer=stripe_customer,
|
||||
last4=card_details['last4'],
|
||||
brand=card_details['brand'],
|
||||
fingerprint=card_details['fingerprint'],
|
||||
exp_month=card_details['exp_month'],
|
||||
exp_year=card_details['exp_year'],
|
||||
card_id=card_details['card_id'],
|
||||
preferred=preferred
|
||||
)
|
||||
return card_detail
|
||||
|
||||
@staticmethod
|
||||
def set_default_card(stripe_api_cus_id, stripe_source_id):
|
||||
"""
|
||||
Sets the given stripe source as the default source for the given
|
||||
Stripe customer
|
||||
:param stripe_api_cus_id: Stripe customer id
|
||||
:param stripe_source_id: The Stripe source id
|
||||
:return:
|
||||
"""
|
||||
stripe_utils = StripeUtils()
|
||||
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
||||
cu = cus_response['response_object']
|
||||
cu.default_source = stripe_source_id
|
||||
cu.save()
|
||||
UserCardDetail.save_default_card_local(
|
||||
stripe_api_cus_id, stripe_source_id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def set_default_card_from_stripe(stripe_api_cus_id):
|
||||
stripe_utils = StripeUtils()
|
||||
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
||||
cu = cus_response['response_object']
|
||||
default_source = cu.default_source
|
||||
if default_source is not None:
|
||||
UserCardDetail.save_default_card_local(
|
||||
stripe_api_cus_id, default_source
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def save_default_card_local(stripe_api_cus_id, card_id):
|
||||
stripe_cust = StripeCustomer.objects.get(stripe_id=stripe_api_cus_id)
|
||||
user_card_detail = UserCardDetail.objects.get(
|
||||
stripe_customer=stripe_cust, card_id=card_id
|
||||
)
|
||||
for card in stripe_cust.usercarddetail_set.all():
|
||||
card.preferred = False
|
||||
card.save()
|
||||
user_card_detail.preferred = True
|
||||
user_card_detail.save()
|
||||
|
||||
@staticmethod
|
||||
def get_user_card_details(stripe_customer, card_details):
|
||||
"""
|
||||
A utility function to check whether a StripeCustomer is already
|
||||
associated with the card having given details
|
||||
|
||||
:param stripe_customer:
|
||||
:param card_details:
|
||||
:return: The UserCardDetails object if it exists, None otherwise
|
||||
"""
|
||||
try:
|
||||
ucd = UserCardDetail.objects.get(
|
||||
stripe_customer=stripe_customer,
|
||||
fingerprint=card_details['fingerprint'],
|
||||
exp_month=card_details['exp_month'],
|
||||
exp_year=card_details['exp_year']
|
||||
)
|
||||
return ucd
|
||||
except UserCardDetail.DoesNotExist:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@
|
|||
margin: 0 auto;
|
||||
max-width: 1120px;
|
||||
}
|
||||
.container-table{
|
||||
margin-top: 35px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.container-table table{
|
||||
overflow-y: auto;
|
||||
}
|
||||
.borderless td {
|
||||
border: none !important;
|
||||
}
|
||||
|
|
@ -35,6 +42,19 @@
|
|||
color: transparent;
|
||||
}
|
||||
|
||||
.inline-headers h3, .inline-headers h4 {
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.space-above {
|
||||
margin-top: 4%;
|
||||
}
|
||||
|
||||
.space-above-big {
|
||||
margin-top: 20%;
|
||||
}
|
||||
|
||||
.table>tbody>tr>td{
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
@ -274,6 +294,26 @@
|
|||
font-size: 16px;
|
||||
}
|
||||
|
||||
.payment-container .credit-card-info {
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.credit-card-info {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.credit-card-info .align-bottom{
|
||||
align-self: flex-end;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.another-card-text {
|
||||
padding: 20px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.credit-card-form {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
|
@ -293,8 +333,15 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.settings-container .credit-card-details-opt {
|
||||
padding-top: 15px;
|
||||
.settings-container .new-card-head {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.settings-container .new-card-head h4 {
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.caps-link .svg-img {
|
||||
|
|
@ -313,7 +360,11 @@
|
|||
.settings-container .btn-vm-contact {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
/* padding: 4px 15px; */
|
||||
}
|
||||
|
||||
.settings-container .choice-btn {
|
||||
letter-spacing: 2px;
|
||||
min-width: 127px;
|
||||
}
|
||||
|
||||
.btn-wide {
|
||||
|
|
@ -355,10 +406,37 @@
|
|||
fill: #999;
|
||||
}
|
||||
|
||||
.card-details-box {
|
||||
border: 1px solid #eee;
|
||||
padding: 5px 25px 25px;
|
||||
}
|
||||
|
||||
.thick-hr {
|
||||
border-top: 5px solid #eee;
|
||||
}
|
||||
|
||||
.locale_date {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.locale_date.done{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.thin-hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.new-card-head {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.new-card-button-margin button{
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
@ -449,230 +449,6 @@ a.unlink:hover {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
/***** DCL payment page **********/
|
||||
.dcl-order-container {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tbl-content {
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
border-bottom: 4px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dcl-order-table-total span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.dcl-place-order-text{
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
text-align: center;
|
||||
color: #000;
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.tbl-total .dcl-price-month {
|
||||
font-size: 16px;
|
||||
text-transform: capitalize;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tbl-no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dcl-billing-sec {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.dcl-order-sec {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.card-warning-content {
|
||||
font-weight: 300;
|
||||
border: 1px solid #a1a1a1;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.card-warning-error {
|
||||
border: 1px solid #EB4D5C;
|
||||
color: #EB4D5C;
|
||||
}
|
||||
|
||||
.card-warning-addtional-margin {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stripe-payment-btn {
|
||||
outline: none;
|
||||
width: auto;
|
||||
float: right;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
position: absolute;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.card-cvc-element label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.card-element {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-element label{
|
||||
width:100%;
|
||||
margin-bottom:0px;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.card-cvc-element .my-input {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#card-errors {
|
||||
clear: both;
|
||||
padding: 0 0 10px;
|
||||
color: #eb4d5c;
|
||||
}
|
||||
|
||||
.credit-card-goup{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.dcl-order-table-total span {
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.dcl-order-sec {
|
||||
padding: 10px 20px 30px 20px;
|
||||
border-bottom: 4px solid #eee;
|
||||
}
|
||||
|
||||
.tbl-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tbl-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
padding: 10px 0;
|
||||
border-bottom: 0px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
padding: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dcl-billing-sec {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-expiry-element {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.card-cvc-element {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#billing-form .form-control {
|
||||
box-shadow: none !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.dcl-order-container {
|
||||
width: 990px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dcl-billing {
|
||||
padding-right: 65px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-creditcard {
|
||||
padding-left: 65px;
|
||||
}
|
||||
|
||||
.tbl-tot {
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.content-dashboard {
|
||||
/*width: auto !important;*/
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1040px) and (min-width: 768px) {
|
||||
.content-dashboard {
|
||||
width: 96% !important;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
margin: 100px auto 40px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
@media(min-width: 768px) {
|
||||
|
|
@ -48,10 +49,6 @@
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.order-detail-container .order-details strong {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
|
@ -60,13 +57,28 @@
|
|||
|
||||
.order-detail-container p {
|
||||
margin-bottom: 5px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container hr {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.order-detail-container .thin-hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.order-detail-container .subtotal-price {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.order-detail-container .subtotal-price .text-primary {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.order-detail-container .total-price {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.order-confirm-btn {
|
||||
text-align: center;
|
||||
|
|
@ -96,4 +108,8 @@
|
|||
|
||||
#virtual_machine_create_form {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.dcl-place-order-text {
|
||||
color: #808080;
|
||||
}
|
||||
|
|
@ -1,19 +1,35 @@
|
|||
|
||||
.payment-container {padding-top:70px; padding-bottom: 11%;}
|
||||
.creditcard-box .panel-title {display: inline;font-weight: bold; font-size:17px;}
|
||||
.creditcard-box .checkbox.pull-right { margin: 0; }
|
||||
.creditcard-box .pl-ziro { padding-left: 0px; }
|
||||
.creditcard-box .form-control.error {
|
||||
border-color: red;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(255,0,0,0.6);
|
||||
.payment-container {
|
||||
padding-top: 70px;
|
||||
padding-bottom: 11%;
|
||||
}
|
||||
|
||||
.creditcard-box .panel-title {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.creditcard-box .checkbox.pull-right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.creditcard-box .pl-ziro {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.creditcard-box .form-control.error {
|
||||
border-color: red;
|
||||
outline: 0;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.creditcard-box label.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
padding: 2px 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.creditcard-box .payment-errors {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
|
|
@ -21,96 +37,221 @@
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* landing page payment new style */
|
||||
.last-p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.dcl-payment-section {
|
||||
max-width: 391px;
|
||||
margin: 0 auto 30px;
|
||||
padding: 0 10px 30px;
|
||||
border-bottom: 1px solid #edebeb;
|
||||
height: 100%;
|
||||
}
|
||||
.dcl-payment-section hr{
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.dcl-payment-section .top-hr {
|
||||
margin-left: -10px;
|
||||
}
|
||||
.dcl-payment-section h3 {
|
||||
font-weight: 600;
|
||||
}
|
||||
.dcl-payment-section p {
|
||||
/*padding: 0 5px;*/
|
||||
font-weight: 400;
|
||||
}
|
||||
.dcl-payment-section .card-warning-content {
|
||||
padding: 8px 10px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.dcl-payment-order strong{
|
||||
font-size: 17px;
|
||||
}
|
||||
.dcl-payment-order p {
|
||||
font-weight: 300;
|
||||
}
|
||||
.dcl-payment-section .form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dcl-payment-section .form-control {
|
||||
box-shadow: none;
|
||||
padding: 6px 12px;
|
||||
height: 32px;
|
||||
}
|
||||
.dcl-payment-user {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
.dcl-order-sec {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.dcl-payment-user h4 {
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
.dcl-billing-sec {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.dcl-order-container {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
border-bottom: 4px solid #eee;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcl-order-table-total span {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
text-align: right;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tbl-no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.card-warning-content {
|
||||
font-weight: 300;
|
||||
border: 1px solid #a1a1a1;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-warning-error {
|
||||
border: 1px solid #EB4D5C;
|
||||
color: #EB4D5C;
|
||||
}
|
||||
|
||||
.card-warning-addtional-margin {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stripe-payment-btn {
|
||||
outline: none;
|
||||
width: auto;
|
||||
float: right;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
position: absolute;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.card-cvc-element label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.card-element {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-element label {
|
||||
width: 100%;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.card-cvc-element .my-input {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#card-errors {
|
||||
clear: both;
|
||||
padding: 0 0 10px;
|
||||
color: #eb4d5c;
|
||||
}
|
||||
|
||||
.credit-card-goup {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.dcl-order-sec {
|
||||
padding: 10px 5px 30px;
|
||||
border-bottom: 4px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-billing-sec {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dcl-billing {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tbl-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total {
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-tot {
|
||||
margin-right: -15px;
|
||||
}
|
||||
|
||||
.tbl-content {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.dcl-order-table-header {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dcl-order-table-content {
|
||||
border-bottom: 0px solid #eee;
|
||||
padding: 10px 0;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total {
|
||||
font-size: 18px;
|
||||
color: #000;
|
||||
padding: 10px 0;
|
||||
border-bottom: 0px solid #eee;
|
||||
}
|
||||
|
||||
.card-expiry-element {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.card-cvc-element {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#billing-form .form-control {
|
||||
box-shadow: none !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dcl-payment-grid {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dcl-payment-box {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
padding: 0 30px;
|
||||
}
|
||||
.dcl-payment-box:nth-child(2) {
|
||||
order: 1;
|
||||
}
|
||||
.dcl-payment-box:nth-child(4) {
|
||||
order: 2;
|
||||
}
|
||||
.dcl-payment-section {
|
||||
padding: 15px 10px;
|
||||
margin-bottom: 0;
|
||||
border-bottom-width: 5px;
|
||||
}
|
||||
.dcl-payment-box:nth-child(2n) .dcl-payment-section {
|
||||
border-bottom: none;
|
||||
}
|
||||
.dcl-payment-box:nth-child(1):after,
|
||||
.dcl-payment-box:nth-child(2):after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
background: #eee;
|
||||
width: 1px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
.dcl-billing {
|
||||
padding-right: 65px;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dcl-creditcard {
|
||||
padding-left: 65px;
|
||||
}
|
||||
|
||||
.dcl-order-table-total .tbl-total,
|
||||
.dcl-order-table-total .tbl-tot {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.tbl-header-center,
|
||||
.tbl-content-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tbl-header-right,
|
||||
.tbl-content-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.dcl-order-container {
|
||||
width: 990px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
/* Create VM calculator */
|
||||
|
||||
.price-calc-section {
|
||||
padding: 80px 40px !important;
|
||||
padding: 20px 0 !important;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -40,19 +42,19 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card {
|
||||
width: 50%;
|
||||
border-radius: 7px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
box-shadow: 1px 3px 6px 2px rgba(0, 0, 0, 0.2);
|
||||
padding-bottom: 30px;
|
||||
text-align: center;
|
||||
max-width: 320px;
|
||||
max-width: 4000px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.price-calc-section .card {
|
||||
margin-left: 0;
|
||||
/* margin-left: 0; */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +87,7 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card .description {
|
||||
padding: 7px 8px 2px;
|
||||
padding: 12px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around !important;
|
||||
|
|
@ -93,7 +95,7 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card .description span {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
margin-left: 5px;
|
||||
/* margin-left: 0px; */
|
||||
/* justify-self: start; */
|
||||
|
|
@ -104,17 +106,18 @@
|
|||
}
|
||||
|
||||
.price-calc-section .card .description .select-number{
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
width: 85px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.price-calc-section .card .description i {
|
||||
color: #29427a;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px 6px 3px;
|
||||
/* border: 1px solid #ccc; */
|
||||
/* padding: 5px 6px 3px; */
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
|
@ -193,7 +196,7 @@
|
|||
.price-calc-section .help-block.with-errors {
|
||||
text-align: center;
|
||||
margin: 0 0;
|
||||
padding: 0 0 5px;
|
||||
padding: 0 0;
|
||||
}
|
||||
.price-calc-section .help-block.with-errors ul {
|
||||
margin-bottom: 0;
|
||||
|
|
@ -209,10 +212,10 @@
|
|||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 18%;
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
height: 1px;
|
||||
width: 65%;
|
||||
width: 100%;
|
||||
background: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -146,9 +146,13 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.vm-vmid-with-warning {
|
||||
padding: 50px 0 33px !important;
|
||||
}
|
||||
|
||||
.vm-vmid .alert {
|
||||
margin-top: 15px;
|
||||
margin-bottom: -60px;
|
||||
margin-bottom: -25px;
|
||||
}
|
||||
|
||||
.vm-item-lg {
|
||||
|
|
@ -183,6 +187,13 @@
|
|||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.vm-terminate-warning {
|
||||
letter-spacing: 0.6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #373636;
|
||||
}
|
||||
|
||||
.vm-contact-us {
|
||||
margin: 25px 0 30px;
|
||||
/* text-align: center; */
|
||||
|
|
@ -269,7 +280,7 @@
|
|||
border: 2px solid #A3C0E2;
|
||||
padding: 5px 25px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1.3px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.btn-vm-contact:hover, .btn-vm-contact:focus {
|
||||
background: #fff;
|
||||
|
|
|
|||
|
|
@ -153,4 +153,125 @@ $( document ).ready(function() {
|
|||
$('.navbar-fixed-top.topnav').css('padding-right', topnavPadding-scrollbarWidth);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------------------------------------------
|
||||
Scripts initialization
|
||||
--------------------------------------------- */
|
||||
var minRam = 1;
|
||||
if(window.minRam){
|
||||
minRam = window.minRam;
|
||||
}
|
||||
var cardPricing = {
|
||||
'cpu': {
|
||||
'id': 'coreValue',
|
||||
'value': 1,
|
||||
'min': 1,
|
||||
'max': 48,
|
||||
'interval': 1
|
||||
},
|
||||
'ram': {
|
||||
'id': 'ramValue',
|
||||
'value': 2,
|
||||
'min': minRam,
|
||||
'max': 200,
|
||||
'interval': 1
|
||||
},
|
||||
'storage': {
|
||||
'id': 'storageValue',
|
||||
'value': 10,
|
||||
'min': 10,
|
||||
'max': 2000,
|
||||
'interval': 10
|
||||
}
|
||||
};
|
||||
|
||||
function _initPricing() {
|
||||
_fetchPricing();
|
||||
|
||||
$('.fa-minus-circle.left').click(function(event) {
|
||||
var data = $(this).data('minus');
|
||||
|
||||
if (cardPricing[data].value > cardPricing[data].min) {
|
||||
if(data === 'ram' && String(cardPricing[data].value) === "1" && minRam === 0.5){
|
||||
cardPricing[data].value = 0.5;
|
||||
$('#ramValue').val('0.5');
|
||||
$("#ramValue").attr('step', 0.5);
|
||||
} else {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) - cardPricing[data].interval;
|
||||
}
|
||||
}
|
||||
_fetchPricing();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
});
|
||||
$('.fa-plus-circle.right').click(function(event) {
|
||||
var data = $(this).data('plus');
|
||||
if (cardPricing[data].value < cardPricing[data].max) {
|
||||
if(data === 'ram' && String(cardPricing[data].value) === "0.5" && minRam === 0.5){
|
||||
cardPricing[data].value = 1;
|
||||
$('#ramValue').val('1');
|
||||
$("#ramValue").attr('step', 1);
|
||||
} else {
|
||||
cardPricing[data].value = Number(cardPricing[data].value) + cardPricing[data].interval;
|
||||
}
|
||||
}
|
||||
_fetchPricing();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
});
|
||||
|
||||
$('.input-price').change(function() {
|
||||
var data = $(this).attr("name");
|
||||
var input = $('input[name=' + data + ']');
|
||||
var inputValue = input.val();
|
||||
|
||||
if(data === 'ram') {
|
||||
var ramInput = $('#ramValue');
|
||||
if ($('#ramValue').data('old-value') < $('#ramValue').val()) {
|
||||
if($('#ramValue').val() === '1' && minRam === 0.5) {
|
||||
$("#ramValue").attr('step', 1);
|
||||
$('#ramValue').val('1');
|
||||
}
|
||||
} else {
|
||||
if($('#ramValue').val() === '0' && minRam === 0.5) {
|
||||
$("#ramValue").attr('step', 0.5);
|
||||
$('#ramValue').val('0.5');
|
||||
}
|
||||
}
|
||||
inputValue = $('#ramValue').val();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
}
|
||||
cardPricing[data].value = inputValue;
|
||||
_fetchPricing();
|
||||
});
|
||||
}
|
||||
|
||||
function _fetchPricing() {
|
||||
Object.keys(cardPricing).map(function(element) {
|
||||
$('input[name=' + element + ']').val(cardPricing[element].value);
|
||||
});
|
||||
_calcPricing();
|
||||
}
|
||||
|
||||
function _calcPricing() {
|
||||
if(typeof window.coresUnitPrice === 'undefined'){
|
||||
window.coresUnitPrice = 5;
|
||||
}
|
||||
if(typeof window.ramUnitPrice === 'undefined'){
|
||||
window.ramUnitPrice = 2;
|
||||
}
|
||||
if(typeof window.ssdUnitPrice === 'undefined'){
|
||||
window.ssdUnitPrice = 0.6;
|
||||
}
|
||||
if(typeof window.discountAmount === 'undefined'){
|
||||
window.discountAmount = 0;
|
||||
}
|
||||
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
|
||||
(cardPricing['ram'].value * window.ramUnitPrice) +
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice) -
|
||||
window.discountAmount;
|
||||
total = parseFloat(total.toFixed(2));
|
||||
$("#total").text(total);
|
||||
}
|
||||
|
||||
_initPricing();
|
||||
$('#ramValue').data('old-value', $('#ramValue').val());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,39 @@ function setBrandIcon(brand) {
|
|||
|
||||
|
||||
$(document).ready(function () {
|
||||
$(function () {
|
||||
$("select#id_generic_payment_form-product_name").change(function () {
|
||||
var gp_form = $('#generic-payment-form');
|
||||
$.ajax({
|
||||
url: gp_form.attr('action'),
|
||||
type: 'POST',
|
||||
data: gp_form.serialize(),
|
||||
init: function () {
|
||||
console.log("init")
|
||||
},
|
||||
success: function (data) {
|
||||
if (data.amount !== undefined) {
|
||||
$("#id_generic_payment_form-amount").val(data.amount);
|
||||
if (data.isSubscription) {
|
||||
$('#id_generic_payment_form-recurring').prop('checked', true);
|
||||
} else {
|
||||
$('#id_generic_payment_form-recurring').prop('checked', false);
|
||||
}
|
||||
} else {
|
||||
$("#id_generic_payment_form-amount").val('');
|
||||
$('#id_generic_payment_form-recurring').prop('checked', false);
|
||||
console.log("No product found")
|
||||
}
|
||||
},
|
||||
error: function (xmlhttprequest, textstatus, message) {
|
||||
$("#id_generic_payment_form-amount").val('');
|
||||
$('#id_generic_payment_form-recurring').prop('checked', false);
|
||||
console.log("Error fetching product")
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr, settings) {
|
||||
function getCookie(name) {
|
||||
|
|
@ -124,17 +157,35 @@ $(document).ready(function () {
|
|||
$('#billing-form').submit();
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
if (parts.length === 2) return parts.pop().split(";").shift();
|
||||
}
|
||||
|
||||
function submitBillingForm() {
|
||||
var billing_form = $('#billing-form');
|
||||
var recurring_input = $('#id_generic_payment_form-recurring');
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-product_name" value="' + $('#id_generic_payment_form-product_name').val() + '" />');
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-amount" value="' + $('#id_generic_payment_form-amount').val() + '" />');
|
||||
if (recurring_input.attr('type') === 'hidden') {
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.val() === 'True' ? 'on' : '') + '" />');
|
||||
} else {
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.prop('checked') ? 'on' : '') + '" />');
|
||||
}
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-description" value="' + $('#id_generic_payment_form-description').val() + '" />');
|
||||
billing_form.submit();
|
||||
}
|
||||
|
||||
var $form_new = $('#payment-form-new');
|
||||
$form_new.submit(payWithStripe_new);
|
||||
|
||||
function payWithStripe_new(e) {
|
||||
e.preventDefault();
|
||||
|
||||
function stripeTokenHandler(token) {
|
||||
// Insert the token ID into the form so it gets submitted to the server
|
||||
$('#id_token').val(token.id);
|
||||
$('#billing-form').submit();
|
||||
submitBillingForm();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -195,5 +246,11 @@ $(document).ready(function () {
|
|||
$(element).closest('.form-group').append(error);
|
||||
}
|
||||
});
|
||||
|
||||
$('.credit-card-info .btn.choice-btn').click(function () {
|
||||
var id = this.dataset['id_card'];
|
||||
$('#id_card').val(id);
|
||||
submitBillingForm();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -134,3 +134,15 @@ $(document).ready(function() {
|
|||
$(this).find('.modal-footer .btn').addClass('hide');
|
||||
})
|
||||
});
|
||||
|
||||
window.onload = function () {
|
||||
var locale_dates = document.getElementsByClassName("locale_date");
|
||||
var formats = ['YYYY-MM-DD hh:mm a'];
|
||||
var i;
|
||||
for (i = 0; i < locale_dates.length; i++) {
|
||||
var oldDate = moment.utc(locale_dates[i].textContent, formats);
|
||||
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
|
||||
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
|
||||
locale_dates[i].className += ' done';
|
||||
}
|
||||
};
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
|
||||
<title>{{ domain }} - {{ hosting }} hosting as easy as possible</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% load staticfiles bootstrap3%}
|
||||
{% load i18n %}
|
||||
{% load staticfiles i18n cms_tags sekizai_tags %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
|
|
@ -29,6 +29,9 @@
|
|||
{% block css_extra %}
|
||||
{% endblock css_extra %}
|
||||
|
||||
{% render_block "css" postprocessor "compressor.contrib.sekizai.compress" %}
|
||||
{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href='//fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
|
||||
<link href="{% static 'datacenterlight/font-awesome/css/font-awesome.min.css' %}" rel="stylesheet" type="text/css">
|
||||
|
|
@ -48,7 +51,7 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% cms_toolbar %}
|
||||
|
||||
{% block navbar %}
|
||||
{% include "hosting/includes/_navbar_user.html" %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
{% extends "hosting/base_short.html" %}
|
||||
{% load staticfiles bootstrap3 i18n %}
|
||||
{% load staticfiles bootstrap3 i18n cms_tags %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="dashboard-container create-vm-container">
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
|
|
@ -17,14 +19,8 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="price-calc-section no-padding">
|
||||
<div class="landing card">
|
||||
<div class="caption">
|
||||
{% include "hosting/calculator_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 hosting-calculator">
|
||||
{% render_placeholder cms_integration.calculator_placeholder %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<tr>
|
||||
<td style="padding-top: 25px; font-size: 16px;">
|
||||
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
|
||||
{% blocktrans %}You are receiving this email because your virutal machine <strong>{{ vm_name }}</strong> has been cancelled.{% endblocktrans %}
|
||||
{% blocktrans %}You are receiving this email because your virtual machine <strong>{{ vm_name }}</strong> has been cancelled.{% endblocktrans %}
|
||||
</p>
|
||||
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
|
||||
{% blocktrans %}You can always order a new VM by clicking the button below.{% endblocktrans %}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% trans "Virtual Machine Cancellation" %}
|
||||
|
||||
{% blocktrans %}You are receiving this email because your virutal machine {{vm_name}} has been cancelled.{% endblocktrans %}
|
||||
{% blocktrans %}You are receiving this email because your virtual machine {{vm_name}} has been cancelled.{% endblocktrans %}
|
||||
{% blocktrans %}You can always order a new VM by following the link below.{% endblocktrans %}
|
||||
|
||||
{{ base_url }}{% url 'hosting:create_virtual_machine' %}
|
||||
|
|
|
|||
50
hosting/templates/hosting/includes/_card_input.html
Normal file
50
hosting/templates/hosting/includes/_card_input.html
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{% load i18n %}
|
||||
|
||||
<form action="" id="payment-form-new" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<input type="hidden" name="id_card" id="id_card" value=""/>
|
||||
<div class="group">
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-5 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-errors"></div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% trans "You are not making any payment yet. After placing your order, you will be taken to the Submit Payment Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-vm-contact btn-wide" type="submit" name="payment-form">{%trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
{% endif %}
|
||||
</span>
|
||||
</p>
|
||||
{% if order %}
|
||||
{% if order and vm %}
|
||||
<p>
|
||||
<strong>{% trans "Status" %}: </strong>
|
||||
<strong>
|
||||
|
|
@ -93,58 +93,106 @@
|
|||
<hr>
|
||||
<div>
|
||||
<h4>{% trans "Order summary" %}</h4>
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{% if vm.name %}
|
||||
{{ vm.name }}
|
||||
{% else %}
|
||||
{{ request.session.template.name }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
{% if vm.created_at %}
|
||||
<p>
|
||||
<span>{% trans "Period" %}: </span>
|
||||
<span>
|
||||
<span class="locale_date" data-format="YYYY/MM/DD">{{ vm.created_at|date:'Y-m-d h:i a' }}</span> - <span class="locale_date" data-format="YYYY/MM/DD">{{ subscription_end_date|date:'Y-m-d h:i a' }}</span>
|
||||
</span>
|
||||
</p>
|
||||
{% if vm %}
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{% if vm.name %}
|
||||
{{ vm.name }}
|
||||
{% else %}
|
||||
{{ request.session.template.name }}
|
||||
{% endif %}
|
||||
<p>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
{% if vm.cores %}
|
||||
<span class="pull-right">{{vm.cores|floatformat}}</span>
|
||||
{% else %}
|
||||
<span class="pull-right">{{vm.cpu|floatformat}}</span>
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
{% if vm.created_at %}
|
||||
<p>
|
||||
<span>{% trans "Period" %}: </span>
|
||||
<span>
|
||||
<span class="locale_date" data-format="YYYY/MM/DD">{{ vm.created_at|date:'Y-m-d h:i a' }}</span> - <span class="locale_date" data-format="YYYY/MM/DD">{{ subscription_end_date|date:'Y-m-d h:i a' }}</span>
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Memory" %}: </span>
|
||||
<span class="pull-right">{{vm.memory}} GB</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Disk space" %}: </span>
|
||||
<span class="pull-right">{{vm.disk_size}} GB</span>
|
||||
</p>
|
||||
{% if vm.vat > 0 %}
|
||||
<p>
|
||||
<strong>{% trans "Subtotal" %}: </strong>
|
||||
<span class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</span>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
{% if vm.cores %}
|
||||
<strong class="pull-right">{{vm.cores|floatformat}}</strong>
|
||||
{% else %}
|
||||
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%): </span>
|
||||
<span class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</span>
|
||||
<span>{% trans "Memory" %}: </span>
|
||||
<strong class="pull-right">{{vm.memory}} GB</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Disk space" %}: </span>
|
||||
<strong class="pull-right">{{vm.disk_size}} GB</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% if vm.vat > 0 or vm.discount.amount > 0 %}
|
||||
<div class="col-sm-6">
|
||||
<div class="subtotal-price">
|
||||
{% if vm.vat > 0 %}
|
||||
<p>
|
||||
<strong>{% trans "Subtotal" %} </strong>
|
||||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
|
||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if vm.discount.amount > 0 %}
|
||||
<p class="text-primary">
|
||||
{%trans "Discount" as discount_name %}
|
||||
<strong>{{ vm.discount.name|default:discount_name }} </strong>
|
||||
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{% trans "Total" %}</strong>
|
||||
<span class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</span>
|
||||
</p>
|
||||
<div class="col-sm-6">
|
||||
<p class="total-price">
|
||||
<strong>{% trans "Total" %} </strong>
|
||||
<strong class="pull-right">{% if vm.total_price %}{{vm.total_price|floatformat:2|intcomma}}{% else %}{{vm.price|floatformat:2|intcomma}}{% endif %} CHF</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{{ product_name }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<span>{% trans "Amount" %}: </span>
|
||||
<strong class="pull-right">{{order.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% if order.generic_payment_description %}
|
||||
<p>
|
||||
<span>{% trans "Description" %}: </span>
|
||||
<strong class="pull-right">{{order.generic_payment_description}}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if order.subscription_id %}
|
||||
<p>
|
||||
<span>{% trans "Recurring" %}: </span>
|
||||
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of every month" %}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% if not order %}
|
||||
{% block submit_btn %}
|
||||
|
|
@ -152,7 +200,7 @@
|
|||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price|intcomma }}CHF/month{% endblocktrans %}.</div>
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.</div>
|
||||
</div>
|
||||
<div class="col-sm-4 order-confirm-btn text-right">
|
||||
<button class="btn choice-btn" id="btn-create-vm" data-href="{% url 'hosting:order-confirmation' %}" data-toggle="modal" data-target="#createvm-modal">
|
||||
|
|
@ -208,17 +256,6 @@
|
|||
<script type="text/javascript">
|
||||
{% trans "Some problem encountered. Please try again later." as err_msg %}
|
||||
var create_vm_error_message = '{{err_msg|safe}}';
|
||||
window.onload = function () {
|
||||
var locale_dates = document.getElementsByClassName("locale_date");
|
||||
var formats = ['YYYY-MM-DD hh:mm a']
|
||||
var i;
|
||||
for (i = 0; i < locale_dates.length; i++) {
|
||||
var oldDate = moment.utc(locale_dates[i].textContent, formats);
|
||||
var outputFormat = locale_dates[i].getAttribute('data-format') || oldDate._f;
|
||||
locale_dates[i].innerHTML = oldDate.local().format(outputFormat);
|
||||
locale_dates[i].className += ' done';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{%endblock%}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
{% for order in orders %}
|
||||
<tr>
|
||||
<td class="xs-td-inline" data-header="{% trans 'Order Nr.' %}">{{ order.id }}</td>
|
||||
<td class="xs-td-bighalf" data-header="{% trans 'Date' %}">{{ order.created_at | date:"M d, Y H:i" }}</td>
|
||||
<td class="xs-td-bighalf locale_date" data-header="{% trans 'Date' %}">{{ order.created_at | date:'Y-m-d h:i a' }}</td>
|
||||
<td class="xs-td-smallhalf" data-header="{% trans 'Amount' %}">{{ order.price|floatformat:2|intcomma }}</td>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-order-detail" href="{% url 'hosting:orders' order.pk %}">{% trans 'See Invoice' %}</a>
|
||||
|
|
|
|||
|
|
@ -9,161 +9,161 @@
|
|||
<!-- Credit card form -->
|
||||
<div class="dcl-order-container">
|
||||
<div class="payment-container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-order-sec">
|
||||
<h3><strong>{%trans "Your Order" %}</strong></h3>
|
||||
<div class="col-xs-6 col-sm-12 col-md-12 col-lg-12 dcl-order-table-header">
|
||||
<div class="col-xs-12 col-sm-2 col-md-1 col-lg-1 tbl-header">
|
||||
{%trans "Cores" %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-4 tbl-header">
|
||||
{%trans "Memory" %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3 col-lg-3 tbl-header">
|
||||
{%trans "Disk space" %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-header">
|
||||
{%trans "Configuration" %}
|
||||
<div class="dcl-order-sec">
|
||||
<h3><strong>{%trans "Your Order" %}</strong></h3>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-12">
|
||||
<div class="dcl-order-table-header">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<div class="tbl-header">
|
||||
{%trans "Cores" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="tbl-header tbl-header-center">
|
||||
{%trans "Memory" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-header tbl-header-center">
|
||||
{%trans "Disk space" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-header tbl-header-right">
|
||||
{%trans "Configuration" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-12 col-md-12 col-lg-12 dcl-order-table-content">
|
||||
<div class="col-xs-12 col-sm-2 col-md-1 col-lg-1 tbl-content">
|
||||
{{request.session.specs.cpu|floatformat}}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-4 col-lg-4 tbl-content">
|
||||
{{request.session.specs.memory|floatformat}} GB
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-3 col-md-3 col-lg-3 tbl-content">
|
||||
{{request.session.specs.disk_size|floatformat|intcomma}} GB
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4 tbl-content">
|
||||
{{request.session.template.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-order-table-total">
|
||||
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-tot tbl-no-padding">
|
||||
{%trans "Total" %} <span>{%trans "including VAT" %}</span>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6 tbl-no-padding">
|
||||
<div class="col-xs-12 col-sm-4 col-md-4 col-lg-4"></div>
|
||||
<div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 tbl-total">{{request.session.specs.price|intcomma}}
|
||||
CHF<span class="dcl-price-month">/{% trans "Month" %}</span>
|
||||
<div class="col-xs-6 col-sm-12">
|
||||
<div class="dcl-order-table-content">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<div class="tbl-content">
|
||||
{{request.session.specs.cpu|floatformat}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="tbl-content tbl-content-center">
|
||||
{{request.session.specs.memory|floatformat}} GB
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-content tbl-content-center">
|
||||
{{request.session.specs.disk_size|floatformat|intcomma}} GB
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="tbl-content tbl-content-right">
|
||||
{{request.session.template.name}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dcl-order-table-total">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="tbl-tot">
|
||||
{%trans "Total" %}
|
||||
<span>{% if vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="tbl-total">
|
||||
{{request.session.specs.price|intcomma}} CHF/{% trans "Month" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if vm_pricing.discount_amount %}
|
||||
<hr class="thin-hr">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="tbl-tot">
|
||||
{%trans "Discount" as discount_name %}
|
||||
{{ vm_pricing.discount_name|default:discount_name }} <br>
|
||||
<span>({% trans "Will be applied at checkout" %})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<div class="tbl-total">
|
||||
<div class="text-primary">- {{ vm_pricing.discount_amount }} CHF/{% trans "Month" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12 dcl-billing-sec">
|
||||
<div class="col-xs-12 col-sm-5 col-md-6 billing dcl-billing">
|
||||
<h3><b>{%trans "Billing Address"%}</b></h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% for field in form %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field field show_label=False type='fields'%}
|
||||
{% endfor %}
|
||||
</form>
|
||||
<div class="dcl-billing-sec">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6">
|
||||
<div class="billing dcl-billing">
|
||||
<h3><b>{%trans "Billing Address"%}</b></h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% bootstrap_field field show_label=False type='fields'%}
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-7 col-md-6 creditcard-box dcl-creditcard">
|
||||
{% with card_list_len=cards_list|length %}
|
||||
<h3><b>{%trans "Credit Card"%}</b></h3>
|
||||
<hr>
|
||||
<div>
|
||||
<p>
|
||||
{% if card_list_len > 0 %}
|
||||
{% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div>
|
||||
{% if credit_card_data.last4 %}
|
||||
<form role="form" id="payment-form-with-creditcard" novalidate>
|
||||
<h5 class="billing-head">Credit Card</h5>
|
||||
<h5 class="membership-lead">Last 4: *****{{credit_card_data.last4}}</h5>
|
||||
<h5 class="membership-lead">Type: {{credit_card_data.cc_brand}}</h5>
|
||||
<input type="hidden" name="credit_card_needed" value="false"/>
|
||||
</form>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-addtional-margin">
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
{% for card in cards_list %}
|
||||
<div class="credit-card-info">
|
||||
<div class="col-xs-6 no-padding">
|
||||
<h5 class="billing-head">{% trans "Credit Card" %}</h5>
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right align-bottom">
|
||||
<a class="btn choice-btn choice-btn-faded" href="#" data-id_card="{{card.id}}">{% trans "SELECT" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button id="payment_button_with_creditcard" class="btn btn-vm-contact" type="submit">{%trans "SUBMIT" %}</button>
|
||||
{% endfor %}
|
||||
{% if card_list_len > 0 %}
|
||||
<div class="new-card-head">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>{% trans "Add a new credit card" %}</h4>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
|
||||
<span class="fa fa-plus"></span> {% trans "NEW CARD" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<form action="" id="payment-form-new" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<div class="group">
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-5 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newcard" class="collapse">
|
||||
<hr class="thick-hr">
|
||||
<div class="card-details-box">
|
||||
<h3>{%trans "New Credit Card" %}</h3>
|
||||
<hr>
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
</div>
|
||||
<div id="card-errors"></div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% trans "You are not making any payment yet. After submitting your card information, you will be taken to the Confirm Order Page." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-vm-contact btn-wide" type="submit">{%trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% else%}
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% comment "Looks as if no more used. To test..." %}
|
||||
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
|
|
@ -191,5 +191,5 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% endcomment %}
|
||||
{%endblock%}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
{% block content %}
|
||||
<div class="dashboard-container wide">
|
||||
{% include 'hosting/includes/_messages.html' %}
|
||||
<div class="dashboard-container-head">
|
||||
<h1 class="dashboard-title-thin"><img src="{% static 'hosting/img/dashboard_settings.svg' %}" class="un-icon wide"> {% trans "My Settings" %}</h1>
|
||||
</div>
|
||||
|
|
@ -14,116 +15,105 @@
|
|||
<div class="settings-container">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6 billing dcl-billing">
|
||||
<h3>{%trans "Billing Address"%}</h3>
|
||||
<h3>{%trans "Billing Address" %}</h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
|
||||
{% endfor %}
|
||||
<div class="form-group text-right">
|
||||
<button type="submit" class="btn btn-vm-contact btn-wide">{% trans "UPDATE" %}</button>
|
||||
<button type="submit" class="btn btn-vm-contact btn-wide" name="billing-form">{% trans "UPDATE" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-6 creditcard-box dcl-creditcard">
|
||||
<h3>{%trans "Credit Card"%}</h3>
|
||||
<h3>{%trans "Credit Card" %}</h3>
|
||||
<hr>
|
||||
<div>
|
||||
{% if credit_card_data.last4 %}
|
||||
{% with card_list_len=cards_list|length %}
|
||||
{% for card in cards_list %}
|
||||
<div class="credit-card-details">
|
||||
<h5 class="billing-head">{% trans "Credit Card" %}</h5>
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: *****{{credit_card_data.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{credit_card_data.cc_brand}}</h5>
|
||||
{% comment %}
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
|
||||
<div class="credit-card-details-opt">
|
||||
<div class="row">
|
||||
{% if card_list_len > 1 %}
|
||||
<div class="col-xs-6">
|
||||
<a class="caps-link" href=""><img src="{% static 'hosting/img/delete.svg' %}" class="svg-img">{% trans "REMOVE CARD" %}</a>
|
||||
<a class="caps-link" href="" data-toggle="modal" data-target="#Modal{{ card.id }}"><img src="{% static 'hosting/img/delete.svg' %}" class="svg-img">{% trans "REMOVE CARD" %}</a>
|
||||
<div class="modal fade" id="Modal{{card.id }}" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Confirm"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon"><i class="fa fa-trash" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Remove Card"%}</h4>
|
||||
<div class="modal-text">
|
||||
<p>{% trans "Do you want to remove this associated card?"%}</p>
|
||||
</div>
|
||||
<form method="post" action="{% url 'hosting:delete_card' card.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-danger btn-wide" name="delete_card">{% trans "Delete"%}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-xs-6 text-right">
|
||||
<a class="btn btn-vm-contact" href="">{% trans "EDIT CARD" %}</a>
|
||||
{% if card.preferred %}
|
||||
{% trans "DEFAULT" %}
|
||||
{% else %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="card" value="{{card.id}}">
|
||||
<a class="btn choice-btn choice-btn-faded" href="#" onclick="$(this).closest('form').submit()">{% trans "SELECT" %}</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% empty %}
|
||||
<div class="no-cards">
|
||||
<h4>{% trans "No Credit Cards Added" %}</h4>
|
||||
<p>{% blocktrans %}We are using <a href="https://stripe.com">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{% comment %}
|
||||
<h4>{% trans "Add a new Card." %}</h4>
|
||||
<p style="margin-bottom: 15px;">
|
||||
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
</p>
|
||||
<form action="" id="payment-form-new" class="credit-card-form" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-sm-4 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-6 col-sm-4 col-sm-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-errors" role="alert"></div>
|
||||
<div>
|
||||
{% if not messages and not form.non_field_errors %}
|
||||
<p class="card-warning-content">
|
||||
{% blocktrans %}You are not making any payment here.{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' or 'make_charge_error' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<p class="card-warning-content card-warning-error">
|
||||
{{ error|escape }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-xs-offset-6 text-right">
|
||||
<button class="btn btn-success stripe-payment-btn" type="submit">{%trans "Submit" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
||||
{% endcomment %}
|
||||
{% endif %}
|
||||
<div class="new-card-head">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>{% trans "Add a new credit card" %}</h4>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<button data-toggle="collapse" data-target="#newcard" class="btn choice-btn">
|
||||
<span class="fa fa-plus"></span> {% trans "NEW CARD" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newcard" class="collapse">
|
||||
<hr class="thick-hr">
|
||||
<div class="card-details-box">
|
||||
<h3>{%trans "New Credit Card" %}</h3>
|
||||
<hr>
|
||||
{% include "hosting/includes/_card_input.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<!-- stripe key data -->
|
||||
{% if stripe_key %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
|
@ -137,13 +127,4 @@
|
|||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
{% if credit_card_data.last4 and credit_card_data.cc_brand %}
|
||||
<script type="text/javascript">
|
||||
(function () {
|
||||
window.hasCreditcard = true;
|
||||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
{% endcomment %}
|
||||
{%endblock%}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
</div>
|
||||
<div class="vm-detail-item">
|
||||
<h2 class="vm-detail-title">{% trans "Status" %} <img src="{% static 'hosting/img/connected.svg' %}" class="un-icon"></h2>
|
||||
<div class="vm-vmid">
|
||||
<div class="vm-vmid vm-vmid-with-warning">
|
||||
<div class="vm-item-subtitle">{% trans "Your VM is" %}</div>
|
||||
<div id="terminate-VM" data-alt="{% trans 'Terminating' %}">
|
||||
{% if virtual_machine.state == 'PENDING' %}
|
||||
|
|
@ -74,6 +74,10 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-terminate-warning text-center">
|
||||
<p>{% trans "Attention:" %}</p>
|
||||
<p>{% trans "terminating VM can not be reverted." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-contact-us">
|
||||
|
|
@ -105,7 +109,7 @@
|
|||
<div class="modal-icon"><i class="fa fa-ban" aria-hidden="true"></i></div>
|
||||
<h4 class="modal-title" id="ModalLabel">{% trans "Terminate your Virtual Machine" %}</h4>
|
||||
<div class="modal-text">
|
||||
<p>{% trans "Do you want to cancel your Virtual Machine" %} ?</p>
|
||||
<p>{% trans "Terminated VMs can not be revived and will not be refunded. Do you want to terminate your VM?" %}</p>
|
||||
<p><strong>{{virtual_machine.name}}</strong></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ urlpatterns = [
|
|||
name='choice_ssh_keys'),
|
||||
url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(),
|
||||
name='delete_ssh_key'),
|
||||
url(r'delete_card/(?P<pk>\d+)/?$', SettingsView.as_view(),
|
||||
name='delete_card'),
|
||||
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
|
||||
name='create_ssh_key'),
|
||||
url(r'^notifications/$', NotificationsView.as_view(),
|
||||
|
|
|
|||
594
hosting/views.py
594
hosting/views.py
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
|
@ -12,13 +11,16 @@ from django.contrib.auth.tokens import default_token_generator
|
|||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse_lazy, reverse
|
||||
from django.http import Http404, HttpResponseRedirect, HttpResponse
|
||||
from django.http import (
|
||||
Http404, HttpResponseRedirect, HttpResponse, JsonResponse
|
||||
)
|
||||
from django.shortcuts import redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import (
|
||||
View, CreateView, FormView, ListView, DetailView, DeleteView,
|
||||
|
|
@ -30,8 +32,10 @@ from stored_messages.api import mark_read
|
|||
from stored_messages.models import Message
|
||||
from stored_messages.settings import stored_messages_settings
|
||||
|
||||
from datacenterlight.models import VMTemplate
|
||||
from datacenterlight.tasks import create_vm_task
|
||||
from datacenterlight.cms_models import DCLCalculatorPluginModel
|
||||
from datacenterlight.models import VMTemplate, VMPricing
|
||||
from datacenterlight.utils import create_vm, get_cms_integration
|
||||
from hosting.models import UserCardDetail
|
||||
from membership.models import CustomUser, StripeCustomer
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from opennebula_api.serializers import (
|
||||
|
|
@ -42,7 +46,7 @@ from utils.forms import (
|
|||
BillingAddressForm, PasswordResetRequestForm, UserBillingAddressForm,
|
||||
ResendActivationEmailForm
|
||||
)
|
||||
from utils.hosting_utils import get_vm_price, get_vm_price_with_vat
|
||||
from utils.hosting_utils import get_vm_price_with_vat, HostingUtils
|
||||
from utils.mailer import BaseEmail
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from utils.tasks import send_plain_email_task
|
||||
|
|
@ -56,7 +60,8 @@ from .forms import (
|
|||
)
|
||||
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
|
||||
from .models import (
|
||||
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail
|
||||
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
|
||||
GenericProduct
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -564,6 +569,7 @@ class SettingsView(LoginRequiredMixin, FormView):
|
|||
template_name = "hosting/settings.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
form_class = BillingAddressForm
|
||||
permission_required = ['view_usercarddetail']
|
||||
|
||||
def get_form(self, form_class):
|
||||
"""
|
||||
|
|
@ -578,33 +584,124 @@ class SettingsView(LoginRequiredMixin, FormView):
|
|||
context = super(SettingsView, self).get_context_data(**kwargs)
|
||||
# Get user
|
||||
user = self.request.user
|
||||
# Get user last order
|
||||
last_hosting_order = HostingOrder.objects.filter(
|
||||
customer__user=user).last()
|
||||
# If user has already an hosting order, get the credit card data from
|
||||
# it
|
||||
if last_hosting_order:
|
||||
credit_card_data = last_hosting_order.get_cc_data()
|
||||
context.update({
|
||||
'credit_card_data': credit_card_data if credit_card_data else None,
|
||||
})
|
||||
stripe_customer = None
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
stripe_customer = user.stripecustomer
|
||||
cards_list = UserCardDetail.get_all_cards_list(
|
||||
stripe_customer=stripe_customer
|
||||
)
|
||||
context.update({
|
||||
'cards_list': cards_list,
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'card' in request.POST and request.POST['card'] is not '':
|
||||
card_id = escape(request.POST['card'])
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
UserCardDetail.set_default_card(
|
||||
stripe_api_cus_id=request.user.stripecustomer.stripe_id,
|
||||
stripe_source_id=user_card_detail.card_id
|
||||
)
|
||||
msg = _(
|
||||
("Your {brand} card ending in {last4} set as "
|
||||
"default card").format(
|
||||
brand=user_card_detail.brand,
|
||||
last4=user_card_detail.last4
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
if 'delete_card' in request.POST:
|
||||
try:
|
||||
card = UserCardDetail.objects.get(pk=self.kwargs.get('pk'))
|
||||
if (request.user.has_perm(self.permission_required[0], card)
|
||||
and
|
||||
request.user
|
||||
.stripecustomer
|
||||
.usercarddetail_set
|
||||
.count() > 1):
|
||||
if card.card_id is not None:
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_utils.dissociate_customer_card(
|
||||
request.user.stripecustomer.stripe_id,
|
||||
card.card_id
|
||||
)
|
||||
if card.preferred:
|
||||
UserCardDetail.set_default_card_from_stripe(
|
||||
request.user.stripecustomer.stripe_id
|
||||
)
|
||||
card.delete()
|
||||
msg = _("Card deassociation successful")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
else:
|
||||
msg = _("You are not permitted to do this operation")
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
except UserCardDetail.DoesNotExist:
|
||||
msg = _("The selected card does not exist")
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
billing_address_data = form.cleaned_data
|
||||
billing_address_data.update({
|
||||
'user': self.request.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
instance=self.request.user.billing_addresses.first(),
|
||||
data=billing_address_data)
|
||||
billing_address_user_form.save()
|
||||
if 'billing-form' in request.POST:
|
||||
billing_address_data = form.cleaned_data
|
||||
billing_address_data.update({
|
||||
'user': self.request.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
instance=self.request.user.billing_addresses.first(),
|
||||
data=billing_address_data)
|
||||
billing_address_user_form.save()
|
||||
msg = _("Billing address updated successfully")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
else:
|
||||
token = form.cleaned_data.get('token')
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
token
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
form.add_error("__all__", card_details.get('error'))
|
||||
return self.render_to_response(self.get_context_data())
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=request.user.email, token=token
|
||||
)
|
||||
card = card_details['response_object']
|
||||
if UserCardDetail.get_user_card_details(stripe_customer, card):
|
||||
msg = _('You seem to have already added this card')
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
else:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
request.user.stripecustomer.stripe_id, token
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
preferred = False
|
||||
if stripe_customer.usercarddetail_set.count() == 0:
|
||||
preferred = True
|
||||
UserCardDetail.create(
|
||||
stripe_customer=stripe_customer,
|
||||
last4=card['last4'],
|
||||
brand=card['brand'],
|
||||
fingerprint=card['fingerprint'],
|
||||
exp_month=card['exp_month'],
|
||||
exp_year=card['exp_year'],
|
||||
card_id=card['card_id'],
|
||||
preferred=preferred
|
||||
)
|
||||
msg = _(
|
||||
"Successfully associated the card with your account"
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
else:
|
||||
billing_address_data = form.cleaned_data
|
||||
|
|
@ -637,21 +734,19 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
context = super(PaymentVMView, self).get_context_data(**kwargs)
|
||||
# Get user
|
||||
user = self.request.user
|
||||
|
||||
# Get user last order
|
||||
last_hosting_order = HostingOrder.objects.filter(
|
||||
customer__user=user).last()
|
||||
|
||||
# If user has already an hosting order, get the credit card data from
|
||||
# it
|
||||
if last_hosting_order:
|
||||
credit_card_data = last_hosting_order.get_cc_data()
|
||||
context.update({
|
||||
'credit_card_data': credit_card_data if credit_card_data else None,
|
||||
})
|
||||
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
stripe_customer = user.stripecustomer
|
||||
else:
|
||||
stripe_customer = None
|
||||
cards_list = UserCardDetail.get_all_cards_list(
|
||||
stripe_customer=stripe_customer
|
||||
)
|
||||
context.update({
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY,
|
||||
'vm_pricing': VMPricing.get_vm_pricing_by_name(
|
||||
self.request.session.get('specs', {}).get('pricing_name')
|
||||
),
|
||||
'cards_list': cards_list,
|
||||
})
|
||||
|
||||
return context
|
||||
|
|
@ -660,6 +755,10 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
def get(self, request, *args, **kwargs):
|
||||
if 'next' in request.session:
|
||||
del request.session['next']
|
||||
HostingUtils.clear_items_from_list(
|
||||
request.session,
|
||||
['token', 'card_id', 'customer', 'user']
|
||||
)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
@method_decorator(decorators)
|
||||
|
|
@ -670,23 +769,51 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
billing_address_data = form.cleaned_data
|
||||
token = form.cleaned_data.get('token')
|
||||
owner = self.request.user
|
||||
# Get or create stripe customer
|
||||
customer = StripeCustomer.get_or_create(email=owner.email,
|
||||
token=token)
|
||||
if not customer:
|
||||
msg = _("Invalid credit card")
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error')
|
||||
return HttpResponseRedirect(
|
||||
reverse('hosting:payment') + '#payment_error')
|
||||
|
||||
if token is '':
|
||||
card_id = form.cleaned_data.get('card')
|
||||
customer = owner.stripecustomer
|
||||
try:
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
if not request.user.has_perm(
|
||||
'view_usercarddetail', user_card_detail
|
||||
):
|
||||
raise UserCardDetail.DoesNotExist(
|
||||
_("{user} does not have permission to access the "
|
||||
"card").format(user=request.user.email)
|
||||
)
|
||||
except UserCardDetail.DoesNotExist as e:
|
||||
ex = str(e)
|
||||
logger.error("Card Id: {card_id}, Exception: {ex}".format(
|
||||
card_id=card_id, ex=ex
|
||||
)
|
||||
)
|
||||
msg = _("An error occurred. Details: {}".format(ex))
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error'
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse('hosting:payment') + '#payment_error'
|
||||
)
|
||||
request.session['card_id'] = user_card_detail.id
|
||||
else:
|
||||
# Get or create stripe customer
|
||||
customer = StripeCustomer.get_or_create(
|
||||
email=owner.email, token=token
|
||||
)
|
||||
if not customer:
|
||||
msg = _("Invalid credit card")
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, msg,
|
||||
extra_tags='make_charge_error')
|
||||
return HttpResponseRedirect(
|
||||
reverse('hosting:payment') + '#payment_error')
|
||||
request.session['token'] = token
|
||||
request.session['billing_address_data'] = billing_address_data
|
||||
request.session['token'] = token
|
||||
request.session['customer'] = customer.stripe_id
|
||||
return HttpResponseRedirect("{url}?{query_params}".format(
|
||||
url=reverse('hosting:order-confirmation'),
|
||||
query_params='page=payment'))
|
||||
query_params='page=payment')
|
||||
)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
|
@ -719,12 +846,6 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
).get_context_data(**kwargs)
|
||||
obj = self.get_object()
|
||||
owner = self.request.user
|
||||
stripe_api_cus_id = self.request.session.get('customer')
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_card_details(
|
||||
stripe_api_cus_id,
|
||||
self.request.session.get('token')
|
||||
)
|
||||
|
||||
if self.request.GET.get('page') == 'payment':
|
||||
context['page_header_text'] = _('Confirm Order')
|
||||
|
|
@ -743,32 +864,21 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
raise Http404
|
||||
|
||||
if obj is not None:
|
||||
# invoice for previous order
|
||||
try:
|
||||
vm_detail = VMDetail.objects.get(vm_id=obj.vm_id)
|
||||
context['vm'] = vm_detail.__dict__
|
||||
context['vm']['name'] = '{}-{}'.format(
|
||||
context['vm']['configuration'], context['vm']['vm_id'])
|
||||
price, vat, vat_percent = get_vm_price_with_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.vm_pricing.name
|
||||
if obj.vm_pricing else 'default')
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = price + vat
|
||||
context['subscription_end_date'] = vm_detail.end_date()
|
||||
except VMDetail.DoesNotExist:
|
||||
if obj.generic_product_id is not None:
|
||||
# generic payment case
|
||||
logger.debug("Generic payment case")
|
||||
context['product_name'] = GenericProduct.objects.get(
|
||||
id=obj.generic_product_id
|
||||
).product_name
|
||||
else:
|
||||
# invoice for previous order
|
||||
logger.debug("Invoice of VM order")
|
||||
try:
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email, password=owner.password
|
||||
)
|
||||
vm = manager.get_vm(obj.vm_id)
|
||||
context['vm'] = VirtualMachineSerializer(vm).data
|
||||
price, vat, vat_percent = get_vm_price_with_vat(
|
||||
vm_detail = VMDetail.objects.get(vm_id=obj.vm_id)
|
||||
context['vm'] = vm_detail.__dict__
|
||||
context['vm']['name'] = '{}-{}'.format(
|
||||
context['vm']['configuration'], context['vm']['vm_id'])
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
|
|
@ -777,33 +887,64 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = price + vat
|
||||
except WrongIdError:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('The VM you are looking for is unavailable at the '
|
||||
'moment. Please contact Data Center Light support.')
|
||||
)
|
||||
self.kwargs['error'] = 'WrongIdError'
|
||||
context['error'] = 'WrongIdError'
|
||||
except ConnectionRefusedError:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('In order to create a VM, you need to create/upload '
|
||||
'your SSH KEY first.')
|
||||
)
|
||||
elif not card_details.get('response_object'):
|
||||
# new order, failed to get card details
|
||||
context['failed_payment'] = True
|
||||
context['card_details'] = card_details
|
||||
context['vm']['total_price'] = price + vat - discount['amount']
|
||||
context['subscription_end_date'] = vm_detail.end_date()
|
||||
except VMDetail.DoesNotExist:
|
||||
try:
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email, password=owner.password
|
||||
)
|
||||
vm = manager.get_vm(obj.vm_id)
|
||||
context['vm'] = VirtualMachineSerializer(vm).data
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.vm_pricing.name
|
||||
if obj.vm_pricing else 'default')
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = (
|
||||
price + vat - discount['amount']
|
||||
)
|
||||
except WrongIdError:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('The VM you are looking for is unavailable at the '
|
||||
'moment. Please contact Data Center Light support.')
|
||||
)
|
||||
self.kwargs['error'] = 'WrongIdError'
|
||||
context['error'] = 'WrongIdError'
|
||||
except ConnectionRefusedError:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('In order to create a VM, you need to create/upload '
|
||||
'your SSH KEY first.')
|
||||
)
|
||||
else:
|
||||
# new order, confirm payment
|
||||
if 'token' in self.request.session:
|
||||
token = self.request.session['token']
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
token
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
card_details_response = card_details['response_object']
|
||||
context['cc_last4'] = card_details_response['last4']
|
||||
context['cc_brand'] = card_details_response['brand']
|
||||
else:
|
||||
card_id = self.request.session.get('card_id')
|
||||
card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
context['cc_last4'] = card_detail.last4
|
||||
context['cc_brand'] = card_detail.brand
|
||||
context['site_url'] = reverse('hosting:create_virtual_machine')
|
||||
context['cc_last4'] = card_details.get('response_object').get(
|
||||
'last4')
|
||||
context['cc_brand'] = card_details.get('response_object').get(
|
||||
'cc_brand')
|
||||
context['vm'] = self.request.session.get('specs')
|
||||
return context
|
||||
|
||||
|
|
@ -814,7 +955,9 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
return HttpResponseRedirect(
|
||||
reverse('hosting:create_virtual_machine')
|
||||
)
|
||||
if 'token' not in self.request.session:
|
||||
|
||||
if ('token' not in self.request.session and
|
||||
'card_id' not in self.request.session):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
self.object = self.get_object()
|
||||
context = self.get_context_data(object=self.object)
|
||||
|
|
@ -833,36 +976,86 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
def post(self, request):
|
||||
template = request.session.get('template')
|
||||
specs = request.session.get('specs')
|
||||
stripe_utils = StripeUtils()
|
||||
# We assume that if the user is here, his/her StripeCustomer
|
||||
# object already exists
|
||||
stripe_customer_id = request.user.stripecustomer.id
|
||||
billing_address_data = request.session.get('billing_address_data')
|
||||
vm_template_id = template.get('id', 1)
|
||||
stripe_api_cus_id = self.request.session.get('customer')
|
||||
# Make stripe charge to a customer
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_card_details(stripe_api_cus_id,
|
||||
request.session.get(
|
||||
'token'))
|
||||
if not card_details.get('response_object'):
|
||||
msg = card_details.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:payment') + '#payment_error')
|
||||
card_details_dict = card_details.get('response_object')
|
||||
stripe_api_cus_id = request.user.stripecustomer.stripe_id
|
||||
if 'token' in self.request.session:
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
request.session['token']
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
return HttpResponseRedirect(reverse('hosting:payment'))
|
||||
card_details_response = card_details['response_object']
|
||||
card_details_dict = {
|
||||
'last4': card_details_response['last4'],
|
||||
'brand': card_details_response['brand'],
|
||||
'card_id': card_details_response['card_id']
|
||||
}
|
||||
ucd = UserCardDetail.get_user_card_details(
|
||||
request.user.stripecustomer, card_details_response
|
||||
)
|
||||
if not ucd:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
stripe_api_cus_id, request.session['token'],
|
||||
set_as_default=True
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=reverse('hosting:payment'),
|
||||
section='payment_error'),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected'
|
||||
' back to the payment page.')
|
||||
)
|
||||
}
|
||||
return JsonResponse(response)
|
||||
else:
|
||||
card_id = request.session.get('card_id')
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
card_details_dict = {
|
||||
'last4': user_card_detail.last4,
|
||||
'brand': user_card_detail.brand,
|
||||
'card_id': user_card_detail.card_id
|
||||
}
|
||||
if not user_card_detail.preferred:
|
||||
UserCardDetail.set_default_card(
|
||||
stripe_api_cus_id=stripe_api_cus_id,
|
||||
stripe_source_id=user_card_detail.card_id
|
||||
)
|
||||
cpu = specs.get('cpu')
|
||||
memory = specs.get('memory')
|
||||
disk_size = specs.get('disk_size')
|
||||
amount_to_be_charged = specs.get('price')
|
||||
plan_name = StripeUtils.get_stripe_plan_name(cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size)
|
||||
stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
|
||||
ram=memory,
|
||||
ssd=disk_size,
|
||||
version=1,
|
||||
app='dcl')
|
||||
amount_to_be_charged = specs.get('total_price')
|
||||
plan_name = StripeUtils.get_stripe_plan_name(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
price=amount_to_be_charged
|
||||
)
|
||||
stripe_plan_id = StripeUtils.get_stripe_plan_id(
|
||||
cpu=cpu,
|
||||
ram=memory,
|
||||
ssd=disk_size,
|
||||
version=1,
|
||||
app='dcl',
|
||||
price=amount_to_be_charged
|
||||
)
|
||||
stripe_plan = stripe_utils.get_or_create_stripe_plan(
|
||||
amount=amount_to_be_charged,
|
||||
name=plan_name,
|
||||
|
|
@ -875,6 +1068,12 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
# Check if the subscription was approved and is active
|
||||
if (stripe_subscription_obj is None or
|
||||
stripe_subscription_obj.status != 'active'):
|
||||
# At this point, we have created a Stripe API card and
|
||||
# associated it with the customer; but the transaction failed
|
||||
# due to some reason. So, we would want to dissociate this card
|
||||
# here.
|
||||
# ...
|
||||
|
||||
msg = subscription_result.get('error')
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
|
|
@ -887,10 +1086,20 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected back to'
|
||||
' the payment page.'))
|
||||
' the payment page.')
|
||||
)
|
||||
}
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
return JsonResponse(response)
|
||||
|
||||
if 'token' in request.session:
|
||||
ucd = UserCardDetail.get_or_create_user_card_detail(
|
||||
stripe_customer=self.request.user.stripecustomer,
|
||||
card_details=card_details_response
|
||||
)
|
||||
UserCardDetail.save_default_card_local(
|
||||
self.request.user.stripecustomer.stripe_id,
|
||||
ucd.card_id
|
||||
)
|
||||
user = {
|
||||
'name': self.request.user.name,
|
||||
'email': self.request.user.email,
|
||||
|
|
@ -899,15 +1108,12 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
'request_host': request.get_host(),
|
||||
'language': get_language(),
|
||||
}
|
||||
create_vm_task.delay(vm_template_id, user, specs, template,
|
||||
stripe_customer_id, billing_address_data,
|
||||
stripe_subscription_obj.id, card_details_dict)
|
||||
|
||||
for session_var in ['specs', 'template', 'billing_address',
|
||||
'billing_address_data',
|
||||
'token', 'customer']:
|
||||
if session_var in request.session:
|
||||
del request.session[session_var]
|
||||
create_vm(
|
||||
billing_address_data, stripe_customer_id, specs,
|
||||
stripe_subscription_obj, card_details_dict, request,
|
||||
vm_template_id, template, user
|
||||
)
|
||||
|
||||
response = {
|
||||
'status': True,
|
||||
|
|
@ -919,8 +1125,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView):
|
|||
' it is ready.'))
|
||||
}
|
||||
|
||||
return HttpResponse(json.dumps(response),
|
||||
content_type="application/json")
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class OrdersHostingListView(LoginRequiredMixin, ListView):
|
||||
|
|
@ -994,7 +1199,29 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
raise ValidationError(_('Invalid number of cores'))
|
||||
|
||||
def validate_memory(self, value):
|
||||
if (value > 200) or (value < 1):
|
||||
if 'pid' in self.request.POST:
|
||||
try:
|
||||
plugin = DCLCalculatorPluginModel.objects.get(
|
||||
id=self.request.POST['pid']
|
||||
)
|
||||
except DCLCalculatorPluginModel.DoesNotExist as dne:
|
||||
logger.error(
|
||||
str(dne) + " plugin_id: " + self.request.POST['pid']
|
||||
)
|
||||
raise ValidationError(_('Invalid calculator properties'))
|
||||
if plugin.enable_512mb_ram:
|
||||
if value % 1 == 0 or value == 0.5:
|
||||
logger.debug(
|
||||
"Given ram {value} is either 0.5 or a"
|
||||
" whole number".format(value=value)
|
||||
)
|
||||
if (value > 200) or (value < 0.5):
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
else:
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
elif (value > 200) or (value < 1) or (value % 1 != 0):
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
else:
|
||||
raise ValidationError(_('Invalid RAM size'))
|
||||
|
||||
def validate_storage(self, value):
|
||||
|
|
@ -1003,7 +1230,10 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
|
||||
@method_decorator(decorators)
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {'templates': VMTemplate.objects.all()}
|
||||
context = {
|
||||
'templates': VMTemplate.objects.all(),
|
||||
'cms_integration': get_cms_integration('default'),
|
||||
}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
@method_decorator(decorators)
|
||||
|
|
@ -1011,22 +1241,38 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
cores = request.POST.get('cpu')
|
||||
cores_field = forms.IntegerField(validators=[self.validate_cores])
|
||||
memory = request.POST.get('ram')
|
||||
memory_field = forms.IntegerField(validators=[self.validate_memory])
|
||||
memory_field = forms.FloatField(validators=[self.validate_memory])
|
||||
storage = request.POST.get('storage')
|
||||
storage_field = forms.IntegerField(validators=[self.validate_storage])
|
||||
template_id = int(request.POST.get('config'))
|
||||
pricing_name = request.POST.get('pricing_name')
|
||||
vm_pricing = VMPricing.get_vm_pricing_by_name(pricing_name)
|
||||
template = VMTemplate.objects.filter(
|
||||
opennebula_vm_template_id=template_id).first()
|
||||
template_data = VMTemplateSerializer(template).data
|
||||
|
||||
if vm_pricing is None:
|
||||
vm_pricing_name_msg = _(
|
||||
"Incorrect pricing name. Please contact support"
|
||||
"{support_email}".format(
|
||||
support_email=settings.DCL_SUPPORT_FROM_ADDRESS
|
||||
)
|
||||
)
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, vm_pricing_name_msg,
|
||||
extra_tags='pricing'
|
||||
)
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
else:
|
||||
vm_pricing_name = vm_pricing.name
|
||||
|
||||
try:
|
||||
cores = cores_field.clean(cores)
|
||||
except ValidationError as err:
|
||||
msg = '{} : {}.'.format(cores, str(err))
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='cores')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:index') + "#order_form")
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
|
||||
try:
|
||||
memory = memory_field.clean(memory)
|
||||
|
|
@ -1034,8 +1280,7 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
msg = '{} : {}.'.format(memory, str(err))
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='memory')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:index') + "#order_form")
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
|
||||
try:
|
||||
storage = storage_field.clean(storage)
|
||||
|
|
@ -1043,15 +1288,25 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View):
|
|||
msg = '{} : {}.'.format(storage, str(err))
|
||||
messages.add_message(self.request, messages.ERROR, msg,
|
||||
extra_tags='storage')
|
||||
return HttpResponseRedirect(
|
||||
reverse('datacenterlight:index') + "#order_form")
|
||||
price = get_vm_price(cpu=cores, memory=memory,
|
||||
disk_size=storage)
|
||||
return redirect(CreateVirtualMachinesView.as_view())
|
||||
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
cpu=cores,
|
||||
memory=memory,
|
||||
ssd_size=storage,
|
||||
pricing_name=vm_pricing_name
|
||||
)
|
||||
|
||||
specs = {
|
||||
'cpu': cores,
|
||||
'memory': memory,
|
||||
'disk_size': storage,
|
||||
'price': price
|
||||
'discount': discount,
|
||||
'price': price,
|
||||
'vat': vat,
|
||||
'vat_percent': vat_percent,
|
||||
'total_price': round(price + vat - discount['amount'], 2),
|
||||
'pricing_name': vm_pricing_name
|
||||
}
|
||||
|
||||
request.session['specs'] = specs
|
||||
|
|
@ -1103,10 +1358,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
for m in storage:
|
||||
pass
|
||||
storage.used = True
|
||||
return HttpResponse(
|
||||
json.dumps({'text': ugettext('Terminated')}),
|
||||
content_type="application/json"
|
||||
)
|
||||
return JsonResponse({'text': ugettext('Terminated')})
|
||||
else:
|
||||
return redirect(reverse('hosting:virtual_machines'))
|
||||
elif self.request.is_ajax():
|
||||
|
|
@ -1180,7 +1432,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
terminated = manager.delete_vm(vm.id)
|
||||
|
||||
if not terminated:
|
||||
logger.debug(
|
||||
logger.error(
|
||||
"manager.delete_vm returned False. Hence, error making "
|
||||
"xml-rpc call to delete vm failed."
|
||||
)
|
||||
|
|
@ -1190,6 +1442,9 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
try:
|
||||
manager.get_vm(vm.id)
|
||||
except WrongIdError:
|
||||
logger.error(
|
||||
"VM {} not found. So, its terminated.".format(vm.id)
|
||||
)
|
||||
response['status'] = True
|
||||
response['text'] = ugettext('Terminated')
|
||||
vm_detail_obj = VMDetail.objects.filter(
|
||||
|
|
@ -1207,6 +1462,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
break
|
||||
else:
|
||||
sleep(2)
|
||||
if not response['status']:
|
||||
response['text'] = _("VM terminate action timed out. Please "
|
||||
"contact support@datacenterlight.ch for "
|
||||
"further information.")
|
||||
context = {
|
||||
'vm_name': vm_name,
|
||||
'base_url': "{0}://{1}".format(
|
||||
|
|
@ -1227,21 +1486,20 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
email = BaseEmail(**email_data)
|
||||
email.send()
|
||||
admin_email_body.update(response)
|
||||
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
|
||||
vm.id,
|
||||
owner.email
|
||||
)
|
||||
email_to_admin_data = {
|
||||
'subject': "Deleted VM and Subscription for VM {vm_id} and "
|
||||
"user: {user}".format(
|
||||
vm_id=vm.id, user=owner.email
|
||||
),
|
||||
'subject': ("Deleted " if response['status']
|
||||
else "ERROR deleting ") + admin_msg_sub,
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': ['info@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]),
|
||||
}
|
||||
send_plain_email_task.delay(email_to_admin_data)
|
||||
return HttpResponse(
|
||||
json.dumps(response),
|
||||
content_type="application/json"
|
||||
)
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
class HostingBillListView(PermissionRequiredMixin, LoginRequiredMixin,
|
||||
|
|
|
|||
|
|
@ -222,21 +222,28 @@ class StripeCustomer(models.Model):
|
|||
# check if user is not in stripe but in database
|
||||
customer = stripe_utils.check_customer(stripe_customer.stripe_id,
|
||||
stripe_customer.user, token)
|
||||
|
||||
if not customer.sources.data:
|
||||
stripe_utils.update_customer_token(customer, token)
|
||||
if "deleted" in customer and customer["deleted"]:
|
||||
raise StripeCustomer.DoesNotExist()
|
||||
return stripe_customer
|
||||
|
||||
except StripeCustomer.DoesNotExist:
|
||||
user = CustomUser.objects.get(email=email)
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_data = stripe_utils.create_customer(token, email, user.name)
|
||||
if stripe_data.get('response_object'):
|
||||
stripe_cus_id = stripe_data.get('response_object').get('id')
|
||||
|
||||
stripe_customer = StripeCustomer.objects. \
|
||||
create(user=user, stripe_id=stripe_cus_id)
|
||||
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
# User already had a Stripe account and we are here
|
||||
# because the account was deleted in dashboard
|
||||
# So, we simply update the stripe_id
|
||||
user.stripecustomer.stripe_id = stripe_cus_id
|
||||
user.stripecustomer.save()
|
||||
stripe_customer = user.stripecustomer
|
||||
else:
|
||||
# The user never had an associated Stripe account
|
||||
# So, create one
|
||||
stripe_customer = StripeCustomer.objects.create(
|
||||
user=user, stripe_id=stripe_cus_id
|
||||
)
|
||||
return stripe_customer
|
||||
else:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -53,27 +53,18 @@ class OpenNebulaManager():
|
|||
ConnectionError: If the connection to the opennebula server can't be
|
||||
established
|
||||
"""
|
||||
return oca.Client("{0}:{1}".format(
|
||||
user.email,
|
||||
user.password),
|
||||
"{protocol}://{domain}:{port}{endpoint}".format(
|
||||
protocol=settings.OPENNEBULA_PROTOCOL,
|
||||
domain=settings.OPENNEBULA_DOMAIN,
|
||||
port=settings.OPENNEBULA_PORT,
|
||||
endpoint=settings.OPENNEBULA_ENDPOINT
|
||||
))
|
||||
return self._get_opennebula_client(user.email, user.password)
|
||||
|
||||
def _get_opennebula_client(self, username, password):
|
||||
return oca.Client("{0}:{1}".format(
|
||||
username,
|
||||
|
||||
password),
|
||||
return oca.Client(
|
||||
"{0}:{1}".format(username, password),
|
||||
"{protocol}://{domain}:{port}{endpoint}".format(
|
||||
protocol=settings.OPENNEBULA_PROTOCOL,
|
||||
domain=settings.OPENNEBULA_DOMAIN,
|
||||
port=settings.OPENNEBULA_PORT,
|
||||
endpoint=settings.OPENNEBULA_ENDPOINT
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
def _get_user(self, user):
|
||||
"""Get the corresponding opennebula user for a CustomUser object
|
||||
|
|
@ -119,7 +110,7 @@ class OpenNebulaManager():
|
|||
raise UserExistsError()
|
||||
except OpenNebulaException as err:
|
||||
logger.error('OpenNebulaException error: {0}'.format(err))
|
||||
logger.debug('User exists but password is wrong')
|
||||
logger.error('User exists but password is wrong')
|
||||
raise UserCredentialError()
|
||||
|
||||
except WrongNameError:
|
||||
|
|
@ -157,7 +148,7 @@ class OpenNebulaManager():
|
|||
)
|
||||
return opennebula_user
|
||||
except ConnectionRefusedError:
|
||||
logger.info(
|
||||
logger.error(
|
||||
'Could not connect to host: {host} via protocol {protocol}'.format(
|
||||
host=settings.OPENNEBULA_DOMAIN,
|
||||
protocol=settings.OPENNEBULA_PROTOCOL)
|
||||
|
|
@ -169,7 +160,7 @@ class OpenNebulaManager():
|
|||
user_pool = oca.UserPool(self.oneadmin_client)
|
||||
user_pool.info()
|
||||
except ConnectionRefusedError:
|
||||
logger.info(
|
||||
logger.error(
|
||||
'Could not connect to host: {host} via protocol {protocol}'.format(
|
||||
host=settings.OPENNEBULA_DOMAIN,
|
||||
protocol=settings.OPENNEBULA_PROTOCOL)
|
||||
|
|
@ -183,7 +174,7 @@ class OpenNebulaManager():
|
|||
vm_pool.info()
|
||||
return vm_pool
|
||||
except AttributeError:
|
||||
logger.info('Could not connect via client, using oneadmin instead')
|
||||
logger.error('Could not connect via client, using oneadmin instead')
|
||||
try:
|
||||
vm_pool = oca.VirtualMachinePool(self.oneadmin_client)
|
||||
vm_pool.info(filter=-2)
|
||||
|
|
@ -192,7 +183,7 @@ class OpenNebulaManager():
|
|||
raise ConnectionRefusedError
|
||||
|
||||
except ConnectionRefusedError:
|
||||
logger.info(
|
||||
logger.error(
|
||||
'Could not connect to host: {host} via protocol {protocol}'.format(
|
||||
host=settings.OPENNEBULA_DOMAIN,
|
||||
protocol=settings.OPENNEBULA_PROTOCOL)
|
||||
|
|
@ -218,32 +209,31 @@ class OpenNebulaManager():
|
|||
except:
|
||||
raise ConnectionRefusedError
|
||||
|
||||
def get_primary_ipv4(self, vm_id):
|
||||
def get_ipv6(self, vm_id):
|
||||
"""
|
||||
Returns the primary IPv4 of the given vm.
|
||||
To be changed later.
|
||||
Returns the first IPv6 of the given vm.
|
||||
|
||||
:return: An IP address string, if it exists else returns None
|
||||
:return: An IPv6 address string, if it exists else returns None
|
||||
"""
|
||||
all_ipv4s = self.get_vm_ipv4_addresses(vm_id)
|
||||
if len(all_ipv4s) > 0:
|
||||
return all_ipv4s[0]
|
||||
ipv6_list = self.get_all_ipv6_addresses(vm_id)
|
||||
if len(ipv6_list) > 0:
|
||||
return ipv6_list[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_vm_ipv4_addresses(self, vm_id):
|
||||
def get_all_ipv6_addresses(self, vm_id):
|
||||
"""
|
||||
Returns a list of IPv4 addresses of the given vm
|
||||
Returns a list of IPv6 addresses of the given vm
|
||||
|
||||
:param vm_id: The ID of the vm
|
||||
:return:
|
||||
"""
|
||||
ipv4s = []
|
||||
ipv6_list = []
|
||||
vm = self.get_vm(vm_id)
|
||||
for nic in vm.template.nics:
|
||||
if hasattr(nic, 'ip'):
|
||||
ipv4s.append(nic.ip)
|
||||
return ipv4s
|
||||
if hasattr(nic, 'ip6_global'):
|
||||
ipv6_list.append(nic.ip6_global)
|
||||
return ipv6_list
|
||||
|
||||
def create_vm(self, template_id, specs, ssh_key=None, vm_name=None):
|
||||
|
||||
|
|
@ -259,8 +249,8 @@ class OpenNebulaManager():
|
|||
vm_specs = vm_specs_formatter.format(
|
||||
vcpu=int(specs['cpu']),
|
||||
cpu=0.1 * int(specs['cpu']),
|
||||
memory=1024 * int(specs['memory']),
|
||||
|
||||
memory=(512 if specs['memory'] == 0.5 else
|
||||
1024 * int(specs['memory'])),
|
||||
)
|
||||
vm_specs += """<DISK>
|
||||
<TYPE>fs</TYPE>
|
||||
|
|
@ -279,8 +269,8 @@ class OpenNebulaManager():
|
|||
vm_specs = vm_specs_formatter.format(
|
||||
vcpu=int(specs['cpu']),
|
||||
cpu=0.1 * int(specs['cpu']),
|
||||
memory=1024 * int(specs['memory']),
|
||||
|
||||
memory=(512 if specs['memory'] == 0.5 else
|
||||
1024 * int(specs['memory'])),
|
||||
)
|
||||
vm_specs += """<DISK>
|
||||
<TYPE>fs</TYPE>
|
||||
|
|
@ -325,7 +315,7 @@ class OpenNebulaManager():
|
|||
return vm_id
|
||||
|
||||
def delete_vm(self, vm_id):
|
||||
TERMINATE_ACTION = 'terminate'
|
||||
TERMINATE_ACTION = 'terminate-hard'
|
||||
vm_terminated = False
|
||||
try:
|
||||
self.oneadmin_client.call(
|
||||
|
|
@ -335,14 +325,14 @@ class OpenNebulaManager():
|
|||
)
|
||||
vm_terminated = True
|
||||
except socket.timeout as socket_err:
|
||||
logger.info("Socket timeout error: {0}".format(socket_err))
|
||||
logger.error("Socket timeout error: {0}".format(socket_err))
|
||||
except OpenNebulaException as opennebula_err:
|
||||
logger.info(
|
||||
logger.error(
|
||||
"OpenNebulaException error: {0}".format(opennebula_err))
|
||||
except OSError as os_err:
|
||||
logger.info("OSError : {0}".format(os_err))
|
||||
logger.error("OSError : {0}".format(os_err))
|
||||
except ValueError as value_err:
|
||||
logger.info("ValueError : {0}".format(value_err))
|
||||
logger.error("ValueError : {0}".format(value_err))
|
||||
|
||||
return vm_terminated
|
||||
|
||||
|
|
@ -352,7 +342,7 @@ class OpenNebulaManager():
|
|||
template_pool.info()
|
||||
return template_pool
|
||||
except ConnectionRefusedError:
|
||||
logger.info(
|
||||
logger.error(
|
||||
"""Could not connect to host: {host} via protocol
|
||||
{protocol}""".format(
|
||||
host=settings.OPENNEBULA_DOMAIN,
|
||||
|
|
@ -362,12 +352,12 @@ class OpenNebulaManager():
|
|||
except:
|
||||
raise ConnectionRefusedError
|
||||
|
||||
def get_templates(self):
|
||||
def get_templates(self, prefix='public-'):
|
||||
try:
|
||||
public_templates = [
|
||||
template
|
||||
for template in self._get_template_pool()
|
||||
if template.name.startswith('public-')
|
||||
if template.name.startswith(prefix)
|
||||
]
|
||||
return public_templates
|
||||
except ConnectionRefusedError:
|
||||
|
|
@ -438,8 +428,9 @@ class OpenNebulaManager():
|
|||
return template_id
|
||||
|
||||
def delete_template(self, template_id):
|
||||
self.oneadmin_client.call(oca.VmTemplate.METHODS[
|
||||
'delete'], template_id, False)
|
||||
self.oneadmin_client.call(
|
||||
oca.VmTemplate.METHODS['delete'], template_id, False
|
||||
)
|
||||
|
||||
def change_user_password(self, passwd_hash):
|
||||
self.oneadmin_client.call(
|
||||
|
|
@ -547,7 +538,7 @@ class OpenNebulaManager():
|
|||
'value': 'sha-.....', # public key as string
|
||||
'state': True # whether key is to be added or
|
||||
} # removed
|
||||
:param hosts: A list of hosts IP addresses
|
||||
:param hosts: A list of hosts IPv6 addresses
|
||||
:param countdown: Parameter to be passed to celery apply_async
|
||||
Allows to delay a task by `countdown` number of seconds
|
||||
:return:
|
||||
|
|
@ -560,12 +551,14 @@ class OpenNebulaManager():
|
|||
link_error=save_ssh_key_error_handler.s())
|
||||
else:
|
||||
logger.debug(
|
||||
"Keys and/or hosts are empty, so not managing any keys")
|
||||
"Keys and/or hosts are empty, so not managing any keys"
|
||||
)
|
||||
|
||||
def get_all_hosts(self):
|
||||
"""
|
||||
A utility function to obtain all hosts of this owner
|
||||
:return: A list of hosts IP addresses, empty if none exist
|
||||
:return: A list of IPv6 addresses of all the hosts of this customer or
|
||||
an empty list if none exist
|
||||
"""
|
||||
owner = CustomUser.objects.filter(
|
||||
email=self.email).first()
|
||||
|
|
@ -576,10 +569,8 @@ class OpenNebulaManager():
|
|||
"the ssh keys.".format(self.email))
|
||||
for order in all_orders:
|
||||
try:
|
||||
vm = self.get_vm(order.vm_id)
|
||||
for nic in vm.template.nics:
|
||||
if hasattr(nic, 'ip'):
|
||||
hosts.append(nic.ip)
|
||||
ip = self.get_ipv6(order.vm_id)
|
||||
hosts.append(ip)
|
||||
except WrongIdError:
|
||||
logger.debug(
|
||||
"VM with ID {} does not exist".format(order.vm_id))
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ class VirtualMachineTemplateSerializer(serializers.Serializer):
|
|||
return int(obj.template.memory) / 1024
|
||||
|
||||
def get_name(self, obj):
|
||||
return obj.name.lstrip('public-')
|
||||
if obj.name.startswith('public-'):
|
||||
return obj.name.lstrip('public-')
|
||||
else:
|
||||
return obj.name
|
||||
|
||||
|
||||
class VirtualMachineSerializer(serializers.Serializer):
|
||||
|
|
@ -133,7 +136,10 @@ class VirtualMachineSerializer(serializers.Serializer):
|
|||
def get_configuration(self, obj):
|
||||
template_id = obj.template.template_id
|
||||
template = OpenNebulaManager().get_template(template_id)
|
||||
return template.name.lstrip('public-')
|
||||
if template.name.startswith('public-'):
|
||||
return template.name.lstrip('public-')
|
||||
else:
|
||||
return template.name
|
||||
|
||||
def get_ipv4(self, obj):
|
||||
"""
|
||||
|
|
@ -162,7 +168,10 @@ class VirtualMachineSerializer(serializers.Serializer):
|
|||
return '-'
|
||||
|
||||
def get_name(self, obj):
|
||||
return obj.name.lstrip('public-')
|
||||
if obj.name.startswith('public-'):
|
||||
return obj.name.lstrip('public-')
|
||||
else:
|
||||
return obj.name
|
||||
|
||||
|
||||
class VMTemplateSerializer(serializers.Serializer):
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ django-meta==1.2
|
|||
django-meta-mixin==0.3.0
|
||||
django-model-utils==2.5
|
||||
django-mptt==0.8.4
|
||||
django-multisite==1.4.1
|
||||
django-parler==1.6.3
|
||||
django-phonenumber-field==1.1.0
|
||||
django-polymorphic==0.9.2
|
||||
|
|
@ -69,7 +68,7 @@ model-mommy==1.2.6
|
|||
phonenumbers==7.4.0
|
||||
phonenumberslite==7.4.0
|
||||
psycopg2==2.7.3.2
|
||||
pycryptodome==3.4
|
||||
pycryptodome==3.6.6
|
||||
pylibmc==1.5.1
|
||||
python-dateutil==2.5.3
|
||||
python-slugify==1.2.0
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@
|
|||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://blog.ungleich.ch/en-us/cms/blog/feed/">
|
||||
<span class="fa-stack fa-lg">
|
||||
<i class="fa fa-circle fa-stack-2x"></i>
|
||||
<i class="fa fa-rss fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="copyright">
|
||||
Copyright © ungleich GmbH {% now "Y" %}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
|
||||
{% block base_content %}
|
||||
{% placeholder "default" %}
|
||||
{% placeholder "base_ungleich_content" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{% extends "base_ungleich.html" %}
|
||||
{% load cms_tags %}
|
||||
{% block base_content %}
|
||||
{% placeholder "default" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.header_slider > .carousel .item .container {
|
||||
.header_slider > .carousel .item .container-fluid {
|
||||
overflow: auto;
|
||||
padding: 50px 20px 60px;
|
||||
height: 100%;
|
||||
|
|
@ -236,7 +236,7 @@
|
|||
.header_slider .carousel-control .fa {
|
||||
font-size: 4em;
|
||||
}
|
||||
.header_slider > .carousel .item .container {
|
||||
.header_slider > .carousel .item .container-fluid {
|
||||
overflow: auto;
|
||||
padding: 75px 50px;
|
||||
}
|
||||
|
|
@ -403,4 +403,4 @@
|
|||
left: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.35);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="author" content="ungleich GmbH">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
<meta name="description" content="{% page_attribute 'meta_description' %}">
|
||||
<title>{% page_attribute "page_title" %}</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</video>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="container">
|
||||
<div class="container-fluid">
|
||||
{% if instance.heading %}
|
||||
<div class="intro-cap">{{ instance.heading }}</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
<meta name="description" content="{% page_attribute 'meta_description' %}">
|
||||
|
||||
|
||||
<title>{% page_attribute "page_title" %}</title>
|
||||
|
||||
|
|
@ -33,7 +34,11 @@
|
|||
{% include "google_analytics.html" %}
|
||||
<!-- End Google Analytics -->
|
||||
|
||||
<link rel="shortcut icon" href="{% static 'ungleich_page/img/favicon.ico' %}" type="image/x-icon">
|
||||
{% if request.current_page.cmsfaviconextension %}
|
||||
<link rel="shortcut icon" href="{% static request.current_page.cmsfaviconextension.favicon.url %}" type="image/x-icon">
|
||||
{% else %}
|
||||
<link rel="shortcut icon" href="{% static 'ungleich_page/img/favicon.ico' %}" type="image/x-icon">
|
||||
{% endif %}
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="index">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from django import forms
|
||||
from .models import ContactMessage, BillingAddress, UserBillingAddress
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from membership.models import CustomUser
|
||||
from .models import ContactMessage, BillingAddress, UserBillingAddress
|
||||
|
||||
|
||||
# from utils.fields import CountryField
|
||||
|
|
@ -66,7 +67,8 @@ class ResendActivationEmailForm(forms.Form):
|
|||
try:
|
||||
c = CustomUser.objects.get(email=email)
|
||||
if c.validated == 1:
|
||||
raise forms.ValidationError(_("The account is already active."))
|
||||
raise forms.ValidationError(
|
||||
_("The account is already active."))
|
||||
return email
|
||||
except CustomUser.DoesNotExist:
|
||||
raise forms.ValidationError(_("User does not exist"))
|
||||
|
|
@ -117,6 +119,7 @@ class EditCreditCardForm(forms.Form):
|
|||
|
||||
class BillingAddressForm(forms.ModelForm):
|
||||
token = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
card = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = BillingAddress
|
||||
|
|
@ -136,6 +139,32 @@ class BillingAddressFormSignup(BillingAddressForm):
|
|||
email = forms.EmailField(label=_('Email Address'))
|
||||
field_order = ['name', 'email']
|
||||
|
||||
class Meta:
|
||||
model = BillingAddress
|
||||
fields = ['name', 'email', 'cardholder_name', 'street_address',
|
||||
'city', 'postal_code', 'country']
|
||||
labels = {
|
||||
'name': 'Name',
|
||||
'email': _('Email'),
|
||||
'cardholder_name': _('Cardholder Name'),
|
||||
'street_address': _('Street Address'),
|
||||
'city': _('City'),
|
||||
'postal_code': _('Postal Code'),
|
||||
'Country': _('Country'),
|
||||
}
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
try:
|
||||
CustomUser.objects.get(email=email)
|
||||
raise forms.ValidationError(
|
||||
_("The email %(email)s is already registered with us. "
|
||||
"Please reset your password and access your account.") %
|
||||
{'email': email}
|
||||
)
|
||||
except CustomUser.DoesNotExist:
|
||||
return email
|
||||
|
||||
|
||||
class UserBillingAddressForm(forms.ModelForm):
|
||||
user = forms.ModelChoiceField(queryset=CustomUser.objects.all(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import decimal
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from oca.pool import WrongIdError
|
||||
|
||||
from datacenterlight.models import VMPricing
|
||||
|
|
@ -79,7 +81,7 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
|
|||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
|
||||
cents = decimal.Decimal('.01')
|
||||
price = price.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
return float(price)
|
||||
return round(float(price), 2)
|
||||
|
||||
|
||||
def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
|
||||
|
|
@ -107,10 +109,12 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
|
|||
)
|
||||
return None
|
||||
|
||||
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
|
||||
(decimal.Decimal(memory) * pricing.ram_unit_price) +
|
||||
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
|
||||
price = (
|
||||
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
|
||||
(decimal.Decimal(memory) * pricing.ram_unit_price) +
|
||||
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
|
||||
)
|
||||
if pricing.vat_inclusive:
|
||||
vat = decimal.Decimal(0)
|
||||
vat_percent = decimal.Decimal(0)
|
||||
|
|
@ -121,4 +125,46 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
|
|||
cents = decimal.Decimal('.01')
|
||||
price = price.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
return float(price), float(vat), float(vat_percent)
|
||||
discount = {
|
||||
'name': pricing.discount_name,
|
||||
'amount': round(float(pricing.discount_amount), 2)
|
||||
}
|
||||
return (round(float(price), 2), round(float(vat), 2),
|
||||
round(float(vat_percent), 2), discount)
|
||||
|
||||
|
||||
def ping_ok(host_ipv6):
|
||||
"""
|
||||
A utility method to check if a host responds to ping requests. Note: the
|
||||
function relies on `ping6` utility of debian to check.
|
||||
|
||||
:param host_ipv6 str type parameter that represets the ipv6 of the host to
|
||||
checked
|
||||
:return True if the host responds to ping else returns False
|
||||
"""
|
||||
try:
|
||||
subprocess.check_output("ping6 -c 1 " + host_ipv6, shell=True)
|
||||
except Exception as ex:
|
||||
logger.debug(host_ipv6 + " not reachable via ping. Error = " + str(ex))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class HostingUtils:
|
||||
@staticmethod
|
||||
def clear_items_from_list(from_list, items_list):
|
||||
"""
|
||||
A utility function to clear items from a given list.
|
||||
Useful when deleting items in bulk from session.
|
||||
e.g.:
|
||||
HostingUtils.clear_items_from_list(
|
||||
request.session,
|
||||
['token', 'billing_address_data', 'card_id',]
|
||||
)
|
||||
:param from_list:
|
||||
:param items_list:
|
||||
:return:
|
||||
"""
|
||||
for var in items_list:
|
||||
if var in from_list:
|
||||
del from_list[var]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-10-10 21:35+0530\n"
|
||||
"POT-Creation-Date: 2018-07-07 19:27+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -777,11 +777,18 @@ msgstr ""
|
|||
msgid "Email Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Street Building"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email"
|
||||
msgstr "E-Mail"
|
||||
|
||||
msgid ""
|
||||
"The email %(email)s is already registered with us. Please reset your "
|
||||
"password and access your account."
|
||||
msgstr ""
|
||||
"Diese E-Mail-Adresse %(email)s existiert bereits. Bitte setze dein Passwort zurück "
|
||||
"auf dein Konto zuzugreifen."
|
||||
|
||||
msgid "Street Building"
|
||||
msgstr "Gebäude"
|
||||
|
||||
msgid "Phone number"
|
||||
msgstr "Telefon"
|
||||
|
|
|
|||
|
|
@ -78,6 +78,22 @@ class StripeUtils(object):
|
|||
customer.source = token
|
||||
customer.save()
|
||||
|
||||
@handleStripeError
|
||||
def associate_customer_card(self, stripe_customer_id, token,
|
||||
set_as_default=False):
|
||||
customer = stripe.Customer.retrieve(stripe_customer_id)
|
||||
card = customer.sources.create(source=token)
|
||||
if set_as_default:
|
||||
customer.default_source = card.id
|
||||
customer.save()
|
||||
return True
|
||||
|
||||
@handleStripeError
|
||||
def dissociate_customer_card(self, stripe_customer_id, card_id):
|
||||
customer = stripe.Customer.retrieve(stripe_customer_id)
|
||||
card = customer.sources.retrieve(card_id)
|
||||
card.delete()
|
||||
|
||||
@handleStripeError
|
||||
def update_customer_card(self, customer_id, token):
|
||||
customer = stripe.Customer.retrieve(customer_id)
|
||||
|
|
@ -93,32 +109,47 @@ class StripeUtils(object):
|
|||
return new_card_data
|
||||
|
||||
@handleStripeError
|
||||
def get_card_details(self, customer_id, token):
|
||||
def get_card_details(self, customer_id):
|
||||
customer = stripe.Customer.retrieve(customer_id)
|
||||
credit_card_raw_data = customer.sources.data.pop()
|
||||
card_details = {
|
||||
'last4': credit_card_raw_data.last4,
|
||||
'brand': credit_card_raw_data.brand
|
||||
'brand': credit_card_raw_data.brand,
|
||||
'exp_month': credit_card_raw_data.exp_month,
|
||||
'exp_year': credit_card_raw_data.exp_year,
|
||||
'fingerprint': credit_card_raw_data.fingerprint,
|
||||
'card_id': credit_card_raw_data.id
|
||||
}
|
||||
return card_details
|
||||
|
||||
def check_customer(self, id, user, token):
|
||||
customers = self.stripe.Customer.all()
|
||||
if not customers.get('data'):
|
||||
@handleStripeError
|
||||
def get_cards_details_from_token(self, token):
|
||||
stripe_token = stripe.Token.retrieve(token)
|
||||
card_details = {
|
||||
'last4': stripe_token.card.last4,
|
||||
'brand': stripe_token.card.brand,
|
||||
'exp_month': stripe_token.card.exp_month,
|
||||
'exp_year': stripe_token.card.exp_year,
|
||||
'fingerprint': stripe_token.card.fingerprint,
|
||||
'card_id': stripe_token.card.id
|
||||
}
|
||||
return card_details
|
||||
|
||||
def check_customer(self, stripe_cus_api_id, user, token):
|
||||
try:
|
||||
customer = stripe.Customer.retrieve(stripe_cus_api_id)
|
||||
except stripe.InvalidRequestError:
|
||||
customer = self.create_customer(token, user.email, user.name)
|
||||
else:
|
||||
try:
|
||||
customer = stripe.Customer.retrieve(id)
|
||||
except stripe.InvalidRequestError:
|
||||
customer = self.create_customer(token, user.email, user.name)
|
||||
user.stripecustomer.stripe_id = customer.get(
|
||||
'response_object').get('id')
|
||||
user.stripecustomer.save()
|
||||
user.stripecustomer.stripe_id = customer.get(
|
||||
'response_object').get('id')
|
||||
user.stripecustomer.save()
|
||||
if type(customer) is dict:
|
||||
customer = customer['response_object']
|
||||
return customer
|
||||
|
||||
@handleStripeError
|
||||
def get_customer(self, id):
|
||||
customer = stripe.Customer.retrieve(id)
|
||||
def get_customer(self, stripe_api_cus_id):
|
||||
customer = stripe.Customer.retrieve(stripe_api_cus_id)
|
||||
# data = customer.get('response_object')
|
||||
return customer
|
||||
|
||||
|
|
@ -233,6 +264,12 @@ class StripeUtils(object):
|
|||
)
|
||||
return subscription_result
|
||||
|
||||
@handleStripeError
|
||||
def set_subscription_metadata(self, subscription_id, metadata):
|
||||
subscription = stripe.Subscription.retrieve(subscription_id)
|
||||
subscription.metadata = metadata
|
||||
subscription.save()
|
||||
|
||||
@handleStripeError
|
||||
def unsubscribe_customer(self, subscription_id):
|
||||
"""
|
||||
|
|
@ -254,7 +291,8 @@ class StripeUtils(object):
|
|||
return charge
|
||||
|
||||
@staticmethod
|
||||
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None):
|
||||
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
|
||||
price=None):
|
||||
"""
|
||||
Returns the Stripe plan id string of the form
|
||||
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
|
||||
|
|
@ -266,6 +304,7 @@ class StripeUtils(object):
|
|||
:param version: The version of the Stripe plans
|
||||
:param app: The application to which the stripe plan belongs
|
||||
to. By default it is 'dcl'
|
||||
:param price: The price for this plan
|
||||
:return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb`
|
||||
"""
|
||||
dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu,
|
||||
|
|
@ -277,16 +316,39 @@ class StripeUtils(object):
|
|||
stripe_plan_id_string = '{app}-v{version}-{plan}'.format(
|
||||
app=app,
|
||||
version=version,
|
||||
plan=dcl_plan_string)
|
||||
return stripe_plan_id_string
|
||||
plan=dcl_plan_string
|
||||
)
|
||||
if price is not None:
|
||||
stripe_plan_id_string_with_price = '{}-{}chf'.format(
|
||||
stripe_plan_id_string,
|
||||
round(price, 2)
|
||||
)
|
||||
return stripe_plan_id_string_with_price
|
||||
else:
|
||||
return stripe_plan_id_string
|
||||
|
||||
@staticmethod
|
||||
def get_stripe_plan_name(cpu, memory, disk_size):
|
||||
def get_stripe_plan_name(cpu, memory, disk_size, price):
|
||||
"""
|
||||
Returns the Stripe plan name
|
||||
:return:
|
||||
"""
|
||||
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size)
|
||||
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
|
||||
"{price} CHF".format(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
price=round(price, 2)
|
||||
)
|
||||
|
||||
@handleStripeError
|
||||
def set_subscription_meta_data(self, subscription_id, meta_data):
|
||||
"""
|
||||
Adds VM metadata to a subscription
|
||||
:param subscription_id: Stripe identifier for the subscription
|
||||
:param meta_data: A dict of meta data to be added
|
||||
:return:
|
||||
"""
|
||||
subscription = stripe.Subscription.retrieve(subscription_id)
|
||||
subscription.metadata = meta_data
|
||||
subscription.save()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue