Merge pull request #81 from levivm/develop

SSH Key, notifications and VM status.
This commit is contained in:
Levi Velázquez 2016-05-30 16:44:31 -05:00
commit c03ff34aec
22 changed files with 392 additions and 22 deletions

View file

@ -54,6 +54,7 @@ INSTALLED_APPS = (
'django.contrib.sites', 'django.contrib.sites',
'easy_thumbnails', 'easy_thumbnails',
'utils', 'utils',
'stored_messages',
'mptt', 'mptt',
'parler', 'parler',
'taggit', 'taggit',
@ -132,6 +133,7 @@ TEMPLATES = [
'DIRS': [os.path.join(PROJECT_DIR, 'cms_templates/'), 'DIRS': [os.path.join(PROJECT_DIR, 'cms_templates/'),
os.path.join(PROJECT_DIR, 'cms_templates/djangocms_blog/'), os.path.join(PROJECT_DIR, 'cms_templates/djangocms_blog/'),
os.path.join(PROJECT_DIR, 'membership'), os.path.join(PROJECT_DIR, 'membership'),
os.path.join(PROJECT_DIR, 'hosting/templates/'),
os.path.join(PROJECT_DIR, 'ungleich/templates/djangocms_blog/'), os.path.join(PROJECT_DIR, 'ungleich/templates/djangocms_blog/'),
os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'), os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'),
os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich') os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich')

View file

@ -1,5 +1,32 @@
from django.contrib import admin from django.contrib import admin
from .models import VirtualMachineType
from utils.mailer import BaseEmail
from .models import VirtualMachineType, VirtualMachinePlan
class VirtualMachinePlanAdmin(admin.ModelAdmin):
list_display = ('name', 'id', 'email')
def email(self, obj):
return obj.hosting_orders.latest('id').customer.user.email
def save_model(self, request, obj, form, change):
email = self.email(obj)
if 'status' in form.changed_data and obj.status == VirtualMachinePlan.ONLINE_STATUS:
context = {
'vm': obj
}
email_data = {
'subject': 'Your VM has been activated',
'to': email,
'context': context,
'template_name': 'vm_activated',
'template_path': 'emails/'
}
email = BaseEmail(**email_data)
email.send()
obj.save()
admin.site.register(VirtualMachineType) admin.site.register(VirtualMachineType)
admin.site.register(VirtualMachinePlan, VirtualMachinePlanAdmin)

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-05-26 02:57
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0018_virtualmachineplan_public_key'),
]
operations = [
migrations.AddField(
model_name='virtualmachineplan',
name='status',
field=models.CharField(choices=[('pending', 'Pending for activation'), ('online', 'Online')], default='online', max_length=20),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-05-26 02:58
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0019_virtualmachineplan_status'),
]
operations = [
migrations.AlterField(
model_name='virtualmachineplan',
name='status',
field=models.CharField(choices=[('pending', 'Pending for activation'), ('online', 'Online')], default='pending', max_length=20),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-05-26 04:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hosting', '0020_auto_20160526_0258'),
]
operations = [
migrations.AlterField(
model_name='virtualmachineplan',
name='status',
field=models.CharField(choices=[('pending', 'Pending for activation'), ('online', 'Online'), ('canceled', 'Canceled')], default='pending', max_length=20),
),
]

View file

@ -3,12 +3,13 @@ import os
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.functional import cached_property from django.utils.functional import cached_property
from membership.models import StripeCustomer
from utils.models import BillingAddress
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from stored_messages.settings import stored_messages_settings
from membership.models import StripeCustomer
from utils.models import BillingAddress
from .managers import VMPlansManager from .managers import VMPlansManager
@ -81,12 +82,24 @@ class VirtualMachineType(models.Model):
class VirtualMachinePlan(models.Model): class VirtualMachinePlan(models.Model):
PENDING_STATUS = 'pending'
ONLINE_STATUS = 'online'
CANCELED_STATUS = 'canceled'
VM_STATUS_CHOICES = (
(PENDING_STATUS, 'Pending for activation'),
(ONLINE_STATUS, 'Online'),
(CANCELED_STATUS, 'Canceled')
)
cores = models.IntegerField() cores = models.IntegerField()
memory = models.IntegerField() memory = models.IntegerField()
disk_size = models.IntegerField() disk_size = models.IntegerField()
vm_type = models.ForeignKey(VirtualMachineType) vm_type = models.ForeignKey(VirtualMachineType)
price = models.FloatField() price = models.FloatField()
public_key = models.TextField() public_key = models.TextField()
status = models.CharField(max_length=20, choices=VM_STATUS_CHOICES, default=PENDING_STATUS)
objects = VMPlansManager() objects = VMPlansManager()
@ -97,11 +110,22 @@ class VirtualMachinePlan(models.Model):
def hosting_company_name(self): def hosting_company_name(self):
return self.vm_type.get_hosting_company_display() return self.vm_type.get_hosting_company_display()
@cached_property
def location(self):
return self.vm_type.get_location_display()
@cached_property @cached_property
def name(self): def name(self):
name = 'vm-%s' % self.id name = 'vm-%s' % self.id
return name return name
@cached_property
def notifications(self):
stripe_customer = StripeCustomer.objects.get(hostingorder__vm_plan=self)
backend = stored_messages_settings.STORAGE_BACKEND()
messages = backend.inbox_list(stripe_customer.user)
return messages
@classmethod @classmethod
def create(cls, data, user): def create(cls, data, user):
instance = cls.objects.create(**data) instance = cls.objects.create(**data)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 741 B

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
NEW VM BOOKED
</body>
</html>

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
NEW VM BOOKED
</body>
</html>

View file

@ -0,0 +1,13 @@
{% load staticfiles bootstrap3%}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
You virtual machine {{vm.name}} has been activated. You can manage your vm on this <a href="{{request.HOS}}{% url 'hosting:virtual_machines' vm.id %}"> link </a>
</body>
</html>

View file

@ -0,0 +1,15 @@
{% load staticfiles bootstrap3%}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
You virtual machine {{vm.name}} has been activated. You can manage your vm in this <a href="{% url 'hosting:virtual_machines' vm.id %}"> link </a>
</body>
</html>

View file

@ -71,7 +71,11 @@
<i class="fa fa-credit-card"></i> My Orders <i class="fa fa-credit-card"></i> My Orders
</a> </a>
</li> </li>
<li>
<a href="{% url 'hosting:notifications' %}">
<i class="fa fa-bell"></i> Notifications
</a>
</li>
<li class="dropdown"> <li class="dropdown">
<a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#"> <a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#">
<i class="glyphicon glyphicon-user"></i> {{request.user.name}} <span class="caret"></span></a> <i class="glyphicon glyphicon-user"></i> {{request.user.name}} <span class="caret"></span></a>

View file

@ -29,7 +29,7 @@
<ul class="pricing {% cycle 'p-red' 'p-black' 'p-red' 'p-yel' %}"> <ul class="pricing {% cycle 'p-red' 'p-black' 'p-red' 'p-yel' %}">
<li class="type"> <li class="type">
<!-- <img src="http://bread.pp.ua/n/settings_g.svg" alt=""> --> <!-- <img src="http://bread.pp.ua/n/settings_g.svg" alt=""> -->
<h3 >{{vm.hosting_company_name}}</h3> <h3 >{{vm.location_code}}</h3>
<br/> <br/>
<img class="img-responsive" src="{{ STATIC_URL }}hosting/img/{{vm.location_code}}_flag.png" alt=""> <img class="img-responsive" src="{{ STATIC_URL }}hosting/img/{{vm.location_code}}_flag.png" alt="">

View file

@ -0,0 +1,90 @@
{% extends "hosting/base_short.html" %}
{% load staticfiles bootstrap3 %}
{% block content %}
<div>
<div class="container virtual-machine-container dashboard-container ">
<div class="row">
<div class="col-md-9 col-md-offset-2">
<div class="col-sm-12">
<h3><i class="fa fa-bell" aria-hidden="true"></i> Notifications</h3>
<hr/>
<div class="col-md-3"> <!-- required for floating -->
<!-- Nav tabs -->
<ul class="nav nav-tabs tabs-left sideways">
<li class="active">
<a href="#unread-v" data-toggle="tab">
Unread <span class="badge">{{unread_notifications|length}}</span>
</a>
</li>
<li>
<a href="#all-v" data-toggle="tab">
All
</a>
</li>
</ul>
</div>
<div class="col-md-9">
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-pane active" id="unread-v">
<div class="row">
<div class="col-md-12">
<h3>Unread notifications</h3>
<hr>
</div>
</div>
<div class="row">
<div class="col-md-12">
{% for notification in unread_notifications %}
<form method="POST" action="{% url 'hosting:read_notification' notification.id %}">
{% csrf_token %}
<span>{{notification}} -</span>
<button type="submit" class="btn btn-link">Mark as read</button>
<span class="pull-right" style="font-size: 11px;color: #999;">{{notification.date}}</span>
</form>
<hr/>
{% endfor %}
</div><!--/col-12-->
</div><!--/row-->
</div>
<div class="tab-pane" id="all-v">
<div class="row">
<div class="col-md-12">
<h3>All notifications</h3>
<hr>
{% for notification in all_notifications %}
<span>{{notification.message}} </span>
<span class="pull-right" style="font-size: 11px;color: #999;">{{notification.message.date}}</span>
<hr/>
{% endfor %}
</div>
</div>
<div class="row">
<div class="col-md-12">
</div><!--/col-12-->
</div><!--/row-->
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
</div>
{%endblock%}

View file

@ -122,12 +122,16 @@
</div><!--/row--> </div><!--/row-->
</div> </div>
<div class="tab-pane" id="status-v"> <div class="tab-pane" id="status-v">
<div class="row "> <div class="row ">
<div class="col-md-12 inline-headers"> <div class="col-md-12 inline-headers">
<h3>Current status</h3> <h3>Current status</h3>
<span class="h3 pull-right label label-success"><strong>Online</strong></span> {% if virtual_machine.status == 'pending' %}
<hr> <span class="h3 pull-right label label-warning"><strong>{{virtual_machine.get_status_display}}</strong></span>
{% elif virtual_machine.status == 'online' %}
<span class="h3 pull-right label label-success"><strong>{{virtual_machine.get_status_display}}</strong></span>
{% else %}
<span class="h3 pull-right label label-error"><strong>{{virtual_machine.get_status_display}}</strong></span>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -11,7 +11,7 @@
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Type</th> <th>Location</th>
<th>Amount</th> <th>Amount</th>
<th></th> <th></th>
</tr> </tr>
@ -20,7 +20,7 @@
{% for vm in vms %} {% for vm in vms %}
<tr> <tr>
<td scope="row">{{vm.name}}</td> <td scope="row">{{vm.name}}</td>
<td>{{vm.hosting_company_name}}</td> <td>{{vm.location}}</td>
<td>{{vm.price}} CHF</td> <td>{{vm.price}} CHF</td>
<td> <td>
<button type="button" class="btn btn-default"><a href="{% url 'hosting:virtual_machines' vm.id %}">View Detail</a></button> <button type="button" class="btn btn-default"><a href="{% url 'hosting:virtual_machines' vm.id %}">View Detail</a></button>

View file

@ -1,9 +1,10 @@
from django.conf.urls import url from django.conf.urls import url
from .views import DjangoHostingView, RailsHostingView, PaymentVMView, \ from .views import DjangoHostingView, RailsHostingView, PaymentVMView,\
NodeJSHostingView, LoginView, SignupView, IndexView, \ NodeJSHostingView, LoginView, SignupView, IndexView, \
OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\ OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\
VirtualMachineDetailView, GenerateVMSSHKeysView, OrdersHostingDeleteView VirtualMachineDetailView, GenerateVMSSHKeysView, OrdersHostingDeleteView, NotificationsView, \
MarkAsReadNotificationView
urlpatterns = [ urlpatterns = [
# url(r'pricing/?$', VMPricingView.as_view(), name='pricing'), # url(r'pricing/?$', VMPricingView.as_view(), name='pricing'),
@ -20,6 +21,9 @@ urlpatterns = [
name='virtual_machines'), name='virtual_machines'),
url(r'my-virtual-machines/(?P<pk>\d+)/key/?$', GenerateVMSSHKeysView.as_view(), url(r'my-virtual-machines/(?P<pk>\d+)/key/?$', GenerateVMSSHKeysView.as_view(),
name='virtual_machine_key'), name='virtual_machine_key'),
url(r'^notifications/$', NotificationsView.as_view(), name='notifications'),
url(r'^notifications/(?P<pk>\d+)/?$', MarkAsReadNotificationView.as_view(),
name='read_notification'),
url(r'login/?$', LoginView.as_view(), name='login'), url(r'login/?$', LoginView.as_view(), name='login'),
url(r'signup/?$', SignupView.as_view(), name='signup'), url(r'signup/?$', SignupView.as_view(), name='signup'),
url(r'^logout/?$', 'django.contrib.auth.views.logout', url(r'^logout/?$', 'django.contrib.auth.views.logout',

View file

@ -2,17 +2,22 @@
from django.shortcuts import get_object_or_404, render,render_to_response from django.shortcuts import get_object_or_404, render,render_to_response
from django.core.urlresolvers import reverse_lazy, reverse from django.core.urlresolvers import reverse_lazy, reverse
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import View, CreateView, FormView, ListView, DetailView,\
from django.views.generic import View, CreateView, FormView, ListView, DetailView, UpdateView, DeleteView DeleteView, TemplateView, UpdateView
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.conf import settings from django.conf import settings
from django.contrib import messages
from stored_messages.settings import stored_messages_settings
from stored_messages.models import Message
from stored_messages.api import mark_read
from membership.models import CustomUser, StripeCustomer from membership.models import CustomUser, StripeCustomer
from utils.stripe_utils import StripeUtils from utils.stripe_utils import StripeUtils
from utils.forms import BillingAddressForm from utils.forms import BillingAddressForm
from utils.models import BillingAddress from utils.mailer import BaseEmail
from .models import VirtualMachineType, VirtualMachinePlan, HostingOrder from .models import VirtualMachineType, VirtualMachinePlan, HostingOrder
from .forms import HostingUserSignupForm, HostingUserLoginForm from .forms import HostingUserSignupForm, HostingUserLoginForm
from .mixins import ProcessVMSelectionMixin from .mixins import ProcessVMSelectionMixin
@ -145,6 +150,34 @@ class SignupView(CreateView):
return HttpResponseRedirect(self.get_success_url()) return HttpResponseRedirect(self.get_success_url())
class NotificationsView(TemplateView):
template_name = 'hosting/notifications.html'
def get_context_data(self, **kwargs):
context = super(NotificationsView, self).get_context_data(**kwargs)
backend = stored_messages_settings.STORAGE_BACKEND()
unread_notifications = backend.inbox_list(self.request.user)
read_notifications = backend.archive_list(self.request.user)
context.update({
'unread_notifications': unread_notifications,
'all_notifications': read_notifications + unread_notifications
})
return context
class MarkAsReadNotificationView(LoginRequiredMixin, UpdateView):
model = Message
success_url = reverse_lazy('hosting:notifications')
fields = '__all__'
def post(self, *args, **kwargs):
message = self.get_object()
backend = stored_messages_settings.STORAGE_BACKEND()
backend.archive_store([self.request.user], message)
mark_read(self.request.user, message)
return HttpResponseRedirect(reverse('hosting:notifications'))
class GenerateVMSSHKeysView(LoginRequiredMixin, DetailView): class GenerateVMSSHKeysView(LoginRequiredMixin, DetailView):
model = VirtualMachinePlan model = VirtualMachinePlan
template_name = 'hosting/virtual_machine_key.html' template_name = 'hosting/virtual_machine_key.html'
@ -174,6 +207,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
context.update({ context.update({
'stripe_key': settings.STRIPE_API_PUBLIC_KEY 'stripe_key': settings.STRIPE_API_PUBLIC_KEY
}) })
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@ -199,7 +233,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
customer = StripeCustomer.get_or_create(email=self.request.user.email, customer = StripeCustomer.get_or_create(email=self.request.user.email,
token=token) token=token)
if not customer: if not customer:
form.add_error("__all__","Invalid credit card") form.add_error("__all__", "Invalid credit card")
return self.render_to_response(self.get_context_data(form=form)) return self.render_to_response(self.get_context_data(form=form))
# Create Virtual Machine Plan # Create Virtual Machine Plan
@ -233,6 +267,18 @@ class PaymentVMView(LoginRequiredMixin, FormView):
# If the Stripe payment was successed, set order status approved # If the Stripe payment was successed, set order status approved
order.set_approved() order.set_approved()
# Send notification to ungleich as soon as VM has been booked
# TODO send email using celery
email_data = {
'subject': 'New VM request',
'to': 'info@ungleich.ch',
'template_name': 'new_booked_vm',
'template_path': 'emails/'
}
email = BaseEmail(**email_data)
email.send()
request.session.update({ request.session.update({
'charge': charge, 'charge': charge,
'order': order.id, 'order': order.id,
@ -265,10 +311,11 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView): class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView):
login_url=reverse_lazy('hosting:login') login_url = reverse_lazy('hosting:login')
success_url = reverse_lazy('hosting:orders') success_url = reverse_lazy('hosting:orders')
model = HostingOrder model = HostingOrder
class VirtualMachinesPlanListView(LoginRequiredMixin, ListView): class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
template_name = "hosting/virtual_machines.html" template_name = "hosting/virtual_machines.html"
login_url = reverse_lazy('hosting:login') login_url = reverse_lazy('hosting:login')

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.4 on 2016-05-26 04:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('auth', '0007_alter_validators_add_error_messages'),
('membership', '0005_customuser_is_admin'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='groups',
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
),
migrations.AddField(
model_name='customuser',
name='is_superuser',
field=models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status'),
),
migrations.AddField(
model_name='customuser',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
]

View file

@ -2,7 +2,7 @@ from datetime import datetime
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User, AbstractBaseUser, BaseUserManager, AbstractUser from django.contrib.auth.models import User, AbstractBaseUser, BaseUserManager, AbstractUser, PermissionsMixin
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -47,7 +47,7 @@ class MyUserManager(BaseUserManager):
return user return user
class CustomUser(AbstractBaseUser): class CustomUser(AbstractBaseUser, PermissionsMixin):
VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated')) VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
site = models.ForeignKey(Site, default=1) site = models.ForeignKey(Site, default=1)
name = models.CharField(max_length=50) name = models.CharField(max_length=50)

View file

@ -18,6 +18,7 @@ easy_thumbnails
django-polymorphic django-polymorphic
model-mommy model-mommy
pycryptodome pycryptodome
django-stored-messages
#PLUGINS #PLUGINS
djangocms_flash djangocms_flash

View file

@ -1,9 +1,32 @@
import six import six
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.conf import settings from django.conf import settings
class BaseEmail(object):
def __init__(self, *args, **kwargs):
self.to = kwargs.get('to')
self.template_name = kwargs.get('template_name')
self.template_path = kwargs.get('template_path')
self.subject = kwargs.get('subject')
self.context = kwargs.get('context', {})
self.template_full_path = '%s%s' % (self.template_path, self.template_name)
text_content = render_to_string('%s.txt' % self.template_full_path, self.context)
html_content = render_to_string('%s.html' % self.template_full_path, self.context)
self.email = EmailMultiAlternatives(self.subject, text_content)
self.email.attach_alternative(html_content, "text/html")
self.email.to = ['info@digitalglarus.ch']
def send(self):
self.email.send()
class BaseMailer(object): class BaseMailer(object):
def __init__(self): def __init__(self):
self._slug = None self._slug = None
@ -50,3 +73,4 @@ class DigitalGlarusRegistrationMailer(BaseMailer):
self.registration = self.message self.registration = self.message
self._message = self._message.format(slug=self._slug) self._message = self._message.format(slug=self._slug)
super().__init__() super().__init__()