diff --git a/email_cron b/email_cron deleted file mode 100644 index 559fd565..00000000 --- a/email_cron +++ /dev/null @@ -1,3 +0,0 @@ -* * * * * (cd /home/app/app/; /usr/bin/python3 manage.py send_mail) -0,20,40 * * * * (cd /home/app/app/; /usr/bin/python3 manage.py retry_deferred) -0 0 * * * (cd /home/app/app/; /usr/bin/python3 manage.py purge_mail_log 7) \ No newline at end of file diff --git a/hosting/forms.py b/hosting/forms.py index c6537355..8007b2f4 100644 --- a/hosting/forms.py +++ b/hosting/forms.py @@ -4,7 +4,7 @@ from django.contrib.auth import authenticate from utils.stripe_utils import StripeUtils -from .models import HostingOrder +from .models import HostingOrder, VirtualMachinePlan class HostingOrderAdminForm(forms.ModelForm): @@ -17,6 +17,10 @@ class HostingOrderAdminForm(forms.ModelForm): customer = self.cleaned_data.get('customer') vm_plan = self.cleaned_data.get('vm_plan') + if vm_plan.status == VirtualMachinePlan.CANCELED_STATUS: + raise forms.ValidationError("""You can't make a charge over + a canceled virtual machine plan""") + # Make a charge to the customer stripe_utils = StripeUtils() charge_response = stripe_utils.make_charge(customer=customer.stripe_id, @@ -53,7 +57,7 @@ class HostingUserLoginForm(forms.Form): CustomUser.objects.get(email=email) return email except CustomUser.DoesNotExist: - raise forms.ValidationError("User does not exists") + raise forms.ValidationError("User does not exist") else: return email diff --git a/hosting/static/hosting/css/landing-page.css b/hosting/static/hosting/css/landing-page.css index fc0b98f2..166a8241 100644 --- a/hosting/static/hosting/css/landing-page.css +++ b/hosting/static/hosting/css/landing-page.css @@ -83,6 +83,12 @@ h6 { height: 100%; } +.intro-reset-password { + background: url(../img/signup-bg.png) no-repeat center center; + background-size: cover; + height: 100%; +} + .intro-message > h1 { margin: 0; font-weight: 400; diff --git a/hosting/templates/emails/password_reset_email.html b/hosting/templates/emails/password_reset_email.html new file mode 100644 index 00000000..682834f5 --- /dev/null +++ b/hosting/templates/emails/password_reset_email.html @@ -0,0 +1,13 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} + {% block reset_link %} + {{ base_url }}{% url 'hosting:reset_password_confirm' uidb64=uid token=token %} + {% endblock %} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} \ No newline at end of file diff --git a/hosting/templates/emails/password_reset_email.txt b/hosting/templates/emails/password_reset_email.txt new file mode 100644 index 00000000..682834f5 --- /dev/null +++ b/hosting/templates/emails/password_reset_email.txt @@ -0,0 +1,13 @@ +{% load i18n %}{% autoescape off %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} + +{% trans "Please go to the following page and choose a new password:" %} + {% block reset_link %} + {{ base_url }}{% url 'hosting:reset_password_confirm' uidb64=uid token=token %} + {% endblock %} + +{% trans "Thanks for using our site!" %} + +{% blocktrans %}The {{ site_name }} team{% endblocktrans %} + +{% endautoescape %} \ No newline at end of file diff --git a/hosting/templates/hosting/base_short.html b/hosting/templates/hosting/base_short.html index 5c6110c5..5f0604a7 100644 --- a/hosting/templates/hosting/base_short.html +++ b/hosting/templates/hosting/base_short.html @@ -53,7 +53,7 @@ <span class="icon-bar"></span> <span class="icon-bar"></span> </button> - <a class="navbar-brand topnav" href="{% url 'ungleich_page:landing' %}"><img src="{% static 'hosting/img/logo_black.svg' %}"></a> + <a class="navbar-brand topnav" href="{{ request.session.hosting_url}}"><img src="{% static 'hosting/img/logo_black.svg' %}"></a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> diff --git a/hosting/templates/hosting/confirm_reset_password.html b/hosting/templates/hosting/confirm_reset_password.html new file mode 100644 index 00000000..92513ced --- /dev/null +++ b/hosting/templates/hosting/confirm_reset_password.html @@ -0,0 +1,38 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3%} +{% block content %} + <div class="intro-auth intro-reset-password"> + <div class="container"> + <div class="col-md-4"> </div> + <div class="col-md-4"> + <div class="intro-message"> + + {% if messages %} + <ul class="list-unstyled"> + {% for message in messages %} + <li>{{ message }}</li> + {% endfor %} + </ul> + {% endif %} + + <h2 class="section-heading">Set your new password</h2> + + <form action="" method="post" class="form" novalidate> + {% csrf_token %} + {% for field in form %} + {% bootstrap_field field show_label=False %} + {% endfor %} + {% buttons %} + <button type="submit" class="btn btn-default"> + Reset + </button> + {% endbuttons %} + </form> + <span>Already have an account ? <a class="unlink" href="{% url 'hosting:login' %}">Log in</a></span> + <ul class="list-inline intro-social-buttons"> + </ul> + </div> + </div> + </div> + </div> +{% endblock %} diff --git a/hosting/templates/hosting/login.html b/hosting/templates/hosting/login.html index 0421533b..08d1220f 100644 --- a/hosting/templates/hosting/login.html +++ b/hosting/templates/hosting/login.html @@ -6,6 +6,14 @@ <div class="container"> <div class="col-md-4 col-md-offset-4"> + {% if messages %} + <ul class="list-unstyled"> + {% for message in messages %} + <li>{{ message }}</li> + {% endfor %} + </ul> + {% endif %} + {% block messages %} {% if request.GET.logged_out %} <div class="alert"> <!-- singular --> @@ -31,6 +39,8 @@ {% endbuttons %} </form> <span>Don't have an account yet ? <a class="unlink" href="{% url 'hosting:signup' %}">Sign up</a></span> + <br/> + <span> <a class="unlink" href="{% url 'hosting:reset_password' %}">Forgot your password ?</a></span> <ul class="list-inline intro-social-buttons"> diff --git a/hosting/templates/hosting/reset_password.html b/hosting/templates/hosting/reset_password.html new file mode 100644 index 00000000..d9dadda9 --- /dev/null +++ b/hosting/templates/hosting/reset_password.html @@ -0,0 +1,29 @@ +{% extends "hosting/base_short.html" %} +{% load staticfiles bootstrap3%} +{% block content %} + <div class="intro-auth intro-reset-password"> + <div class="container"> + <div class="col-md-4"> </div> + <div class="col-md-4"> + <div class="intro-message"> + <h2 class="section-heading">Reset your password</h2> + + <form action="{% url 'hosting:reset_password' %}" method="post" class="form" novalidate> + {% csrf_token %} + {% for field in form %} + {% bootstrap_field field show_label=False %} + {% endfor %} + {% buttons %} + <button type="submit" class="btn btn-default"> + Reset + </button> + {% endbuttons %} + </form> + <span>Already have an account ? <a class="unlink" href="{% url 'hosting:login' %}">Log in</a></span> + <ul class="list-inline intro-social-buttons"> + </ul> + </div> + </div> + </div> + </div> +{% endblock %} diff --git a/hosting/templates/hosting/signup.html b/hosting/templates/hosting/signup.html index 663d67b5..12e7e096 100644 --- a/hosting/templates/hosting/signup.html +++ b/hosting/templates/hosting/signup.html @@ -3,8 +3,8 @@ {% block content %} <div class="intro-auth intro-signup"> <div class="container"> - <div class="col-md-4"> </div> - <div class="col-md-4"> + <div class="col-md-4 col-sm-4 col-xs-4"> </div> + <div class="col-md-4 col-sm-6 col-xs-6"> <div class="intro-message"> <h2 class="section-heading">Sign up</h2> diff --git a/hosting/urls.py b/hosting/urls.py index 83bab74b..225dd19e 100644 --- a/hosting/urls.py +++ b/hosting/urls.py @@ -4,7 +4,7 @@ from .views import DjangoHostingView, RailsHostingView, PaymentVMView,\ NodeJSHostingView, LoginView, SignupView, IndexView, \ OrdersHostingListView, OrdersHostingDetailView, VirtualMachinesPlanListView,\ VirtualMachineView, GenerateVMSSHKeysView, OrdersHostingDeleteView, NotificationsView, \ - MarkAsReadNotificationView + MarkAsReadNotificationView, PasswordResetView, PasswordResetConfirmView urlpatterns = [ url(r'index/?$', IndexView.as_view(), name='index'), @@ -27,6 +27,9 @@ urlpatterns = [ name='read_notification'), url(r'login/?$', LoginView.as_view(), name='login'), url(r'signup/?$', SignupView.as_view(), name='signup'), + url(r'reset-password/?$', PasswordResetView.as_view(), name='reset_password'), + url(r'reset-password-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$', + PasswordResetConfirmView.as_view(), name='reset_password_confirm'), url(r'^logout/?$', 'django.contrib.auth.views.logout', {'next_page': '/hosting/login?logged_out=true'}, name='logout') ] diff --git a/hosting/views.py b/hosting/views.py index ca591c45..09f94ac9 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -4,6 +4,10 @@ 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,\ DeleteView, TemplateView, UpdateView +from django.contrib.auth.tokens import default_token_generator +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.contrib import messages +from django.utils.encoding import force_bytes from django.http import HttpResponseRedirect from django.contrib.auth import authenticate, login from django.conf import settings @@ -16,7 +20,7 @@ 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.forms import BillingAddressForm, PasswordResetRequestForm, SetPasswordForm from utils.mailer import BaseEmail from .models import VirtualMachineType, VirtualMachinePlan, HostingOrder from .forms import HostingUserSignupForm, HostingUserLoginForm @@ -164,6 +168,71 @@ class SignupView(CreateView): return HttpResponseRedirect(self.get_success_url()) +class PasswordResetView(FormView): + template_name = 'hosting/reset_password.html' + form_class = PasswordResetRequestForm + success_message = "The link to reset your email has been sent to your email" + success_url = reverse_lazy('hosting:login') + # form_valid_message = 'Thank you for registering' + + def form_valid(self, form): + + email = form.cleaned_data.get('email') + user = CustomUser.objects.get(email=email) + + messages.add_message(self.request, messages.SUCCESS, self.success_message) + + context = { + 'user': user, + 'token': default_token_generator.make_token(user), + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'site_name': 'ungleich', + 'base_url': "{0}://{1}".format(self.request.scheme, self.request.get_host()) + + } + email_data = { + 'subject': 'Password Reset', + 'to': email, + 'context': context, + 'template_name': 'password_reset_email', + 'template_path': 'emails/' + } + email = BaseEmail(**email_data) + email.send() + + return HttpResponseRedirect(self.get_success_url()) + + +class PasswordResetConfirmView(FormView): + template_name = 'hosting/confirm_reset_password.html' + form_class = SetPasswordForm + success_url = reverse_lazy('hosting:login') + + def post(self, request, uidb64=None, token=None, *arg, **kwargs): + try: + uid = urlsafe_base64_decode(uidb64) + user = CustomUser.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): + user = None + + form = self.form_class(request.POST) + + if user is not None and default_token_generator.check_token(user, token): + if form.is_valid(): + new_password = form.cleaned_data['new_password2'] + user.set_password(new_password) + user.save() + messages.success(request, 'Password has been reset.') + return self.form_valid(form) + else: + messages.error(request, 'Password reset has not been unsuccessful.') + return self.form_invalid(form) + + else: + messages.error(request, 'The reset password link is no longer valid.') + return self.form_invalid(form) + + class NotificationsView(TemplateView): template_name = 'hosting/notifications.html' @@ -286,16 +355,6 @@ class PaymentVMView(LoginRequiredMixin, FormView): # Send notification to ungleich as soon as VM has been booked # TODO send email using celery - from django.core.mail import send_mail - - send_mail( - 'Subject here', - 'Here is the message.', - 'levinoelvm@gmail.com', - ['levinoelvm@gmail.com'], - fail_silently=False, - ) - context = { 'vm': plan, 'order': order, diff --git a/utils/forms.py b/utils/forms.py index 0e64020a..7af6f99a 100644 --- a/utils/forms.py +++ b/utils/forms.py @@ -3,9 +3,52 @@ from .models import ContactMessage, BillingAddress from django.template.loader import render_to_string from django.core.mail import EmailMultiAlternatives from django.utils.translation import ugettext_lazy as _ +from membership.models import CustomUser # from utils.fields import CountryField +class PasswordResetRequestForm(forms.Form): + email = forms.CharField(widget=forms.EmailInput()) + + class Meta: + fields = ['email'] + + def clean_email(self): + email = self.cleaned_data.get('email') + try: + CustomUser.objects.get(email=email) + return email + except CustomUser.DoesNotExist: + raise forms.ValidationError("User does not exist") + else: + return email + + +class SetPasswordForm(forms.Form): + """ + A form that lets a user change set their password without entering the old + password + """ + error_messages = { + 'password_mismatch': ("The two password fields didn't match."), + } + new_password1 = forms.CharField(label=("New password"), + widget=forms.PasswordInput) + new_password2 = forms.CharField(label=("New password confirmation"), + widget=forms.PasswordInput) + + def clean_new_password2(self): + password1 = self.cleaned_data.get('new_password1') + password2 = self.cleaned_data.get('new_password2') + if password1 and password2: + if password1 != password2: + raise forms.ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch',) + return password2 + + + class BillingAddressForm(forms.ModelForm): token = forms.CharField(widget=forms.HiddenInput())