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 350d09f339
22 changed files with 392 additions and 22 deletions

View file

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

View file

@ -1,5 +1,32 @@
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(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.utils.translation import ugettext_lazy as _
from django.utils.functional import cached_property
from membership.models import StripeCustomer
from utils.models import BillingAddress
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
@ -81,12 +82,24 @@ class VirtualMachineType(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()
memory = models.IntegerField()
disk_size = models.IntegerField()
vm_type = models.ForeignKey(VirtualMachineType)
price = models.FloatField()
public_key = models.TextField()
status = models.CharField(max_length=20, choices=VM_STATUS_CHOICES, default=PENDING_STATUS)
objects = VMPlansManager()
@ -97,11 +110,22 @@ class VirtualMachinePlan(models.Model):
def hosting_company_name(self):
return self.vm_type.get_hosting_company_display()
@cached_property
def location(self):
return self.vm_type.get_location_display()
@cached_property
def name(self):
name = 'vm-%s' % self.id
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
def create(cls, data, user):
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
</a>
</li>
<li>
<a href="{% url 'hosting:notifications' %}">
<i class="fa fa-bell"></i> Notifications
</a>
</li>
<li class="dropdown">
<a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#">
<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' %}">
<li class="type">
<!-- <img src="http://bread.pp.ua/n/settings_g.svg" alt=""> -->
<h3 >{{vm.hosting_company_name}}</h3>
<h3 >{{vm.location_code}}</h3>
<br/>
<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>
<div class="tab-pane" id="status-v">
<div class="row ">
<div class="col-md-12 inline-headers">
<h3>Current status</h3>
<span class="h3 pull-right label label-success"><strong>Online</strong></span>
<hr>
{% if virtual_machine.status == 'pending' %}
<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>

View file

@ -11,7 +11,7 @@
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Location</th>
<th>Amount</th>
<th></th>
</tr>
@ -20,7 +20,7 @@
{% for vm in vms %}
<tr>
<td scope="row">{{vm.name}}</td>
<td>{{vm.hosting_company_name}}</td>
<td>{{vm.location}}</td>
<td>{{vm.price}} CHF</td>
<td>
<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 .views import DjangoHostingView, RailsHostingView, PaymentVMView, \
from .views import DjangoHostingView, RailsHostingView, PaymentVMView,\
NodeJSHostingView, LoginView, SignupView, IndexView, \
OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\
VirtualMachineDetailView, GenerateVMSSHKeysView, OrdersHostingDeleteView
VirtualMachineDetailView, GenerateVMSSHKeysView, OrdersHostingDeleteView, NotificationsView, \
MarkAsReadNotificationView
urlpatterns = [
# url(r'pricing/?$', VMPricingView.as_view(), name='pricing'),
@ -20,6 +21,9 @@ urlpatterns = [
name='virtual_machines'),
url(r'my-virtual-machines/(?P<pk>\d+)/key/?$', GenerateVMSSHKeysView.as_view(),
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'signup/?$', SignupView.as_view(), name='signup'),
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.core.urlresolvers import reverse_lazy, reverse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import View, CreateView, FormView, ListView, DetailView, UpdateView, DeleteView
from django.http import HttpResponseRedirect, HttpResponse
from django.views.generic import View, CreateView, FormView, ListView, DetailView,\
DeleteView, TemplateView, UpdateView
from django.http import HttpResponseRedirect
from django.contrib.auth import authenticate, login
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 utils.stripe_utils import StripeUtils
from utils.forms import BillingAddressForm
from utils.models import BillingAddress
from utils.mailer import BaseEmail
from .models import VirtualMachineType, VirtualMachinePlan, HostingOrder
from .forms import HostingUserSignupForm, HostingUserLoginForm
from .mixins import ProcessVMSelectionMixin
@ -145,6 +150,34 @@ class SignupView(CreateView):
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):
model = VirtualMachinePlan
template_name = 'hosting/virtual_machine_key.html'
@ -174,6 +207,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
context.update({
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
})
return context
def post(self, request, *args, **kwargs):
@ -199,7 +233,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
customer = StripeCustomer.get_or_create(email=self.request.user.email,
token=token)
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))
# Create Virtual Machine Plan
@ -233,6 +267,18 @@ class PaymentVMView(LoginRequiredMixin, FormView):
# If the Stripe payment was successed, set order status 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({
'charge': charge,
'order': order.id,
@ -265,10 +311,11 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
class OrdersHostingDeleteView(LoginRequiredMixin, DeleteView):
login_url=reverse_lazy('hosting:login')
login_url = reverse_lazy('hosting:login')
success_url = reverse_lazy('hosting:orders')
model = HostingOrder
class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
template_name = "hosting/virtual_machines.html"
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.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.core.validators import RegexValidator
from django.contrib.auth.models import User
@ -47,7 +47,7 @@ class MyUserManager(BaseUserManager):
return user
class CustomUser(AbstractBaseUser):
class CustomUser(AbstractBaseUser, PermissionsMixin):
VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
site = models.ForeignKey(Site, default=1)
name = models.CharField(max_length=50)

View file

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

View file

@ -1,9 +1,32 @@
import six
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
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):
def __init__(self):
self._slug = None
@ -50,3 +73,4 @@ class DigitalGlarusRegistrationMailer(BaseMailer):
self.registration = self.message
self._message = self._message.format(slug=self._slug)
super().__init__()