Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • startuplab/courses/tjts5901-continuous-software-engineering/TJTS5901-K23_template
  • planet-of-the-apes/tjts-5901-apeuction
  • uunot-yliopiston-leivissa/tjts-5901-uunot
  • contain-the-cry/tjts-5901-auction-system
  • avengers/avengers
  • cse6/cse-6
  • 13th/13-sins-of-gitlab
  • fast-and-furious/fast-and-furious
  • back-to-the-future/delorean-auction
  • monty-pythons-the-meaning-of-life/the-meaning-of-life
  • team-atlantis/the-empire
  • code-with-the-wind/auction-project
  • the-pirates/the-pirates
  • do-the-right-thing/do-the-right-thing
  • inception/inception
  • the-social-network-syndicate/the-social-auction-network
  • team-the-hunt-for-red-october/tjts-5901-k-23-red-october
  • good-on-paper/good-paper-project
  • desperados/desperados
19 results
Show changes
Showing
with 13081 additions and 6 deletions
"""
==============
Logging module
==============
In this module we'll create a new :class:`~Logger` interface, using pythons inbuild :module:`logging` module.
By default flask sends messages to stdout.
To use this module, import it into your application and call :func:`~init_logging` function:
>>> from tjts5901.logging import init_logging
>>> init_logging(app)
To use logging in your application, import the logger instance, and use it as follows:
>>> import logging
>>> logger = logging.getLogger(__name__)
>>> logger.info("Hello world!")
"""
import logging
from os import environ
import sentry_sdk
from flask import Flask
from flask.logging import default_handler as flask_handler
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.pymongo import PyMongoIntegration
from .utils import get_version
def init_logging(app: Flask):
"""
Integrate our own logging interface into application.
To bind logger into your application instance use::
>>> init_logging(app)
:param app: :class:`~Flask` instance to use as logging basis.
"""
# Setup own logger instance. Usually you'll see something like
# >>> logger = logging.getLogger(__name__)
# where `__name__` reflects the package name, which is usually `"__main__"`,
# or in this exact case `tjts5901.logging`. I'll rather define static name.
# To get access to your logger in outside of module scope you can then
# use the same syntax as follows.
logger = logging.getLogger("tjts5901")
# If flask is running in debug mode, set our own handler to log also debug
# messages.
if app.config.get("DEBUG"):
logger.setLevel(level=logging.DEBUG)
# Add flask default logging handler as one of our target handlers.
# When changes to flask logging handler is made, our logging handler
# adapts automatically. Logging pipeline:
# our appcode -> our logger -> flask handler -> ????
logger.addHandler(flask_handler)
logger.debug("TJTS5901 Logger initialised.")
# Try to get enviroment name from different sources
if enviroment := environ.get('CI_ENVIRONMENT_NAME'):
enviroment = enviroment.lower()
elif app.testing:
enviroment = "testing"
elif app.debug:
enviroment = "development"
# Populate config with environment variables for sentry logging
app.config.setdefault('SENTRY_DSN', environ.get('SENTRY_DSN'))
app.config.setdefault('SENTRY_ENVIRONMENT', enviroment)
app.config.setdefault('CI_COMMIT_SHA', environ.get('CI_COMMIT_SHA'))
# Setup sentry logging
sentry_dsn = app.config.get("SENTRY_DSN")
release = app.config.get("CI_COMMIT_SHA", get_version() or "dev")
enviroment = app.config.get("CI_ENVIRONMENT_NAME")
if sentry_dsn:
sentry = sentry_sdk.init(
dsn=sentry_dsn,
integrations=[
# Flask integration
FlaskIntegration(),
# Mongo integration. Mongoengine uses pymongo, so we need to
# integrate pymongo.
PyMongoIntegration(),
# Sentry will automatically pick up the logging module.
#LoggingIntegration(level=logging.INFO, event_level=logging.ERROR),
],
# Set traces_sample_rate to 1.0 to capture 100%
# of transactions for performance monitoring.
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
# Set sentry debug mode to true if flask is running in debug mode.
#debug=bool(app.debug),
# By default the SDK will try to use the SENTRY_RELEASE
# environment variable, or infer a git commit
# SHA as release, however you may want to set
# something more human-readable.
release=release,
environment=enviroment,
)
app.config.setdefault("SENTRY_RELEASE", sentry._client.options["release"])
logger.info("Sentry logging enabled.", extra={"SENTRY_DSN": sentry_dsn})
else:
logger.warning("Sentry DSN not found. Sentry logging disabled.")
from PIL import Image
from io import BytesIO
from datetime import datetime
from secrets import token_urlsafe
from .db import db
from mongoengine import (
StringField,
IntField,
ReferenceField,
DateTimeField,
EmailField,
FileField,
BooleanField,
EnumField,
)
from .i18n import SupportedLocales
from flask_login import UserMixin
from bson import ObjectId
class User(UserMixin, db.Document):
"""
Model representing a user of the auction site.
"""
id: ObjectId
email = EmailField(required=True, unique=True)
password = StringField(required=True)
locale = EnumField(SupportedLocales)
currency = StringField(max_length=3)
"The user's preferred currency."
created_at = DateTimeField(required=True, default=datetime.utcnow)
is_disabled = BooleanField(default=False)
"Whether the user is disabled."
@property
def is_active(self) -> bool:
"""
Return whether the user is active.
This is used by Flask-Login to determine whether the user is
allowed to log in.
"""
return not self.is_disabled
def get_id(self) -> str:
"""
Return the user's id as a string.
"""
return str(self.id)
class Item(db.Document):
"""
A model for items that are listed on the auction site.
"""
# Create index for sorting items by closing date
meta = {"indexes": [
{"fields": [
"closes_at",
"closed_processed",
"seller_informed_about_result"
]}
]}
title = StringField(max_length=100, required=True)
description = StringField(max_length=2000, required=True)
starting_bid = IntField(required=True, min_value=0)
seller = ReferenceField(User, required=True)
created_at = DateTimeField(required=True, default=datetime.utcnow)
closes_at = DateTimeField()
closed_processed = BooleanField(required=True, default=False)
"Flag indicating if closing of auction was already processed"
seller_informed_about_result = BooleanField(required=True, default=False)
"Flag indicating if seller has already been informed about the result of the auction"
image = FileField()
def save(self, *args, **kwargs):
# Resize image before saving
if self.image:
image = Image.open(BytesIO(self.image.read()))
image.thumbnail((200, 200))
output = BytesIO()
image.save(output, format='JPEG')
self.image.replace(output.getvalue(), content_type='image/jpeg')
super().save(*args, **kwargs)
@property
def is_open(self) -> bool:
"""
Return whether the item is open for bidding.
"""
return self.closes_at > datetime.utcnow()
class Bid(db.Document):
"""
A model for bids on items.
"""
meta = {"indexes": [
{"fields": [
"amount",
"item",
"created_at",
"auction_end_processed",
"winning_bid",
"bidder_informed"
]}
]}
amount = IntField(required=True, min_value=0)
"Indicates the value of the bid."
bidder = ReferenceField(User, required=True)
"User who placed the bid."
item = ReferenceField(Item, required=True)
"Item that the bid is for."
created_at = DateTimeField(required=True, default=datetime.utcnow)
"Date and time that the bid was placed."
auction_end_processed = BooleanField(required=True, default=False)
"Flag indicating, that closed auction has been processes"
winning_bid = BooleanField(required=True, default=False)
"Flag indicating, whether the bid was the winning bid of the auction"
bidder_informed = BooleanField(required=True, default=False)
"Flag indicating, whether the bidder has been informed about the result of the auction"
class AccessToken(db.Document):
"""
Access token for a user.
This is used to authenticate API requests.
"""
meta = {"indexes": [
{"fields": [
"token",
"user",
"expires",
]}
]}
name = StringField(max_length=100, required=True)
"Human-readable name for the token."
user = ReferenceField(User, required=True)
"User that the token is for."
token = StringField(required=True, unique=True, default=token_urlsafe)
"The token string."
last_used_at = DateTimeField(required=False)
"Date and time that the token was last used."
created_at = DateTimeField(required=True, default=datetime.utcnow)
"Date and time that the token was created."
expires = DateTimeField(required=False)
"Date and time that the token expires."
Image diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Sell items{% endblock %}</h1>
{% endblock %}
{% block content %}
<body>
<header>
<meta charset="utf-8">
<title>Sell Item</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
</header>
<main>
<form class="container-fluid" action="{{ url_for('views.add_item') }}" method="POST" enctype="multipart/form-data">
<div class="container">
<div class="mb-3">
<label for="nItem" class="form-label">Item's name</label>
<input type="text" class="form-control" id="nItem" name="nItem">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" maxlength="700"></textarea>
</div>
<div class="mb-3">
<label for="sPrice" class="form-label">Starting price</label>
<input type="number" class="form-control" id="sPrice" name="sPrice" min="0" max="100000000">
</div>
<div class="mb-3">
<label for="currency" class="form-label">Currency</label>
<select name="currency" id="currency" class="form-control">
{% for code, name in currencies.items() %}
<option value="{{ code }}" {% if code == default_currency %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
</select>
<label for="closing" class="form-label">Duration of auction in hours and minutes</label>
<input type="time" id="duration" name="duration" step="60">
</select>
</div>
<div class="mb-3">
<label for="image" class="form-label">Image (Please add image in .jpg format)</label>
<input type="file" class="form-control" id="image" name="image">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</main>
<footer>
</footer>
</body>
</html>
{% endblock %}
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Log In{% endblock %}</h1>
{% endblock %}
{% block content %}
<section>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 d-flex align-items-center justify-content-center">
<div class="card card-tertiary w-100 fmxw-400">
<div class="card-header text-center">
<span>Sign in to our platform</span>
</div>
<div class="login-dialog-img"></div>
<div class="card-body">
<form action="{{ url_for('auth.login') }}" class="mt-4" method="POST">
<div class="form-group">
<label for="email" class="mb-2">Email</label>
<input name="email" id="email" type="email" class="form-control" placeholder="Your email"
required="">
</div>
<div class="form-group">
<div class="form-group">
<label for="password" class="mb-2">Password</label>
<input name="password" id="password" type="password" class="form-control"
placeholder="Your password" required="">
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="form-check">
<label class="form-check-label">
<input name="remember-me" class="form-check-input" type="checkbox">
<span class="form-check-x"></span>
<span class="form-check-sign"></span>
Remember me
</label>
</div>
<p class="m-0"><a href="#" class="text-right">Lost password?</a></p>
</div>
</div>
<button type="submit" class="btn btn-block btn-primary">Login</button>
</form>
<div class="d-block d-sm-flex justify-content-center align-items-center mt-4">
<p class="font-weight-normal">
Not registered?
<a href="{{ url_for('auth.register') }}" class="font-weight-bold">Create an account</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% extends 'base.html' %}
{% block header %}
<div class="container">
<h1>{% block title %}{{user.email}}'s Profile Page{% endblock %}</h1>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-sm-3"></div>
<div class="col-sm-6 profile-section">
<div class="card card-secondary">
<div class="card-header text-center">
<span>{{ user.email }}</span>
</div>
<div class="card-body">
<div class="form-group d-flex align-items-center justify-content-between">
<label for="default" class="mr-3">Email:</label>
<input id="default" type="text" class="form-control w-75" value="{{ user.email }}">
</div>
</div>
<div class="card-footer">
</div>
</div>
</div>
<div class="col-sm-3"></div>
</div>
<div class="row">
<div class="col-sm-3"></div>
<div class="col-sm-6 auction-section">
<h3>Recent Auctions</h3>
<div class="card-deck">
{% for auction in items %}
<div class="card">
<img src="data:image/png;base64,{{ auction.image_base64 }}" class="card-img-top" alt="No image available">
<div class="card-body">
<h5 class="card-title">{{ auction.title }}</h5>
<p class="card-text">{{ auction.description }}</p>
<p class="card-text">Starting Bid: {{ auction.starting_bid }} €</p>
<p class="card-text">Current Price: {{ auction.current_price }} €</p>
<p class="card-text">Starting Time: {{ auction.created_at }} </p>
<p class="card-text">Closing Time: {{ auction.closes_at }} </p>
</div>
<div class="card-footer">
</div>
</div>
{% endfor %}
</div>
<div class="col-sm-3"></div>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}
<style>
.login-dialog-img {
background-image: url("https://openai-labs-public-images-prod.azureedge.net/user-eKykIYWOdPMyg5hmQc750Q6a/generations/generation-GZTU47SaNB9GeJVLaJXUfztk/image.webp");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
width: 100%;
height: 8em;
}
</style>
{% block content %}
<section>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 d-flex align-items-center justify-content-center">
<div class="card card-tertiary w-100 fmxw-400">
<div class="card-header text-center">
<span>Register on our platform</span>
</div>
<div class="login-dialog-img"></div>
<div class="card-body">
<form action="{{ url_for('auth.register') }}" method="POST" class="mt-4">
<div class="form-group">
<label for="email" class="mb-2">Email</label>
<input name="email" id="email" type="email" class="form-control" placeholder="Your email" required="">
</div>
<div class="form-group">
<div class="form-group">
<label for="password" class="mb-2">Password</label>
<input name="password" id="password" type="password" class="form-control" placeholder="Your password" required="">
</div>
<div class="form-group">
<label for="confirmPassword" class="mb-2">Confirm password</label>
<input name="password2" id="confirmPassword" type="password" class="form-control" placeholder="Confirm password"
required="">
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
</div>
</div>
<button type="submit" class="btn btn-block btn-primary">Register account</button>
</form>
<div class="d-block d-sm-flex justify-content-center align-items-center mt-4">
<p class="font-weight-normal">
Already have an account?
<a href="{{ url_for('auth.login') }}" class="font-weight-bold">Login here</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Access tokens{% endblock %}</h1>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-4">
<h4>{{("Personal Access Tokens")}}</h4>
<p>
Personal access tokens allow third-party services to authenticate with our application on your behalf.
</p>
</div>
<div class="col-md-8">
{% if token %}
<div class="alert alert-success" role="alert">
<h4 class="alert-heading">{{("Your new personal access token")}}</h4>
<p>
Your new personal access token is shown below. You may now use this token to make API requests.
</p>
<div class="input-group mb-3">
<input type="text" class="form-control" id="token" value="{{ token.token }}" readonly>
<button class="btn btn-outline-secondary" type="button" id="copy-token" onclick="copyToken()">{{("Copy")}}</button>
<script>
function copyToken() {
var copyText = document.getElementById("token");
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
}
</script>
</div>
<small class="form-text text-muted">{{ ("Make sure to copy your new token now. You won't be able to see it again!") }}</small>
<hr>
</div>
{% endif %}
<div class="card">
<div class="card-header">
<div class="text-center">{{ ("Create access token") }}</div>
</div>
<form action="{{url_for('auth.user_access_tokens', email='me')}}" method="post" class="card-body">
<div class="form-group">
<label for="name">{{ ("Name") }}</label>
<input type="text" class="form-control" name="name" id="name" placeholder="{{ (" Enter token name") }}">
<div class="form-text text-muted">{{ ("Give your token a descriptive name so you can easily identify it in the future.") }}</div>
</div>
<div class="form-group">
<label class="form-check-label" for="expires">{{ ("Expires at") }}</label>
<input type="date" class="form-control" name="expires" id="expires">
<div class="form-text text-muted">{{ ("Leave blank to never expire.") }}</div>
</div>
<button type="submit" class="btn btn-primary">{{ ("Create access token") }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-8 offset-md-4">
<h2 class="text-center mb-4">{{ ("Active Access Tokens") }}</h2>
<table class="table table-striped">
<thead>
<tr>
<th>{{ ("Token name") }}</th>
<th>{{ ("Created") }}</th>
<th>{{ ("Last used") }}</th>
<th>{{ ("Expires") }}</th>
<th>{{ ("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for access_token in tokens %}
<tr>
<td>{{ access_token.name }}</td>
<td>{{ access_token.created_at}}</td>
<td>
{% if access_token.last_used_at %}
{{ access_token.last_used_at }}
{% else %}{{ ("Never") }}
{% endif %}
</td>
<td>
{% if access_token.expires_at %}
{{ access_token.expires_at }}
{% else %}{{ ("Never") }}
{% endif %}
</td>
<td>
<form action="{{ url_for('auth.delete_user_access_token', email='me', id=access_token.id) }}"
method="post">
<button type="submit" class="btn btn-danger">{{ ("Delete") }}</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
<!doctype html>
<html lang="en">
<head>
<title>{% block title %}{% endblock %} - {{ config['BRAND'] }}</title>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<style>
body {
background-image: url("{{ url_for('static', filename='Winter.jpg') }}");
}
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{url_for('views.list_bid')}}">{{config['BRAND']}}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01"
aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarTogglerDemo03">
"""
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="{{ url_for('views.list_bid') }}">Buy</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('views.add_item') }}">Sell</a>
</li>
</ul>
"""
<ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item"><span>{{current_user['email'] }}</span></li>
<li class="nav-item"><a href="{{ url_for('auth.profile', email='me') }}" class="nav-link">My Profile</a></li>
<li class="nav-item"><a href="{{ url_for('auth.logout') }}" class="nav-link">Log Out</a></li>
{% else %}
<li class="nav-item"><a href="{{ url_for('auth.register') }}" class="nav-link">Register</a>
<li class="nav-item"><a href="{{ url_for('auth.login') }}" class="nav-link">Log In</a>
{% endif %}
</ul>
</div>
</nav>
</head>
<body>
<header>
{% block header %}
<h1>{{ config['BRAND'] }}</h1>
{% endblock %}
</header>
<main class="content">
{% for message in get_flashed_messages() %}
<div class="container">
<div class="flash alert alert-primary">{{ message }}</div>
</div>
{% endfor %}
{% block content %}
<!-- MAIN CONTENT BLOCK MISSING -->
{% endblock %}
</main>
<!-- Option 1: jQuery and Bootstrap Bundle (includes Popper) -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.10.2/dist/umd/popper.min.js" integrity="sha384-7+zCNj/IqJ95wo16oMtfsKbZ9ccEh31eOz1HGyDuCQ6wgnyJNSYdrPa03rtR1zdB" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.min.js" integrity="sha384-QJHtvGhmr9XOIpI6YVutG+2QOK9T+ZnN4kzFN1RtK3zEFEIsxhlmWl5/YESvpZ13" crossorigin="anonymous"></script>
</body>
</html>
......@@ -10,5 +10,8 @@
</head>
<body>
<h1>Hello, world!</h1>
<h2>Hallo, Welt!</h2>
<h2>Bonjour le monde!</h2>
<h2>Hei maailma!</h2>
</body>
</html>
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}View all items currently on auction {% endblock %}</h1>
{% endblock %}
{% block content %}
<head>
<meta charset="utf-8">
<title>View items</title>
</head>
<body>
<main>
<div class="list-group container">
{%for item in items %}
<div class="d-flex w-100 justify-content-between mb-4">
<img src="data:image/png;base64,{{ item.image_base64 }}">
<div class="list-group-item list-group-item-action" aria-current="true">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ item.title }}</h5>
<small>{{ item.created_at }}</small>
</div>
<p class="mb-1">{{ item.description }}</p>
<small>{{ item.current_price|localcurrency}}</small>
<a href="{{ url_for('views.page_bid', id=item.id) }}"><button class="btn btn-primary">Bid</button></a>
</div>
<div class="row justify-content-md-center">
<div class="col-md-auto">
<a href="https://twitter.com/intent/tweet?url={{ url_for('views.list_bid', id=item.id, _external=True) | urlencode }}&text={{ "Check out this awesome and cheap item." | urlencode }}" target="_blank" style="background-color: #1DA1F2; color:#fff;">
<button class="btn btn-primary" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-twitter" viewBox="0 0 16 16">
<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"/>
</svg>
</button>
</a>
<a href="https://www.facebook.com/sharer.php?u={{ url_for('views.list_bid', id=item.id, _external=True) | urlencode }}" target="_blank" style="background-color: #4267B2; color:#fff;">
<button class="btn btn-primary" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-facebook" viewBox="0 0 16 16">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"/>
</svg>
</button>
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</main>
<footer>
</footer>
</body>
</html>
{% endblock %}
\ No newline at end of file
<span title="{{ base_amount|e }}">{{ local_amount }}</span>
\ No newline at end of file
{% extends 'base.html' %}
{% block header %}
<div class="container">
<div class="d-flex align-items-center">
<h1 class="mr-auto">{% block title %}{{item.title}}{% endblock %}</h1>
</div>
</div>
{% endblock %}
{% block content %}
<div class="d-flex mb-4 container">
<div class="col-md-4">
<img src="data:image/png;base64,{{ item.image_base64 }}">
</div>
<div class="col-md-8">
<h5 class="mb-1">{{ item.title }}</h5>
<p class="mb-1">{{ item.description }}</p>
<p class="mb-1">Created : {{ item.created_at }}</p>
<p class="mb-1">Current bid : {{ current_price }}</p><br/>
<form method="POST" action="{{ url_for('views.bid', id=item.id)}}" class="row" enctype="multipart/form-data">
<input class="col-md-6" id="bid" name ="bid" type="number" min="{{ min_bid }}" placeholder="write your bid">
<button for="bid" type="submit" class="btn btn-primary col-md-3">Validate </button>
</form>
<small class="form-text text-muted">
{{ _("Minimum bid is %(min_bid)s", min_bid=min_bid|localcurrency) }}
</small>
</div>
</div>
{% endblock %}
\ No newline at end of file
src/tjts5901/test.jpeg

