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 2730 additions and 0 deletions
{% extends 'base.html' %}
{% block header %}
<div class="container">
<h1>{% block title %}{{user|striptags}}'s' Profile Page{% endblock %}</h1>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-sm-2 offset-sm-2">
<img src="{{ user.image_url }}" class="img-fluid" alt="{{_("Profile image for %(email)s", email=user.email)|e}}" style="max-width: 100%">
</div>
<div class="col-sm-6 profile-section">
<div class="card card-secondary">
<div class="card-header text-center">
{{ user }}
</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 }}" readonly>
</div>
</div>
<div class="card-footer">
{% if current_user == user %}
<div class="btn-group" role="group">
<ahref="{{ url_for('items.sell') }}" class="btn btn-primary">{{ _("Sell an Item") }}</a>
</div>
<div class="btn-group" role="group">
{# Notice: I have omited the email -attribute here, and made "me" as default in auth.py for tokens page. #}
<a href="{{ url_for('auth.user_access_tokens') }}" class="btn btn-primary">{{ _("Tokens") }}</a>
</div>
<div class="btn-group" role="group">
<a href="{{ url_for('auth.logout') }}" class="btn btn-danger">{{ _("Logout") }} </a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<section class="row">
<div class="col-md-10 offset-md-1 purchases">
<h3> {{ _("My items") }} </h3>
<div class="container">
<div class="card">
{% for item in won_items %}
<article>
<header class="card-header">
<div class="row">
<div class="col-sm-3">
<h6>{{ _("Purchase date") }}</h5>
<p>{{ item.winning_bid.created_at|dateformat }}</p>
</div>
<div class="col-sm-3">
<h6>{{ _("Item ID") }}</h5>
<p class="text-truncate">{{ item.id }}</p>
</div>
<div class="col-md-2">
<h6>{{ _("Seller") }}</h5>
<p><a href="{{ url_for('auth.profile', email=item.seller.email) }}">{{ item.seller }}</a></p>
</div>
</header>
<main class="card-body">
<div class="row">
<div class="col-sm-2">
<img src="{{ item.image_url }}" class="img-fluid" alt="{{_("Product image for %(title)s", title=item.title)|e}}">
</div>
<div class="col-sm-6">
<h5>{{ item.title }}</h5>
<p>{{ item.description|truncate(1000) }}</p>
</div>
<div class="col-sm-1 text-nowrap ">
<h6>{{ _("Price") }}</h5>
<p>${{ item.winning_bid.amount|localcurrency }}</p>
</div>
<div class="col-sm-3 actions">
<form>
<div>
<button type="submit" class="btn btn-primary border-dark btn-block">{{_("Pay")}}</button>
</div>
<div>
<a href="{{ url_for('items.view', id=item.id) }}" class="btn btn-outline btn-block">{{_("View item")}}</a>
</div>
</form>
</div>
</main>
</article>
{% endfor %}
</div>
</div>
</div>
</section>
<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="..." class="card-img-top" alt="...">
<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>
</div>
<div class="card-footer">
{% if auction.is_open %}
{% if current_user == auction.seller %}
<a href="{{ url_for('items.update', id=auction.id) }}" class="btn btn-primary">Update</a>
{% else %}
<a href="{{ url_for('items.bid', id=auction.id) }}" class="btn btn-primary">Place a bid</a>
{% endif %}
{% else %}
<div class="text-muted">Auction is Closed</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<div class="col-sm-3"></div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Register{% endblock %}</h1>
{% endblock %}
{% block content %}
<style>
.login-dialog-img {
background-image: url("https://openai-labs-public-images-prod.azureedge.net/user-eKykIYWOdPMyg5hmQc750Q6a/generations/generation-nFKGWPKYsJ3r1eJBLrHatf4I/image.webp");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
width: 100%;
height: 8em;
}
</style>
<section class="min-vh-100 d-flex align-items-center bg-secondary">
<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">
<input type="hidden" name="timezone" id="timezone">
<script>
function getTimezone() {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
document.getElementById('timezone').value = tz;
}
getTimezone();
</script>
<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 class="form-check">
<label class="form-check-label">
<input name="terms" class="form-check-input" type="checkbox">
<span class="form-check-x"></span>
<span class="form-check-sign"></span>
{{ _("I agree to the %(terms)s", terms="<a href=\"#\">"|safe+_("terms and conditions")+"</a>"|safe) }}.
</label>
</div>
</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="./login.html" 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>
{%trans%}Personal access tokens allow third-party services to authenticate with our application on your behalf.{%endtrans%}
</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>
{%trans%}Your new personal access token is shown below. You may now use this token to make API requests.{%endtrans%}
</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|dateformat }}</td>
<td>
{% if access_token.last_used_at %}
{{ access_token.last_used_at|dateformat }}
{% else %}{{ _("Never") }}
{% endif %}
</td>
<td>
{% if access_token.expires_at %}
{{ access_token.expires_at|dateformat }}
{% 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 %}
\ No newline at end of file
<!doctype html>
<html lang="{{get_locale()}}">
<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@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
<link rel="stylesheet" href="{{ url_for('static', filename='windows-95-ui-kit/css/w95.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{# Sentry tracing for client side. See: https://docs.sentry.io/platforms/javascript/install/cdn/ #}
{% if config['SENTRY_DSN'] %}
{{sentry_trace}}
<script
src="https://browser.sentry-cdn.com/7.35.0/bundle.tracing.min.js"
integrity="sha384-CjDPchuHUNlGb4GlhyuebuZegU12keiasU1R69+B0VhN5ShdBb06nBWZRfGi73G1"
crossorigin="anonymous">
</script>
<script>
Sentry.onLoad(function() {
Sentry.init({
dsn: {{config['SENTRY_DSN'] | tojson}},
release: {{config['SENTRY_RELEASE'] | tojson}},
environment: {{config['SENTRY_ENVIRONMENT'] | tojson}},
integrations: [new Sentry.BrowserTracing()],
});
});
</script>
{% endif %}
</head>
<body class="bg-secondary">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="{{url_for('index')}}">{{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('items.index') }}">{{_("Home")}}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('items.sell') }}">{{_("Sell")}}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">{{_("About")}}</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<div class="dropdown">
<button class="btn nav-link dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
{{_("Language")}}
</button>
<div class="dropdown-menu" id="locale-selector">
{% for _, lang in locales.items() %}
<a class="dropdown-item {% if lang|lower == get_locale()|lower %}active{% endif %}" href="{{ url_for(request.endpoint, **dict(request.view_args, locale=lang)) }}">{{lang.display_name}}</a>
{% endfor %}
</div>
</div>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item"><a href="{{ url_for('auth.profile', email="me") }}" class="nav-link">{{ current_user['email'] }}</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>
<form class="form-inline">
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-info my-2 my-sm-0" type="submit">{{_("Search")}}</button>
</form>
</div>
</nav>
<header>
{% block header %}
<h1>{{ config['BRAND'] }}</h1>
{% endblock %}
</header>
<main class="content">
{% block content %}
<!-- MAIN CONTENT BLOCK MISSING -->
{% endblock %}
</main>
{# Flash messages #}
<div class="position-fixed top-0 right-0 p-3 m-5">
<div class="container toast-container" id="messages">
<template id="message-toast">
<!-- Template for flahed messages -->
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="mr-auto">{{ _("Message") }}</strong>
<small><time class="created-at"></time></small>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="{{_("Close")}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<span class="message"></span>
</div>
</div>
<div class="clearfix"></div>
</template>
</div>
</div>
<script>
// Timout is in milliseconds
const NOTIFICATION_WAIT_TIME = 30 * 1000;
const NOTIFICATION_URL = {{ url_for('notification.user_notifications')|tojson }};
var notifications = {{ get_notifications()|tojson }};
</script>
<script src="{{url_for('static', filename='notifications.js')}}"></script>
<!-- Moment library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Option 1: jQuery and Bootstrap Bundle (includes Popper) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
</body>
</html>
{# Template file is in Jinja syntax:
https://jinja.palletsprojects.com/en/3.1.x/
#}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{title}}</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
{% extends 'base.html' %}
{% block header %}
<style>
#header-img {
background-image: url("{{url_for('static', filename='img/city.png')}}");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
width: 100%;
height: 12em;
color: var(--orange);
}
#header-img h1 {
background-color: rgba(33, 33, 33, 0.8);
}
</style>
<div id="header-img" class="d-flex container-flex align-items-center justify-content-center">
<h1>{% block title %}{{_("Items on sale")}}{% endblock %}</h1>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-12">
<table class="table">
<thead class="thead-light">
<tr>
<th>{{ _("Title") }}</th>
<th>{{ _("Description") }}</th>
<th>{{ _("Starting Bid") }}</th>
<th>{{ _("Seller") }}</th>
<th>{{ _("Created At") }}</th>
<th>{{ _("Closes At") }}</th>
</tr>
</thead>
<tbody>
{% for item in items.items %}
<tr>
<td>
<a href="{{ url_for('items.view', id=item.id)}}">{{ item.title }}</a>
{% if current_user == item.seller %}
<a class="action btn btn-primary" href="{{ url_for('items.update', id=item['id']) }}">Edit</a>
{% endif %}
</td>
<td>{{ item.description }}</td>
<td>{{ item.starting_bid|localcurrency }}</td>
<td>{{ item.seller.email }}</td>
<td>{{ item.created_at|datetimeformat }}</td>
<td>{{ item.closes_at|datetimeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% for page in items.iter_pages() %}
<li class="page-item {% if page == items.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('items.index', page=page) }}">{{ page }}</a>
</li>
{% endfor %}
</ul>
</nav>
</div>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}{{ _("Sell an Item")}}{% endblock %}</h1>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card p-4">
<h3 class="text-center mb-4">{{ _("Add Item") }}</h3>
<form method="post" action="{{ url_for('items.sell') }}">
<div class="form-group">
<label for="title">{{ _("Title") }}</label>
<input type="text" name="title" id="title" class="form-control" value="{{ request.form['title'] }}">
</div>
<div class="form-group">
<label for="description">{{ _("Description") }}</label>
<textarea name="description" id="description" class="form-control">{{ request.form['body'] }}</textarea>
</div>
<div class="row">
<div class="form-group col-8">
<label for="starting_bid">{{ _("Starting Bid") }}</label>
<input type="number" name="starting_bid" id="starting_bid" min="0" class="form-control">
</div>
<div class="form-group col-4">
<label for="starting_bid">{{ _("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>
{% if config['DEBUG'] %}
<div class="form-group">
<input name="flash-sale" type="checkbox" id="flash-sale">
<label class="form-check-label" for="flash-sale">
{{_("Flash sale!")}}
</label>
</div>
{% endif %}
<div class="form-group">
<button class="btn btn-primary btn-block">{{ _("Add to listing") }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Update an Item{% endblock %}</h1>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<div class="card p-4">
<h3 class="text-center mb-4">Update Item</h3>
<form method="post" id="delete-form" action="{{ url_for('items.delete', id=item.id) }}"></form>
<form method="post" id="update-form" action="{{ url_for('items.update', id=item.id) }}">
<div class="form-group">
<label for="title">{{ _("Title") }}</label>
<input type="text" name="title" id="title" class="form-control" value="{{item.title}}">
</div>
<div class="form-group">
<label for="description">{{ _("Description") }}</label>
<textarea name="description" id="description" class="form-control">{{item.description}}</textarea>
</div>
<div class="form-group">
<label for="starting_price">{{ _("Starting Price") }}</label>
<input type="number" name="starting_bid" readonly id="starting_bid" min="0" class="form-control" value={{item.starting_bid}}>
</div>
<div class="form-group">
<div class="d-flex justify-content-end">
<button name="action" value="update" class="btn btn-primary border-dark">{{ _("Update listing") }}</button>
<button name="action" form="delete-form" value="delete" class="btn btn-danger" onclick="return confirm({{ _("Are you sure?")|tojson }});">{{ _("Delete") }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% 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="container">
<div class="card mb-3">
<div class="row no-gutters">
<div class="col-md-4">
{# Maybe on future we'll have image support. Feels like an important feature to have. #}
<img src="#" alt="...">
</div>
<div class="col-md-8">
<div class="card-body border-0">
<h5 class="card-title">{{ _("%(item)s by %(seller)s", item=item.title, seller=item.seller.email)}}</h5>
<p class="card-text">
{% if item.is_open %}
<div class="form-group row">
<div class="col-sm-4 col-form-label">
{# TODO: Follow dark patterns and change this to "time left" #}
{{ _("Closes at") }}
</div>
<div class="col-sm-8">
<time datetime="{{ item.closes_at.isoformat() }}">{{ item.closes_at|datetimeformat }}</time>
</div>
</div>
<div class="form-group row">
<div class="col-sm-4 col-form-label">
{{ _("Added at") }}
</div>
<div class="col-sm-8">
<time datetime="{{ item.created_at.isoformat() }}">{{ item.created_at|datetimeformat }}</time>
</div>
</div>
<hr /><!-- ------------------------------------------------ -->
<div class="form-group row">
<div class="col-sm-4 col-form-label">
{# TODO: Follow dark patterns and change this to "time left" #}
{{ _("Starting bid") }}
</div>
<div class="col-sm-8">
<strong>{{ min_bid|localcurrency }}</strong>
</div>
</div>
<form action="{{ url_for('items.bid', id=item.id)}}" method="post">
<input type="hidden" name="currency" value="{{ local_currency }}">
<div class="form-group row">
<label for="bid" class="col-sm-3 col-form-label">Your bid</label>
<div class="col-sm-6">
<label class="sr-only" for="bid">{{_("Bid amount")}}</label>
<div class="input-group">
<input type="number" name="amount" class="" id="bid" placeholder="Bid amount" required step="0.01">
</div>
<small class="form-text text-muted">
{{ _("Minimum bid is %(min_bid)s", min_bid=min_bid|localcurrency) }}
</small>
</div>
<div class="col-sm-3">
<button type="submit" class="btn btn-info btn-lg btn-block border-dark">{{_("Bid")}}</button>
</div>
</div>
</form>
{% elif item.closed and item.winning_bid.bidder == current_user %}
<div class="alert alert-success">
<strong>{{_("Congratulations!")}}</strong> {{ _("You won the bid!")}}
</div>
<form action="#" method="post">
<input type="hidden" name="id" value="{{ item.id }}">
<div class="row">
<div class="col-sm-6">
<div class="">
<h5 class="lead">{{_("Final price")}}</h5>
<p class="h4">{{ item.winning_bid.amount|localcurrency }}</p>
<div>
<small class="form-text text-muted">
{{ _("You won this item at %(closes_at)s", closes_at=item.winning_bid.created_at|datetimeformat) }}
</small>
</div>
</div>
</div>
<div class="col-sm-3">
<button type="submit" class="btn btn-info btn-lg btn-block border-dark">{{_("Pay")}}</button>
</div>
</div>
</form>
{% else %}
<div class="alert alert-info">
<strong>{{_("Item is no longer at sale.")}}</strong> {{ _("Next time, act faster!")}}
</div>
{% endif %}
</p>
<hr />
{# Contact seller buttons #}
<div class="row">
<div class="col">
<a href="mailto:{{item.seller.email}}" class="btn btn-primary btn-sm">{{_("%(icon)s Contact seller", icon="💌") }}</a>
<a href="tel:{{item.seller.phone}}" class="btn btn-primary btn-sm">{{_("%(icon)s Call seller", icon="☎️")}}</a>
</div>
</div>
</div>
</div>
<div clas="row">
<div class="col-md-12">
<p>{{item.description}}</p>
</div>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-md-auto">
<a href="https://twitter.com/intent/tweet?url={{ url_for('items.view', id=item.id, _external=True) | urlencode }}&text={{ "Check out this awesome and cheap item." | urlencode }}" class="btn btn-twitter" target="_blank" style="background-color: #1DA1F2; color:#fff;">
🐦
{{ _("Elon-senpai notice me!") }}
</a>
<a href="https://www.facebook.com/sharer.php?u={{ url_for('items.view', id=item.id, _external=True) | urlencode }}" target="_blank" class="btn btn-twitter" target="_blank" style="background-color: #4267B2; color:#fff;">
🌙
{{ _("Zuckdaddy, you so alpha!") }}
</a>
<a href="mailto:?subject={{ item.title | urlencode }}&body={{ url_for('items.view', id=item.id, _external=True) | urlencode }}" target="_blank" class="btn btn-primary" target="_blank">
📧
{{ _("Email to a friend") }}
</a>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
<span title="{{ base_amount|e }}">{{ local_amount }}</span>
msgid ""
msgstr ""
"Project-Id-Version: TJTS5901-K23\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-02-19 12:49+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fi\n"
"Language-Team: fi <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.11.0\n"
#: src/tjts5901/app.py:42
msgid "Hill Valley DMC dealership"
msgstr "Hill Valley DMC -liike"
#: src/tjts5901/app.py:78 tests/test_i18n.py:79 tests/test_i18n.py:103
msgid "Hello, World!"
msgstr "Hei Maailma!"
#: src/tjts5901/auth.py:255
msgid "Required field missing"
msgstr "Pakollinen kenttä puuttuu"
#: src/tjts5901/auth.py:258
#, python-format
msgid "Error creating token: %s"
msgstr "Virhe luotaessa tunnistetta: %s"
#: src/tjts5901/auth.py:260
#, python-format
msgid "Created token: %s"
msgstr "Luotu tunniste: %s"
#: src/tjts5901/items.py:187
#, python-format
msgid "Starting bid must be greater than %(amount)s."
msgstr "Huudon tulee olla vähintään %(amount)s"
#: src/tjts5901/items.py:203
msgid "Item listed successfully!"
msgstr "Tuote lisätty onnistuneesti!"
#: src/tjts5901/items.py:206
#, python-format
msgid "Error creating item: %(exc)s"
msgstr "Virhe luotaessa tuotetta: %(exc)s"
#: src/tjts5901/items.py:251
msgid "Congratulations! You won the auction!"
msgstr "Onnittelut! Voitit huutokaupan!"
#: src/tjts5901/items.py:253 src/tjts5901/items.py:423
msgid "This item is no longer on sale."
msgstr "Tämä tuote ei ole enää myynnissä"
#: src/tjts5901/items.py:256
msgid "This item is closing soon! Act now! Now! Now!"
msgstr "Tämä tuote sulkeutuu pian! Toimi nyt! Heti! Nyt!"
#: src/tjts5901/items.py:275
msgid "Title is required."
msgstr "Otsikko on pakollinen."
#: src/tjts5901/items.py:282
#, python-format
msgid "Error updating item: %(exc)s"
msgstr "Virhe päivitettäessä tuotetta: %(exc)s"
#: src/tjts5901/items.py:287
msgid "Item updated successfully!"
msgstr "Tuote päivitetty onnistuneesti!"
#: src/tjts5901/items.py:306
#, python-format
msgid "Error deleting item: %(exc)s"
msgstr "Virhe poistaessa tuotetta: %(exc)s"
#: src/tjts5901/items.py:308
msgid "Item deleted successfully!"
msgstr "Tuote poistettu onnistuneesti!"
#: src/tjts5901/items.py:334 src/tjts5901/items.py:417
#, python-format
msgid "Bid must be at least %(min_amount)s"
msgstr "Huudon tulee olla vähintään %(min_amount)s"
#: src/tjts5901/items.py:351 src/tjts5901/items.py:442
#, python-format
msgid "Error placing bid: %(exc)s"
msgstr "Virhe asettaessa huutoa: %(exc)s"
#: src/tjts5901/items.py:353
msgid "Bid placed successfully!"
msgstr "Huuto asetettu onnistuneesti!"
#: src/tjts5901/items.py:401
#, python-format
msgid "Missing required argument %(argname)s"
msgstr "Tarvittava argumentti %(argname)s puuttuu"
#: src/tjts5901/items.py:406
#, python-format
msgid "Invalid value for argument %(argname)s"
msgstr "Virheellinen arvo argumentille %(argname)s"
#: src/tjts5901/items.py:411
#, python-format
msgid "Error parsing argument %(argname)s: %(exc)s"
msgstr "Virhe käsiteltäessä argumenttia %(argname)s: %(exc)s"
#: src/tjts5901/templates/base.html:48
msgid "Home"
msgstr "Etusivu"
#: src/tjts5901/templates/base.html:51
msgid "Sell"
msgstr "Myy"
#: src/tjts5901/templates/base.html:54
msgid "About"
msgstr "Tietoa"
#: src/tjts5901/templates/base.html:61
msgid "Language"
msgstr "Kieli"
#: src/tjts5901/templates/base.html:72
msgid "Log Out"
msgstr "Kirjaudu ulos"
#: src/tjts5901/templates/base.html:74
msgid "Register"
msgstr "Rekisteröidy"
#: src/tjts5901/templates/base.html:75
msgid "Log In"
msgstr "Kirjaudu sisään"
#: src/tjts5901/templates/base.html:80
msgid "Search"
msgstr "Etsi"
#: src/tjts5901/templates/base.html:103
msgid "Message"
msgstr "Viesti"
#: src/tjts5901/templates/base.html:105
msgid "Close"
msgstr "Sulje"
#: src/tjts5901/templates/auth/login.html:25
msgid "Sign in to our platform"
msgstr "Kirjaudu palveluun"
#: src/tjts5901/templates/auth/login.html:32
#: src/tjts5901/templates/auth/register.html:31
msgid "Email"
msgstr "Sähköposti"
#: src/tjts5901/templates/auth/login.html:33
msgid "Your email"
msgstr "Sähköpostisi"
#: src/tjts5901/templates/auth/login.html:40
#: src/tjts5901/templates/auth/register.html:38
msgid "Password"
msgstr "Salasana"
#: src/tjts5901/templates/auth/login.html:42
msgid "Your password"
msgstr "Salasanasi"
#: src/tjts5901/templates/auth/login.html:51
msgid "Remember me"
msgstr "Muista minut"
#: src/tjts5901/templates/auth/login.html:54
msgid "Lost password?"
msgstr "Salasana unohtunut?"
#: src/tjts5901/templates/auth/login.html:57
msgid "Login"
msgstr "Kirjaudu"
#: src/tjts5901/templates/auth/login.html:61
msgid "Not registered?"
msgstr "Ei vielä tunnuksia?"
#: src/tjts5901/templates/auth/login.html:62
msgid "Create an account"
msgstr "Luo tunnus"
#: src/tjts5901/templates/auth/profile.html:37
msgid "My items"
msgstr ""
#: src/tjts5901/templates/auth/profile.html:44
msgid "Purchase date"
msgstr ""
#: src/tjts5901/templates/auth/profile.html:49
msgid "Item ID"
msgstr ""
#: src/tjts5901/templates/auth/profile.html:53
#: src/tjts5901/templates/items/index.html:34
msgid "Seller"
msgstr "Myyjä"
#: src/tjts5901/templates/auth/profile.html:59
#, python-format
msgid "Product image for %(title)s"
msgstr ""
#: src/tjts5901/templates/auth/register.html:24
msgid "Register on our platform"
msgstr "Rekisteröidy palveluun"
#: src/tjts5901/templates/auth/register.html:42
msgid "Confirm password"
msgstr "Vahvista salasana"
#: src/tjts5901/templates/auth/register.html:53
#, python-format
msgid "I agree to the %(terms)s"
msgstr "Hyväksyn %(terms)s"
#: src/tjts5901/templates/auth/register.html:53
msgid "terms and conditions"
msgstr "käyttöehdot"
#: src/tjts5901/templates/auth/register.html:58
msgid "Register account"
msgstr "Rekisteröi tunnus"
#: src/tjts5901/templates/auth/register.html:62
msgid "Already have an account?"
msgstr "Onko sinulla jo tunnus?"
#: src/tjts5901/templates/auth/register.html:63
msgid "Login here"
msgstr "Kirjaudu tästä"
#: src/tjts5901/templates/auth/tokens.html:11
msgid "Personal Access Tokens"
msgstr "Henkilökohtaiset poletit"
#: src/tjts5901/templates/auth/tokens.html:13
msgid ""
"Personal access tokens allow third-party services to authenticate with "
"our application on your behalf."
msgstr ""
"Henkilökohtaiset poletit mahdollistavat kolmannen osapuolen "
"palveluidenautentikoinnin sovelluksen puolesta."
#: src/tjts5901/templates/auth/tokens.html:19
msgid "Your new personal access token"
msgstr "Uusi henkilökohtainen poletti"
#: src/tjts5901/templates/auth/tokens.html:21
msgid ""
"Your new personal access token is shown below. You may now use this token"
" to make API requests."
msgstr ""
"Uusi henkilökohtainen poletti näkyy alla. Voit nyt käyttää tätä "
"polettiaAPI-pyyntöihin."
#: src/tjts5901/templates/auth/tokens.html:25
msgid "Copy"
msgstr "Kopioi"
#: src/tjts5901/templates/auth/tokens.html:35
msgid "Make sure to copy your new token now. You won't be able to see it again!"
msgstr "Varmista, että kopioit uuden poletin. Et voi nähdä sitä uudelleen!"
#: src/tjts5901/templates/auth/tokens.html:41
#: src/tjts5901/templates/auth/tokens.html:54
msgid "Create access token"
msgstr "Luo poletti"
#: src/tjts5901/templates/auth/tokens.html:45
msgid "Name"
msgstr "Nimi"
#: src/tjts5901/templates/auth/tokens.html:46
msgid " Enter token name"
msgstr "Anna poletin nimi"
#: src/tjts5901/templates/auth/tokens.html:47
msgid ""
"Give your token a descriptive name so you can easily identify it in the "
"future."
msgstr ""
"Anna poletille kuvaava nimi, jotta voit helposti tunnistaa sen "
"tulevaisuudessa."
#: src/tjts5901/templates/auth/tokens.html:50
msgid "Expires at"
msgstr "Vanhenee"
#: src/tjts5901/templates/auth/tokens.html:52
msgid "Leave blank to never expire."
msgstr "Jätä tyhjäksi, jos et halua poletin vanhentuman."
#: src/tjts5901/templates/auth/tokens.html:64
msgid "Active Access Tokens"
msgstr "Aktiiviset poletit"
#: src/tjts5901/templates/auth/tokens.html:68
msgid "Token name"
msgstr "Poletin nimi"
#: src/tjts5901/templates/auth/tokens.html:69
msgid "Created"
msgstr "Luotu"
#: src/tjts5901/templates/auth/tokens.html:70
msgid "Last used"
msgstr "Viimeksi käytetty"
#: src/tjts5901/templates/auth/tokens.html:71
msgid "Expires"
msgstr "Vanhenee"
#: src/tjts5901/templates/auth/tokens.html:72
msgid "Actions"
msgstr "Toiminnot"
#: src/tjts5901/templates/auth/tokens.html:83
#: src/tjts5901/templates/auth/tokens.html:89
msgid "Never"
msgstr "Ei koskaan"
#: src/tjts5901/templates/auth/tokens.html:95
#: src/tjts5901/templates/items/update.html:31
msgid "Delete"
msgstr "Poista"
#: src/tjts5901/templates/items/index.html:20
msgid "Items on sale"
msgstr "Myynnissä olevat tuotteet"
#: src/tjts5901/templates/items/index.html:31
#: src/tjts5901/templates/items/sell.html:15
#: src/tjts5901/templates/items/update.html:17
msgid "Title"
msgstr "Otsikko"
#: src/tjts5901/templates/items/index.html:32
#: src/tjts5901/templates/items/sell.html:19
#: src/tjts5901/templates/items/update.html:21
msgid "Description"
msgstr "Kuvaus"
#: src/tjts5901/templates/items/index.html:33
#: src/tjts5901/templates/items/sell.html:24
msgid "Starting Bid"
msgstr "Aloitushinta"
#: src/tjts5901/templates/items/index.html:35
msgid "Created At"
msgstr "Luotu"
#: src/tjts5901/templates/items/index.html:36
msgid "Closes At"
msgstr "Sulkeutuu"
#: src/tjts5901/templates/items/sell.html:4
msgid "Sell an Item"
msgstr "Lisää tuote myyntiin"
#: src/tjts5901/templates/items/sell.html:12
msgid "Add Item"
msgstr "Lisää tuote"
#: src/tjts5901/templates/items/sell.html:28
msgid "Currency"
msgstr "Valuutta"
#: src/tjts5901/templates/items/sell.html:40
msgid "Flash sale!"
msgstr "Salamakauppa!"
#: src/tjts5901/templates/items/sell.html:46
msgid "Add to listing"
msgstr "Lisää myyntiin"
#: src/tjts5901/templates/items/update.html:25
msgid "Starting Price"
msgstr "Aloitushinta"
#: src/tjts5901/templates/items/update.html:30
msgid "Update listing"
msgstr "Päivitä myynti-ilmoitus"
#: src/tjts5901/templates/items/update.html:31
msgid "Are you sure?"
msgstr "Oletko varma?"
#: src/tjts5901/templates/items/view.html:21
#, python-format
msgid "%(item)s by %(seller)s"
msgstr "%(item)s, myyjänä %(seller)s"
#: src/tjts5901/templates/items/view.html:27
msgid "Closes at"
msgstr "Sulkeutuu"
#: src/tjts5901/templates/items/view.html:35
msgid "Added at"
msgstr "Lisätty"
#: src/tjts5901/templates/items/view.html:45
msgid "Starting bid"
msgstr "Aloitushinta"
#: src/tjts5901/templates/items/view.html:56
msgid "Bid amount"
msgstr "Tarjous"
#: src/tjts5901/templates/items/view.html:62
#, python-format
msgid "Minimum bid is %(min_bid)s"
msgstr "Vähimmäistarjous on %(min_bid)s"
#: src/tjts5901/templates/items/view.html:66
msgid "Bid"
msgstr "Tarjoa"
#: src/tjts5901/templates/items/view.html:72
msgid "Congratulations!"
msgstr "Onnittelut!"
#: src/tjts5901/templates/items/view.html:72
msgid "You won the bid!"
msgstr "Voitit huutokisan!"
#: src/tjts5901/templates/items/view.html:80
msgid "Final price"
msgstr "Lopullinen hinta"
#: src/tjts5901/templates/items/view.html:84
#, python-format
msgid "You won this item at %(closes_at)s"
msgstr "Voitit tämän tuotteen %(closes_at)s"
#: src/tjts5901/templates/items/view.html:90
msgid "Pay"
msgstr "Maksa"
#: src/tjts5901/templates/items/view.html:97
msgid "Item is no longer at sale."
msgstr "Myynti on päättynyt."
#: src/tjts5901/templates/items/view.html:97
msgid "Next time, act faster!"
msgstr "Jatkossa toimi nopeammin!"
#: src/tjts5901/templates/items/view.html:105
#, python-format
msgid "%(icon)s Contact seller"
msgstr "%(icon)s Ota yhteyttä myyjään"
#: src/tjts5901/templates/items/view.html:106
#, python-format
msgid "%(icon)s Call seller"
msgstr "%(icon)s Soita myyjälle"
#: src/tjts5901/templates/items/view.html:121
msgid "Elon-senpai notice me!"
msgstr "Elon-senpai huomioi minut!"
#: src/tjts5901/templates/items/view.html:125
msgid "Zuckdaddy, you so alpha!"
msgstr "Zuckdaddy, olet niin alpha!"
#: src/tjts5901/templates/items/view.html:129
msgid "Email to a friend"
msgstr "Lähetä ystävälle"
#~ msgid "Your item was not sold"
#~ msgstr "Tuotetteesi ei käynyt kaupaksi"
#~ msgid "Your item <a href=\"%(url)s\">%(title)s</a> was not sold."
#~ msgstr "Tuotteesi <a href=\"%(url)s\">%(title)s</a> ei käynyt kaupaksi."
This diff is collapsed.
This diff is collapsed.
from os import environ
from importlib.metadata import version, PackageNotFoundError
def get_version() -> str:
"""
Get the version of the application.
This is useful for checking the version of the application, and for
monitoring the application.
"""
# Get the version from the environment varible. It's setup by CI/CD pipeline.
ver = environ.get("CI_COMMIT_SHA", None)
if ver is None:
try:
# Get the version from the package.
ver = version('tjts5901')
except PackageNotFoundError:
# Package is not installed.
ver = "0.0.0-dev"
return str(ver)
from os import environ
import pytest
from flask import Flask
from faker import Faker
from werkzeug.security import generate_password_hash
# Disable Flask debug mode for testing
environ["FLASK_DEBUG"] = "0"
def pytest_addoption(parser: pytest.Parser):
"""
Callback to add command-line options for pytest.
Add option to define environment url to test.
Usage example from agruments::
$ pytest --environment-url "https://example.com"
Note: GitLab CI automatically sets environment variable `CI_ENVIRONMENT_URL`
to the address of environment to test.
Usage example from environment variable::
$ CI_ENVIRONMENT_URL="https://example.com" pytest
Note: If you want to test locally, you can define environment variable in
.env file.
"""
parser.addoption("--environment-url",
dest="CI_ENVIRONMENT_URL",
help="Deployment webaddress",
default=environ.get("CI_ENVIRONMENT_URL"))
@pytest.fixture(scope="session")
def app():
"""
Application fixture.
Every test that requires `app` as parameter can use this fixture.
Example:
>>> def test_mytest(app: Flask):
>>> ...
"""
from tjts5901.app import create_app
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',
})
# 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()
@pytest.fixture(scope='session', autouse=True)
def faker_session_locale(app: Flask):
"""
Fixture to set faker locale.
Compares the available locales in the application with the locales
available in faker. If there is a match, the locale is set to the
application locale.
This fixture is autouse, so it will be applied to all tests.
"""
from faker.config import AVAILABLE_LOCALES, DEFAULT_LOCALE
languages = []
with app.app_context():
for locale in app.extensions['babel'].instance.list_translations():
locale_code = str(locale)
if locale_code in AVAILABLE_LOCALES:
languages.append(locale_code)
if len(languages) == 0:
languages = [DEFAULT_LOCALE]
return languages
@pytest.fixture()
def user(app: Flask, faker: Faker):
"""
User fixture.
Creates a user into database and returns it.
"""
from tjts5901.models import User
print(faker.locales[0])
with app.app_context():
password = faker.password()
user = User(
email=faker.email(),
password=generate_password_hash(password),
locale=f"{faker.locales[0]}.UTF-8",
)
user.save()
setattr(user, '_plaintext_password', password)
yield user
user.delete()
"""
Flask based tests
=================
Uses the flask testclient to retrieve and post pages in application.
"""
import pytest
from flask.testing import FlaskClient
# This is the string we are looking for in the frontpage.
IN_TITLE = "DMC"
def test_fetch_mainpage(client: FlaskClient):
"""
Fetch frontpage and check that it has <title>.
:param client: Flask test client. See conftest.py for more info how it is
created.
"""
# Request frontpage
page = client.get('/')
# Check that page contains something.
# Note about assert; assert is special kind of keyword. By default
# assert statements are run, but if they are deemed too costly, python
# interepter can be defined to pass them with `python -O` flag.
# If assert statement return False, new assert exception is raised, and
# and test is deemed to fail. After first statement - condition - human
# readable reason for failure can be provided.
# >>> assert (condition-for-failure, "human readable reason")
assert page, "Did not get anything"
assert IN_TITLE.encode() in page.data, f"Page didn't have {IN_TITLE}"
from tjts5901 import create_app
def test_config():
assert not create_app().testing
assert create_app({'TESTING': True}).testing
def test_hello(client):
response = client.get('/hello', headers={'Accept-Language': 'en_US'})
assert response.data == b'Hello, World!'
"""
Translation tests
=================
"""
import os
from typing import List
import pytest
from flask import Flask
from flask_babel import (
Babel,
Locale,
force_locale,
gettext,
get_translations,
)
from babel.messages.extract import extract_from_dir
from tjts5901.i18n import SupportedLocales
from tjts5901 import __file__ as pkg_file
@pytest.fixture
def babel(app: Flask):
"""
Babel translation fixture.
Returns babel tranlaslation fixture registered in flask app
"""
with app.app_context():
yield app.extensions['babel'].instance
def test_for_supported_locales(app: Flask, babel: Babel):
"""
Compare supported locales with locales with translations available.
"""
with app.app_context():
languages: List[Locale] = babel.list_translations()
# Using list comprehension to convert Enum to list of Locales
# required_languages = [Locale.parse(locale.value) for locale in SupportedLocales]
required_languages: List[Locale] = list()
for locale in SupportedLocales:
required_languages.append(Locale.parse(locale.value))
for required in required_languages:
# Skip English, as it is the default language of the application.
if required.language == 'en':
continue
assert required in languages, f"Missing translation for language {required.language}"
def test_babel_translations(app: Flask, babel: Babel):
"""
Test that translations exists for test string "Hello, world!". This test
will fail if the translation is missing for any language.
This test is not intended to test the translation itself, but rather to
ensure that the translation exists.
And if the actual translation for "Hello, world!" is "Hello, world!", then
the test needs to be updated to use a different test string.
"""
# For flask_babel to work, we need to run in app context
with app.app_context():
# Iterate through all of the languages available.
languages: List[Locale] = babel.list_translations()
for locale in languages:
if locale.language == "en":
# By default everything should be in english
continue
with force_locale(locale):
assert gettext("Hello, World!") != "Hello, World!", f"Message is not translated for language {locale.language}"
def test_app_language_detection(client, babel):
"""
Similar to :func:`test_babel_translations`, but uses e2e test client
to test translations.
Uses the Accept-Language header to set the language for the request.
TODO: Write variation that includes the territory code in the Accept-Language header.
"""
# Iterate through all of the languages available.
with client.application.app_context():
languages: List[Locale] = babel.list_translations()
for locale in languages:
if locale.language == "en":
# By default everything should be in english
continue
response = client.get('/hello', headers={'Accept-Language': locale.language})
resp_as_string = response.data.decode('utf-8')
assert gettext("Hello, World!") == resp_as_string, f"Message is not translated for language {locale.language}"
@pytest.fixture(scope="session")
def app_strings():
"""
Fixture for extracting strings from the application source code.
"""
# TODO: Read method_map from config file
method_map = [
('**.py', 'python'),
('**/templates/**.html', 'jinja2'),
]
# Collect all of the messages from the source code
dir_path = os.path.dirname(pkg_file)
messages = set()
for msg in extract_from_dir(dir_path, method_map):
messages.add(msg[2])
return messages
@pytest.mark.parametrize("locale", SupportedLocales)
def test_app_translation_status(locale, app, babel, app_strings, fail_treshold=0.15):
"""
Check that the majority of strings in application are translated.
This test will fail if the percentage of untranslated strings is greater
than the :param:`fail_treshold`.
:param fail_treshold: Acceptable percentile for untraslated strings.
"""
unique_messages = len(app_strings)
with app.app_context():
locale = Locale.parse(locale.value)
if locale.language == "en":
# By default everything should be in english
return
with force_locale(locale):
untranslated_messages = 0
# Get the catalog for the current locale, and check if the extracted message
# is in the catalog
catalog = get_translations()._catalog # pylint: disable=protected-access
for msg in app_strings:
# If the message is not in the catalog, then it is untranslated
if catalog.get(msg, "") == "":
untranslated_messages += 1
# Calculate the percentage of untranslated messages
untranslated_percent = untranslated_messages / unique_messages
assert untranslated_percent < fail_treshold, f"Too many untranslated strings for language {locale.language} ({untranslated_percent:.2%})"
This diff is collapsed.
This diff is collapsed.