summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Smith <smithdc@gmail.com>2020-04-22 21:16:29 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-04-30 08:12:09 +0200
commitbb13711451157d5081c2d2a297820f6bc131ac27 (patch)
tree009faa342f4e0986c0961d65d06c651882ad6a20
parentf12162107327b88a2f1faaab15d048e2535ec642 (diff)
Fixed #25712 -- Reorganized templates docs.
-rw-r--r--docs/howto/_images/postmortem.png (renamed from docs/topics/_images/postmortem.png)bin12685 -> 12685 bytes
-rw-r--r--docs/howto/_images/template-lines.png (renamed from docs/topics/_images/template-lines.png)bin15579 -> 15579 bytes
-rw-r--r--docs/howto/custom-template-backend.txt173
-rw-r--r--docs/howto/index.txt1
-rw-r--r--docs/index.txt3
-rw-r--r--docs/topics/templates.txt521
6 files changed, 351 insertions, 347 deletions
diff --git a/docs/topics/_images/postmortem.png b/docs/howto/_images/postmortem.png
index 84b30aabb0..84b30aabb0 100644
--- a/docs/topics/_images/postmortem.png
+++ b/docs/howto/_images/postmortem.png
Binary files differ
diff --git a/docs/topics/_images/template-lines.png b/docs/howto/_images/template-lines.png
index 128e99ae9d..128e99ae9d 100644
--- a/docs/topics/_images/template-lines.png
+++ b/docs/howto/_images/template-lines.png
Binary files differ
diff --git a/docs/howto/custom-template-backend.txt b/docs/howto/custom-template-backend.txt
new file mode 100644
index 0000000000..3e9c87a07a
--- /dev/null
+++ b/docs/howto/custom-template-backend.txt
@@ -0,0 +1,173 @@
+=======================
+Custom template backend
+=======================
+
+Custom backends
+---------------
+
+Here's how to implement a custom template backend in order to use another
+template system. A template backend is a class that inherits
+``django.template.backends.base.BaseEngine``. It must implement
+``get_template()`` and optionally ``from_string()``. Here's an example for a
+fictional ``foobar`` template library::
+
+ from django.template import TemplateDoesNotExist, TemplateSyntaxError
+ from django.template.backends.base import BaseEngine
+ from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy
+
+ import foobar
+
+
+ class FooBar(BaseEngine):
+
+ # Name of the subdirectory containing the templates for this engine
+ # inside an installed application.
+ app_dirname = 'foobar'
+
+ def __init__(self, params):
+ params = params.copy()
+ options = params.pop('OPTIONS').copy()
+ super().__init__(params)
+
+ self.engine = foobar.Engine(**options)
+
+ def from_string(self, template_code):
+ try:
+ return Template(self.engine.from_string(template_code))
+ except foobar.TemplateCompilationFailed as exc:
+ raise TemplateSyntaxError(exc.args)
+
+ def get_template(self, template_name):
+ try:
+ return Template(self.engine.get_template(template_name))
+ except foobar.TemplateNotFound as exc:
+ raise TemplateDoesNotExist(exc.args, backend=self)
+ except foobar.TemplateCompilationFailed as exc:
+ raise TemplateSyntaxError(exc.args)
+
+
+ class Template:
+
+ def __init__(self, template):
+ self.template = template
+
+ def render(self, context=None, request=None):
+ if context is None:
+ context = {}
+ if request is not None:
+ context['request'] = request
+ context['csrf_input'] = csrf_input_lazy(request)
+ context['csrf_token'] = csrf_token_lazy(request)
+ return self.template.render(context)
+
+See `DEP 182`_ for more information.
+
+.. _template-debug-integration:
+
+Debug integration for custom engines
+------------------------------------
+
+The Django debug page has hooks to provide detailed information when a template
+error arises. Custom template engines can use these hooks to enhance the
+traceback information that appears to users. The following hooks are available:
+
+.. _template-postmortem:
+
+Template postmortem
+~~~~~~~~~~~~~~~~~~~
+
+The postmortem appears when :exc:`~django.template.TemplateDoesNotExist` is
+raised. It lists the template engines and loaders that were used when trying to
+find a given template. For example, if two Django engines are configured, the
+postmortem will appear like:
+
+.. image:: _images/postmortem.png
+
+Custom engines can populate the postmortem by passing the ``backend`` and
+``tried`` arguments when raising :exc:`~django.template.TemplateDoesNotExist`.
+Backends that use the postmortem :ref:`should specify an origin
+<template-origin-api>` on the template object.
+
+Contextual line information
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If an error happens during template parsing or rendering, Django can display
+the line the error happened on. For example:
+
+.. image:: _images/template-lines.png
+
+Custom engines can populate this information by setting a ``template_debug``
+attribute on exceptions raised during parsing and rendering. This attribute is
+a :class:`dict` with the following values:
+
+* ``'name'``: The name of the template in which the exception occurred.
+
+* ``'message'``: The exception message.
+
+* ``'source_lines'``: The lines before, after, and including the line the
+ exception occurred on. This is for context, so it shouldn't contain more than
+ 20 lines or so.
+
+* ``'line'``: The line number on which the exception occurred.
+
+* ``'before'``: The content on the error line before the token that raised the
+ error.
+
+* ``'during'``: The token that raised the error.
+
+* ``'after'``: The content on the error line after the token that raised the
+ error.
+
+* ``'total'``: The number of lines in ``source_lines``.
+
+* ``'top'``: The line number where ``source_lines`` starts.
+
+* ``'bottom'``: The line number where ``source_lines`` ends.
+
+Given the above template error, ``template_debug`` would look like::
+
+ {
+ 'name': '/path/to/template.html',
+ 'message': "Invalid block tag: 'syntax'",
+ 'source_lines': [
+ (1, 'some\n'),
+ (2, 'lines\n'),
+ (3, 'before\n'),
+ (4, 'Hello {% syntax error %} {{ world }}\n'),
+ (5, 'some\n'),
+ (6, 'lines\n'),
+ (7, 'after\n'),
+ (8, ''),
+ ],
+ 'line': 4,
+ 'before': 'Hello ',
+ 'during': '{% syntax error %}',
+ 'after': ' {{ world }}\n',
+ 'total': 9,
+ 'bottom': 9,
+ 'top': 1,
+ }
+
+.. _template-origin-api:
+
+Origin API and 3rd-party integration
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django templates have an :class:`~django.template.base.Origin` object available
+through the ``template.origin`` attribute. This enables debug information to be
+displayed in the :ref:`template postmortem <template-postmortem>`, as well as
+in 3rd-party libraries, like the `Django Debug Toolbar`_.
+
+Custom engines can provide their own ``template.origin`` information by
+creating an object that specifies the following attributes:
+
+* ``'name'``: The full path to the template.
+
+* ``'template_name'``: The relative path to the template as passed into the
+ template loading methods.
+
+* ``'loader_name'``: An optional string identifying the function or class used
+ to load the template, e.g. ``django.template.loaders.filesystem.Loader``.
+
+.. _DEP 182: https://github.com/django/deps/blob/master/final/0182-multiple-template-engines.rst
+.. _Django Debug Toolbar: https://github.com/jazzband/django-debug-toolbar
diff --git a/docs/howto/index.txt b/docs/howto/index.txt
index 433ab248e7..ffe4c5519e 100644
--- a/docs/howto/index.txt
+++ b/docs/howto/index.txt
@@ -14,6 +14,7 @@ you quickly accomplish common tasks.
custom-management-commands
custom-model-fields
custom-lookups
+ custom-template-backend
custom-template-tags
custom-file-storage
deployment/index
diff --git a/docs/index.txt b/docs/index.txt
index e55d28b22f..9e8a3fdfd6 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -173,7 +173,8 @@ designers and how it can be extended by programmers:
* **For programmers:**
:doc:`Template API <ref/templates/api>` |
- :doc:`Custom tags and filters <howto/custom-template-tags>`
+ :doc:`Custom tags and filters <howto/custom-template-tags>` |
+ :doc:`Custom template backend <howto/custom-template-backend>`
Forms
=====
diff --git a/docs/topics/templates.txt b/docs/topics/templates.txt
index 63ae8b72f4..4b00028e45 100644
--- a/docs/topics/templates.txt
+++ b/docs/topics/templates.txt
@@ -15,7 +15,8 @@ A Django project can be configured with one or several template engines (or
even zero if you don't use templates). Django ships built-in backends for its
own template system, creatively called the Django template language (DTL), and
for the popular alternative Jinja2_. Backends for other template languages may
-be available from third-parties.
+be available from third-parties. You can also write your own custom backend,
+see :doc:`Custom template backend </howto/custom-template-backend>`
Django defines a standard API for loading and rendering templates regardless
of the backend. Loading consists of finding the template for a given identifier
@@ -43,11 +44,183 @@ namespace.
since template authors can do things like perform XSS attacks and access
properties of template variables that may contain sensitive information.
+.. _template-language-intro:
+
+The Django template language
+============================
+
+.. highlight:: html+django
+
+Syntax
+------
+
+.. admonition:: About this section
+
+ This is an overview of the Django template language's syntax. For details
+ see the :doc:`language syntax reference </ref/templates/language>`.
+
+A Django template is a text document or a Python string marked-up using the
+Django template language. Some constructs are recognized and interpreted by the
+template engine. The main ones are variables and tags.
+
+A template is rendered with a context. Rendering replaces variables with their
+values, which are looked up in the context, and executes tags. Everything else
+is output as is.
+
+The syntax of the Django template language involves four constructs.
+
+Variables
+~~~~~~~~~
+
+A variable outputs a value from the context, which is a dict-like object
+mapping keys to values.
+
+Variables are surrounded by ``{{`` and ``}}`` like this::
+
+ My first name is {{ first_name }}. My last name is {{ last_name }}.
+
+With a context of ``{'first_name': 'John', 'last_name': 'Doe'}``, this template
+renders to::
+
+ My first name is John. My last name is Doe.
+
+Dictionary lookup, attribute lookup and list-index lookups are implemented with
+a dot notation::
+
+ {{ my_dict.key }}
+ {{ my_object.attribute }}
+ {{ my_list.0 }}
+
+If a variable resolves to a callable, the template system will call it with no
+arguments and use its result instead of the callable.
+
+Tags
+~~~~
+
+Tags provide arbitrary logic in the rendering process.
+
+This definition is deliberately vague. For example, a tag can output content,
+serve as a control structure e.g. an "if" statement or a "for" loop, grab
+content from a database, or even enable access to other template tags.
+
+Tags are surrounded by ``{%`` and ``%}`` like this::
+
+ {% csrf_token %}
+
+Most tags accept arguments::
+
+ {% cycle 'odd' 'even' %}
+
+Some tags require beginning and ending tags::
+
+ {% if user.is_authenticated %}Hello, {{ user.username }}.{% endif %}
+
+A :ref:`reference of built-in tags <ref-templates-builtins-tags>` is
+available as well as :ref:`instructions for writing custom tags
+<howto-writing-custom-template-tags>`.
+
+Filters
+~~~~~~~
+
+Filters transform the values of variables and tag arguments.
+
+They look like this::
+
+ {{ django|title }}
+
+With a context of ``{'django': 'the web framework for perfectionists with
+deadlines'}``, this template renders to::
+
+ The Web Framework For Perfectionists With Deadlines
+
+Some filters take an argument::
+
+ {{ my_date|date:"Y-m-d" }}
+
+A :ref:`reference of built-in filters <ref-templates-builtins-filters>` is
+available as well as :ref:`instructions for writing custom filters
+<howto-writing-custom-template-filters>`.
+
+Comments
+~~~~~~~~
+
+Comments look like this::
+
+ {# this won't be rendered #}
+
+A :ttag:`{% comment %} <comment>` tag provides multi-line comments.
+
+Components
+----------
+
+.. admonition:: About this section
+
+ This is an overview of the Django template language's APIs. For details
+ see the :doc:`API reference </ref/templates/api>`.
+
+Engine
+~~~~~~
+
+:class:`django.template.Engine` encapsulates an instance of the Django
+template system. The main reason for instantiating an
+:class:`~django.template.Engine` directly is to use the Django template
+language outside of a Django project.
+
+:class:`django.template.backends.django.DjangoTemplates` is a thin wrapper
+adapting :class:`django.template.Engine` to Django's template backend API.
+
+Template
+~~~~~~~~
+
+:class:`django.template.Template` represents a compiled template. Templates are
+obtained with :meth:`.Engine.get_template` or :meth:`.Engine.from_string`.
+
+Likewise ``django.template.backends.django.Template`` is a thin wrapper
+adapting :class:`django.template.Template` to the common template API.
+
+Context
+~~~~~~~
+
+:class:`django.template.Context` holds some metadata in addition to the context
+data. It is passed to :meth:`.Template.render` for rendering a template.
+
+:class:`django.template.RequestContext` is a subclass of
+:class:`~django.template.Context` that stores the current
+:class:`~django.http.HttpRequest` and runs template context processors.
+
+The common API doesn't have an equivalent concept. Context data is passed in a
+plain :class:`dict` and the current :class:`~django.http.HttpRequest` is passed
+separately if needed.
+
+Loaders
+~~~~~~~
+
+Template loaders are responsible for locating templates, loading them, and
+returning :class:`~django.template.Template` objects.
+
+Django provides several :ref:`built-in template loaders <template-loaders>`
+and supports :ref:`custom template loaders <custom-template-loaders>`.
+
+Context processors
+~~~~~~~~~~~~~~~~~~
+
+Context processors are functions that receive the current
+:class:`~django.http.HttpRequest` as an argument and return a :class:`dict` of
+data to be added to the rendering context.
+
+Their main use is to add common data shared by all templates to the context
+without repeating code in every view.
+
+Django provides many :ref:`built-in context processors <context-processors>`,
+and you can implement your own additional context processors, too.
+
.. _template-engines:
Support for template engines
============================
+.. highlight:: python
+
Configuration
-------------
@@ -483,348 +656,4 @@ templates, as shown in the example above. Jinja2's global namespace removes the
need for template context processors. The Django template language doesn't have
an equivalent of Jinja2 tests.
-Custom backends
----------------
-
-Here's how to implement a custom template backend in order to use another
-template system. A template backend is a class that inherits
-``django.template.backends.base.BaseEngine``. It must implement
-``get_template()`` and optionally ``from_string()``. Here's an example for a
-fictional ``foobar`` template library::
-
- from django.template import TemplateDoesNotExist, TemplateSyntaxError
- from django.template.backends.base import BaseEngine
- from django.template.backends.utils import csrf_input_lazy, csrf_token_lazy
-
- import foobar
-
-
- class FooBar(BaseEngine):
-
- # Name of the subdirectory containing the templates for this engine
- # inside an installed application.
- app_dirname = 'foobar'
-
- def __init__(self, params):
- params = params.copy()
- options = params.pop('OPTIONS').copy()
- super().__init__(params)
-
- self.engine = foobar.Engine(**options)
-
- def from_string(self, template_code):
- try:
- return Template(self.engine.from_string(template_code))
- except foobar.TemplateCompilationFailed as exc:
- raise TemplateSyntaxError(exc.args)
-
- def get_template(self, template_name):
- try:
- return Template(self.engine.get_template(template_name))
- except foobar.TemplateNotFound as exc:
- raise TemplateDoesNotExist(exc.args, backend=self)
- except foobar.TemplateCompilationFailed as exc:
- raise TemplateSyntaxError(exc.args)
-
-
- class Template:
-
- def __init__(self, template):
- self.template = template
-
- def render(self, context=None, request=None):
- if context is None:
- context = {}
- if request is not None:
- context['request'] = request
- context['csrf_input'] = csrf_input_lazy(request)
- context['csrf_token'] = csrf_token_lazy(request)
- return self.template.render(context)
-
-See `DEP 182`_ for more information.
-
-.. _template-debug-integration:
-
-Debug integration for custom engines
-------------------------------------
-
-The Django debug page has hooks to provide detailed information when a template
-error arises. Custom template engines can use these hooks to enhance the
-traceback information that appears to users. The following hooks are available:
-
-.. _template-postmortem:
-
-Template postmortem
-~~~~~~~~~~~~~~~~~~~
-
-The postmortem appears when :exc:`~django.template.TemplateDoesNotExist` is
-raised. It lists the template engines and loaders that were used when trying
-to find a given template. For example, if two Django engines are configured,
-the postmortem will appear like:
-
-.. image:: _images/postmortem.png
-
-Custom engines can populate the postmortem by passing the ``backend`` and
-``tried`` arguments when raising :exc:`~django.template.TemplateDoesNotExist`.
-Backends that use the postmortem :ref:`should specify an origin
-<template-origin-api>` on the template object.
-
-Contextual line information
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-If an error happens during template parsing or rendering, Django can display
-the line the error happened on. For example:
-
-.. image:: _images/template-lines.png
-
-Custom engines can populate this information by setting a ``template_debug``
-attribute on exceptions raised during parsing and rendering. This attribute
-is a :class:`dict` with the following values:
-
-* ``'name'``: The name of the template in which the exception occurred.
-
-* ``'message'``: The exception message.
-
-* ``'source_lines'``: The lines before, after, and including the line the
- exception occurred on. This is for context, so it shouldn't contain more than
- 20 lines or so.
-
-* ``'line'``: The line number on which the exception occurred.
-
-* ``'before'``: The content on the error line before the token that raised the
- error.
-
-* ``'during'``: The token that raised the error.
-
-* ``'after'``: The content on the error line after the token that raised the
- error.
-
-* ``'total'``: The number of lines in ``source_lines``.
-
-* ``'top'``: The line number where ``source_lines`` starts.
-
-* ``'bottom'``: The line number where ``source_lines`` ends.
-
-Given the above template error, ``template_debug`` would look like::
-
- {
- 'name': '/path/to/template.html',
- 'message': "Invalid block tag: 'syntax'",
- 'source_lines': [
- (1, 'some\n'),
- (2, 'lines\n'),
- (3, 'before\n'),
- (4, 'Hello {% syntax error %} {{ world }}\n'),
- (5, 'some\n'),
- (6, 'lines\n'),
- (7, 'after\n'),
- (8, ''),
- ],
- 'line': 4,
- 'before': 'Hello ',
- 'during': '{% syntax error %}',
- 'after': ' {{ world }}\n',
- 'total': 9,
- 'bottom': 9,
- 'top': 1,
- }
-
-.. _template-origin-api:
-
-Origin API and 3rd-party integration
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Django templates have an :class:`~django.template.base.Origin` object available
-through the ``template.origin`` attribute. This enables debug information to be
-displayed in the :ref:`template postmortem <template-postmortem>`, as well as
-in 3rd-party libraries, like the `Django Debug Toolbar`_.
-
-Custom engines can provide their own ``template.origin`` information by
-creating an object that specifies the following attributes:
-
-* ``'name'``: The full path to the template.
-
-* ``'template_name'``: The relative path to the template as passed into the
- template loading methods.
-
-* ``'loader_name'``: An optional string identifying the function or class used
- to load the template, e.g. ``django.template.loaders.filesystem.Loader``.
-
-.. currentmodule:: django.template
-
-.. _template-language-intro:
-
-The Django template language
-============================
-
-.. highlight:: html+django
-
-Syntax
-------
-
-.. admonition:: About this section
-
- This is an overview of the Django template language's syntax. For details
- see the :doc:`language syntax reference </ref/templates/language>`.
-
-A Django template is a text document or a Python string marked-up using the
-Django template language. Some constructs are recognized and interpreted by the
-template engine. The main ones are variables and tags.
-
-A template is rendered with a context. Rendering replaces variables with their
-values, which are looked up in the context, and executes tags. Everything else
-is output as is.
-
-The syntax of the Django template language involves four constructs.
-
-Variables
-~~~~~~~~~
-
-A variable outputs a value from the context, which is a dict-like object
-mapping keys to values.
-
-Variables are surrounded by ``{{`` and ``}}`` like this::
-
- My first name is {{ first_name }}. My last name is {{ last_name }}.
-
-With a context of ``{'first_name': 'John', 'last_name': 'Doe'}``, this
-template renders to::
-
- My first name is John. My last name is Doe.
-
-Dictionary lookup, attribute lookup and list-index lookups are implemented
-with a dot notation::
-
- {{ my_dict.key }}
- {{ my_object.attribute }}
- {{ my_list.0 }}
-
-If a variable resolves to a callable, the template system will call it with no
-arguments and use its result instead of the callable.
-
-Tags
-~~~~
-
-Tags provide arbitrary logic in the rendering process.
-
-This definition is deliberately vague. For example, a tag can output content,
-serve as a control structure e.g. an "if" statement or a "for" loop, grab
-content from a database, or even enable access to other template tags.
-
-Tags are surrounded by ``{%`` and ``%}`` like this::
-
- {% csrf_token %}
-
-Most tags accept arguments::
-
- {% cycle 'odd' 'even' %}
-
-Some tags require beginning and ending tags::
-
- {% if user.is_authenticated %}Hello, {{ user.username }}.{% endif %}
-
-A :ref:`reference of built-in tags <ref-templates-builtins-tags>` is
-available as well as :ref:`instructions for writing custom tags
-<howto-writing-custom-template-tags>`.
-
-Filters
-~~~~~~~
-
-Filters transform the values of variables and tag arguments.
-
-They look like this::
-
- {{ django|title }}
-
-With a context of ``{'django': 'the web framework for perfectionists with
-deadlines'}``, this template renders to::
-
- The Web Framework For Perfectionists With Deadlines
-
-Some filters take an argument::
-
- {{ my_date|date:"Y-m-d" }}
-
-A :ref:`reference of built-in filters <ref-templates-builtins-filters>` is
-available as well as :ref:`instructions for writing custom filters
-<howto-writing-custom-template-filters>`.
-
-Comments
-~~~~~~~~
-
-Comments look like this::
-
- {# this won't be rendered #}
-
-A :ttag:`{% comment %} <comment>` tag provides multi-line comments.
-
-Components
-----------
-
-.. admonition:: About this section
-
- This is an overview of the Django template language's APIs. For details
- see the :doc:`API reference </ref/templates/api>`.
-
-Engine
-~~~~~~
-
-:class:`django.template.Engine` encapsulates an instance of the Django
-template system. The main reason for instantiating an
-:class:`~django.template.Engine` directly is to use the Django template
-language outside of a Django project.
-
-:class:`django.template.backends.django.DjangoTemplates` is a thin wrapper
-adapting :class:`django.template.Engine` to Django's template backend API.
-
-Template
-~~~~~~~~
-
-:class:`django.template.Template` represents a compiled template.
-Templates are obtained with :meth:`Engine.get_template()
-<django.template.Engine.get_template>` or :meth:`Engine.from_string()
-<django.template.Engine.from_string>`
-
-Likewise ``django.template.backends.django.Template`` is a thin wrapper
-adapting :class:`django.template.Template` to the common template API.
-
-Context
-~~~~~~~
-
-:class:`django.template.Context` holds some metadata in addition to the
-context data. It is passed to :meth:`Template.render()
-<django.template.Template.render>` for rendering a template.
-
-:class:`django.template.RequestContext` is a subclass of
-:class:`~django.template.Context` that stores the current
-:class:`~django.http.HttpRequest` and runs template context processors.
-
-The common API doesn't have an equivalent concept. Context data is passed in a
-plain :class:`dict` and the current :class:`~django.http.HttpRequest` is passed
-separately if needed.
-
-Loaders
-~~~~~~~
-
-Template loaders are responsible for locating templates, loading them, and
-returning :class:`~django.template.Template` objects.
-
-Django provides several :ref:`built-in template loaders <template-loaders>`
-and supports :ref:`custom template loaders <custom-template-loaders>`.
-
-Context processors
-~~~~~~~~~~~~~~~~~~
-
-Context processors are functions that receive the current
-:class:`~django.http.HttpRequest` as an argument and return a :class:`dict` of
-data to be added to the rendering context.
-
-Their main use is to add common data shared by all templates to the context
-without repeating code in every view.
-
-Django provides many :ref:`built-in context processors <context-processors>`,
-and you can implement your own additional context processors, too.
-
-.. _Jinja2: http://jinja.pocoo.org/
-.. _DEP 182: https://github.com/django/deps/blob/master/final/0182-multiple-template-engines.rst
-.. _Django Debug Toolbar: https://github.com/jazzband/django-debug-toolbar
+.. _Jinja2: https://jinja.palletsprojects.com/