{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}
Hunting down big muskies requires a rod that can handle big, heavy rigs and crushing hits. Fast-action, SCII graphite, with medium-heavy to extra-heavy power, tames hard-thrashing fish. All have aluminum-oxide guides, tip tops with zirconium inserts, premium-grade cork split-grip handles and DPS reel seats.
{% blocktrans %}This part of the site requires us to verify that
13 | you are who you claim to be. For this purpose, we require that you
14 | verify ownership of your e-mail address. {% endblocktrans %}
15 |
16 |
{% blocktrans %}We have sent an e-mail to you for
17 | verification. Please click on the link inside this e-mail. Please
18 | contact us if you do not receive it within a few minutes.{% endblocktrans %}
35 | {% endblock content %}
36 |
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this
4 | software and associated documentation files (the "Software"), to deal in the Software
5 | without restriction, including without limitation the rights to use, copy, modify,
6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7 | permit persons to whom the Software is furnished to do so.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/users/forms.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model, forms
2 | from django.core.exceptions import ValidationError
3 | from django.utils.translation import ugettext_lazy as _
4 |
5 | User = get_user_model()
6 |
7 |
8 | class UserChangeForm(forms.UserChangeForm):
9 |
10 | class Meta(forms.UserChangeForm.Meta):
11 | model = User
12 |
13 |
14 | class UserCreationForm(forms.UserCreationForm):
15 |
16 | error_message = forms.UserCreationForm.error_messages.update(
17 | {"duplicate_username": _("This username has already been taken.")}
18 | )
19 |
20 | class Meta(forms.UserCreationForm.Meta):
21 | model = User
22 |
23 | def clean_username(self):
24 | username = self.cleaned_data["username"]
25 |
26 | try:
27 | User.objects.get(username=username)
28 | except User.DoesNotExist:
29 | return username
30 |
31 | raise ValidationError(self.error_messages["duplicate_username"])
32 |
--------------------------------------------------------------------------------
/lambda_at_edge/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Add index.html to the source uri to handle a default URI
5 | * in a single behaviour with other content.
6 | *
7 | * Taken from Ronnie Eichler's blog post:
8 | * https://aws.amazon.com/blogs/compute/implementing-default-directory-indexes-in-amazon-s3-backed-amazon-cloudfront-origins-using-lambdaedge/
9 | */
10 | exports.index_rewrite = (event, context, callback) => {
11 | // Extract the request from the CloudFront event that is sent to Lambda@Edge
12 | var request = event.Records[0].cf.request;
13 |
14 | // Extract the URI from the request
15 | var olduri = request.uri;
16 |
17 | // Match any '/' that occurs at the end of a URI. Replace it with a default index
18 | var newuri = olduri.replace(/\/$/, '\/index.html');
19 |
20 | // Replace the received URI with the URI that includes the index page
21 | request.uri = newuri;
22 |
23 | // Return to CloudFront
24 | return callback(null, request);
25 | };
26 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/templates/account/password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Password Reset" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
11 |
{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}
17 |
18 |
23 |
24 |
{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}
25 | {% endblock %}
26 |
27 |
--------------------------------------------------------------------------------
/backend/src/main/java/fishing/lee/backend/OrderController.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.backend;
2 |
3 | import de.mkammerer.argon2.Argon2;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.PathVariable;
7 | import org.springframework.web.bind.annotation.PostMapping;
8 | import org.springframework.web.bind.annotation.RestController;
9 |
10 | import java.util.UUID;
11 |
12 | @RestController
13 | public class OrderController {
14 | private final Argon2 argon;
15 |
16 | @Autowired
17 | public OrderController(Argon2 argon) {
18 | this.argon = argon;
19 | }
20 |
21 | @GetMapping("/order/{id}")
22 | String one(@PathVariable UUID id) {
23 | return "whatever";
24 | }
25 |
26 | @PostMapping("/order")
27 | String post() {
28 | return argon.hash(75, 65536, Math.min(Runtime.getRuntime().availableProcessors() / 2, 1), "flibble");
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/docs/exercise_2_post.rst:
--------------------------------------------------------------------------------
1 | Exercise #2 - Checklist for completion
2 | ======================================
3 |
4 | 1. Pop over to the CloudFront service in the Console and copy the domain name
5 | for the Distribution and load it up in a new tab.
6 | 2. Click Rods and then the Muskie Casting Rod
7 | 3. Buy the item (it will make you sign up etc.)
8 |
9 | .. Attention:: **DO NOT ENTER CREDIT CARD INFORMATION ON THIS FORM**
10 |
11 | Below you can see a picture of how are architecture looks now. The area with
12 | the green background is the area that has changed since our initial deployment.
13 |
14 | .. image:: images/exercise_2_arch.png
15 | :height: 600px
16 | :align: center
17 |
18 | Because we have replaced the existing service with an Amazon API Gateway and
19 | a Lambda Function, we have more flexibity to override methods in our existing
20 | service and benefit from AWS Lambda being in control of scaling. Because AWS
21 | Lambda takes care of scaling for you, there's no need to configure Auto Scaling
22 | Groups.
23 |
--------------------------------------------------------------------------------
/infrastructure/src/main/java/fishing/lee/infrastructure/PostgresProps.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.infrastructure;
2 |
3 | import java.util.Objects;
4 |
5 | interface PostgresProps {
6 | ShopVpc getShopVpc();
7 |
8 | static PostgresProps.Builder builder() {
9 | return new PostgresProps.Builder();
10 | }
11 |
12 | final class Builder {
13 | ShopVpc _shopVpc;
14 |
15 | PostgresProps.Builder withShopVpc(ShopVpc shopVpc) {
16 | this._shopVpc = shopVpc;
17 | return this;
18 | }
19 |
20 | PostgresProps build() {
21 | return new PostgresProps() {
22 | private ShopVpc shopVpc;
23 |
24 | {
25 | shopVpc = Objects.requireNonNull(Builder.this._shopVpc, "shopVpc Required");
26 | }
27 |
28 | @Override
29 | public ShopVpc getShopVpc() {
30 | return this.shopVpc;
31 | }
32 | };
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frontend_minisite/README.md:
--------------------------------------------------------------------------------
1 | # HTML Boilerplate
2 |
3 | > Frontend HTML boilerplate based on Bootstrap 4, Webpack for assets management, and Handlebars as webpack html template engine. Useful for quick interactive mockup development, easily transposable to any Backend Framework.
4 |
5 | ## Frontend features
6 |
7 | * HTML5 boilerplate with basic home-about-contact pages
8 | * Bootstrap 4 beta
9 | * Slick Carrousel
10 | * Sweet Alert 2
11 | * Vue.js
12 |
13 | ## Quick start
14 |
15 | ```shell
16 | yarn
17 | yarn dev
18 | ```
19 |
20 | Just as easy !
21 | Web site should be accessible from localhost:3000.
22 | Port is configurable by `.env` file settings (just copy `.env.example`).
23 |
24 | ## Development
25 |
26 | ### Commands
27 |
28 | * `yarn dev`
29 | Start Webpack assets compilation and express server with hot reloading support.
30 |
31 | * `yarn build`
32 | Compile all JS application into redistributable static files into `dist` folder.
33 |
34 | ## License
35 |
36 | This project is open-sourced software licensed under the [MIT license](https://adr1enbe4udou1n.mit-license.org).
37 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/templates/account/password_reset_from_key.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
6 |
7 | {% block inner %}
8 |
{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}
9 |
10 | {% if token_fail %}
11 | {% url 'account_reset_password' as passwd_reset_url %}
12 |
{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}
13 | {% else %}
14 | {% if form %}
15 |
20 | {% else %}
21 |
{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}
12 |
13 | {% get_providers as socialaccount_providers %}
14 |
15 | {% if socialaccount_providers %}
16 |
{% blocktrans with site.name as site_name %}Please sign in with one
17 | of your existing third party accounts. Or, sign up
18 | for a {{ site_name }} account and sign in below:{% endblocktrans %}
19 |
20 |
21 |
22 |
23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %}
24 |
52 | {% endblock %}
53 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/fishing_net/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-10-03 07:44
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('fishing_equipment', '0003_auto_20181002_2026'),
15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name='Basket',
21 | fields=[
22 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
23 | ('deleted_at', models.DateTimeField(blank=True, null=True)),
24 | ('created_date', models.DateTimeField(auto_now_add=True)),
25 | ('modified_date', models.DateTimeField(auto_now=True)),
26 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)),
27 | ],
28 | ),
29 | migrations.CreateModel(
30 | name='BasketItem',
31 | fields=[
32 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
33 | ('item_price', models.IntegerField()),
34 | ('quantity', models.IntegerField(default=0)),
35 | ('created_date', models.DateTimeField(auto_now_add=True)),
36 | ('modified_date', models.DateTimeField(auto_now=True)),
37 | ('basket', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='fishing_net.Basket')),
38 | ('item_sku', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='fishing_equipment.Product')),
39 | ],
40 | ),
41 | ]
42 |
--------------------------------------------------------------------------------
/sqs_order_forwarder/src/main/java/fishing/lee/sqsforwarder/OrderForwarder.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.sqsforwarder;
2 |
3 | import com.amazonaws.services.lambda.runtime.Context;
4 | import com.amazonaws.services.lambda.runtime.LambdaLogger;
5 | import com.amazonaws.services.lambda.runtime.RequestHandler;
6 | import com.amazonaws.services.lambda.runtime.events.SQSEvent;
7 | import org.apache.http.client.methods.CloseableHttpResponse;
8 | import org.apache.http.client.methods.HttpPost;
9 | import org.apache.http.entity.StringEntity;
10 | import org.apache.http.impl.client.CloseableHttpClient;
11 | import org.apache.http.impl.client.HttpClients;
12 | import org.apache.http.util.EntityUtils;
13 |
14 | import java.io.IOException;
15 |
16 | @SuppressWarnings("unused")
17 | public class OrderForwarder implements RequestHandler {
18 | private LambdaLogger logger;
19 |
20 | @Override
21 | public String handleRequest(SQSEvent input, Context context) {
22 | logger = context.getLogger();
23 |
24 | input.getRecords().forEach(sqsMessage -> {
25 | String orderDetail = sqsMessage.getBody();
26 | sendOrder(orderDetail);
27 | });
28 |
29 | return "";
30 | }
31 |
32 | private void sendOrder(String orderDetail) {
33 | logger.log("PROCESSING " + orderDetail);
34 |
35 | final String url = System.getenv("SHOPBACKEND_ORDER_URL") + "/order";
36 |
37 | try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
38 | HttpPost httpPost = new HttpPost(url);
39 | httpPost.addHeader("Content-Type", "application/json");
40 | httpPost.setEntity(new StringEntity(orderDetail));
41 |
42 | CloseableHttpResponse response = httpclient.execute(httpPost);
43 | logger.log(EntityUtils.toString(response.getEntity()));
44 | } catch (IOException e) {
45 | // We're cool and we'll ignore it
46 | e.printStackTrace();
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/frontend/README.rst:
--------------------------------------------------------------------------------
1 | Lee Fishing
2 | ===========
3 |
4 | Keeping it Reel!
5 |
6 | .. image:: https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg
7 | :target: https://github.com/pydanny/cookiecutter-django/
8 | :alt: Built with Cookiecutter Django
9 |
10 |
11 | :License: MIT
12 |
13 |
14 | Settings
15 | --------
16 |
17 | Moved to settings_.
18 |
19 | .. _settings: http://cookiecutter-django.readthedocs.io/en/latest/settings.html
20 |
21 | Basic Commands
22 | --------------
23 |
24 | Setting Up Your Users
25 | ^^^^^^^^^^^^^^^^^^^^^
26 |
27 | * To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go.
28 |
29 | * To create an **superuser account**, use this command::
30 |
31 | $ python manage.py createsuperuser
32 |
33 | For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users.
34 |
35 | Type checks
36 | ^^^^^^^^^^^
37 |
38 | Running type checks with mypy:
39 |
40 | ::
41 |
42 | $ mypy lee_fishing
43 |
44 | Test coverage
45 | ^^^^^^^^^^^^^
46 |
47 | To run the tests, check your test coverage, and generate an HTML coverage report::
48 |
49 | $ coverage run -m pytest
50 | $ coverage html
51 | $ open htmlcov/index.html
52 |
53 | Running tests with py.test
54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
55 |
56 | ::
57 |
58 | $ pytest
59 |
60 | Live reloading and Sass CSS compilation
61 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 |
63 | Moved to `Live reloading and SASS compilation`_.
64 |
65 | .. _`Live reloading and SASS compilation`: http://cookiecutter-django.readthedocs.io/en/latest/live-reloading-and-sass-compilation.html
66 |
67 |
68 |
69 |
70 |
71 | Deployment
72 | ----------
73 |
74 | The following details how to deploy this application.
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/infrastructure/src/main/java/fishing/lee/infrastructure/ShopStack.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.infrastructure;
2 |
3 | import software.amazon.awscdk.App;
4 | import software.amazon.awscdk.Stack;
5 | import software.amazon.awscdk.StackProps;
6 |
7 | class ShopStack extends Stack {
8 | ShopStack(final App parent, final String name) {
9 | this(parent, name, null);
10 | }
11 |
12 | private ShopStack(final App parent, final String name, final StackProps props) {
13 | super(parent, name, props);
14 |
15 | ShopVpc shopVpc = new ShopVpc(this, "Vpc");
16 |
17 | Postgres postgres = new Postgres(this, "ShopDatabase", PostgresProps.builder()
18 | .withShopVpc(shopVpc)
19 | .build());
20 |
21 | Bastion bastion = new Bastion(this, "Bastion", BastionProps.builder()
22 | .withShopVpc(shopVpc)
23 | .withSshKeyPairName("FishingKey")
24 | .build());
25 |
26 | ShopBackend shopBackend = new ShopBackend(this, "ShopBackend", ShopBackendProps.builder()
27 | .withShopVpc(shopVpc)
28 | .withBastionSecurityGroup(bastion.getBastionSecurityGroup())
29 | .build());
30 |
31 | new ScheduledApiCaller(this, "ApiPing", ScheduledApiCallerProps.builder()
32 | .withShopVpc(shopVpc)
33 | .withApiToPing(shopBackend.getLambdaApiUrl())
34 | .build());
35 |
36 | new ShopFrontend(this, "ShopFrontend", ShopFrontendProps.builder()
37 | .withShopVpc(shopVpc)
38 | .withShopBackend(shopBackend)
39 | .withPostgres(postgres)
40 | .build());
41 |
42 | new QueueProxy(this, "QueueProxy", QueueProxyProps.builder()
43 | .withShopVpc(shopVpc)
44 | .withApiUrl(shopBackend.getLambdaApiUrl())
45 | .build());
46 |
47 | new Decoupling(this, "Decoupling");
48 |
49 | new DeploymentAssets(this, "DeploymentAssets");
50 |
51 | new ContentDistribution(this, "CDN");
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/config/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.urls import include, path
3 | from django.conf.urls.static import static
4 | from django.contrib import admin
5 | from django.views.generic import TemplateView
6 | from django.views import defaults as default_views
7 |
8 | from lee_fishing.fishing_equipment.views import RootRedirect
9 |
10 | urlpatterns = [
11 | path("", RootRedirect.as_view(), name="home"),
12 | path(
13 | "about/",
14 | TemplateView.as_view(template_name="pages/about.html"),
15 | name="about",
16 | ),
17 | # Django Admin, use {% url 'admin:index' %}
18 | path(settings.ADMIN_URL, admin.site.urls),
19 | # User management
20 | path(
21 | "users/",
22 | include("lee_fishing.users.urls", namespace="users"),
23 | ),
24 | path("accounts/", include("allauth.urls")),
25 | # Your stuff: custom urls includes go here
26 | path("basket/", include("lee_fishing.fishing_net.urls", namespace="baskets")),
27 | path("shop/", include("lee_fishing.fishing_equipment.urls", namespace="products")),
28 | ] + static(
29 | settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
30 | )
31 |
32 | if settings.DEBUG:
33 | # This allows the error pages to be debugged during development, just visit
34 | # these url in browser to see how these error pages look like.
35 | urlpatterns += [
36 | path(
37 | "400/",
38 | default_views.bad_request,
39 | kwargs={"exception": Exception("Bad Request!")},
40 | ),
41 | path(
42 | "403/",
43 | default_views.permission_denied,
44 | kwargs={"exception": Exception("Permission Denied")},
45 | ),
46 | path(
47 | "404/",
48 | default_views.page_not_found,
49 | kwargs={"exception": Exception("Page not Found")},
50 | ),
51 | path("500/", default_views.server_error),
52 | ]
53 | if "debug_toolbar" in settings.INSTALLED_APPS:
54 | import debug_toolbar
55 |
56 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
57 |
--------------------------------------------------------------------------------
/infrastructure/src/main/java/fishing/lee/infrastructure/ShopVpc.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.infrastructure;
2 |
3 | import software.amazon.awscdk.AwsRegion;
4 | import software.amazon.awscdk.Construct;
5 | import software.amazon.awscdk.services.ec2.*;
6 |
7 | import java.util.Collections;
8 | import java.util.stream.Collectors;
9 |
10 | class ShopVpc extends Construct {
11 | private final VpcNetwork vpc;
12 | private final SecurityGroup apiEndpointSecurityGroup;
13 | private final CfnVPCEndpoint apiEndpoint;
14 |
15 | ShopVpc(Construct parent, String id) {
16 | super(parent, id);
17 |
18 | vpc = new VpcNetwork(this, "Fishing", VpcNetworkProps.builder()
19 | .withMaxAZs(2)
20 | .withCidr("10.0.0.0/16")
21 | .withEnableDnsSupport(true)
22 | .withEnableDnsHostnames(true)
23 | .withNatGateways(1)
24 | .build());
25 |
26 | apiEndpointSecurityGroup = new SecurityGroup(this, "APISecurityGroup", SecurityGroupProps.builder()
27 | .withVpc(vpc)
28 | .build());
29 |
30 | apiEndpoint = new CfnVPCEndpoint(this, "APIEndpoint", CfnVPCEndpointProps.builder()
31 | .withVpcId(vpc.getVpcId())
32 | .withServiceName("com.amazonaws." + new AwsRegion() + ".execute-api")
33 | .withPrivateDnsEnabled(true)
34 | .withSecurityGroupIds(Collections.singletonList(apiEndpointSecurityGroup.getSecurityGroupId()))
35 | .withVpcEndpointType("Interface")
36 | .withSubnetIds(vpc.getPrivateSubnets()
37 | .stream()
38 | .map(VpcSubnetRef::getSubnetId)
39 | .collect(Collectors.toList()))
40 | .build());
41 | }
42 |
43 | VpcNetwork getVpc() {
44 | return vpc;
45 | }
46 |
47 | void addSecurityGroupToApi(SecurityGroup securityGroup) {
48 | apiEndpointSecurityGroup.addIngressRule(securityGroup, new TcpPort(443));
49 | }
50 |
51 | CfnVPCEndpoint getApiEndpoint() {
52 | return apiEndpoint;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/fishing_net/models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.db import models
4 | from django.contrib.auth import get_user_model
5 |
6 | from lee_fishing.fishing_net.managers import BasketManager
7 |
8 |
9 | class Basket(models.Model):
10 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
11 | created_by = models.ForeignKey(get_user_model(), on_delete=models.DO_NOTHING)
12 |
13 | deleted_at = models.DateTimeField(null=True, blank=True)
14 | created_date = models.DateTimeField(auto_now_add=True)
15 | modified_date = models.DateTimeField(auto_now=True)
16 | order_id = models.TextField(null=True, blank=True, max_length=500)
17 |
18 | objects = BasketManager()
19 |
20 | def items_with_quantity(self):
21 | return self.basketitem_set.filter(quantity__gt=0)
22 |
23 | def add_item(self, item):
24 | if not self.modified_date:
25 | self.save()
26 |
27 | basket_item, created = self.basketitem_set.update_or_create(
28 | item_sku=item,
29 | defaults={
30 | 'basket': self,
31 | 'item_price': item.price,
32 | }
33 | )
34 |
35 | basket_item.quantity = 1 if created else basket_item.quantity+1
36 | basket_item.save()
37 |
38 | def remove_item(self, item):
39 | if not self.modified_date:
40 | return
41 |
42 | self.basketitem_set.filter(item_sku=item).update(quantity=0)
43 |
44 |
45 | class BasketItem(models.Model):
46 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
47 | basket = models.ForeignKey('Basket', on_delete=models.DO_NOTHING)
48 | item_sku = models.ForeignKey('fishing_equipment.Product', on_delete=models.DO_NOTHING)
49 | item_price = models.IntegerField()
50 | quantity = models.IntegerField(default=0)
51 |
52 | created_date = models.DateTimeField(auto_now_add=True)
53 | modified_date = models.DateTimeField(auto_now=True)
54 |
55 | class Meta:
56 | unique_together = ('basket', 'item_sku',)
57 |
58 | @property
59 | def total_price_formatted(self):
60 | v = self.item_price * self.quantity
61 | return '$%.2f' % (v / 100)
62 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/fishing_equipment/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-10-02 20:24
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import imagekit.models.fields
6 | import uuid
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Category',
19 | fields=[
20 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
21 | ('title', models.CharField(max_length=100)),
22 | ('slug', models.SlugField(unique=True)),
23 | ('created_date', models.DateTimeField(auto_now_add=True)),
24 | ('modified_date', models.DateTimeField(auto_now=True)),
25 | ],
26 | options={
27 | 'verbose_name_plural': 'Categories',
28 | },
29 | ),
30 | migrations.CreateModel(
31 | name='Product',
32 | fields=[
33 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
34 | ('title', models.CharField(max_length=100)),
35 | ('description', models.TextField(blank=True)),
36 | ('slug', models.SlugField(unique=True)),
37 | ('picture', models.ImageField(upload_to='products')),
38 | ('smaller_picture', imagekit.models.fields.ProcessedImageField(upload_to='')),
39 | ('in_stock', models.BooleanField()),
40 | ('price', models.IntegerField()),
41 | ('deleted_at', models.DateTimeField(blank=True, null=True)),
42 | ('created_date', models.DateTimeField(auto_now_add=True)),
43 | ('modified_date', models.DateTimeField(auto_now=True)),
44 | ('categories', models.ManyToManyField(blank=True, to='fishing_equipment.Category')),
45 | ('primary_category', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='primary_products', to='fishing_equipment.Category')),
46 | ],
47 | ),
48 | ]
49 |
--------------------------------------------------------------------------------
/frontend/config/settings/test.py:
--------------------------------------------------------------------------------
1 | """
2 | With these settings, tests run faster.
3 | """
4 |
5 | from .base import * # noqa
6 | from .base import env
7 |
8 | # GENERAL
9 | # ------------------------------------------------------------------------------
10 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug
11 | DEBUG = False
12 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
13 | SECRET_KEY = env("DJANGO_SECRET_KEY", default="Btn1bmrIAResyjcP90xmqUrie5vtuz8zew5XLm96W5O2ij4ZVuL7y8MPSSu8IeAI")
14 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
15 | TEST_RUNNER = "django.test.runner.DiscoverRunner"
16 |
17 | # CACHES
18 | # ------------------------------------------------------------------------------
19 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches
20 | CACHES = {
21 | "default": {
22 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": ""
23 | }
24 | }
25 |
26 | # PASSWORDS
27 | # ------------------------------------------------------------------------------
28 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
29 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
30 |
31 | # TEMPLATES
32 | # ------------------------------------------------------------------------------
33 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates
34 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405
35 | TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405
36 | (
37 | "django.template.loaders.cached.Loader",
38 | [
39 | "django.template.loaders.filesystem.Loader",
40 | "django.template.loaders.app_directories.Loader",
41 | ],
42 | )
43 | ]
44 |
45 | # EMAIL
46 | # ------------------------------------------------------------------------------
47 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
48 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
49 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
50 | EMAIL_HOST = "localhost"
51 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
52 | EMAIL_PORT = 1025
53 |
54 | # Your stuff...
55 | # ------------------------------------------------------------------------------
56 |
--------------------------------------------------------------------------------
/infrastructure/src/main/java/fishing/lee/infrastructure/Bastion.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.infrastructure;
2 |
3 | import software.amazon.awscdk.Construct;
4 | import software.amazon.awscdk.Output;
5 | import software.amazon.awscdk.OutputProps;
6 | import software.amazon.awscdk.services.autoscaling.AutoScalingGroup;
7 | import software.amazon.awscdk.services.autoscaling.AutoScalingGroupProps;
8 | import software.amazon.awscdk.services.ec2.*;
9 |
10 | class Bastion extends Construct {
11 | private final SecurityGroup bastionSecurityGroup;
12 |
13 | Bastion(Construct parent, String id, BastionProps properties) {
14 | super(parent, id);
15 |
16 | AutoScalingGroup bastionAsg = new AutoScalingGroup(this, "AutoScalingGroup", AutoScalingGroupProps.builder()
17 | .withAllowAllOutbound(true)
18 | .withDesiredCapacity(1)
19 | .withMinSize(1)
20 | .withMaxSize(1)
21 | .withVpc(properties.getShopVpc().getVpc())
22 | .withVpcPlacement(VpcPlacementStrategy.builder()
23 | .withSubnetsToUse(SubnetType.Public)
24 | .build())
25 | .withInstanceType(new InstanceTypePair(InstanceClass.Burstable3, InstanceSize.Micro))
26 | .withMachineImage(new AmazonLinuxImage())
27 | .withKeyName(properties.getSshKeyPairName())
28 | .build());
29 |
30 | bastionSecurityGroup = new SecurityGroup(this, "SecurityGroup", SecurityGroupProps.builder()
31 | .withVpc(properties.getShopVpc().getVpc())
32 | .build());
33 |
34 | properties.getShopVpc().addSecurityGroupToApi(bastionSecurityGroup);
35 |
36 | bastionAsg.addSecurityGroup(bastionSecurityGroup);
37 | bastionAsg.addUserData(
38 | "yum update -y",
39 | "yum install -y jq"
40 | );
41 |
42 | bastionSecurityGroup.addIngressRule(new AnyIPv4(), new TcpPort(22));
43 |
44 | new Output(this, "AutoScalingGroupName", OutputProps.builder()
45 | .withValue(bastionAsg.getAutoScalingGroupName())
46 | .build());
47 | }
48 |
49 | SecurityGroup getBastionSecurityGroup() {
50 | return bastionSecurityGroup;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/frontend_minisite/assets/sass/app.scss:
--------------------------------------------------------------------------------
1 | // Variables
2 | @import "variables";
3 |
4 | // Bootstrap
5 | @import "~bootstrap/scss/bootstrap";
6 | @import "~bootstrap-vue/dist/bootstrap-vue.css";
7 |
8 | // Plugins
9 | @import "~sweetalert2/src/sweetalert2";
10 | @import "~slick-carousel/slick/slick";
11 | @import "~slick-carousel/slick/slick-theme";
12 |
13 | /* Space out content a bit */
14 | body {
15 | padding-top: 20px;
16 | padding-bottom: 20px;
17 | }
18 |
19 | [v-cloak] {
20 | display: none;
21 | }
22 |
23 | /* Everything but the jumbotron gets side spacing for mobile first views */
24 | .header,
25 | .marketing,
26 | .footer {
27 | padding-left: 15px;
28 | padding-right: 15px;
29 | }
30 |
31 | /* Custom page header */
32 | .header {
33 | border-bottom: 1px solid #e5e5e5;
34 |
35 | /* Make the masthead heading the same height as the navigation */
36 | h3 {
37 | margin-top: 0;
38 | margin-bottom: 0;
39 | line-height: 40px;
40 | padding-bottom: 19px;
41 | }
42 | }
43 |
44 | /* Custom page footer */
45 | .footer {
46 | padding-top: 19px;
47 | color: #777;
48 | border-top: 1px solid #e5e5e5;
49 | }
50 |
51 | .container-narrow > hr {
52 | margin: 30px 0;
53 | }
54 |
55 | /* Main marketing message and sign up button */
56 | .jumbotron {
57 | text-align: center;
58 | border-bottom: 1px solid #e5e5e5;
59 | .btn {
60 | font-size: 21px;
61 | padding: 14px 24px;
62 | }
63 | }
64 |
65 | /* Supporting marketing content */
66 | .marketing {
67 | margin: 40px 0;
68 | p + h4 {
69 | margin-top: 28px;
70 | }
71 | }
72 |
73 | .slick-list {
74 | .slick-loading & {
75 | background: none;
76 | }
77 | }
78 |
79 | .slick-slide {
80 | text-align: center;
81 |
82 | h3 {
83 | line-height: 200px;
84 | background-color: #eee;
85 | margin: 0 10px;
86 | }
87 | }
88 |
89 | .btn {
90 | background-color: lightgray;
91 | }
92 |
93 | /* Responsive: Portrait tablets and up */
94 | @media screen and (min-width: 768px) {
95 | .container {
96 | max-width: 730px;
97 | }
98 |
99 | /* Remove the padding we set earlier */
100 | .header,
101 | .marketing,
102 | .footer {
103 | padding-left: 0;
104 | padding-right: 0;
105 | }
106 |
107 | /* Space out the masthead */
108 | .header {
109 | margin-bottom: 30px;
110 | }
111 |
112 | /* Remove the bottom border on the jumbotron for visual effect */
113 | .jumbotron {
114 | border-bottom: 0;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/frontend_minisite/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "html-boilerplate",
3 | "version": "1.0.0",
4 | "description": "HTML Frontend boilerplate based on Bootstrap for pure showcase/prototype sites, with Webpack for assets management.",
5 | "scripts": {
6 | "dev": "cross-env NODE_ENV=development webpack-dev-server --mode development --hot --progress --open",
7 | "build": "rimraf dist && cross-env NODE_ENV=production webpack --mode production --progress",
8 | "lint": "eslint --ext .js,.vue assets/js views"
9 | },
10 | "engines": {
11 | "node": ">=6"
12 | },
13 | "author": "Adrien Beaudouin",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@babel/core": "^7.1.2",
17 | "@babel/preset-env": "^7.1.0",
18 | "@fortawesome/fontawesome-svg-core": "^1.2.4",
19 | "@fortawesome/free-brands-svg-icons": "^5.3.1",
20 | "@fortawesome/free-regular-svg-icons": "^5.3.1",
21 | "@fortawesome/free-solid-svg-icons": "^5.3.1",
22 | "@fortawesome/vue-fontawesome": "^0.1.1",
23 | "autoprefixer": "^9.1.5",
24 | "axios": "^0.18.0",
25 | "babel-eslint": "^10.0.1",
26 | "babel-loader": "^8.0.4",
27 | "bootstrap": "^4.1.1",
28 | "bootstrap-vue": "^2.0.0-rc.10",
29 | "cross-env": "^5.1.5",
30 | "css-loader": "^1.0.0",
31 | "cssnano": "^4.1.4",
32 | "dotenv": "^6.0.0",
33 | "eslint": "^5.5.0",
34 | "eslint-config-prettier": "^3.1.0",
35 | "eslint-loader": "^2.1.0",
36 | "eslint-plugin-prettier": "^2.7.0",
37 | "eslint-plugin-vue": "^5.0.0-beta.3",
38 | "file-loader": "^2.0.0",
39 | "friendly-errors-webpack-plugin": "^1.7.0",
40 | "handlebars": "^4.0.11",
41 | "handlebars-loader": "^1.7.0",
42 | "html-loader": "^0.5.5",
43 | "html-webpack-plugin": "^3.2.0",
44 | "jquery": "^3.3.1",
45 | "mini-css-extract-plugin": "^0.4.0",
46 | "node-sass": "^4.9.0",
47 | "popper.js": "^1.14.3",
48 | "postcss-loader": "^3.0.0",
49 | "prettier": "^1.14.3",
50 | "rimraf": "^2.6.2",
51 | "sass-loader": "^7.0.1",
52 | "slick-carousel": "^1.8.1",
53 | "style-loader": "^0.23.0",
54 | "sweetalert2": "^7.28.4",
55 | "url-loader": "^1.1.1",
56 | "vue": "^2.5.16",
57 | "vue-loader": "^15.1.0",
58 | "vue-template-compiler": "^2.5.16",
59 | "webpack": "^4.20.2",
60 | "webpack-cli": "^3.1.1",
61 | "webpack-dev-server": "^3.1.4",
62 | "webpack-manifest-plugin": "^2.0.2",
63 | "webpack-notifier": "^1.6.0"
64 | },
65 | "dependencies": {},
66 | "browserslist": [
67 | "> 1%",
68 | "last 2 versions"
69 | ]
70 | }
71 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/fishing_equipment/fixtures/category.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "fishing_equipment.category",
4 | "pk": "021831c9-22ab-4437-9505-edb6b24e2e0f",
5 | "fields": {
6 | "title": "Rods",
7 | "slug": "rods",
8 | "created_date": "2018-10-02T20:28:29.073Z",
9 | "modified_date": "2018-10-02T20:37:13.183Z"
10 | }
11 | },
12 | {
13 | "model": "fishing_equipment.category",
14 | "pk": "9be69ffd-1fb9-4cba-8203-1446898857c1",
15 | "fields": {
16 | "title": "Reels",
17 | "slug": "reels",
18 | "created_date": "2018-10-02T20:28:29.073Z",
19 | "modified_date": "2018-10-02T20:37:13.183Z"
20 | }
21 | },
22 | {
23 | "model": "fishing_equipment.category",
24 | "pk": "be3f1c9d-c3bd-45e7-acb0-80efb40c61ef",
25 | "fields": {
26 | "title": "Hard baits",
27 | "slug": "hard-baits",
28 | "created_date": "2018-10-02T20:28:29.073Z",
29 | "modified_date": "2018-10-02T20:37:13.183Z"
30 | }
31 | },
32 | {
33 | "model": "fishing_equipment.category",
34 | "pk": "282a8539-a263-49e1-b028-1bb4acc7216d",
35 | "fields": {
36 | "title": "Soft baits",
37 | "slug": "soft-baits",
38 | "created_date": "2018-10-02T20:28:29.073Z",
39 | "modified_date": "2018-10-02T20:37:13.183Z"
40 | }
41 | },
42 | {
43 | "model": "fishing_equipment.category",
44 | "pk": "21edc280-8357-479f-a35e-94402bab367d",
45 | "fields": {
46 | "title": "Swimbaits",
47 | "slug": "swim-baits",
48 | "created_date": "2018-10-02T20:28:29.073Z",
49 | "modified_date": "2018-10-02T20:37:13.183Z"
50 | }
51 | },
52 | {
53 | "model": "fishing_equipment.category",
54 | "pk": "9608833d-18ed-4952-bc61-faf83e7514f8",
55 | "fields": {
56 | "title": "Tackle storage",
57 | "slug": "tackle-storage",
58 | "created_date": "2018-10-02T20:28:29.073Z",
59 | "modified_date": "2018-10-02T20:37:13.183Z"
60 | }
61 | },
62 | {
63 | "model": "fishing_equipment.category",
64 | "pk": "50b2418b-b9e8-4d7e-9ff6-2bdf801239ea",
65 | "fields": {
66 | "title": "Waders",
67 | "slug": "waders",
68 | "created_date": "2018-10-02T20:28:29.073Z",
69 | "modified_date": "2018-10-02T20:37:13.183Z"
70 | }
71 | }
72 | ]
--------------------------------------------------------------------------------
/backend/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/null_lambda/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/sqs_order_forwarder/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/fishing_net/views.py:
--------------------------------------------------------------------------------
1 | from timeit import default_timer
2 |
3 | from django.contrib import messages
4 | from django.contrib.auth.mixins import LoginRequiredMixin
5 | from django.http import HttpResponseRedirect
6 | from django.urls import reverse
7 | from django.views.generic import TemplateView, DetailView, RedirectView
8 |
9 | from lee_fishing.fishing_equipment.models import Product
10 | from lee_fishing.fishing_net.backend import BackendClient
11 | from lee_fishing.fishing_net.models import Basket
12 |
13 |
14 | class DummyView(TemplateView):
15 | template_name = 'fishing_net/ping.html'
16 |
17 | def get(self, request, *args, **kwargs):
18 | backend_client = BackendClient()
19 | kwargs['pong'] = backend_client.ping()
20 |
21 | return super().get(request, *args, **kwargs)
22 |
23 |
24 | class BasketDetailView(LoginRequiredMixin, DetailView):
25 | model = Basket
26 |
27 | def get_object(self, queryset=None):
28 | return self.model.objects.current_for_user(self.request.user)
29 |
30 |
31 | class BasketDeleteItemView(LoginRequiredMixin, RedirectView):
32 | permanent = False
33 |
34 | def get_redirect_url(self, *args, **kwargs):
35 | basket = Basket.objects.current_for_user(self.request.user)
36 | basket.remove_item(Product.objects.get(slug=self.kwargs['item']))
37 | return reverse('baskets:basket')
38 |
39 |
40 | class BasketAddItemView(LoginRequiredMixin, RedirectView):
41 | permanent = False
42 |
43 | def get_redirect_url(self, *args, **kwargs):
44 | basket = Basket.objects.current_for_user(self.request.user)
45 | basket.add_item(Product.objects.not_deleted().get(slug=self.kwargs['item']))
46 | return reverse('baskets:basket')
47 |
48 |
49 | class CheckoutView(LoginRequiredMixin, TemplateView):
50 | template_name = 'fishing_net/checkout.html'
51 |
52 | def get(self, request, *args, **kwargs):
53 | return super().get(request, *args, **kwargs)
54 |
55 | def post(self, request, *args, **kwargs):
56 | start = default_timer()
57 | basket = Basket.objects.current_for_user(self.request.user)
58 | backend_client = BackendClient()
59 |
60 | r = backend_client.place_order()
61 | basket.order_id = r
62 | basket.save()
63 |
64 | end = default_timer()
65 |
66 | messages.add_message(request, messages.INFO, "It took " + str(end-start) + " seconds to process the order")
67 |
68 | return HttpResponseRedirect(reverse('basket:congrats'))
69 |
70 |
71 | class CongratulationsView(LoginRequiredMixin, TemplateView):
72 | template_name = 'fishing_net/congrats.html'
73 |
--------------------------------------------------------------------------------
/backend/src/test/java/fishing/lee/backend/BasketControllerTest.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.backend;
2 |
3 | import org.junit.Test;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 |
6 | import java.util.HashSet;
7 | import java.util.UUID;
8 |
9 | public class BasketControllerTest extends BaseDynamoDBTest {
10 | @Autowired
11 | BasketController basketController;
12 |
13 | @Test(expected = BasketNotFoundException.class)
14 | public void oneUnknown() {
15 | basketController.one(UUID.randomUUID());
16 | }
17 |
18 | private BasketModel getMockedBasketModel(String singleItem) {
19 | BasketModel basketModel = new BasketModel();
20 | basketModel.setItems(new HashSet<>());
21 | basketModel.setCreationPoint(null);
22 | basketModel.addItem(singleItem);
23 | return basketModel;
24 | }
25 |
26 | @Test
27 | public void newBasket() {
28 | BasketModel basketModel = getMockedBasketModel("test");
29 |
30 | BasketModel result = basketController.newBasket(basketModel);
31 | assert(result.getItems().contains("test"));
32 | assert(result.getId() != null);
33 | assert(result.getCreationPoint() != null);
34 |
35 | BasketModel storedResult = basketController.one(result.getId());
36 | assert(result.getId().equals(storedResult.getId()));
37 | }
38 |
39 | @Test
40 | public void addItem() {
41 | BasketModel basketModel = getMockedBasketModel("dummy");
42 |
43 | BasketModel result = basketController.newBasket(basketModel);
44 | basketController.addItem(result.getId(), "item");
45 |
46 | BasketModel storedResult = basketController.one(result.getId());
47 | assert(storedResult.getItems().contains("item"));
48 | }
49 |
50 | @Test(expected = BasketNotFoundException.class)
51 | public void deleteBasket() {
52 | BasketModel basketModel = getMockedBasketModel("dummy");
53 |
54 | BasketModel result = basketController.newBasket(basketModel);
55 | basketController.deleteBasket(result.getId());
56 |
57 | basketController.one(result.getId());
58 | }
59 |
60 | @Test
61 | public void deleteItem() {
62 | BasketModel basketModel = getMockedBasketModel("dummy");
63 | basketModel.addItem("deletedItem");
64 |
65 | BasketModel result = basketController.newBasket(basketModel);
66 | result = basketController.deleteItem(result.getId(), "deletedItem");
67 | assert(!result.getItems().contains("deletedItem"));
68 |
69 | BasketModel storedResult = basketController.one(result.getId());
70 | assert(!storedResult.getItems().contains("deletedItem"));
71 | }
72 | }
--------------------------------------------------------------------------------
/backend/src/test/java/fishing/lee/backend/BaseDynamoDBTest.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.backend;
2 |
3 | import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
4 | import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
5 | import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
6 | import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
7 | import com.amazonaws.xray.AWSXRay;
8 | import org.junit.*;
9 | import org.junit.runner.RunWith;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.boot.test.context.SpringBootTest;
12 | import org.springframework.test.context.ActiveProfiles;
13 | import org.springframework.test.context.TestPropertySource;
14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
15 | import org.springframework.test.context.web.WebAppConfiguration;
16 |
17 | import static fishing.lee.backend.DynamoDBConfig.checkOrCreateTable;
18 |
19 | @RunWith(SpringJUnit4ClassRunner.class)
20 | @SpringBootTest(classes = Application.class)
21 | @WebAppConfiguration
22 | @ActiveProfiles("local")
23 | @TestPropertySource(properties = {
24 | "amazon.aws.mode=local"
25 | })
26 | public abstract class BaseDynamoDBTest {
27 | private static DynamoDBProxyServer serverRunner;
28 |
29 | @Autowired
30 | protected AmazonDynamoDB amazonDynamoDB;
31 |
32 | @Autowired
33 | protected BasketRepository basketRepository;
34 |
35 | @Autowired
36 | protected DynamoDBMapper dynamoDBMapper;
37 |
38 | @BeforeClass
39 | public static void runDynamoDB() {
40 | System.setProperty("sqlite4java.library.path", "./build/libs/");
41 |
42 | final String[] localArgs = { "-inMemory", "-port", "8001" };
43 |
44 | try {
45 | serverRunner = ServerRunner.createServerFromCommandLineArgs(localArgs);
46 | serverRunner.start();
47 |
48 | } catch (Exception e) {
49 | e.printStackTrace();
50 | Assert.fail(e.getMessage());
51 | }
52 | }
53 |
54 | @AfterClass
55 | public static void shutdownDynamoDB() {
56 | if (serverRunner != null) {
57 | try {
58 | serverRunner.stop();
59 | } catch (Exception e) {
60 | e.printStackTrace();
61 | }
62 | }
63 | }
64 |
65 | @Before
66 | public void setup() {
67 | AWSXRay.beginSegment("BasketRepositoryIntegrationTest");
68 |
69 | checkOrCreateTable(amazonDynamoDB, dynamoDBMapper, BasketModel.class);
70 |
71 | dynamoDBMapper.batchDelete(
72 | basketRepository.findAll()
73 | );
74 | }
75 |
76 | @After
77 | public void tearDown() {
78 | AWSXRay.endSegment();
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/fishing_equipment/models.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | from django.db import models
4 | from imagekit.models import ProcessedImageField
5 | from imagekit.processors import ResizeToFill
6 |
7 | from lee_fishing.fishing_equipment.managers import ProductManager
8 |
9 |
10 | class Category(models.Model):
11 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
12 | title = models.CharField(max_length=100, null=False, blank=False)
13 | slug = models.SlugField(null=False, blank=False, unique=True)
14 |
15 | created_date = models.DateTimeField(auto_now_add=True)
16 | modified_date = models.DateTimeField(auto_now=True)
17 |
18 | def __str__(self):
19 | return self.title
20 |
21 | class Meta:
22 | ordering = ['title']
23 | verbose_name_plural = 'Categories'
24 |
25 |
26 | class Product(models.Model):
27 | id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
28 | title = models.CharField(max_length=100, null=False, blank=False)
29 | description = models.TextField(blank=True)
30 | slug = models.SlugField(null=False, blank=False, unique=True)
31 |
32 | primary_category = models.ForeignKey(Category,
33 | on_delete=models.DO_NOTHING, null=False, blank=False,
34 | related_name='primary_products'
35 | )
36 | categories = models.ManyToManyField(Category, blank=True)
37 |
38 | picture = models.ImageField(upload_to='products', null=True, blank=True)
39 | smaller_picture = ProcessedImageField(upload_to='product_thumbs',
40 | processors=[ResizeToFill(200, 200)],
41 | format='JPEG',
42 | options={'quality': 70},
43 | null=True, blank=True)
44 |
45 | in_stock = models.BooleanField()
46 | price = models.IntegerField()
47 |
48 | deleted_at = models.DateTimeField(null=True, blank=True)
49 |
50 | created_date = models.DateTimeField(auto_now_add=True)
51 | modified_date = models.DateTimeField(auto_now=True)
52 |
53 | objects = ProductManager()
54 |
55 | @property
56 | def formatted_price(self):
57 | """
58 | Formatted price (as a string) for this product. There's so much
59 | wrong with this, you shouldn't use this for any serious
60 | ecommerce site!
61 |
62 | :return: formatted price... ish
63 | :rtype: str
64 | """
65 |
66 | return '$%.2f' % (self.price / 100)
67 |
68 | def __str__(self):
69 | return self.title
70 |
71 | class Meta:
72 | ordering = ['title']
73 |
--------------------------------------------------------------------------------
/sqs_order_forwarder/src/main/java/fishing/lee/sqsforwarder/QueueGrabber.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.sqsforwarder;
2 |
3 | import com.amazonaws.services.lambda.runtime.Context;
4 | import com.amazonaws.services.lambda.runtime.LambdaLogger;
5 | import com.amazonaws.services.lambda.runtime.RequestHandler;
6 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
7 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
8 | import com.amazonaws.services.sqs.AmazonSQS;
9 | import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
10 | import com.amazonaws.services.sqs.model.SendMessageResult;
11 |
12 | import java.util.HashMap;
13 |
14 | @SuppressWarnings("unused")
15 | public class QueueGrabber implements RequestHandler {
16 | @Override
17 | public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
18 | final String sqsQueueName = System.getenv("SHOPBACKEND_SQS_QUEUE");
19 | LambdaLogger logger = context.getLogger();
20 |
21 | if (input.getHttpMethod().equals("OPTIONS")) {
22 | return new APIGatewayProxyResponseEvent()
23 | .withHeaders(new HashMap() {{
24 | put("Access-Control-Allow-Origin", "*");
25 | put("Access-Control-Allow-Methods", "GET,HEAD,POST");
26 | put("Access-Control-Max-Age", "1800");
27 | put("Allow", "GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH");
28 | }})
29 | .withStatusCode(200);
30 | }
31 |
32 | switch (input.getPath()) {
33 | case "/ping":
34 | return new APIGatewayProxyResponseEvent()
35 | .withHeaders(new HashMap() {{
36 | put("Content-Type", "application/json");
37 | }})
38 | .withStatusCode(200)
39 | .withBody("PONG");
40 | case "/order":
41 | AmazonSQS amazonSQS = AmazonSQSClientBuilder.defaultClient();
42 | SendMessageResult result = amazonSQS.sendMessage(sqsQueueName, input.getBody());
43 |
44 | return new APIGatewayProxyResponseEvent()
45 | .withHeaders(new HashMap() {{
46 | put("Content-Type", "application/json");
47 | }})
48 | .withStatusCode(200)
49 | .withBody(result.getMessageId());
50 | default:
51 | return new APIGatewayProxyResponseEvent()
52 | .withStatusCode(404);
53 | }
54 |
55 |
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/docs/pycharm/configuration.rst:
--------------------------------------------------------------------------------
1 | Docker Remote Debugging
2 | =======================
3 |
4 | To connect to python remote interpreter inside docker, you have to make sure first, that Pycharm is aware of your docker.
5 |
6 | Go to *Settings > Build, Execution, Deployment > Docker*. If you are on linux, you can use docker directly using its socket `unix:///var/run/docker.sock`, if you are on Windows or Mac, make sure that you have docker-machine installed, then you can simply *Import credentials from Docker Machine*.
7 |
8 | .. image:: images/1.png
9 |
10 | Configure Remote Python Interpreter
11 | -----------------------------------
12 |
13 | This repository comes with already prepared "Run/Debug Configurations" for docker.
14 |
15 | .. image:: images/2.png
16 |
17 | But as you can see, at the beggining there is something wrong with them. They have red X on django icon, and they cannot be used, without configuring remote python interpteter. To do that, you have to go to *Settings > Build, Execution, Deployment* first.
18 |
19 |
20 | Next, you have to add new remote python interpreter, based on already tested deployment settings. Go to *Settings > Project > Project Interpreter*. Click on the cog icon, and click *Add Remote*.
21 |
22 | .. image:: images/3.png
23 |
24 | Switch to *Docker Compose* and select `local.yml` file from directory of your project, next set *Service name* to `django`
25 |
26 | .. image:: images/4.png
27 |
28 | Having that, click *OK*. Close *Settings* panel, and wait few seconds...
29 |
30 | .. image:: images/7.png
31 |
32 | After few seconds, all *Run/Debug Configurations* should be ready to use.
33 |
34 | .. image:: images/8.png
35 |
36 | **Things you can do with provided configuration**:
37 |
38 | * run and debug python code
39 | .. image:: images/f1.png
40 | * run and debug tests
41 | .. image:: images/f2.png
42 | .. image:: images/f3.png
43 | * run and debug migrations or different django management commands
44 | .. image:: images/f4.png
45 | * and many others..
46 |
47 | Known issues
48 | ------------
49 |
50 | * Pycharm hangs on "Connecting to Debugger"
51 |
52 | .. image:: images/issue1.png
53 |
54 | This might be fault of your firewall. Take a look on this ticket - https://youtrack.jetbrains.com/issue/PY-18913
55 |
56 | * Modified files in `.idea` directory
57 |
58 | Most of the files from `.idea/` were added to `.gitignore` with a few exceptions, which were made, to provide "ready to go" configuration. After adding remote interpreter some of these files are altered by PyCharm:
59 |
60 | .. image:: images/issue2.png
61 |
62 | In theory you can remove them from repository, but then, other people will lose a ability to initialize a project from provided configurations as you did. To get rid of this annoying state, you can run command::
63 |
64 | $ git update-index --assume-unchanged lee_fishing.iml
65 |
--------------------------------------------------------------------------------
/frontend/config/settings/local.py:
--------------------------------------------------------------------------------
1 | from .base import * # noqa
2 | from .base import env
3 |
4 | # GENERAL
5 | # ------------------------------------------------------------------------------
6 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug
7 | DEBUG = True
8 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
9 | SECRET_KEY = env('DJANGO_SECRET_KEY', default='rgr6jdNR1qFrcSnpxEJgDCX4lovbVWbOPw2masEwuAvy4K8BQ76IliMlirUyv4BB')
10 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
11 | ALLOWED_HOSTS = [
12 | "localhost",
13 | "0.0.0.0",
14 | "127.0.0.1",
15 | ]
16 |
17 | # CACHES
18 | # ------------------------------------------------------------------------------
19 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches
20 | CACHES = {
21 | 'default': {
22 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
23 | 'LOCATION': ''
24 | }
25 | }
26 |
27 | # TEMPLATES
28 | # ------------------------------------------------------------------------------
29 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates
30 | TEMPLATES[0]['OPTIONS']['debug'] = DEBUG # noqa F405
31 |
32 | # EMAIL
33 | # ------------------------------------------------------------------------------
34 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
35 | EMAIL_BACKEND = env('DJANGO_EMAIL_BACKEND', default='django.core.mail.backends.console.EmailBackend')
36 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
37 | EMAIL_HOST = 'localhost'
38 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
39 | EMAIL_PORT = 1025
40 |
41 | # django-debug-toolbar
42 | # ------------------------------------------------------------------------------
43 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites
44 | INSTALLED_APPS += ['debug_toolbar'] # noqa F405
45 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware
46 | MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] # noqa F405
47 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config
48 | DEBUG_TOOLBAR_CONFIG = {
49 | 'DISABLE_PANELS': [
50 | 'debug_toolbar.panels.redirects.RedirectsPanel',
51 | ],
52 | 'SHOW_TEMPLATE_CONTEXT': True,
53 | }
54 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips
55 | INTERNAL_IPS = ['127.0.0.1', '10.0.2.2']
56 |
57 |
58 | # django-extensions
59 | # ------------------------------------------------------------------------------
60 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
61 | INSTALLED_APPS += ['django_extensions'] # noqa F405
62 |
63 | # Your stuff...
64 | # ------------------------------------------------------------------------------
65 |
--------------------------------------------------------------------------------
/infrastructure/src/main/java/fishing/lee/infrastructure/ScheduledApiCaller.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.infrastructure;
2 |
3 | import software.amazon.awscdk.Construct;
4 | import software.amazon.awscdk.services.ec2.*;
5 | import software.amazon.awscdk.services.events.EventRule;
6 | import software.amazon.awscdk.services.events.EventRuleProps;
7 | import software.amazon.awscdk.services.lambda.*;
8 | import software.amazon.awscdk.services.lambda.Runtime;
9 |
10 | import software.amazon.awscdk.services.s3.Bucket;
11 | import software.amazon.awscdk.services.s3.BucketRef;
12 | import software.amazon.awscdk.services.s3.BucketRefProps;
13 | import software.amazon.awscdk.services.sns.*;
14 |
15 | import java.util.Collections;
16 | import java.util.HashMap;
17 |
18 | class ScheduledApiCaller extends Construct {
19 | private SecurityGroup securityGroup;
20 |
21 | ScheduledApiCaller(Construct parent, String id, ScheduledApiCallerProps properties) {
22 | super(parent, id);
23 |
24 | BucketRef bucket = Bucket.import_(this, "Bucket", BucketRefProps.builder()
25 | .withBucketName("leepac-fishing-assets")
26 | .build());
27 |
28 | securityGroup = new SecurityGroup(this, "SecurityGroup", SecurityGroupProps.builder()
29 | .withVpc(properties.getShopVpc().getVpc())
30 | .build());
31 |
32 | properties.getShopVpc().addSecurityGroupToApi(securityGroup);
33 |
34 | Function function = new Function(this, "Function", FunctionProps.builder()
35 | .withCode(Code.bucket(bucket, "dummy-1.0-SNAPSHOT-all.jar"))
36 | .withHandler("fishing.lee.backend.SNSLambdaHandler::handleRequest")
37 | .withMemorySize(1024)
38 | .withTimeout(60)
39 | .withTracing(Tracing.Active)
40 | .withRuntime(Runtime.JAVA8)
41 | .withVpc(properties.getShopVpc().getVpc())
42 | .withVpcPlacement(VpcPlacementStrategy.builder()
43 | .withSubnetsToUse(SubnetType.Private)
44 | .build())
45 | .withSecurityGroup(securityGroup)
46 | .withEnvironment(new HashMap() {{
47 | put("SHOPBACKEND_API_URL", properties.getApiToPing() + "/ping");
48 | }})
49 | .build());
50 |
51 | Topic topic = new Topic(this, "Topic", TopicProps.builder()
52 | .build());
53 |
54 | topic.subscribeLambda(function);
55 |
56 | new EventRule(this, "EventRule", EventRuleProps.builder()
57 | .withTargets(Collections.singletonList(topic))
58 | .withScheduleExpression("rate(2 minutes)")
59 | .build());
60 | }
61 |
62 | public SecurityGroup getSecurityGroup() {
63 | return securityGroup;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/frontend/utility/install_os_dependencies.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | WORK_DIR="$(dirname "$0")"
4 | DISTRO_NAME=$(lsb_release -sc)
5 | OS_REQUIREMENTS_FILENAME="requirements-$DISTRO_NAME.apt"
6 |
7 | cd $WORK_DIR
8 |
9 | # Check if a requirements file exist for the current distribution.
10 | if [ ! -r "$OS_REQUIREMENTS_FILENAME" ]; then
11 | cat <<-EOF >&2
12 | There is no requirements file for your distribution.
13 | You can see one of the files listed below to help search the equivalent package in your system:
14 | $(find ./ -name "requirements-*.apt" -printf " - %f\n")
15 | EOF
16 | exit 1;
17 | fi
18 |
19 | # Handle call with wrong command
20 | function wrong_command()
21 | {
22 | echo "${0##*/} - unknown command: '${1}'" >&2
23 | usage_message
24 | }
25 |
26 | # Print help / script usage
27 | function usage_message()
28 | {
29 | cat <<-EOF
30 | Usage: $WORK_DIR/${0##*/}
31 | Available commands are:
32 | list Print a list of all packages defined on ${OS_REQUIREMENTS_FILENAME} file
33 | help Print this help
34 |
35 | Commands that require superuser permission:
36 | install Install packages defined on ${OS_REQUIREMENTS_FILENAME} file. Note: This
37 | does not upgrade the packages already installed for new versions, even if
38 | new version is available in the repository.
39 | upgrade Same that install, but upgrade the already installed packages, if new
40 | version is available.
41 | EOF
42 | }
43 |
44 | # Read the requirements.apt file, and remove comments and blank lines
45 | function list_packages(){
46 | grep -v "#" "${OS_REQUIREMENTS_FILENAME}" | grep -v "^$";
47 | }
48 |
49 | function install_packages()
50 | {
51 | list_packages | xargs apt-get --no-upgrade install -y;
52 | }
53 |
54 | function upgrade_packages()
55 | {
56 | list_packages | xargs apt-get install -y;
57 | }
58 |
59 | function install_or_upgrade()
60 | {
61 | P=${1}
62 | PARAN=${P:-"install"}
63 |
64 | if [[ $EUID -ne 0 ]]; then
65 | cat <<-EOF >&2
66 | You must run this script with root privilege
67 | Please do:
68 | sudo $WORK_DIR/${0##*/} $PARAN
69 | EOF
70 | exit 1
71 | else
72 |
73 | apt-get update
74 |
75 | # Install the basic compilation dependencies and other required libraries of this project
76 | if [ "$PARAN" == "install" ]; then
77 | install_packages;
78 | else
79 | upgrade_packages;
80 | fi
81 |
82 | # cleaning downloaded packages from apt-get cache
83 | apt-get clean
84 |
85 | exit 0
86 | fi
87 | }
88 |
89 | # Handle command argument
90 | case "$1" in
91 | install) install_or_upgrade;;
92 | upgrade) install_or_upgrade "upgrade";;
93 | list) list_packages;;
94 | help|"") usage_message;;
95 | *) wrong_command "$1";;
96 | esac
97 |
--------------------------------------------------------------------------------
/backend/src/main/java/fishing/lee/backend/BasketController.java:
--------------------------------------------------------------------------------
1 | package fishing.lee.backend;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.web.bind.annotation.*;
5 |
6 | import java.util.Objects;
7 | import java.util.Optional;
8 | import java.util.UUID;
9 |
10 | /**
11 | * Baskets, they're a thing... provide an API to them
12 | */
13 | @RestController
14 | public class BasketController {
15 | private final BasketRepository basketRepository;
16 |
17 | @Autowired
18 | public BasketController(BasketRepository basketRepository) {
19 | this.basketRepository = basketRepository;
20 | }
21 |
22 | /**
23 | * Get a single basket
24 | *
25 | * @param id UUID of basket to fetch
26 | * @return Basket instance
27 | * @throws BasketNotFoundException for returning a 404 via HTTP
28 | */
29 | @SuppressWarnings("WeakerAccess")
30 | @GetMapping("/baskets/{id}")
31 | BasketModel one(@PathVariable UUID id) throws BasketNotFoundException {
32 | Optional basket = basketRepository.findById(id);
33 | return basket.orElseThrow(BasketNotFoundException::new);
34 | }
35 |
36 | @PostMapping("/baskets")
37 | BasketModel newBasket(@RequestBody BasketModel basketModel) {
38 | // TODO(leepac): This feels suboptimal but the only way to guarantee it's set
39 | if (Objects.isNull(basketModel.getCreationPoint())) {
40 | basketModel.setCreationPoint(null);
41 | }
42 |
43 | return basketRepository.save(basketModel);
44 | }
45 |
46 | /**
47 | * Add an item to a basket
48 | *
49 | * @param id the ID of the Basket
50 | * @param item the item to add
51 | * @return new version of BasketModel
52 | */
53 | @PostMapping("/baskets/{id}/add/{item}")
54 | BasketModel addItem(@PathVariable UUID id, @PathVariable String item) {
55 | BasketModel basketModel = one(id);
56 | basketModel.addItem(item);
57 | basketRepository.save(basketModel);
58 | return basketModel;
59 | }
60 |
61 | /**
62 | * Delete a basket
63 | *
64 | * @param id the ID of the basket
65 | */
66 | @DeleteMapping("/baskets/{id}")
67 | void deleteBasket(@PathVariable UUID id) {
68 | BasketModel basketModel = one(id);
69 | basketRepository.delete(basketModel);
70 | }
71 |
72 | /**
73 | * Delete an item from a basket
74 | *
75 | * @param id the ID of the basket
76 | * @param item the item to remove
77 | * @return the modified basket
78 | */
79 | @DeleteMapping("/baskets/{id}/add/{item}")
80 | BasketModel deleteItem(@PathVariable UUID id, @SuppressWarnings("SameParameterValue") @PathVariable String item) {
81 | BasketModel basketModel = one(id);
82 | basketModel.deleteItem(item);
83 | basketRepository.save(basketModel);
84 | return basketModel;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/frontend/lee_fishing/templates/account/email.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "account/base.html" %}
3 |
4 | {% load i18n %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Account" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
{% trans "E-mail Addresses" %}
11 |
12 | {% if user.emailaddress_set.all %}
13 |
{% trans 'The following e-mail addresses are associated with your account:' %}
14 |
15 |
44 |
45 | {% else %}
46 |
{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}