14.2 KiB

......@@ -3,22 +3,411 @@ Basic views for Application
===========================
"""
from flask import Blueprint, render_template, send_from_directory
import base64
import logging
from typing import Optional
from datetime import datetime, timedelta
from mongoengine.queryset.visitor import Q
from flask import Blueprint, flash, redirect, render_template, request, url_for, jsonify
from .models import Item, Bid
from .auth import login_required, current_user
from flask_babel import _, get_locale
from werkzeug.exceptions import abort
from markupsafe import Markup
from .auth import login_required, current_user
from .models import Bid, Item
from .currency import (
convert_currency,
format_converted_currency,
convert_from_currency,
get_currencies,
get_preferred_currency,
REF_CURRENCY,
)
# Main blueprint.
bp = Blueprint('views', __name__)
api = Blueprint('api', __name__, url_prefix='/api')
logger = logging.getLogger(__name__)
# Blueprint for documentation.
docs_bp = Blueprint('docs', __name__)
MIN_BID_INCREMENT = 1
def get_winning_bid(item: Item) -> Optional[Bid]:
"""
Return the (currently) winning bid for the given item.
If there are no bids, or the item is not yet closed, return None.
:param item: The item to get the winning bid for.
:return: The winning bid, or None.
"""
winning_bid = None
try:
winning_bid = Bid.objects(item=item) \
.filter(created_at__lt=item.closes_at) \
.order_by('-amount') \
.first()
except Exception as exc:
logger.warning("Error getting winning bid: %s", exc, exc_info=True, extra={
'item_id': item.id,
})
return winning_bid
def get_item_price(item: Item) -> int:
"""
Return the current price of the given item.
If there are no bids, return the starting bid.
:param item: The item to get the price for.
:return: The current price.
"""
winning_bid = get_winning_bid(item)
if winning_bid:
return winning_bid.amount
else:
return item.starting_bid
def check_auction_ends():
"""
Check if all previously not processed auctions have ended now
Get all closed items, check if they belong to the current user and inform about
the ending of the auction.
Then process all bids on these items and check if they won
"""
items = Item.objects.filter(Q(closes_at=datetime.utcnow()) & Q(closed_processed=False)) \
.order_by('-closes_at')
for item in items:
item.update(closed_processed = True)
message = None
if item.seller == current_user:
message = "The auction of your item \'{}\' has ended. ".format(item.title)
item.update(seller_informed_about_result = True)
# Get (potential) bids for the item and process them
bids = Bid.objects(item=item).order_by('-amount')
if len(bids) >= 1:
# Set winning_bid = True for winning bid
bids[0].update(winning_bid = True)
bids[0].update(auction_end_processed = True)
# Set winning_bid = False for all other bids
for bid in bids[1:]:
bid.update(winning_bid = False)
bid.update(auction_end_processed = True)
# Inform seller if this is current user
if item.seller == current_user:
message += "Congrats! It was sold for {}.".format(bids[0].amount)
# Inform current user if no one bid on the item
else:
if item.seller == current_user:
message += "No one bid on your item. Try again!"
#Display message if not None
if message is not None:
flash((message))
def check_bids_ended():
"""
Method for checking all bids of current user, marked as auction_end_processed
and inform bidder if neccessary
"""
bids = Bid.objects.filter(Q(bidder=current_user) & Q(auction_end_processed=True) & Q(bidder_informed=False))
if len(bids) >= 1:
for bid in bids:
if bid.winning_bid == True:
flash(("Congrats! You won the auction of '{}' with your bid over {}.".format(bid.item.title, bid.amount)))
else:
flash(("Pity! You were not successfull with the auction of '{}' with your bid over {}.".format(bid.item.title, bid.amount)))
bid.update(bidder_informed = True)
@bp.route("/test")
def test_item_adding():
"""
Test item is added
"""
#load test image
with open("src/tjts5901/test.jpeg", "rb") as image_file:
image = image_file.read()
# Create a new item
item = Item()
item.title = "Test title"
item.description = "This is a test description"
item.starting_bid = 100
item.seller = current_user
item.image = image
item.closes_at = datetime.utcnow() + timedelta(days=1)
item.save()
return "OK"
@bp.route("/addItem", methods=('GET', 'POST'))
@login_required
def add_item():
"""
AddItem page.
"""
if request.method == 'POST':
nItem = request.form.get('nItem')
description = request.form.get('description')
currency = request.form.get('currency', REF_CURRENCY)
starting_price = convert_from_currency(request.form['sPrice'], currency)
image = request.files['image']
duration = request.form.get('duration')
error = None
if not nItem:
error = 'Item name is required.'
if not starting_price or starting_price < 1:
error = Markup(_("Starting bid must be greater than %(amount)s.", amount=format_converted_currency(1, currency)))
if not duration:
error = 'Duration is required.'
print(duration, type(duration))
if error is None:
try:
item = Item(
title=nItem,
description=description,
starting_bid=starting_price,
seller=current_user,
closes_at=datetime.utcnow() + timedelta(hours=int(duration[:2]), minutes=int(duration[-2:])),
image=image
)
item.save()
flash(_('Item listed successfully!'))
except Exception as exc:
error = f"Error creating item: {exc!s}"
print('error:', exc)
else:
return redirect(url_for('views.list_bid'))
flash(error)
# Get the list of currencies, and map them to their localized names
currencies = {}
names = get_locale().currencies
for currency in get_currencies():
currencies[currency] = names.get(currency, currency)
item = render_template('addItem.html', currencies=currencies, default_currency=get_preferred_currency())
return item
@bp.route("/")
def index() -> str:
@bp.route("/listBid")
@login_required
def list_bid():
"""
Index page.
page that lists all items currently on auction and checks for notification
"""
check_auction_ends()
check_bids_ended()
# only on sale items are shown
items = Item.objects.filter(closes_at__gt=datetime.utcnow()) \
.order_by('-closes_at')
for item in items:
item.image_base64 = base64.b64encode(item.image.read()).decode('utf-8')
item.created_at = item.created_at.strftime("%Y-%m-%d %H:%M:%S")
item.current_price = get_item_price(item)
# Render template file. Template file is using Jinja2 syntax, and can be passed an arbitrary number
# of arguments to be used in the template. The template file is located in the templates directory.
html = render_template("index.html.j2", title="TJTS5901 Example. I should be changed.")
html = render_template("listBid.html", items=items)
return html
@bp.route("item/<id>/bid", methods=('POST',))
@login_required
def bid(id):
"""
method that saves bid on object
If the bid is valid, create a new bid and redirect to the item view page.
Otherwise, display an error message and redirect back to the item view page.
"""
# frontend not implemented yet, therefore not active
item = Item.objects.get_or_404(id=id)
min_amount = item.starting_bid
local_currency = get_preferred_currency()
local_min_bid = convert_currency(min_amount, local_currency)
amount = int(request.form.get('bid'))
if amount <= min_amount:
flash(_("Bid must be at least %(min_amount)s", min_amount=format_converted_currency(min_amount)))
return redirect(url_for('views.list_bid'))
if item.closes_at < datetime.utcnow():
flash("This item is no longer on sale.")
return redirect(url_for('views.list_bid'))
if item.seller == current_user:
flash("You cannot bid on your own item. Bid was not saved.")
return redirect(url_for('views.list_bid'))
try:
# Notice: if you have integrated the flask-login extension, use current_user
# instead of g.user
bid = Bid(
item=item,
bidder=current_user,
amount=amount,
)
bid.save()
except Exception as exc:
flash(_("Error placing bid: %(exc)s", exc=exc))
else:
flash(("Bid placed successfully!"))
return redirect(url_for('views.list_bid', id=id))
@api.route('<id>/bids', methods=('GET',))
@login_required
def api_item_bids(id):
"""
Get the bids for an item.
:param id: The id of the item to get bids for.
:return: A JSON response containing the bids.
"""
item = Item.objects.get_or_404(id=id)
bids = []
for bid in Bid.objects(item=item).order_by('-amount'):
bids.append(bid.to_json())
return jsonify({
'success': True,
'bids': bids
})
@api.route('<id>/bids', methods=('POST',))
@login_required
def api_item_place_bid(id):
"""
Place a bid on an item.
If the bid is valid, create a new bid and return the bid.
Otherwise, return an error message.
Only accepts `REF_CURRENCY` bids.
:param id: The id of the item to bid on.
:return: A JSON response containing the bid.
"""
item = Item.objects.get_or_404(id=id)
min_amount = get_item_price(item) + MIN_BID_INCREMENT
try:
local_amount = request.form['amount']
currency = request.form.get('currency', REF_CURRENCY)
amount = convert_from_currency(local_amount, currency)
except KeyError:
return jsonify({
'success': False,
'error': ("Missing required argument %(argname)s", argname:='amount')
})
except ValueError:
return jsonify({
'success': False,
'error': ("Invalid value for argument %(argname)s", argname:='amount')
})
except Exception as exc:
return jsonify({
'success': False,
'error': ("Error parsing argument %(argname)s: %(exc)s", argname:='amount', exc:=exc)
})
if amount < min_amount:
return jsonify({
'success': False,
'error': ("Bid must be at least %(min_amount)s", min_amount:=min_amount)
})
if item.closes_at < datetime.utcnow():
return jsonify({
'success': False,
'error': ("This item is no longer on sale.")
})
try:
bid = Bid(
item=item,
bidder=current_user,
amount=amount,
)
bid.save()
except Exception as exc:
logger.error("Error placing bid: %s", exc, exc_info=True, extra={
'item_id': item.id,
'bidder_id': current_user.id,
'amount': amount,
})
return jsonify({
'success': False,
'error': ("Error placing bid: %(exc)s", exc:=exc)
})
print(type(bid))
return jsonify({
'success': True,
'bid': bid.to_json()
})
@bp.route("/view/<id>")
@login_required
def page_bid(id):
"""
Item view page.
Displays the item details, and a form to place a bid.
"""
item = Item.objects.get_or_404(id=id)
min_bid = get_item_price(item) + MIN_BID_INCREMENT
local_currency = get_preferred_currency()
local_min_bid = convert_currency(min_bid, local_currency)
item.image_base64 = base64.b64encode(item.image.read()).decode('utf-8')
item.created_at = item.created_at.strftime("%Y-%m-%d %H:%M:%S")
return render_template('view.html',
item=item, min_bid=min_bid,
local_min_bid=local_min_bid,
local_currency=local_currency)
import pytest
from flask import Flask
from tjts5901.app import create_app
@pytest.fixture
def app():
"""
Application fixture.
Every test that requires `app` as parameter can use this fixture.
Example:
>>> def test_mytest(app: Flask):
>>> ...
"""
flask_app = create_app({
'TESTING': True,
'DEBUG': False,
# We need to set SERVER_NAME and PREFERRED_URL_SCHEME for testing.
'SERVER_NAME': 'localhost.localdomain',
'PREFERRED_URL_SCHEME': 'http',
'MONGO_URL' : "mongodb://mongodb:27017/tjts5901"
})
# If you have done ties4080 course and have used Flask-WTF, you might
# have noticed that CSRF protection is enabled by default. This is
# problematic for testing, because we don't have a browser to generate
# CSRF tokens. We can disable CSRF protection for testing, but we need
# to make sure that we don't have CSRF protection enabled in production.
# flask_app.config['WTF_CSRF_ENABLED'] = False
# flask_app.config['WTF_CSRF_METHODS'] = []
# flask_app.config['WTF_CSRF_CHECK_DEFAULT'] = False
flask_app.testing = True
yield flask_app
# Do some cleanup here if needed.
...
@pytest.fixture
def client(app):
return app.test_client()
import random
from tjts5901.models import User, Item
def test_db():
"""
Testing the db connection via previously defined test method
"""
# for test purpose create random number (unique mail address required)
ran_int = str(random.randint(0,10000))
# Create a new user
user = User()
user.email = "test" + ran_int + "@gmail.com"
user.password = "placeholder"
user.save()
#load test image
with open("src/tjts5901/test.jpeg", "rb") as image_file:
image = image_file.read()
# Create a new item
item = Item()
item.title = "Test title"
item.description = "This is a test description"
item.starting_bid = 100
item.seller = user
item.image = image
item.save()
from tjts5901 import create_app
def test_config():
assert not create_app().testing
assert create_app({'TESTING': True}).testing
## The GitLab Server URL (with protocol) that want to register the runner against
## ref: https://docs.gitlab.com/runner/commands/index.html#gitlab-runner-register
##
gitlabUrl: "https://gitlab.jyu.fi"
## The Registration Token for adding new Runners to the GitLab Server. This must
## be retrieved from your GitLab Instance.
## ref: https://docs.gitlab.com/ce/ci/runners/index.html
## NOTE: In TJTS5901 we set this using kubernetes secrets.
##
# runnerRegistrationToken: gitlab-runner-secret
## Unregister all runners before termination
##
## Updating the runner's chart version or configuration will cause the runner container
## to be terminated and created again. This may cause your Gitlab instance to reference
## non-existant runners. Un-registering the runner before termination mitigates this issue.
## ref: https://docs.gitlab.com/runner/commands/index.html#gitlab-runner-unregister
##
unregisterRunners: true
## Configure the maximum number of concurrent jobs
## - Documentation: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
## - Default value: 10
concurrent: 2
## For RBAC support:
rbac:
create: true
clusterWideAccess: false
# rules:
# - apiGroups: [""]
# resources: ["pods"]
# verbs: ["get", "create", "delete"]
# - apiGroups: [""]
# resources: ["pods/exec"]
# verbs: ["get", "create"]
# - apiGroups: [""]
# resources: ["pods/attach"]
# verbs: ["create"]
# - apiGroups: [""]
# resources: ["secrets", "configmaps"]
# verbs: ["create", "update", "delete"]
# - apiGroups: [""]
# resources: ["services"]
# verbs: ["create"]
## Configuration for the Pods that the runner launches for each new job
##
runners:
image: ubuntu:20.04
builds: {}
services: {}
helpers: {}
name: "Kubernetes runner on Azure for TJTS5901"
## Tags associated with the runner. Comma-separated list of tags.
## - Documentation: https://docs.gitlab.com/ce/ci/runners/#using-tags
tags: kubernetes, cluster
## Determine whether the runner should also run jobs without tags.
## - Documentation: https://docs.gitlab.com/ee/ci/runners/configure_runners.html#set-a-runner-to-run-untagged-jobs
runUntagged: true
# runner configuration, where the multi line strings is evaluated as
# template so you can specify helm values inside of it.
#
# tpl: https://helm.sh/docs/howto/charts_tips_and_tricks/#using-the-tpl-function
# runner configuration: https://docs.gitlab.com/runner/configuration/advanced-configuration.html
config: |
[[runners]]
environment = ["DOCKER_DRIVER=overlay2"]
[runners.kubernetes]
namespace = "{{.Release.Namespace}}"
privileged = true
[[runners.kubernetes.volumes.empty_dir]]
name = "docker-certs"
mount_path = "/certs/client"
medium = "Memory"
## The name of the secret containing runner-token and runner-registration-token
secret: runner-registration-token
## Run all containers with the privileged flag enabled
## This will allow the docker:dind image to run if you need to run Docker
## commands. Please read the docs before turning this on:
## - Documentation: https://docs.gitlab.com/runner/executors/kubernetes.html#using-docker-dind
privileged: true