Initial commit
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
deps
|
||||
assets/node_modules
|
||||
Dockerfile
|
||||
.env
|
||||
dev-compose.yml
|
||||
docker-compose.yml
|
||||
rel
|
||||
.vscode
|
||||
.elixir_ls
|
||||
._build
|
||||
5
.formatter.exs
Normal file
5
.formatter.exs
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
import_deps: [:ecto, :phoenix],
|
||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
|
||||
subdirectories: ["priv/*/migrations"]
|
||||
]
|
||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where 3rd-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
confient-*.tar
|
||||
|
||||
# If NPM crashes, it generates a log, let's ignore it too.
|
||||
npm-debug.log
|
||||
|
||||
# The directory NPM downloads your dependencies sources to.
|
||||
/assets/node_modules/
|
||||
|
||||
# Since we are building assets from assets/,
|
||||
# we ignore priv/static. You may want to comment
|
||||
# this depending on your deployment strategy.
|
||||
/priv/static/
|
||||
|
||||
.vscode
|
||||
|
||||
rel
|
||||
82
Dockerfile
Normal file
82
Dockerfile
Normal file
@@ -0,0 +1,82 @@
|
||||
FROM elixir:1.11 AS build
|
||||
|
||||
# install build dependencies
|
||||
RUN set -eux \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& curl -sS https://deb.nodesource.com/gpgkey/nodesource.gpg.key | gpg --dearmor > /etc/apt/trusted.gpg.d/nodesource.gpg \
|
||||
&& echo "deb https://deb.nodesource.com/node_14.x stretch main" > /etc/apt/sources.list.d/nodesource.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# prepare build dir
|
||||
WORKDIR /opt/confient
|
||||
|
||||
# install hex + rebar
|
||||
RUN mix local.hex --force && \
|
||||
mix local.rebar --force
|
||||
|
||||
# set build ENV
|
||||
ENV MIX_ENV=prod
|
||||
|
||||
# install mix dependencies
|
||||
COPY mix.exs mix.lock ./
|
||||
COPY config config
|
||||
RUN mix do deps.get, deps.compile
|
||||
|
||||
# build assets
|
||||
COPY assets/package.json assets/package-lock.json ./assets/
|
||||
RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
|
||||
|
||||
COPY priv priv
|
||||
COPY assets assets
|
||||
RUN npm run --prefix ./assets deploy
|
||||
RUN mix phx.digest
|
||||
|
||||
# compile and build release
|
||||
COPY lib lib
|
||||
# uncomment COPY if rel/ exists
|
||||
# COPY rel rel
|
||||
RUN mix do compile, release
|
||||
|
||||
# prepare release image
|
||||
FROM elixir:1.11 AS app
|
||||
|
||||
RUN set -eux \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
postgresql-client \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd -ms /bin/bash confient
|
||||
|
||||
RUN mkdir -p /opt/confient /srv/confient/uploads
|
||||
|
||||
WORKDIR /opt/confient
|
||||
|
||||
RUN chown confient:confient /opt/confient /srv/confient/uploads
|
||||
|
||||
USER confient
|
||||
|
||||
COPY --from=build --chown=confient:confient /opt/confient/_build/prod/rel/confient ./
|
||||
|
||||
COPY entrypoint.sh /opt/confient/entrypoint.sh
|
||||
|
||||
ENV HOME=/opt/confient
|
||||
|
||||
# image-spec annotations using labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL org.opencontainers.image.source="https://git.athelas-conseils.fr/papey/confient"
|
||||
LABEL org.opencontainers.image.authors="Wilfried OLLIVIER"
|
||||
LABEL org.opencontainers.image.title="Confient"
|
||||
LABEL org.opencontainers.image.description="Confient runtime"
|
||||
LABEL org.opencontainers.image.licences="Unlicense"
|
||||
|
||||
CMD ["./entrypoint.sh"]
|
||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
62
README.md
Normal file
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Confient
|
||||
|
||||
A tiny [Phoenix](https://www.phoenixframework.org/) web app for teacher to
|
||||
collect students works
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Elixir](https://elixir-lang.org/)
|
||||
|
||||
### Installing
|
||||
|
||||
#### Get Confient
|
||||
|
||||
##### From source
|
||||
|
||||
Clone this repo and run
|
||||
|
||||
```sh
|
||||
mix deps.get
|
||||
```
|
||||
|
||||
To download all the deps
|
||||
|
||||
```sh
|
||||
cd assets && npm install
|
||||
```
|
||||
|
||||
To get Node.js deps
|
||||
|
||||
This apps needs a [PostgreSQL](https://www.postgresql.org/) data base
|
||||
|
||||
As a startup you can une the one in `compose/dev`
|
||||
|
||||
```sh
|
||||
cd compose/dev && docker-compose up
|
||||
```
|
||||
|
||||
When DB is up, run the migrations
|
||||
|
||||
```sh
|
||||
mix ecto.migrate
|
||||
```
|
||||
|
||||
Then start the server
|
||||
|
||||
```sh
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
Visit [`localhost:4000`](http://localhost:4000) from your browser : 🎉
|
||||
|
||||
If you need details on how to run it in production, see [Phoenix deployment guides](https://hexdocs.pm/phoenix/deployment.html).
|
||||
|
||||
## Authors
|
||||
|
||||
- Wilfried OLLIVIER - Main author - [papey](https://git.athelas-conseils.fr/papey)
|
||||
|
||||
## License
|
||||
|
||||
[LICENSE](LICENSE) file for details
|
||||
5
assets/.babelrc
Normal file
5
assets/.babelrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env"
|
||||
]
|
||||
}
|
||||
122
assets/css/app.scss
Normal file
122
assets/css/app.scss
Normal file
@@ -0,0 +1,122 @@
|
||||
/* This file is for your main application css. */
|
||||
@import "./phoenix.css";
|
||||
@import "./colors.scss";
|
||||
|
||||
/* Alerts and form errors */
|
||||
.alert {
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.alert-info {
|
||||
color: #31708f;
|
||||
background-color: #d9edf7;
|
||||
border-color: #bce8f1;
|
||||
}
|
||||
.alert-warning {
|
||||
color: #8a6d3b;
|
||||
background-color: #fcf8e3;
|
||||
border-color: #faebcc;
|
||||
}
|
||||
.alert-danger {
|
||||
color: #a94442;
|
||||
background-color: #f2dede;
|
||||
border-color: #ebccd1;
|
||||
}
|
||||
.alert p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.alert:empty {
|
||||
display: none;
|
||||
}
|
||||
.invalid-feedback {
|
||||
color: #a94442;
|
||||
display: block;
|
||||
margin: -1rem 0 2rem;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
border-radius: 3px;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: $lblack;
|
||||
}
|
||||
|
||||
.navbar li {
|
||||
float: left;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar li a {
|
||||
display: block;
|
||||
color: white;
|
||||
text-align: center;
|
||||
background-color: $lblack;
|
||||
padding: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar li a:hover {
|
||||
background-color: $hover;
|
||||
border-top: 0.5rem solid #eaeaea;
|
||||
}
|
||||
|
||||
.sign-in {
|
||||
margin-bottom: 2rem;
|
||||
color: $link;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 10rem;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $link;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
background-color: $blue;
|
||||
border: 0.1rem solid $blue;
|
||||
border-radius: 0.4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
height: 3.8rem;
|
||||
letter-spacing: 0.1rem;
|
||||
line-height: 3.8rem;
|
||||
padding: 0 3rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #fdfdfd;
|
||||
border-top: 0.5rem solid #eaeaea;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
11
assets/css/colors.scss
Normal file
11
assets/css/colors.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
/* Colorscheme */
|
||||
$blue: #4682b4;
|
||||
$lblue: #269ccc;
|
||||
$gray: #696969;
|
||||
$lblack: #333;
|
||||
$lgray: #d3d3d3;
|
||||
|
||||
$link: $blue;
|
||||
$bg: $gray;
|
||||
$hover: $blue;
|
||||
$hero-bg: $lgray;
|
||||
635
assets/css/phoenix.css
Normal file
635
assets/css/phoenix.css
Normal file
@@ -0,0 +1,635 @@
|
||||
/* Includes some default style for the starter application.
|
||||
* This can be safely deleted to start fresh.
|
||||
*/
|
||||
|
||||
/* Milligram v1.3.0 https://milligram.github.io
|
||||
* Copyright (c) 2017 CJ Patoilo Licensed under the MIT license
|
||||
*/
|
||||
|
||||
*,
|
||||
*:after,
|
||||
*:before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
html {
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
font-size: 62.5%;
|
||||
}
|
||||
body {
|
||||
min-height: 100%;
|
||||
color: #000000;
|
||||
font-family: "Helvetica", "Arial", sans-serif;
|
||||
font-size: 1.6em;
|
||||
font-weight: 300;
|
||||
line-height: 1.6;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 0.3rem solid #d1d1d1;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
blockquote *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.button,
|
||||
button,
|
||||
input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
background-color: #0069d9;
|
||||
border: 0.1rem solid #0069d9;
|
||||
border-radius: 0.4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
height: 3.8rem;
|
||||
letter-spacing: 0.1rem;
|
||||
line-height: 3.8rem;
|
||||
padding: 0 3rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.button:focus,
|
||||
.button:hover,
|
||||
button:focus,
|
||||
button:hover,
|
||||
input[type="button"]:focus,
|
||||
input[type="button"]:hover,
|
||||
input[type="reset"]:focus,
|
||||
input[type="reset"]:hover,
|
||||
input[type="submit"]:focus,
|
||||
input[type="submit"]:hover {
|
||||
background-color: #606c76;
|
||||
border-color: #606c76;
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
}
|
||||
.button[disabled],
|
||||
button[disabled],
|
||||
input[type="button"][disabled],
|
||||
input[type="reset"][disabled],
|
||||
input[type="submit"][disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.button[disabled]:focus,
|
||||
.button[disabled]:hover,
|
||||
button[disabled]:focus,
|
||||
button[disabled]:hover,
|
||||
input[type="button"][disabled]:focus,
|
||||
input[type="button"][disabled]:hover,
|
||||
input[type="reset"][disabled]:focus,
|
||||
input[type="reset"][disabled]:hover,
|
||||
input[type="submit"][disabled]:focus,
|
||||
input[type="submit"][disabled]:hover {
|
||||
background-color: #0069d9;
|
||||
border-color: #0069d9;
|
||||
}
|
||||
.button.button-outline,
|
||||
button.button-outline,
|
||||
input[type="button"].button-outline,
|
||||
input[type="reset"].button-outline,
|
||||
input[type="submit"].button-outline {
|
||||
background-color: transparent;
|
||||
color: #0069d9;
|
||||
}
|
||||
.button.button-outline:focus,
|
||||
.button.button-outline:hover,
|
||||
button.button-outline:focus,
|
||||
button.button-outline:hover,
|
||||
input[type="button"].button-outline:focus,
|
||||
input[type="button"].button-outline:hover,
|
||||
input[type="reset"].button-outline:focus,
|
||||
input[type="reset"].button-outline:hover,
|
||||
input[type="submit"].button-outline:focus,
|
||||
input[type="submit"].button-outline:hover {
|
||||
background-color: transparent;
|
||||
border-color: #606c76;
|
||||
color: #606c76;
|
||||
}
|
||||
.button.button-outline[disabled]:focus,
|
||||
.button.button-outline[disabled]:hover,
|
||||
button.button-outline[disabled]:focus,
|
||||
button.button-outline[disabled]:hover,
|
||||
input[type="button"].button-outline[disabled]:focus,
|
||||
input[type="button"].button-outline[disabled]:hover,
|
||||
input[type="reset"].button-outline[disabled]:focus,
|
||||
input[type="reset"].button-outline[disabled]:hover,
|
||||
input[type="submit"].button-outline[disabled]:focus,
|
||||
input[type="submit"].button-outline[disabled]:hover {
|
||||
border-color: inherit;
|
||||
color: #0069d9;
|
||||
}
|
||||
.button.button-clear,
|
||||
button.button-clear,
|
||||
input[type="button"].button-clear,
|
||||
input[type="reset"].button-clear,
|
||||
input[type="submit"].button-clear {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #0069d9;
|
||||
}
|
||||
.button.button-clear:focus,
|
||||
.button.button-clear:hover,
|
||||
button.button-clear:focus,
|
||||
button.button-clear:hover,
|
||||
input[type="button"].button-clear:focus,
|
||||
input[type="button"].button-clear:hover,
|
||||
input[type="reset"].button-clear:focus,
|
||||
input[type="reset"].button-clear:hover,
|
||||
input[type="submit"].button-clear:focus,
|
||||
input[type="submit"].button-clear:hover {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
color: #606c76;
|
||||
}
|
||||
.button.button-clear[disabled]:focus,
|
||||
.button.button-clear[disabled]:hover,
|
||||
button.button-clear[disabled]:focus,
|
||||
button.button-clear[disabled]:hover,
|
||||
input[type="button"].button-clear[disabled]:focus,
|
||||
input[type="button"].button-clear[disabled]:hover,
|
||||
input[type="reset"].button-clear[disabled]:focus,
|
||||
input[type="reset"].button-clear[disabled]:hover,
|
||||
input[type="submit"].button-clear[disabled]:focus,
|
||||
input[type="submit"].button-clear[disabled]:hover {
|
||||
color: #0069d9;
|
||||
}
|
||||
code {
|
||||
background: #f4f5f6;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 86%;
|
||||
margin: 0 0.2rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
pre {
|
||||
background: #f4f5f6;
|
||||
border-left: 0.3rem solid #0069d9;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
pre > code {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 0.1rem solid #f4f5f6;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="search"],
|
||||
input[type="tel"],
|
||||
input[type="text"],
|
||||
input[type="url"],
|
||||
textarea,
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0.1rem solid #d1d1d1;
|
||||
border-radius: 0.4rem;
|
||||
box-shadow: none;
|
||||
box-sizing: inherit;
|
||||
height: 3.8rem;
|
||||
padding: 0.6rem 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="url"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border-color: #0069d9;
|
||||
outline: 0;
|
||||
}
|
||||
select {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%23d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')
|
||||
center right no-repeat;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
select:focus {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%230069d9" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||
}
|
||||
textarea {
|
||||
min-height: 6.5rem;
|
||||
}
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
fieldset {
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline;
|
||||
}
|
||||
.label-inline {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.row.row-no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
.row.row-no-padding > .column {
|
||||
padding: 0;
|
||||
}
|
||||
.row.row-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.row.row-top {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.row.row-bottom {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.row.row-center {
|
||||
align-items: center;
|
||||
}
|
||||
.row.row-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
.row.row-baseline {
|
||||
align-items: baseline;
|
||||
}
|
||||
.row .column {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
margin-left: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.row .column.column-offset-10 {
|
||||
margin-left: 10%;
|
||||
}
|
||||
.row .column.column-offset-20 {
|
||||
margin-left: 20%;
|
||||
}
|
||||
.row .column.column-offset-25 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
.row .column.column-offset-33,
|
||||
.row .column.column-offset-34 {
|
||||
margin-left: 33.3333%;
|
||||
}
|
||||
.row .column.column-offset-50 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
.row .column.column-offset-66,
|
||||
.row .column.column-offset-67 {
|
||||
margin-left: 66.6666%;
|
||||
}
|
||||
.row .column.column-offset-75 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
.row .column.column-offset-80 {
|
||||
margin-left: 80%;
|
||||
}
|
||||
.row .column.column-offset-90 {
|
||||
margin-left: 90%;
|
||||
}
|
||||
.row .column.column-10 {
|
||||
flex: 0 0 10%;
|
||||
max-width: 10%;
|
||||
}
|
||||
.row .column.column-20 {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
.row .column.column-25 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
.row .column.column-33,
|
||||
.row .column.column-34 {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
.row .column.column-40 {
|
||||
flex: 0 0 40%;
|
||||
max-width: 40%;
|
||||
}
|
||||
.row .column.column-50 {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
.row .column.column-60 {
|
||||
flex: 0 0 60%;
|
||||
max-width: 60%;
|
||||
}
|
||||
.row .column.column-66,
|
||||
.row .column.column-67 {
|
||||
flex: 0 0 66.6666%;
|
||||
max-width: 66.6666%;
|
||||
}
|
||||
.row .column.column-75 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
}
|
||||
.row .column.column-80 {
|
||||
flex: 0 0 80%;
|
||||
max-width: 80%;
|
||||
}
|
||||
.row .column.column-90 {
|
||||
flex: 0 0 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
.row .column .column-top {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.row .column .column-bottom {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.row .column .column-center {
|
||||
-ms-grid-row-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
@media (min-width: 40rem) {
|
||||
.row {
|
||||
flex-direction: row;
|
||||
margin-left: -1rem;
|
||||
width: calc(100% + 2rem);
|
||||
}
|
||||
.row .column {
|
||||
margin-bottom: inherit;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: #0069d9;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #606c76;
|
||||
}
|
||||
dl,
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
dl dl,
|
||||
dl ol,
|
||||
dl ul,
|
||||
ol dl,
|
||||
ol ol,
|
||||
ol ul,
|
||||
ul dl,
|
||||
ul ol,
|
||||
ul ul {
|
||||
font-size: 90%;
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
}
|
||||
ol {
|
||||
list-style: decimal inside;
|
||||
}
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
}
|
||||
.button,
|
||||
button,
|
||||
dd,
|
||||
dt,
|
||||
li {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
fieldset,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
form,
|
||||
ol,
|
||||
p,
|
||||
pre,
|
||||
table,
|
||||
ul {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
border-bottom: 0.1rem solid #e1e1e1;
|
||||
padding: 1.2rem 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
td:first-child,
|
||||
th:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
td:last-child,
|
||||
th:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.1rem;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 4.6rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
h2 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
h3 {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
h4 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -0.08rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -0.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
h6 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.float-left {
|
||||
float: left;
|
||||
}
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* General style */
|
||||
h1 {
|
||||
font-size: 3.6rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
h3 {
|
||||
font-size: 2.2rem;
|
||||
letter-spacing: -0.08rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.8rem;
|
||||
letter-spacing: -0.05rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
h6 {
|
||||
font-size: 1.4rem;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
pre {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 80rem;
|
||||
padding: 0 2rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Phoenix promo and logo */
|
||||
.phx-hero {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
background: #eee;
|
||||
border-radius: 6px;
|
||||
padding: 3em 3em 1em;
|
||||
margin-bottom: 3rem;
|
||||
font-weight: 200;
|
||||
font-size: 120%;
|
||||
}
|
||||
.phx-hero input {
|
||||
background: #ffffff;
|
||||
}
|
||||
.phx-logo {
|
||||
min-width: 300px;
|
||||
margin: 1rem;
|
||||
display: block;
|
||||
}
|
||||
.phx-logo img {
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
header {
|
||||
width: 100%;
|
||||
background: #fdfdfd;
|
||||
border-bottom: 0.5rem solid #eaeaea;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
header section {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
header section :first-child {
|
||||
order: 2;
|
||||
}
|
||||
header section :last-child {
|
||||
order: 1;
|
||||
}
|
||||
header nav ul,
|
||||
header nav li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
header nav ul {
|
||||
margin: 1rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
header nav a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
/* Small devices (landscape phones, 576px and up) */
|
||||
header section {
|
||||
flex-direction: row;
|
||||
}
|
||||
header nav ul {
|
||||
margin: 1rem;
|
||||
}
|
||||
.phx-logo {
|
||||
flex-basis: 527px;
|
||||
margin: 2rem 1rem;
|
||||
}
|
||||
}
|
||||
15
assets/js/app.js
Normal file
15
assets/js/app.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// We need to import the CSS so that webpack will load it.
|
||||
// The MiniCssExtractPlugin is used to separate it out into
|
||||
// its own CSS file.
|
||||
import "../css/app.scss"
|
||||
|
||||
// webpack automatically bundles all modules in your
|
||||
// entry points. Those entry points can be configured
|
||||
// in "webpack.config.js".
|
||||
//
|
||||
// Import deps with the dep name or local files with a relative path, for example:
|
||||
//
|
||||
// import {Socket} from "phoenix"
|
||||
// import socket from "./socket"
|
||||
//
|
||||
import "phoenix_html"
|
||||
63
assets/js/socket.js
Normal file
63
assets/js/socket.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// NOTE: The contents of this file will only be executed if
|
||||
// you uncomment its entry in "assets/js/app.js".
|
||||
|
||||
// To use Phoenix channels, the first step is to import Socket,
|
||||
// and connect at the socket path in "lib/web/endpoint.ex".
|
||||
//
|
||||
// Pass the token on params as below. Or remove it
|
||||
// from the params if you are not using authentication.
|
||||
import {Socket} from "phoenix"
|
||||
|
||||
let socket = new Socket("/socket", {params: {token: window.userToken}})
|
||||
|
||||
// When you connect, you'll often need to authenticate the client.
|
||||
// For example, imagine you have an authentication plug, `MyAuth`,
|
||||
// which authenticates the session and assigns a `:current_user`.
|
||||
// If the current user exists you can assign the user's token in
|
||||
// the connection for use in the layout.
|
||||
//
|
||||
// In your "lib/web/router.ex":
|
||||
//
|
||||
// pipeline :browser do
|
||||
// ...
|
||||
// plug MyAuth
|
||||
// plug :put_user_token
|
||||
// end
|
||||
//
|
||||
// defp put_user_token(conn, _) do
|
||||
// if current_user = conn.assigns[:current_user] do
|
||||
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
|
||||
// assign(conn, :user_token, token)
|
||||
// else
|
||||
// conn
|
||||
// end
|
||||
// end
|
||||
//
|
||||
// Now you need to pass this token to JavaScript. You can do so
|
||||
// inside a script tag in "lib/web/templates/layout/app.html.eex":
|
||||
//
|
||||
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
|
||||
//
|
||||
// You will need to verify the user token in the "connect/3" function
|
||||
// in "lib/web/channels/user_socket.ex":
|
||||
//
|
||||
// def connect(%{"token" => token}, socket, _connect_info) do
|
||||
// # max_age: 1209600 is equivalent to two weeks in seconds
|
||||
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
|
||||
// {:ok, user_id} ->
|
||||
// {:ok, assign(socket, :user, user_id)}
|
||||
// {:error, reason} ->
|
||||
// :error
|
||||
// end
|
||||
// end
|
||||
//
|
||||
// Finally, connect to the socket:
|
||||
socket.connect()
|
||||
|
||||
// Now that you are connected, you can join channels with a topic:
|
||||
let channel = socket.channel("topic:subtopic", {})
|
||||
channel.join()
|
||||
.receive("ok", resp => { console.log("Joined successfully", resp) })
|
||||
.receive("error", resp => { console.log("Unable to join", resp) })
|
||||
|
||||
export default socket
|
||||
18129
assets/package-lock.json
generated
Normal file
18129
assets/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
assets/package.json
Normal file
28
assets/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"repository": {},
|
||||
"description": " ",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"deploy": "webpack --mode production",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^3.4.2",
|
||||
"hard-source-webpack-plugin": "^0.13.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"terser-webpack-plugin": "^2.3.2",
|
||||
"webpack": "4.41.5",
|
||||
"webpack-cli": "^3.3.2"
|
||||
}
|
||||
}
|
||||
BIN
assets/static/favicon.ico
Normal file
BIN
assets/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/static/images/confient.png
Normal file
BIN
assets/static/images/confient.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
assets/static/images/phoenix.png
Normal file
BIN
assets/static/images/phoenix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
5
assets/static/robots.txt
Normal file
5
assets/static/robots.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||
#
|
||||
# To ban all spiders from the entire site uncomment the next two lines:
|
||||
# User-agent: *
|
||||
# Disallow: /
|
||||
53
assets/webpack.config.js
Normal file
53
assets/webpack.config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = (env, options) => {
|
||||
const devMode = options.mode !== 'production';
|
||||
|
||||
return {
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }),
|
||||
new OptimizeCSSAssetsPlugin({})
|
||||
]
|
||||
},
|
||||
entry: {
|
||||
'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, '../priv/static/js'),
|
||||
publicPath: '/js/'
|
||||
},
|
||||
devtool: devMode ? 'eval-cheap-module-source-map' : undefined,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.[s]?css$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
|
||||
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
|
||||
]
|
||||
.concat(devMode ? [new HardSourceWebpackPlugin()] : [])
|
||||
}
|
||||
};
|
||||
3
compose/dev/.env
Normal file
3
compose/dev/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=confient_dev
|
||||
11
compose/dev/docker-compose.yml
Normal file
11
compose/dev/docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
environment:
|
||||
- POSTGRES_USER=$POSTGRES_USER
|
||||
- POSTGRES_DB=$POSTGRES_DB
|
||||
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||
7
compose/release/.env
Normal file
7
compose/release/.env
Normal file
@@ -0,0 +1,7 @@
|
||||
POSTGRES_USER=confient
|
||||
POSTGRES_DB=confient
|
||||
POSTGRES_PASSWORD=password
|
||||
CONFIENT_DATA=/tmp/confient/uploads
|
||||
POSTGRES_DATA=/tmp/confient/pg
|
||||
# gen one with mix phx.gen.secret
|
||||
SECRET_KEY_BASE=aQFwnczmCcy1JOQ/opboa+YSETybKWCELSCjI7i7atIBnLesYJ2dVYVgouIi7SYs
|
||||
26
compose/release/docker-compose.yml
Normal file
26
compose/release/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
volumes:
|
||||
- $POSTGRES_DATA:/var/lib/postgres
|
||||
environment:
|
||||
- POSTGRES_USER=$POSTGRES_USER
|
||||
- POSTGRES_DB=$POSTGRES_DB
|
||||
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||
|
||||
confient:
|
||||
image: papey/confient:latest
|
||||
ports:
|
||||
- 127.0.0.1:4000:4000
|
||||
volumes:
|
||||
- $CONFIENT_DATA:/srv/confient/uploads
|
||||
environment:
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_USER=$POSTGRES_USER
|
||||
- DATABASE_URL=ecto://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
|
||||
- SECRET_KEY_BASE=$SECRET_KEY_BASE
|
||||
- CONFIENT_UPLOAD_DIR=/srv/confient/uploads
|
||||
depends_on:
|
||||
- postgres
|
||||
36
config/config.exs
Normal file
36
config/config.exs
Normal file
@@ -0,0 +1,36 @@
|
||||
# This file is responsible for configuring your application
|
||||
# and its dependencies with the aid of the Mix.Config module.
|
||||
#
|
||||
# This configuration file is loaded before any dependency and
|
||||
# is restricted to this project.
|
||||
|
||||
# General application configuration
|
||||
use Mix.Config
|
||||
|
||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||
|
||||
config :confient,
|
||||
ecto_repos: [Confient.Repo],
|
||||
upload_dir: System.get_env("CONFIENT_UPLOAD_DIR", "/srv/confient/uploads"),
|
||||
timezone: System.get_env("CONFIENT_TIMEZONE", "Europe/Paris"),
|
||||
domain: System.get_env("CONFIENT_DOMAIN", "http://localhost:4000")
|
||||
|
||||
# Configures the endpoint
|
||||
config :confient, ConfientWeb.Endpoint,
|
||||
url: [host: "localhost"],
|
||||
secret_key_base: "rTob+2honR0wBwOdVleXuVzLPI/5r71ALpLYwXff0fUeV6zinIKcaVvjLh/ZxG5i",
|
||||
render_errors: [view: ConfientWeb.ErrorView, accepts: ~w(html json), layout: false],
|
||||
pubsub_server: Confient.PubSub,
|
||||
live_view: [signing_salt: "+X6U/dHw"]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
metadata: [:request_id]
|
||||
|
||||
# Use Jason for JSON parsing in Phoenix
|
||||
config :phoenix, :json_library, Jason
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env()}.exs"
|
||||
76
config/dev.exs
Normal file
76
config/dev.exs
Normal file
@@ -0,0 +1,76 @@
|
||||
use Mix.Config
|
||||
|
||||
# Configure your database
|
||||
config :confient, Confient.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "confient_dev",
|
||||
hostname: "localhost",
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
pool_size: 10
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
# The watchers configuration can be used to run external
|
||||
# watchers to your application. For example, we use it
|
||||
# with webpack to recompile .js and .css sources.
|
||||
config :confient, ConfientWeb.Endpoint,
|
||||
http: [port: 4000],
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
check_origin: false,
|
||||
watchers: [
|
||||
node: [
|
||||
"node_modules/webpack/bin/webpack.js",
|
||||
"--mode",
|
||||
"development",
|
||||
"--watch-stdin",
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
]
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# In order to use HTTPS in development, a self-signed
|
||||
# certificate can be generated by running the following
|
||||
# Mix task:
|
||||
#
|
||||
# mix phx.gen.cert
|
||||
#
|
||||
# Note that this task requires Erlang/OTP 20 or later.
|
||||
# Run `mix help phx.gen.cert` for more information.
|
||||
#
|
||||
# The `http:` config above can be replaced with:
|
||||
#
|
||||
# https: [
|
||||
# port: 4001,
|
||||
# cipher_suite: :strong,
|
||||
# keyfile: "priv/cert/selfsigned_key.pem",
|
||||
# certfile: "priv/cert/selfsigned.pem"
|
||||
# ],
|
||||
#
|
||||
# If desired, both `http:` and `https:` keys can be
|
||||
# configured to run both http and https servers on
|
||||
# different ports.
|
||||
|
||||
# Watch static and templates for browser reloading.
|
||||
config :confient, ConfientWeb.Endpoint,
|
||||
live_reload: [
|
||||
patterns: [
|
||||
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"priv/gettext/.*(po)$",
|
||||
~r"lib/confient_web/(live|views)/.*(ex)$",
|
||||
~r"lib/confient_web/templates/.*(eex)$"
|
||||
]
|
||||
]
|
||||
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
55
config/prod.exs
Normal file
55
config/prod.exs
Normal file
@@ -0,0 +1,55 @@
|
||||
use Mix.Config
|
||||
|
||||
# For production, don't forget to configure the url host
|
||||
# to something meaningful, Phoenix uses this information
|
||||
# when generating URLs.
|
||||
#
|
||||
# Note we also include the path to a cache manifest
|
||||
# containing the digested version of static files. This
|
||||
# manifest is generated by the `mix phx.digest` task,
|
||||
# which you should run after static files are built and
|
||||
# before starting your production server.
|
||||
config :confient, ConfientWeb.Endpoint,
|
||||
url: [host: "example.com", port: 80],
|
||||
cache_static_manifest: "priv/static/cache_manifest.json"
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
|
||||
# ## SSL Support
|
||||
#
|
||||
# To get SSL working, you will need to add the `https` key
|
||||
# to the previous section and set your `:url` port to 443:
|
||||
#
|
||||
# config :confient, ConfientWeb.Endpoint,
|
||||
# ...
|
||||
# url: [host: "example.com", port: 443],
|
||||
# https: [
|
||||
# port: 443,
|
||||
# cipher_suite: :strong,
|
||||
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
|
||||
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
|
||||
# transport_options: [socket_opts: [:inet6]]
|
||||
# ]
|
||||
#
|
||||
# The `cipher_suite` is set to `:strong` to support only the
|
||||
# latest and more secure SSL ciphers. This means old browsers
|
||||
# and clients may not be supported. You can set it to
|
||||
# `:compatible` for wider support.
|
||||
#
|
||||
# `:keyfile` and `:certfile` expect an absolute path to the key
|
||||
# and cert in disk or a relative path inside priv, for example
|
||||
# "priv/ssl/server.key". For all supported SSL configuration
|
||||
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
|
||||
#
|
||||
# We also recommend setting `force_ssl` in your endpoint, ensuring
|
||||
# no data is ever sent via http, always redirecting to https:
|
||||
#
|
||||
# config :confient, ConfientWeb.Endpoint,
|
||||
# force_ssl: [hsts: true]
|
||||
#
|
||||
# Check `Plug.SSL` for all available options in `force_ssl`.
|
||||
|
||||
# Finally import the config/prod.secret.exs which loads secrets
|
||||
# and configuration from environment variables.
|
||||
# import_config "prod.secret.exs"
|
||||
46
config/releases.exs
Normal file
46
config/releases.exs
Normal file
@@ -0,0 +1,46 @@
|
||||
# In this file, we load production configuration and secrets
|
||||
# from environment variables. You can also hardcode secrets,
|
||||
# although such is generally not recommended and you have to
|
||||
# remember to add this file to your .gitignore.
|
||||
import Config
|
||||
|
||||
database_url =
|
||||
System.get_env("DATABASE_URL") ||
|
||||
raise """
|
||||
environment variableDATABASE_URL=$DATABASE_URL DATABASE_URL is missing.
|
||||
For example: ecto://USER:PASS@HOST/DATABASE
|
||||
"""
|
||||
|
||||
config :confient, Confient.Repo,
|
||||
# ssl: true,
|
||||
url: database_url,
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
||||
|
||||
secret_key_base =
|
||||
System.get_env("SECRET_KEY_BASE") ||
|
||||
raise """
|
||||
environment variable SECRET_KEY_BASE is missing.
|
||||
You can generate one by calling: mix phx.gen.secret
|
||||
"""
|
||||
|
||||
config :confient, ConfientWeb.Endpoint,
|
||||
http: [
|
||||
port: String.to_integer(System.get_env("PORT") || "4000"),
|
||||
transport_options: [socket_opts: [:inet6]]
|
||||
],
|
||||
secret_key_base: secret_key_base
|
||||
|
||||
config :confient,
|
||||
base_upload_dir: System.get_env("CONFIENT_BASE_UPLOAD_DIR", "/srv/confient/uploads"),
|
||||
timezone: System.get_env("CONFIENT_TIMEZONE", "Europe/Paris"),
|
||||
domain: System.get_env("CONFIENT_DOMAIN", "http://localhost:4000")
|
||||
|
||||
# ## Using releases (Elixir v1.9+)
|
||||
#
|
||||
# If you are doing OTP releases, you need to instruct Phoenix
|
||||
# to start each relevant endpoint:
|
||||
#
|
||||
config :confient, ConfientWeb.Endpoint, server: true
|
||||
#
|
||||
# Then you can assemble a release by calling `mix release`.
|
||||
# See `mix help release` for more information.
|
||||
22
config/test.exs
Normal file
22
config/test.exs
Normal file
@@ -0,0 +1,22 @@
|
||||
use Mix.Config
|
||||
|
||||
# Configure your database
|
||||
#
|
||||
# The MIX_TEST_PARTITION environment variable can be used
|
||||
# to provide built-in test partitioning in CI environment.
|
||||
# Run `mix help test` for more information.
|
||||
config :confient, Confient.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "confient_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
hostname: "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :confient, ConfientWeb.Endpoint,
|
||||
http: [port: 4002],
|
||||
server: false
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
14
entrypoint.sh
Executable file
14
entrypoint.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# wait until PG is ready
|
||||
while ! pg_isready -q -h $POSTGRES_HOST -p 5432 -U $POSTGRES_USER
|
||||
do
|
||||
echo "$(date) - waiting for database to start"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# run migrations
|
||||
./bin/confient eval Confient.Release.migrate
|
||||
|
||||
# start the app
|
||||
./bin/confient start
|
||||
9
lib/confient.ex
Normal file
9
lib/confient.ex
Normal file
@@ -0,0 +1,9 @@
|
||||
defmodule Confient do
|
||||
@moduledoc """
|
||||
Confient keeps the contexts that define your domain
|
||||
and business logic.
|
||||
|
||||
Contexts are also responsible for managing your data, regardless
|
||||
if it comes from the database, an external API or others.
|
||||
"""
|
||||
end
|
||||
113
lib/confient/account.ex
Normal file
113
lib/confient/account.ex
Normal file
@@ -0,0 +1,113 @@
|
||||
defmodule Confient.Account do
|
||||
@moduledoc """
|
||||
The Account context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Confient.Repo
|
||||
|
||||
alias Confient.Account.User
|
||||
|
||||
@doc """
|
||||
Returns the list of users.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_users()
|
||||
[%User{}, ...]
|
||||
|
||||
"""
|
||||
def list_users do
|
||||
Repo.all(User)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single user.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user!(123)
|
||||
%User{}
|
||||
|
||||
iex> get_user!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
def get_user_by_username(username) do
|
||||
query =
|
||||
from u in User,
|
||||
where: u.username == ^username,
|
||||
select: u
|
||||
|
||||
Repo.one(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_user(%{field: value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> create_user(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_user(attrs \\ %{}) do
|
||||
%User{}
|
||||
|> User.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_user(user, %{field: new_value})
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> update_user(user, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_user(%User{} = user, attrs) do
|
||||
user
|
||||
|> User.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_user(user)
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> delete_user(user)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_user(%User{} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_user(user)
|
||||
%Ecto.Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
def change_user(%User{} = user, attrs \\ %{}) do
|
||||
User.changeset(user, attrs)
|
||||
end
|
||||
end
|
||||
31
lib/confient/account/auth.ex
Normal file
31
lib/confient/account/auth.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule Confient.Account.Auth do
|
||||
def login(%{"username" => username, "password" => password}) do
|
||||
case Confient.Account.get_user_by_username(username) do
|
||||
nil ->
|
||||
:error
|
||||
|
||||
u ->
|
||||
if auth(u, password) do
|
||||
{:ok, u}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp auth(user, _password) when user == nil, do: false
|
||||
|
||||
defp auth(user, password) do
|
||||
case Confient.Account.Encryption.validate_password(user, password) do
|
||||
{:ok, _u} -> true
|
||||
{:error, _} -> false
|
||||
end
|
||||
end
|
||||
|
||||
def signed_in?(conn), do: conn.assigns[:current_user]
|
||||
|
||||
def get_username(conn) do
|
||||
user = conn.assigns[:current_user]
|
||||
user.username
|
||||
end
|
||||
end
|
||||
8
lib/confient/account/encryption.ex
Normal file
8
lib/confient/account/encryption.ex
Normal file
@@ -0,0 +1,8 @@
|
||||
defmodule Confient.Account.Encryption do
|
||||
alias Comeonin.Bcrypt
|
||||
|
||||
def hash_password(password), do: Bcrypt.hashpwsalt(password)
|
||||
|
||||
def validate_password(%Confient.Account.User{} = user, password),
|
||||
do: Bcrypt.check_pass(user, password)
|
||||
end
|
||||
39
lib/confient/account/user.ex
Normal file
39
lib/confient/account/user.ex
Normal file
@@ -0,0 +1,39 @@
|
||||
defmodule Confient.Account.User do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Confient.Account.Encryption
|
||||
|
||||
schema "users" do
|
||||
field :encrypted_password, :string
|
||||
field :username, :string
|
||||
|
||||
field :password, :string, virtual: true
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:username, :encrypted_password, :password])
|
||||
|> validate_length(:password, min: 6)
|
||||
|> validate_format(:username, ~r/^[a-z0-9][a-z0-9]+[a-z0-9]$/i)
|
||||
|> validate_length(:username, min: 3)
|
||||
|> unique_constraint(:username)
|
||||
|> update_change(:username, &String.downcase/1)
|
||||
|> encrypt()
|
||||
|> validate_required([:username, :encrypted_password])
|
||||
end
|
||||
|
||||
defp encrypt(changeset) do
|
||||
IO.inspect(get_change(changeset, :password))
|
||||
|
||||
case get_change(changeset, :password) do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
pass ->
|
||||
put_change(changeset, :encrypted_password, Encryption.hash_password(pass))
|
||||
end
|
||||
end
|
||||
end
|
||||
34
lib/confient/application.ex
Normal file
34
lib/confient/application.ex
Normal file
@@ -0,0 +1,34 @@
|
||||
defmodule Confient.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Start the Ecto repository
|
||||
Confient.Repo,
|
||||
# Start the Telemetry supervisor
|
||||
ConfientWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Confient.PubSub},
|
||||
# Start the Endpoint (http/https)
|
||||
ConfientWeb.Endpoint
|
||||
# Start a worker by calling: Confient.Worker.start_link(arg)
|
||||
# {Confient.Worker, arg}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Confient.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
# Tell Phoenix to update the endpoint configuration
|
||||
# whenever the application is updated.
|
||||
def config_change(changed, _new, removed) do
|
||||
ConfientWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
24
lib/confient/release.ex
Normal file
24
lib/confient/release.ex
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule Confient.Release do
|
||||
@app :confient
|
||||
|
||||
def migrate do
|
||||
load_app()
|
||||
|
||||
for repo <- repos() do
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
|
||||
end
|
||||
end
|
||||
|
||||
def rollback(repo, version) do
|
||||
load_app()
|
||||
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
|
||||
end
|
||||
|
||||
defp repos do
|
||||
Application.fetch_env!(@app, :ecto_repos)
|
||||
end
|
||||
|
||||
defp load_app do
|
||||
Application.load(@app)
|
||||
end
|
||||
end
|
||||
5
lib/confient/repo.ex
Normal file
5
lib/confient/repo.ex
Normal file
@@ -0,0 +1,5 @@
|
||||
defmodule Confient.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :confient,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
236
lib/confient/school.ex
Normal file
236
lib/confient/school.ex
Normal file
@@ -0,0 +1,236 @@
|
||||
defmodule Confient.School do
|
||||
@moduledoc """
|
||||
The School context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Confient.Repo
|
||||
|
||||
alias Confient.School.Class
|
||||
|
||||
@doc """
|
||||
Returns the list of classes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_classes()
|
||||
[%Class{}, ...]
|
||||
|
||||
"""
|
||||
def list_classes do
|
||||
Repo.all(Class) |> Repo.preload(:students)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single class.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Class does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_class!(123)
|
||||
%Class{}
|
||||
|
||||
iex> get_class!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_class!(id), do: Repo.get!(Class, id) |> Repo.preload(:students)
|
||||
|
||||
def get_class(id) do
|
||||
case Repo.get(Class, id) |> Repo.preload(:students) |> Repo.preload(:assignments) do
|
||||
nil -> {:error, :no_class}
|
||||
class -> {:ok, class}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a class.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_class(%{field: value})
|
||||
{:ok, %Class{}}
|
||||
|
||||
iex> create_class(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_class(attrs \\ %{}) do
|
||||
%Class{}
|
||||
|> Class.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a class.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_class(class, %{field: new_value})
|
||||
{:ok, %Class{}}
|
||||
|
||||
iex> update_class(class, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_class(%Class{} = class, attrs) do
|
||||
class
|
||||
|> Class.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a class.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_class(class)
|
||||
{:ok, %Class{}}
|
||||
|
||||
iex> delete_class(class)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_class(%Class{} = class) do
|
||||
Repo.delete(class)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking class changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_class(class)
|
||||
%Ecto.Changeset{data: %Class{}}
|
||||
|
||||
"""
|
||||
def change_class(%Class{} = class, attrs \\ %{}) do
|
||||
Class.changeset(class, attrs)
|
||||
end
|
||||
|
||||
alias Confient.School.Student
|
||||
|
||||
@doc """
|
||||
Returns the list of students.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_students()
|
||||
[%Student{}, ...]
|
||||
|
||||
"""
|
||||
def list_students do
|
||||
Repo.all(Student) |> Repo.preload(:class)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single student.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Student does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_student!(123)
|
||||
%Student{}
|
||||
|
||||
iex> get_student!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_student!(id), do: Repo.get!(Student, id) |> Repo.preload(:class)
|
||||
|
||||
def get_full_student!(id) do
|
||||
Repo.one(
|
||||
from s in Student,
|
||||
where: s.id == ^id,
|
||||
preload: [{:students_works, :assignment}, :class],
|
||||
select: s
|
||||
)
|
||||
end
|
||||
|
||||
def get_student(id) do
|
||||
case Repo.get(Student, id) |> Repo.preload(:class) do
|
||||
nil -> {:error, :no_student}
|
||||
student -> {:ok, student}
|
||||
end
|
||||
end
|
||||
|
||||
def student_exists?(lastname, firstname, %Class{id: class_id}) do
|
||||
Repo.exists?(
|
||||
from s in Student,
|
||||
where: s.lastname == ^lastname and s.firstname == ^firstname and s.class_id == ^class_id
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a student.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_student(%{field: value})
|
||||
{:ok, %Student{}}
|
||||
|
||||
iex> create_student(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_student(attrs \\ %{}) do
|
||||
%Student{}
|
||||
|> Student.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def create_student(%Class{id: class_id}, attrs) do
|
||||
%Student{class_id: class_id}
|
||||
|> Student.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a student.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_student(student, %{field: new_value})
|
||||
{:ok, %Student{}}
|
||||
|
||||
iex> update_student(student, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_student(%Student{} = student, attrs) do
|
||||
student
|
||||
|> Student.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a student.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_student(student)
|
||||
{:ok, %Student{}}
|
||||
|
||||
iex> delete_student(student)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_student(%Student{} = student) do
|
||||
Repo.delete(student)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking student changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_student(student)
|
||||
%Ecto.Changeset{data: %Student{}}
|
||||
|
||||
"""
|
||||
def change_student(%Student{} = student, attrs \\ %{}) do
|
||||
Student.changeset(student, attrs)
|
||||
end
|
||||
end
|
||||
22
lib/confient/school/class.ex
Normal file
22
lib/confient/school/class.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule Confient.School.Class do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "classes" do
|
||||
field :name, :string
|
||||
|
||||
has_many :students, Confient.School.Student
|
||||
has_many :assignments, Confient.Works.Assignment
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(class, attrs) do
|
||||
class
|
||||
|> cast(attrs, [:name])
|
||||
|> unique_constraint(:name, message: "Existe déjà")
|
||||
|> update_change(:name, &String.upcase/1)
|
||||
|> validate_required([:name], message: "Le champ ne peut être vide")
|
||||
end
|
||||
end
|
||||
25
lib/confient/school/student.ex
Normal file
25
lib/confient/school/student.ex
Normal file
@@ -0,0 +1,25 @@
|
||||
defmodule Confient.School.Student do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "students" do
|
||||
field :firstname, :string
|
||||
field :lastname, :string
|
||||
|
||||
belongs_to :class, Confient.School.Class
|
||||
has_many :students_works, Confient.Student.Work
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(student, attrs) do
|
||||
student
|
||||
|> cast(attrs, [:lastname, :firstname, :class_id])
|
||||
|> unique_constraint(:students_lastname_firstname_class_id_index, message: "Existe déjà")
|
||||
|> assoc_constraint(:class)
|
||||
|> update_change(:lastname, fn e -> String.upcase(e) end)
|
||||
|> update_change(:firstname, fn e -> String.downcase(e) |> String.capitalize() end)
|
||||
|> validate_required([:lastname, :firstname, :class_id], message: "Le champ ne peut être vide")
|
||||
end
|
||||
end
|
||||
29
lib/confient/student/work.ex
Normal file
29
lib/confient/student/work.ex
Normal file
@@ -0,0 +1,29 @@
|
||||
defmodule Confient.Student.Work do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Confient.Repo
|
||||
|
||||
schema "students_works" do
|
||||
field :path, :string
|
||||
|
||||
belongs_to :student, Confient.School.Student
|
||||
belongs_to :assignment, Confient.Works.Assignment
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(work, attrs) do
|
||||
work
|
||||
|> cast(attrs, [:path, :student_id, :assignment_id])
|
||||
|> assoc_constraint(:student)
|
||||
|> assoc_constraint(:assignment)
|
||||
|> validate_required([:path, :student_id, :assignment_id])
|
||||
end
|
||||
|
||||
def create_work!(attrs \\ %{}) do
|
||||
%Confient.Student.Work{}
|
||||
|> changeset(attrs)
|
||||
|> Repo.insert!()
|
||||
end
|
||||
end
|
||||
163
lib/confient/works.ex
Normal file
163
lib/confient/works.ex
Normal file
@@ -0,0 +1,163 @@
|
||||
defmodule Confient.Works do
|
||||
@moduledoc """
|
||||
The Works context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Confient.Repo
|
||||
|
||||
alias Confient.Works.Assignment
|
||||
|
||||
@doc """
|
||||
Returns the list of assignments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_assignments()
|
||||
[%Assignment{}, ...]
|
||||
|
||||
"""
|
||||
def list_assignments do
|
||||
Repo.all(Assignment) |> Repo.preload(:class)
|
||||
end
|
||||
|
||||
def get_next_assignments(%Confient.School.Class{id: id}) do
|
||||
date = DateTime.now!(Application.fetch_env!(:confient, :timezone)) |> DateTime.to_date()
|
||||
|
||||
query =
|
||||
from a in Assignment,
|
||||
where: a.id == ^id,
|
||||
where: a.due >= ^date,
|
||||
select: a
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single assignment.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Assignment does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_assignment!(123)
|
||||
%Assignment{}
|
||||
|
||||
iex> get_assignment!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_assignment!(id), do: Repo.get!(Assignment, id) |> Repo.preload(:class)
|
||||
|
||||
def get_full_assignment!(id) do
|
||||
Repo.one!(
|
||||
from a in Assignment,
|
||||
where: a.id == ^id,
|
||||
preload: [{:students_works, :student}, :class],
|
||||
select: a
|
||||
)
|
||||
end
|
||||
|
||||
def get_missing_works_from_student_in_assignement(id, class_id) do
|
||||
squery =
|
||||
from a in Assignment,
|
||||
where: a.id == ^id,
|
||||
left_join: w in Confient.Student.Work,
|
||||
select: %{id: w.student_id}
|
||||
|
||||
query =
|
||||
from studs in subquery(squery),
|
||||
right_join: s in Confient.School.Student,
|
||||
on: studs.id == s.id,
|
||||
where: is_nil(studs.id) and s.class_id == ^class_id,
|
||||
select: s
|
||||
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
def get_assignment(id) do
|
||||
case Repo.get(Assignment, id) |> Repo.preload(:class) do
|
||||
nil -> {:error, :no_assignment}
|
||||
assignment -> {:ok, assignment}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a assignment.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_assignment(%{field: value})
|
||||
{:ok, %Assignment{}}
|
||||
|
||||
iex> create_assignment(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_assignment(attrs \\ %{}) do
|
||||
%Assignment{}
|
||||
|> Assignment.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a assignment.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_assignment(assignment, %{field: new_value})
|
||||
{:ok, %Assignment{}}
|
||||
|
||||
iex> update_assignment(assignment, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_assignment(%Assignment{} = assignment, attrs) do
|
||||
assignment
|
||||
|> Assignment.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a assignment.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_assignment(assignment)
|
||||
{:ok, %Assignment{}}
|
||||
|
||||
iex> delete_assignment(assignment)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_assignment(%Assignment{} = assignment) do
|
||||
Repo.delete(assignment)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking assignment changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_assignment(assignment)
|
||||
%Ecto.Changeset{data: %Assignment{}}
|
||||
|
||||
"""
|
||||
def change_assignment(%Assignment{} = assignment, attrs \\ %{}) do
|
||||
Assignment.changeset(assignment, attrs)
|
||||
end
|
||||
|
||||
def create_or_update_work(attrs = %{path: _path, student_id: sid, assignment_id: aid} \\ %{}) do
|
||||
work =
|
||||
case Repo.one(
|
||||
from w in Confient.Student.Work,
|
||||
where: w.assignment_id == ^aid and w.student_id == ^sid,
|
||||
select: w
|
||||
) do
|
||||
nil -> %Confient.Student.Work{}
|
||||
data -> data
|
||||
end
|
||||
|
||||
work |> Confient.Student.Work.changeset(attrs) |> Repo.insert_or_update()
|
||||
end
|
||||
end
|
||||
26
lib/confient/works/assignment.ex
Normal file
26
lib/confient/works/assignment.ex
Normal file
@@ -0,0 +1,26 @@
|
||||
defmodule Confient.Works.Assignment do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "assignments" do
|
||||
field :due, :date
|
||||
field :title, :string
|
||||
field :slug, :string
|
||||
|
||||
belongs_to :class, Confient.School.Class
|
||||
has_many :students_works, Confient.Student.Work
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(assignment, attrs) do
|
||||
assignment
|
||||
|> cast(attrs, [:title, :due, :slug, :class_id])
|
||||
|> unique_constraint(:assignments_title_class_id_index, message: "Existe déjà")
|
||||
|> validate_length(:slug, min: 3, max: 5, message: "Doit être compris en 3 et 5 caractères")
|
||||
|> assoc_constraint(:class)
|
||||
|> update_change(:slug, fn e -> String.downcase(e) end)
|
||||
|> validate_required([:title, :due, :slug, :class_id], message: "Le champ ne peut être vide")
|
||||
end
|
||||
end
|
||||
83
lib/confient_web.ex
Normal file
83
lib/confient_web.ex
Normal file
@@ -0,0 +1,83 @@
|
||||
defmodule ConfientWeb do
|
||||
@moduledoc """
|
||||
The entrypoint for defining your web interface, such
|
||||
as controllers, views, channels and so on.
|
||||
|
||||
This can be used in your application as:
|
||||
|
||||
use ConfientWeb, :controller
|
||||
use ConfientWeb, :view
|
||||
|
||||
The definitions below will be executed for every view,
|
||||
controller, etc, so keep them short and clean, focused
|
||||
on imports, uses and aliases.
|
||||
|
||||
Do NOT define functions inside the quoted expressions
|
||||
below. Instead, define any helper function in modules
|
||||
and import those modules here.
|
||||
"""
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller, namespace: ConfientWeb
|
||||
|
||||
import Plug.Conn
|
||||
import ConfientWeb.Gettext
|
||||
alias ConfientWeb.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
||||
def view do
|
||||
quote do
|
||||
use Phoenix.View,
|
||||
root: "lib/confient_web/templates",
|
||||
namespace: ConfientWeb
|
||||
|
||||
# Import convenience functions from controllers
|
||||
import Phoenix.Controller,
|
||||
only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
|
||||
|
||||
import Confient.Account.Auth, only: [signed_in?: 1, get_username: 1]
|
||||
|
||||
# Include shared imports and aliases for view
|
||||
unquote(view_helpers())
|
||||
end
|
||||
end
|
||||
|
||||
def router do
|
||||
quote do
|
||||
use Phoenix.Router
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
end
|
||||
end
|
||||
|
||||
def channel do
|
||||
quote do
|
||||
use Phoenix.Channel
|
||||
import ConfientWeb.Gettext
|
||||
end
|
||||
end
|
||||
|
||||
defp view_helpers do
|
||||
quote do
|
||||
# Use all HTML functionality (forms, tags, etc)
|
||||
use Phoenix.HTML
|
||||
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
|
||||
import ConfientWeb.ErrorHelpers
|
||||
import ConfientWeb.Gettext
|
||||
alias ConfientWeb.Router.Helpers, as: Routes
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used, dispatch to the appropriate controller/view/etc.
|
||||
"""
|
||||
defmacro __using__(which) when is_atom(which) do
|
||||
apply(__MODULE__, which, [])
|
||||
end
|
||||
end
|
||||
35
lib/confient_web/channels/user_socket.ex
Normal file
35
lib/confient_web/channels/user_socket.ex
Normal file
@@ -0,0 +1,35 @@
|
||||
defmodule ConfientWeb.UserSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
## Channels
|
||||
# channel "room:*", ConfientWeb.RoomChannel
|
||||
|
||||
# Socket params are passed from the client and can
|
||||
# be used to verify and authenticate a user. After
|
||||
# verification, you can put default assigns into
|
||||
# the socket that will be set for all channels, ie
|
||||
#
|
||||
# {:ok, assign(socket, :user_id, verified_user_id)}
|
||||
#
|
||||
# To deny connection, return `:error`.
|
||||
#
|
||||
# See `Phoenix.Token` documentation for examples in
|
||||
# performing token verification on connect.
|
||||
@impl true
|
||||
def connect(_params, socket, _connect_info) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
# Socket id's are topics that allow you to identify all sockets for a given user:
|
||||
#
|
||||
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
|
||||
#
|
||||
# Would allow you to broadcast a "disconnect" event and terminate
|
||||
# all active sockets and channels for a given user:
|
||||
#
|
||||
# ConfientWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
|
||||
#
|
||||
# Returning `nil` makes this socket anonymous.
|
||||
@impl true
|
||||
def id(_socket), do: nil
|
||||
end
|
||||
124
lib/confient_web/controllers/assignment_controller.ex
Normal file
124
lib/confient_web/controllers/assignment_controller.ex
Normal file
@@ -0,0 +1,124 @@
|
||||
defmodule ConfientWeb.AssignmentController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
import Phoenix.HTML
|
||||
|
||||
alias Confient.Works
|
||||
alias Confient.Works.Assignment
|
||||
|
||||
def index(conn, _params) do
|
||||
assignments = Works.list_assignments()
|
||||
render(conn, "index.html", assignments: assignments)
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset = Works.change_assignment(%Assignment{})
|
||||
classes = Confient.School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
|
||||
render(conn, "new.html", changeset: changeset, classes: classes)
|
||||
end
|
||||
|
||||
def create(conn, %{"assignment" => assignment_params}) do
|
||||
IO.inspect(assignment_params)
|
||||
|
||||
case Works.create_assignment(assignment_params) do
|
||||
{:ok, assignment} ->
|
||||
conn
|
||||
|> put_flash(:info, "Devoir créé avec succès.")
|
||||
|> redirect(to: Routes.assignment_path(conn, :show, assignment))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
classes = Confient.School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
render(conn, "new.html", changeset: changeset, classes: classes)
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
assignment = Works.get_full_assignment!(id)
|
||||
missing = Works.get_missing_works_from_student_in_assignement(id, assignment.class_id)
|
||||
render(conn, "show.html", assignment: assignment, missing: missing)
|
||||
end
|
||||
|
||||
def edit(conn, %{"id" => id}) do
|
||||
classes = Confient.School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
assignment = Works.get_assignment!(id)
|
||||
changeset = Works.change_assignment(assignment)
|
||||
render(conn, "edit.html", assignment: assignment, changeset: changeset, classes: classes)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "assignment" => assignment_params}) do
|
||||
assignment = Works.get_assignment!(id)
|
||||
|
||||
case Works.update_assignment(assignment, assignment_params) do
|
||||
{:ok, assignment} ->
|
||||
conn
|
||||
|> put_flash(:info, "Devoir mis à jour avec succès.")
|
||||
|> redirect(to: Routes.assignment_path(conn, :show, assignment))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, "edit.html", assignment: assignment, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
assignment = Works.get_assignment!(id)
|
||||
{:ok, _assignment} = Works.delete_assignment(assignment)
|
||||
|
||||
File.rm_rf(Confient.Deposit.get_upload_dir(assignment.class.name, assignment.slug))
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Devoir supprimé avec succès.")
|
||||
|> redirect(to: Routes.assignment_path(conn, :index))
|
||||
end
|
||||
|
||||
def archive(conn, %{"id" => assignment_id}) do
|
||||
case Works.get_full_assignment!(assignment_id) do
|
||||
nil ->
|
||||
conn |> put_flash(:error, "Devoir inexistant") |> redirect(to: "/")
|
||||
|
||||
assignment ->
|
||||
base = Application.get_env(:confiant, :base_upload_dir, "/tmp/confient/uploads")
|
||||
|
||||
files =
|
||||
Enum.map(assignment.students_works, fn v -> v.path end)
|
||||
|> Enum.map(&String.to_charlist/1)
|
||||
|
||||
archive_name = "#{assignment.class.name}_#{assignment.slug}.zip"
|
||||
archive_sub = "#{assignment.class.name}/#{assignment.slug}"
|
||||
archive_path = "#{base}/#{archive_sub}/#{archive_name}"
|
||||
|
||||
case :zip.create(
|
||||
String.to_charlist(archive_path),
|
||||
files,
|
||||
[{:cwd, "#{base}/#{archive_sub}"}]
|
||||
) do
|
||||
{:ok, _fname} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:info,
|
||||
raw(
|
||||
"Archive créé est disponible <a href=#{
|
||||
Path.join([
|
||||
Application.fetch_env!(:confient, :domain),
|
||||
"uploads",
|
||||
assignment.class.name,
|
||||
assignment.slug,
|
||||
archive_name
|
||||
])
|
||||
}>#{archive_name}</href>"
|
||||
)
|
||||
)
|
||||
|> redirect(to: Routes.assignment_path(conn, :show, assignment))
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error(reason)
|
||||
|
||||
conn
|
||||
|> put_flash(:error, "Error lors de la création de l'archive")
|
||||
|> redirect(to: Routes.assignment_path(conn, :show, assignment))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
65
lib/confient_web/controllers/class_controller.ex
Normal file
65
lib/confient_web/controllers/class_controller.ex
Normal file
@@ -0,0 +1,65 @@
|
||||
defmodule ConfientWeb.ClassController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
alias Confient.School
|
||||
alias Confient.School.Class
|
||||
|
||||
def index(conn, _params) do
|
||||
classes = School.list_classes()
|
||||
render(conn, "index.html", classes: classes)
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset = School.change_class(%Class{})
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
|
||||
def create(conn, %{"class" => class_params}) do
|
||||
case School.create_class(class_params) do
|
||||
{:ok, class} ->
|
||||
conn
|
||||
|> put_flash(:info, "Classe créé avec succès.")
|
||||
|> redirect(to: Routes.class_path(conn, :show, class))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
class = School.get_class!(id)
|
||||
render(conn, "show.html", class: class)
|
||||
end
|
||||
|
||||
def edit(conn, %{"id" => id}) do
|
||||
class = School.get_class!(id)
|
||||
changeset = School.change_class(class)
|
||||
render(conn, "edit.html", class: class, changeset: changeset)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "class" => class_params}) do
|
||||
class = School.get_class!(id)
|
||||
|
||||
case School.update_class(class, class_params) do
|
||||
{:ok, class} ->
|
||||
conn
|
||||
|> put_flash(:info, "Classe mise à jour avec succès.")
|
||||
|> redirect(to: Routes.class_path(conn, :show, class))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, "edit.html", class: class, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
class = School.get_class!(id)
|
||||
{:ok, _class} = School.delete_class(class)
|
||||
|
||||
dir = Application.fetch_env!(:confient, :upload_dir)
|
||||
File.rm_rf("#{dir}/#{class.name}")
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Classe supprimée.")
|
||||
|> redirect(to: Routes.class_path(conn, :index))
|
||||
end
|
||||
end
|
||||
166
lib/confient_web/controllers/deposit_controller.ex
Normal file
166
lib/confient_web/controllers/deposit_controller.ex
Normal file
@@ -0,0 +1,166 @@
|
||||
defmodule ConfientWeb.DepositController do
|
||||
use ConfientWeb, :controller
|
||||
import Phoenix.HTML
|
||||
|
||||
def index(conn, _params) do
|
||||
works = Confient.Deposit.list_works()
|
||||
render(conn, "index.html", works: works)
|
||||
end
|
||||
|
||||
def form(conn, params = %{"id" => id}) do
|
||||
case Confient.School.get_class(id) do
|
||||
{:error, :no_class} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"La classe demandée n'existe pas."
|
||||
)
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:ok, class} ->
|
||||
next_assignments = Confient.Works.get_next_assignments(class)
|
||||
|
||||
if length(next_assignments) > 0 && length(class.students) do
|
||||
changeset = Confient.Student.Work.changeset(%Confient.Student.Work{}, %{})
|
||||
|
||||
student =
|
||||
with {:ok, id} <- Map.fetch(params, "student_id"),
|
||||
{val, _} <- Integer.parse(id) do
|
||||
Enum.find(class.students, Enum.at(class.students, 0), &(&1.id == val))
|
||||
else
|
||||
:error -> Enum.at(class.students, 0)
|
||||
end
|
||||
|
||||
assignment =
|
||||
with {:ok, id} <- Map.fetch(params, "assignment_id"),
|
||||
{val, _} <- Integer.parse(id) do
|
||||
Enum.find(next_assignments, Enum.at(next_assignments, 0), &(&1.id == val))
|
||||
else
|
||||
:error -> Enum.at(next_assignments, 0)
|
||||
end
|
||||
|
||||
render(conn, "form.html",
|
||||
assignment_id: assignment.id,
|
||||
student_id: student.id,
|
||||
changeset: changeset,
|
||||
class: class,
|
||||
assignments: next_assignments
|
||||
)
|
||||
else
|
||||
conn
|
||||
|> put_flash(
|
||||
:info,
|
||||
"Aucun devoir à rendre pour la classe #{class.name}"
|
||||
)
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def deposit(conn, %{
|
||||
"id" => id,
|
||||
"work" => %{"assignment_id" => assignment_id, "student_id" => student_id, "file" => work}
|
||||
}) do
|
||||
with {:ok, class} <- Confient.School.get_class(id),
|
||||
{:ok, student} <- Confient.School.get_student(student_id),
|
||||
{:ok, assignment} <- Confient.Works.get_assignment(assignment_id),
|
||||
:ok <- Confient.Deposit.verify_date(assignment.due),
|
||||
{:ok, ext} <- Confient.Deposit.verify_file(work) do
|
||||
dir =
|
||||
Confient.Deposit.get_upload_dir(class.name, assignment.slug)
|
||||
|> Confient.Deposit.ensure_upload_dir()
|
||||
|
||||
fname = Confient.Deposit.gen_filename(class.name, assignment.slug, student, ext)
|
||||
|
||||
case File.copy(work.path, "#{dir}/#{fname}") do
|
||||
{:ok, _} ->
|
||||
Confient.Works.create_or_update_work(%{
|
||||
path: fname,
|
||||
student_id: student.id,
|
||||
assignment_id: assignment.id
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_flash(
|
||||
:info,
|
||||
raw("Fichier <strong>#{work.filename}</strong> déposé avec succès<br>
|
||||
Elève : #{student.lastname} #{student.firstname}<br>
|
||||
Devoir : #{assignment.title}<br>
|
||||
Classe : #{class.name}")
|
||||
)
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:error, _} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Erreur lors du téléversement du fichier."
|
||||
)
|
||||
|> redirect(
|
||||
to:
|
||||
Routes.deposit_path(conn, :form, id,
|
||||
student_id: student.id,
|
||||
assignment_id: assignment.id
|
||||
)
|
||||
)
|
||||
end
|
||||
else
|
||||
{:error, :date_too_late} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"La date de rendue pour le devoir sélectionné est dépassée."
|
||||
)
|
||||
|> redirect(to: Routes.deposit_path(conn, :form, id))
|
||||
|
||||
{:error, :no_class} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"La classe demandé n'existe pas."
|
||||
)
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:error, :no_student} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"L'étudiant demandé n'existe pas."
|
||||
)
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:error, :no_assignment} ->
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Le devoir demandé n'existe pas."
|
||||
)
|
||||
|> redirect(to: "/")
|
||||
|
||||
{:error, :invalid_file_type} ->
|
||||
{:ok, student} = Confient.School.get_student(student_id)
|
||||
{:ok, assignment} = Confient.Works.get_assignment(assignment_id)
|
||||
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
raw("Format de fichier invalide pour le fichier <strong>#{work.filename}</strong><br>
|
||||
Elève : #{student.lastname} #{student.firstname}<br>
|
||||
Devoir : #{assignment.title}")
|
||||
)
|
||||
|> redirect(
|
||||
to:
|
||||
Routes.deposit_path(conn, :form, id,
|
||||
student_id: student.id,
|
||||
assignment_id: assignment.id
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def deposit(conn, %{"id" => id}),
|
||||
do:
|
||||
conn
|
||||
|> put_flash(:error, "Aucun fichier spécifié")
|
||||
|> redirect(to: Routes.deposit_path(conn, :form, id))
|
||||
end
|
||||
9
lib/confient_web/controllers/error_controller.ex
Normal file
9
lib/confient_web/controllers/error_controller.ex
Normal file
@@ -0,0 +1,9 @@
|
||||
defmodule ConfientWeb.ErrorController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
def notfound(conn, _params) do
|
||||
conn
|
||||
|> put_flash(:error, "Fichier inexistant")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
10
lib/confient_web/controllers/page_controller.ex
Normal file
10
lib/confient_web/controllers/page_controller.ex
Normal file
@@ -0,0 +1,10 @@
|
||||
defmodule ConfientWeb.PageController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
alias Confient.School
|
||||
|
||||
def index(conn, _params) do
|
||||
classes = School.list_classes()
|
||||
render(conn, "index.html", classes: classes)
|
||||
end
|
||||
end
|
||||
33
lib/confient_web/controllers/session_controller.ex
Normal file
33
lib/confient_web/controllers/session_controller.ex
Normal file
@@ -0,0 +1,33 @@
|
||||
defmodule ConfientWeb.SessionController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
alias Confient.Account.Auth
|
||||
import Phoenix.HTML
|
||||
|
||||
def new(conn, _params) do
|
||||
render(conn, "new.html")
|
||||
end
|
||||
|
||||
def create(conn, %{"session" => auth}) do
|
||||
case Auth.login(auth) do
|
||||
{:ok, user} ->
|
||||
conn
|
||||
|> put_session(:current_user_id, user.id)
|
||||
|> put_flash(:info, raw("Utilisateur <strong>#{user.username}</strong> connecté."))
|
||||
|> redirect(to: "/")
|
||||
|
||||
:error ->
|
||||
conn
|
||||
|> put_flash(:error, "Il y a un problème avec la combinaison utilisateur/mot de passe")
|
||||
|> redirect(to: Routes.session_path(conn, :new))
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
conn
|
||||
|> delete_session(:current_user_id)
|
||||
|> delete_session(:current_user)
|
||||
|> put_flash(:info, "Déconnecté.")
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
end
|
||||
99
lib/confient_web/controllers/student_controller.ex
Normal file
99
lib/confient_web/controllers/student_controller.ex
Normal file
@@ -0,0 +1,99 @@
|
||||
defmodule ConfientWeb.StudentController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
alias Confient.School
|
||||
alias Confient.School.Student
|
||||
|
||||
def index(conn, _params) do
|
||||
students = School.list_students()
|
||||
render(conn, "index.html", students: students)
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset = School.change_student(%Student{})
|
||||
classes = School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
|
||||
render(conn, "new.html",
|
||||
changeset: changeset,
|
||||
classes: classes
|
||||
)
|
||||
end
|
||||
|
||||
def bulkform(conn, _params) do
|
||||
classes = School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
|
||||
render(conn, "bulkform.html",
|
||||
classes: classes,
|
||||
token: get_csrf_token()
|
||||
)
|
||||
end
|
||||
|
||||
def create(conn, %{"student" => student_params}) do
|
||||
case School.create_student(student_params) do
|
||||
{:ok, student} ->
|
||||
conn
|
||||
|> put_flash(:info, "Elève créé avec succès.")
|
||||
|> redirect(to: Routes.student_path(conn, :show, student))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
classes = Confient.School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
render(conn, "new.html", changeset: changeset, classes: classes)
|
||||
end
|
||||
end
|
||||
|
||||
def bulkcreate(conn, %{"file" => %{"list" => ""}}) do
|
||||
conn
|
||||
|> put_flash(:error, "Aucun fichier CSV.")
|
||||
|> redirect(to: Routes.student_path(conn, :bulkform))
|
||||
end
|
||||
|
||||
def bulkcreate(conn, %{"file" => %{"list" => file}, "class" => %{"class_id" => class_id}}) do
|
||||
class = School.get_class!(class_id)
|
||||
{:ok, students} = CSV.parse(file.path)
|
||||
|
||||
for {lastname, firstname} <- students do
|
||||
unless School.student_exists?(lastname, firstname, class) do
|
||||
School.create_student(class, %{"lastname" => lastname, "firstname" => firstname})
|
||||
end
|
||||
end
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Elèves créés avec succès.")
|
||||
|> redirect(to: Routes.student_path(conn, :bulkform))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
student = School.get_full_student!(id)
|
||||
render(conn, "show.html", student: student)
|
||||
end
|
||||
|
||||
def edit(conn, %{"id" => id}) do
|
||||
student = School.get_student!(id)
|
||||
changeset = School.change_student(student)
|
||||
classes = School.list_classes() |> Enum.map(&{&1.name, &1.id})
|
||||
render(conn, "edit.html", student: student, changeset: changeset, classes: classes)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "student" => student_params}) do
|
||||
student = School.get_student!(id)
|
||||
|
||||
case School.update_student(student, student_params) do
|
||||
{:ok, student} ->
|
||||
conn
|
||||
|> put_flash(:info, "Elève mis à jour avec succès.")
|
||||
|> redirect(to: Routes.student_path(conn, :show, student))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, "edit.html", student: student, changeset: changeset)
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
student = School.get_student!(id)
|
||||
{:ok, _student} = School.delete_student(student)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Elève supprimé.")
|
||||
|> redirect(to: Routes.student_path(conn, :index))
|
||||
end
|
||||
end
|
||||
6
lib/confient_web/controllers/user_controller.ex
Normal file
6
lib/confient_web/controllers/user_controller.ex
Normal file
@@ -0,0 +1,6 @@
|
||||
defmodule ConfientWeb.UserController do
|
||||
use ConfientWeb, :controller
|
||||
|
||||
# alias Confient.Account
|
||||
# alias Confient.Account.User
|
||||
end
|
||||
54
lib/confient_web/endpoint.ex
Normal file
54
lib/confient_web/endpoint.ex
Normal file
@@ -0,0 +1,54 @@
|
||||
defmodule ConfientWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :confient
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# this means its contents can be read but not tampered with.
|
||||
# Set :encryption_salt if you would also like to encrypt it.
|
||||
@session_options [
|
||||
store: :cookie,
|
||||
key: "_confient_key",
|
||||
signing_salt: "kUFTx1vs"
|
||||
]
|
||||
|
||||
socket "/socket", ConfientWeb.UserSocket,
|
||||
websocket: true,
|
||||
longpoll: false
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
# You should set gzip to true if you are running phx.digest
|
||||
# when deploying your static files in production.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :confient,
|
||||
gzip: false,
|
||||
only: ~w(css fonts images js favicon.ico robots.txt)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
if code_reloading? do
|
||||
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
|
||||
plug Phoenix.LiveReloader
|
||||
plug Phoenix.CodeReloader
|
||||
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :confient
|
||||
end
|
||||
|
||||
plug Phoenix.LiveDashboard.RequestLogger,
|
||||
param_key: "request_logger",
|
||||
cookie_key: "request_logger"
|
||||
|
||||
plug Plug.RequestId
|
||||
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
|
||||
|
||||
plug Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
|
||||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
plug Plug.Session, @session_options
|
||||
plug ConfientWeb.Router
|
||||
end
|
||||
24
lib/confient_web/gettext.ex
Normal file
24
lib/confient_web/gettext.ex
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule ConfientWeb.Gettext do
|
||||
@moduledoc """
|
||||
A module providing Internationalization with a gettext-based API.
|
||||
|
||||
By using [Gettext](https://hexdocs.pm/gettext),
|
||||
your module gains a set of macros for translations, for example:
|
||||
|
||||
import ConfientWeb.Gettext
|
||||
|
||||
# Simple translation
|
||||
gettext("Here is the string to translate")
|
||||
|
||||
# Plural translation
|
||||
ngettext("Here is the string to translate",
|
||||
"Here are the strings to translate",
|
||||
3)
|
||||
|
||||
# Domain-based translation
|
||||
dgettext("errors", "Here is the error message to translate")
|
||||
|
||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||
"""
|
||||
use Gettext, otp_app: :confient
|
||||
end
|
||||
40
lib/confient_web/plugs/auth.ex
Normal file
40
lib/confient_web/plugs/auth.ex
Normal file
@@ -0,0 +1,40 @@
|
||||
defmodule ConfientWeb.Plugs.Auth do
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
||||
alias Confient.Account
|
||||
|
||||
def init(args), do: args
|
||||
|
||||
def call(conn, _args) do
|
||||
if uid = Plug.Conn.get_session(conn, :current_user_id) do
|
||||
user = Account.get_user!(uid)
|
||||
|
||||
conn
|
||||
|> assign(:current_user, user)
|
||||
else
|
||||
conn
|
||||
|> redirect(to: "/login")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule ConfientWeb.Plugs.InjectUser do
|
||||
import Plug.Conn
|
||||
|
||||
alias Confient.Account
|
||||
|
||||
def init(args), do: args
|
||||
|
||||
def call(conn, _args) do
|
||||
if uid = Plug.Conn.get_session(conn, :current_user_id) do
|
||||
user = Account.get_user!(uid)
|
||||
|
||||
conn
|
||||
|> assign(:current_user, user)
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
end
|
||||
82
lib/confient_web/router.ex
Normal file
82
lib/confient_web/router.ex
Normal file
@@ -0,0 +1,82 @@
|
||||
defmodule ConfientWeb.Router do
|
||||
use ConfientWeb, :router
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug :fetch_flash
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
end
|
||||
|
||||
pipeline :uploads do
|
||||
plug :fetch_session
|
||||
plug :fetch_flash
|
||||
|
||||
plug Plug.Static,
|
||||
at: "/uploads/",
|
||||
from: Application.get_env(:confiant, :base_upload_dir, "/tmp/confient/uploads")
|
||||
end
|
||||
|
||||
scope "/", ConfientWeb do
|
||||
scope "/uploads" do
|
||||
pipe_through [:uploads, ConfientWeb.Plugs.Auth]
|
||||
get "/*path", ErrorController, :notfound
|
||||
end
|
||||
|
||||
pipe_through :browser
|
||||
|
||||
scope "/" do
|
||||
pipe_through ConfientWeb.Plugs.Auth
|
||||
|
||||
resources "/classes", ClassController
|
||||
|
||||
resources "/students", StudentController
|
||||
get "/students-bulk-create", StudentController, :bulkform
|
||||
post "/students-bulk-create", StudentController, :bulkcreate
|
||||
|
||||
resources "/assignments", AssignmentController
|
||||
get "/assignements/:id/archive", AssignmentController, :archive
|
||||
|
||||
get "/deposits/", DepositController, :index
|
||||
|
||||
delete "/logout", SessionController, :delete
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through ConfientWeb.Plugs.InjectUser
|
||||
get "/", PageController, :index
|
||||
end
|
||||
|
||||
get "/login", SessionController, :new
|
||||
post "/login", SessionController, :create
|
||||
|
||||
get "/deposit/:id", DepositController, :form
|
||||
post "/deposit/:id", DepositController, :deposit
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
# scope "/api", ConfientWeb do
|
||||
# pipe_through :api
|
||||
# end
|
||||
|
||||
# Enables LiveDashboard only for development
|
||||
#
|
||||
# If you want to use the LiveDashboard in production, you should put
|
||||
# it behind authentication and allow only admins to access it.
|
||||
# If your application does not have an admins-only section yet,
|
||||
# you can use Plug.BasicAuth to set up some basic authentication
|
||||
# as long as you are also using SSL (which you should anyway).
|
||||
if Mix.env() in [:dev, :test] do
|
||||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
scope "/" do
|
||||
pipe_through :browser
|
||||
live_dashboard "/dashboard", metrics: ConfientWeb.Telemetry
|
||||
end
|
||||
end
|
||||
end
|
||||
55
lib/confient_web/telemetry.ex
Normal file
55
lib/confient_web/telemetry.ex
Normal file
@@ -0,0 +1,55 @@
|
||||
defmodule ConfientWeb.Telemetry do
|
||||
use Supervisor
|
||||
import Telemetry.Metrics
|
||||
|
||||
def start_link(arg) do
|
||||
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_arg) do
|
||||
children = [
|
||||
# Telemetry poller will execute the given period measurements
|
||||
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
|
||||
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
|
||||
# Add reporters as children of your supervision tree.
|
||||
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
def metrics do
|
||||
[
|
||||
# Phoenix Metrics
|
||||
summary("phoenix.endpoint.stop.duration",
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
summary("phoenix.router_dispatch.stop.duration",
|
||||
tags: [:route],
|
||||
unit: {:native, :millisecond}
|
||||
),
|
||||
|
||||
# Database Metrics
|
||||
summary("confient.repo.query.total_time", unit: {:native, :millisecond}),
|
||||
summary("confient.repo.query.decode_time", unit: {:native, :millisecond}),
|
||||
summary("confient.repo.query.query_time", unit: {:native, :millisecond}),
|
||||
summary("confient.repo.query.queue_time", unit: {:native, :millisecond}),
|
||||
summary("confient.repo.query.idle_time", unit: {:native, :millisecond}),
|
||||
|
||||
# VM Metrics
|
||||
summary("vm.memory.total", unit: {:byte, :kilobyte}),
|
||||
summary("vm.total_run_queue_lengths.total"),
|
||||
summary("vm.total_run_queue_lengths.cpu"),
|
||||
summary("vm.total_run_queue_lengths.io")
|
||||
]
|
||||
end
|
||||
|
||||
defp periodic_measurements do
|
||||
[
|
||||
# A module, function and arguments to be invoked periodically.
|
||||
# This function must call :telemetry.execute/3 and a metric must be added above.
|
||||
# {ConfientWeb, :count_users, []}
|
||||
]
|
||||
end
|
||||
end
|
||||
7
lib/confient_web/templates/assignment/edit.html.eex
Normal file
7
lib/confient_web/templates/assignment/edit.html.eex
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Editer un devoir</h1>
|
||||
|
||||
<span><%= link "Retour", to: Routes.assignment_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.assignment_path(@conn, :update, @assignment)) %>
|
||||
26
lib/confient_web/templates/assignment/form.html.eex
Normal file
26
lib/confient_web/templates/assignment/form.html.eex
Normal file
@@ -0,0 +1,26 @@
|
||||
<%= form_for @changeset, @action, fn f -> %>
|
||||
<%= if @changeset.action do %>
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, quelque chose c'est mal passé, merci de vérifier le formulaire</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= label f, "Classe" %>
|
||||
<%= select f, :class_id, @classes %>
|
||||
|
||||
<%= label f, "Titre" %>
|
||||
<%= text_input f, :title %>
|
||||
<%= error_tag f, :title %>
|
||||
|
||||
<%= label f, "Dénomination" %>
|
||||
<%= text_input f, :slug %>
|
||||
<%= error_tag f, :slug %>
|
||||
|
||||
<%= label f, "Echéance" %>
|
||||
<%= date_select f, :due %>
|
||||
<%= error_tag f, :due %>
|
||||
|
||||
<div>
|
||||
<%= submit "Sauvegarder" %>
|
||||
</div>
|
||||
<% end %>
|
||||
38
lib/confient_web/templates/assignment/index.html.eex
Normal file
38
lib/confient_web/templates/assignment/index.html.eex
Normal file
@@ -0,0 +1,38 @@
|
||||
<h1>Liste des devoirs</h1>
|
||||
|
||||
<span><%= link "Ajouter un devoir", to: Routes.assignment_path(@conn, :new) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titre</th>
|
||||
<th>Dénominateur</th>
|
||||
<th>Echéance</th>
|
||||
<th>Classe</th>
|
||||
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for assignment <- @assignments do %>
|
||||
<tr>
|
||||
<td><%= assignment.title %></td>
|
||||
<td><%= assignment.slug %></td>
|
||||
<td><%= assignment.due %></td>
|
||||
<%= if assignment.class do %>
|
||||
<td><%= link assignment.class.name, to: Routes.class_path(@conn, :show, assignment.class.id) %></td>
|
||||
<% else %>
|
||||
<td></td>
|
||||
<% end %>
|
||||
|
||||
<td>
|
||||
<span><%= link "Voir", to: Routes.assignment_path(@conn, :show, assignment) %></span>
|
||||
<span><%= link "Editer", to: Routes.assignment_path(@conn, :edit, assignment) %></span>
|
||||
<span><%= link "Supprimer", to: Routes.assignment_path(@conn, :delete, assignment), method: :delete, data: [confirm: "Vous êtes sur ?"] %></span>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
7
lib/confient_web/templates/assignment/new.html.eex
Normal file
7
lib/confient_web/templates/assignment/new.html.eex
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Nouveau devoir</h1>
|
||||
|
||||
<span><%= link "Retour", to: Routes.assignment_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.assignment_path(@conn, :create)) %>
|
||||
67
lib/confient_web/templates/assignment/show.html.eex
Normal file
67
lib/confient_web/templates/assignment/show.html.eex
Normal file
@@ -0,0 +1,67 @@
|
||||
<h1>Devoir : <%= @assignment.title %> | Classe : <%= @assignment.class.name %></h1>
|
||||
|
||||
<span><%= link "Editer", to: Routes.assignment_path(@conn, :edit, @assignment) %></span>
|
||||
<span><%= link "Retour", to: Routes.assignment_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Informations générales</h2>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<strong>Classe:</strong>
|
||||
<%= @assignment.class.name %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Echéance:</strong>
|
||||
<%= @assignment.due %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Dénominateur:</strong>
|
||||
<%= @assignment.slug %>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Liste des rendus pour ce devoir</h2>
|
||||
|
||||
<%= if length(@assignment.students_works) > 0 do %>
|
||||
<%= link "Générer une archive de tous les rendus", to: Routes.assignment_path(@conn, :archive, @assignment.id), target: "_blank" %>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Elève</th>
|
||||
<th>Date</th>
|
||||
<th>Action</th>
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for work <- @assignment.students_works do %>
|
||||
<tr>
|
||||
<td><%= link "#{work.student.lastname} #{work.student.firstname}", to: Routes.student_path(@conn, :show, work.student.id) %></td>
|
||||
<td><%= work.inserted_at %></td>
|
||||
<td><%= link "Télécharger", to: Path.join([Application.fetch_env!(:confient, :domain), "uploads", @assignment.class.name, @assignment.slug, work.path]), target: "_blank" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>Aucun rendu</p>
|
||||
<% end %>
|
||||
|
||||
<%= if length(@assignment.students_works) > 0 do %>
|
||||
|
||||
<h2>Liste des élèves n'ayant pas rendu ce devoir</h2>
|
||||
|
||||
<ul>
|
||||
<%= for stud <- @missing do %>
|
||||
<li><%= link "#{stud.lastname} #{stud.firstname}", to: Routes.student_path(@conn, :show, stud.id) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
7
lib/confient_web/templates/class/edit.html.eex
Normal file
7
lib/confient_web/templates/class/edit.html.eex
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Editer une classe</h1>
|
||||
|
||||
<span><%= link "Retour", to: Routes.class_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.class_path(@conn, :update, @class)) %>
|
||||
15
lib/confient_web/templates/class/form.html.eex
Normal file
15
lib/confient_web/templates/class/form.html.eex
Normal file
@@ -0,0 +1,15 @@
|
||||
<%= form_for @changeset, @action, [multipart: true], fn f -> %>
|
||||
<%= if @changeset.action do %>
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, quelque chose c'est mal passé, merci de vérifier le formulaire</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= label f, "Dénomination" %>
|
||||
<%= text_input f, :name %>
|
||||
<%= error_tag f, :name %>
|
||||
|
||||
<div>
|
||||
<%= submit "Sauvegarder" %>
|
||||
</div>
|
||||
<% end %>
|
||||
29
lib/confient_web/templates/class/index.html.eex
Normal file
29
lib/confient_web/templates/class/index.html.eex
Normal file
@@ -0,0 +1,29 @@
|
||||
<h1>Liste des classes</h1>
|
||||
|
||||
<span><%= link "Ajouter une classe", to: Routes.class_path(@conn, :new) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Nombre d'élèves</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for class <- @classes do %>
|
||||
<tr>
|
||||
<td><%= class.name %></td>
|
||||
<td><%= length(class.students) %></td>
|
||||
|
||||
<td>
|
||||
<span><%= link "Voir", to: Routes.class_path(@conn, :show, class) %></span>
|
||||
<span><%= link "Editer", to: Routes.class_path(@conn, :edit, class) %></span>
|
||||
<span><%= link "Supprimer", to: Routes.class_path(@conn, :delete, class), method: :delete, data: [confirm: "Vous êtes sur ?"] %></span>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
7
lib/confient_web/templates/class/new.html.eex
Normal file
7
lib/confient_web/templates/class/new.html.eex
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Nouvelle classe</h1>
|
||||
|
||||
<span><%= link "Retour", to: Routes.class_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.class_path(@conn, :create)) %>
|
||||
14
lib/confient_web/templates/class/show.html.eex
Normal file
14
lib/confient_web/templates/class/show.html.eex
Normal file
@@ -0,0 +1,14 @@
|
||||
<h1><%= @class.name %></h1>
|
||||
|
||||
<span><%= link "Editer", to: Routes.class_path(@conn, :edit, @class) %></span>
|
||||
<span><%= link "Retour", to: Routes.class_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Liste des élèves</h2>
|
||||
|
||||
<ul>
|
||||
<%= for student <- @class.students do %>
|
||||
<li><%= link "#{student.lastname} - #{student.firstname}", to: Routes.student_path(@conn, :show, student) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
23
lib/confient_web/templates/deposit/form.html.eex
Normal file
23
lib/confient_web/templates/deposit/form.html.eex
Normal file
@@ -0,0 +1,23 @@
|
||||
<h1>Rendu des devoirs - <%= @class.name %></h1>
|
||||
|
||||
<%= if @changeset.action do %>
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, quelque chose c'est mal passé, merci de vérifier le formulaire</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_for @changeset, Routes.deposit_path(@conn, :deposit, @class.id), [multipart: true], fn f -> %>
|
||||
<%= label f, "Elève" %>
|
||||
<%= select f, :student_id, Enum.map(@class.students, &{"#{&1.lastname} #{&1.firstname}", &1.id}), selected: @student_id %>
|
||||
|
||||
<%= label f, "Devoir" %>
|
||||
<%= select f, :assignment_id, Enum.map(@assignments, &{"#{&1.title} - #{&1.due}", &1.id}), selected: @assignment_id %>
|
||||
|
||||
<%= label f, "Fichier (formats acceptés : pdf, rtf, odt, doc, docx)" %>
|
||||
<%= file_input f, :file %>
|
||||
|
||||
<div>
|
||||
<%= submit "Envoyer" %>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
24
lib/confient_web/templates/deposit/index.html.eex
Normal file
24
lib/confient_web/templates/deposit/index.html.eex
Normal file
@@ -0,0 +1,24 @@
|
||||
<h1>Liste des rendus</h1>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Elève</th>
|
||||
<th>Classe</th>
|
||||
<th>Devoir</th>
|
||||
<th>Validé</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for work <- @works do %>
|
||||
<tr>
|
||||
<td><%= link "#{work.student.lastname} #{work.student.firstname}", to: Routes.student_path(@conn, :show, work.student.id) %></td>
|
||||
<td><%= link work.student.class.name, to: Routes.class_path(@conn, :show, work.student.class.id) %></td>
|
||||
<td><%= link "#{work.assignment.title} | #{work.assignment.slug}", to: Routes.assignment_path(@conn, :show, work.assignment.id) %></td>
|
||||
<td><%= work.inserted_at %></td>
|
||||
<td><%= link "Télécharger", to: Path.join([Application.fetch_env!(:confient, :domain), "uploads", work.student.class.name, work.assignment.slug, work.path]), target: "_blank" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
46
lib/confient_web/templates/layout/app.html.eex
Normal file
46
lib/confient_web/templates/layout/app.html.eex
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Confi-ENT | Rendus de devois</title>
|
||||
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
||||
<script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<section class="container">
|
||||
<%= if signed_in?(@conn) do %>
|
||||
<%= link "#{String.upcase(get_username(@conn))} (Deconnexion)", to: Routes.session_path(@conn, :delete), method: :delete %>
|
||||
<% else %>
|
||||
<%= link "Accès Enseignant", to: Routes.session_path(@conn, :new) %>
|
||||
<% end %>
|
||||
<a href="/">
|
||||
<img src="/images/logo.png" alt="">
|
||||
</a>
|
||||
</section>
|
||||
<%= if signed_in?(@conn) do %>
|
||||
<div class="container">
|
||||
<div class="navbar">
|
||||
<ul>
|
||||
<li><%= link "Classes", to: Routes.class_path(@conn, :index) %></li>
|
||||
<li><%= link "Elèves", to: Routes.student_path(@conn, :index) %></li>
|
||||
<li><%= link "Devoirs", to: Routes.assignment_path(@conn, :index) %></li>
|
||||
<li><%= link "Rendus", to: Routes.deposit_path(@conn, :index) %></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</header>
|
||||
<main role="main" class="container main">
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a href="https://www.phoenixframework.org/">🔥🐦</a><a href="https://git.athelas-conseils.fr/papey/confient">👨💻</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
15
lib/confient_web/templates/page/index.html.eex
Normal file
15
lib/confient_web/templates/page/index.html.eex
Normal file
@@ -0,0 +1,15 @@
|
||||
<section class="hero">
|
||||
<h1>Rendre un devoir</h1>
|
||||
<hr>
|
||||
<%= if length(@classes) > 0 do %>
|
||||
<p>Sélectionner une classe</p>
|
||||
<ul>
|
||||
<%= for class <- @classes do %>
|
||||
<li style="font-size: 2rem;"><%= link class.name, to: Routes.deposit_path(@conn, :form, class.id) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<p>Il n'y a actuellement aucune classe dans la base de données</p>
|
||||
<% end %>
|
||||
<hr>
|
||||
</section>
|
||||
9
lib/confient_web/templates/session/new.html.eex
Normal file
9
lib/confient_web/templates/session/new.html.eex
Normal file
@@ -0,0 +1,9 @@
|
||||
<h1>Connexion à l'espace enseignant</h1>
|
||||
|
||||
<%= form_for @conn, Routes.session_path(@conn, :new), [as: :session], fn f -> %>
|
||||
<%= label f, "Identifiant" %>
|
||||
<%= text_input f, :username, placeholder: "Utilisateur" %>
|
||||
<%= label f, "Mot de passe" %>
|
||||
<%= password_input f, :password %>
|
||||
<%= submit "Se connecter" %>
|
||||
<% end %>
|
||||
17
lib/confient_web/templates/student/bulkform.html.eex
Normal file
17
lib/confient_web/templates/student/bulkform.html.eex
Normal file
@@ -0,0 +1,17 @@
|
||||
<h1>Ajouter un lot d'élèves</h1>
|
||||
|
||||
<form action="<%= Routes.student_path(@conn, :bulkcreate) %>" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" value="<%= @token %>" name="_csrf_token"/>
|
||||
|
||||
<%= label :class, "Classe" %>
|
||||
<%= select :class, :class_id, @classes %>
|
||||
|
||||
<%= label :class, "Fichier" %>
|
||||
<%= file_input :file, :list %>
|
||||
|
||||
<div>
|
||||
<%= submit "Ajouter" %>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<span><%= link "Retour", to: Routes.student_path(@conn, :index) %></span>
|
||||
7
lib/confient_web/templates/student/edit.html.eex
Normal file
7
lib/confient_web/templates/student/edit.html.eex
Normal file
@@ -0,0 +1,7 @@
|
||||
<h1>Editer un élève</h1>
|
||||
|
||||
<span><%= link "Retour", to: Routes.student_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.student_path(@conn, :update, @student)) %>
|
||||
22
lib/confient_web/templates/student/form.html.eex
Normal file
22
lib/confient_web/templates/student/form.html.eex
Normal file
@@ -0,0 +1,22 @@
|
||||
<%= form_for @changeset, @action, fn f -> %>
|
||||
<%= if @changeset.action do %>
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, quelque chose c'est mal passé, merci de vérifier le formulaire</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= label f, "Nom" %>
|
||||
<%= text_input f, :lastname %>
|
||||
<%= error_tag f, :lastname %>
|
||||
|
||||
<%= label f, "Prénom" %>
|
||||
<%= text_input f, :firstname %>
|
||||
<%= error_tag f, :firstname %>
|
||||
|
||||
<%= label f, "Classe" %>
|
||||
<%= select f, :class_id, @classes %>
|
||||
|
||||
<div>
|
||||
<%= submit "Sauvegarder" %>
|
||||
</div>
|
||||
<% end %>
|
||||
32
lib/confient_web/templates/student/index.html.eex
Normal file
32
lib/confient_web/templates/student/index.html.eex
Normal file
@@ -0,0 +1,32 @@
|
||||
<h1>Liste des élèves</h1>
|
||||
|
||||
<span><%= link "Ajouter un élève", to: Routes.student_path(@conn, :new) %></span> |
|
||||
<span><%= link "Ajouter un lot d'élèves", to: Routes.student_path(@conn, :bulkform) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Prénom</th>
|
||||
<th>Nom</th>
|
||||
<th>Classe</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for student <- @students do %>
|
||||
<tr>
|
||||
<td><%= student.lastname %></td>
|
||||
<td><%= student.firstname %></td>
|
||||
<td><%= link student.class.name, to: Routes.class_path(@conn, :show, student.class.id) %></td>
|
||||
|
||||
<td>
|
||||
<span><%= link "Voir", to: Routes.student_path(@conn, :show, student) %></span>
|
||||
<span><%= link "Editer", to: Routes.student_path(@conn, :edit, student) %></span>
|
||||
<span><%= link "Supprimer", to: Routes.student_path(@conn, :delete, student), method: :delete, data: [confirm: "Vous êtes sur ?"] %></span>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
5
lib/confient_web/templates/student/new.html.eex
Normal file
5
lib/confient_web/templates/student/new.html.eex
Normal file
@@ -0,0 +1,5 @@
|
||||
<h1>Ajouter un nouvel élève</h1>
|
||||
|
||||
<%= render "form.html", Map.put(assigns, :action, Routes.student_path(@conn, :create)) %>
|
||||
|
||||
<span><%= link "Retour", to: Routes.student_path(@conn, :index) %></span>
|
||||
52
lib/confient_web/templates/student/show.html.eex
Normal file
52
lib/confient_web/templates/student/show.html.eex
Normal file
@@ -0,0 +1,52 @@
|
||||
<h1><%= @student.lastname %> - <%= @student.firstname %></h1>
|
||||
|
||||
<span><%= link "Editer", to: Routes.student_path(@conn, :edit, @student) %></span>
|
||||
<span><%= link "Retour", to: Routes.student_path(@conn, :index) %></span>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Informations générales</h2>
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<strong>Nom:</strong>
|
||||
<%= @student.lastname %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Prénom:</strong>
|
||||
<%= @student.firstname %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>Classe:</strong>
|
||||
<%= @student.class.name %>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<h2>Rendus</h2>
|
||||
|
||||
<%= if length(@student.students_works) > 0 do %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titre</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for work <- @student.students_works do %>
|
||||
<tr>
|
||||
<td><%= work.assignment.title %></td>
|
||||
<td><%= work.inserted_at %></td>
|
||||
<td><%= link "Télécharger", to: Path.join([ConfientWeb.Endpoint.url(), "uploads", @student.class.name, work.assignment.slug, work.path]), target: "_blank" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>Aucun devoir rendu</p>
|
||||
<% end %>
|
||||
3
lib/confient_web/views/assignment_view.ex
Normal file
3
lib/confient_web/views/assignment_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.AssignmentView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
3
lib/confient_web/views/class_view.ex
Normal file
3
lib/confient_web/views/class_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.ClassView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
3
lib/confient_web/views/deposit_view.ex
Normal file
3
lib/confient_web/views/deposit_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.DepositView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
47
lib/confient_web/views/error_helpers.ex
Normal file
47
lib/confient_web/views/error_helpers.ex
Normal file
@@ -0,0 +1,47 @@
|
||||
defmodule ConfientWeb.ErrorHelpers do
|
||||
@moduledoc """
|
||||
Conveniences for translating and building error messages.
|
||||
"""
|
||||
|
||||
use Phoenix.HTML
|
||||
|
||||
@doc """
|
||||
Generates tag for inlined form input errors.
|
||||
"""
|
||||
def error_tag(form, field) do
|
||||
Enum.map(Keyword.get_values(form.errors, field), fn error ->
|
||||
content_tag(:span, translate_error(error),
|
||||
class: "invalid-feedback",
|
||||
phx_feedback_for: input_id(form, field)
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Translates an error message using gettext.
|
||||
"""
|
||||
def translate_error({msg, opts}) do
|
||||
# When using gettext, we typically pass the strings we want
|
||||
# to translate as a static argument:
|
||||
#
|
||||
# # Translate "is invalid" in the "errors" domain
|
||||
# dgettext("errors", "is invalid")
|
||||
#
|
||||
# # Translate the number of files with plural rules
|
||||
# dngettext("errors", "1 file", "%{count} files", count)
|
||||
#
|
||||
# Because the error messages we show in our forms and APIs
|
||||
# are defined inside Ecto, we need to translate them dynamically.
|
||||
# This requires us to call the Gettext module passing our gettext
|
||||
# backend as first argument.
|
||||
#
|
||||
# Note we use the "errors" domain, which means translations
|
||||
# should be written to the errors.po file. The :count option is
|
||||
# set by Ecto and indicates we should also apply plural rules.
|
||||
if count = opts[:count] do
|
||||
Gettext.dngettext(ConfientWeb.Gettext, "errors", msg, msg, count, opts)
|
||||
else
|
||||
Gettext.dgettext(ConfientWeb.Gettext, "errors", msg, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
16
lib/confient_web/views/error_view.ex
Normal file
16
lib/confient_web/views/error_view.ex
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule ConfientWeb.ErrorView do
|
||||
use ConfientWeb, :view
|
||||
|
||||
# If you want to customize a particular status code
|
||||
# for a certain format, you may uncomment below.
|
||||
# def render("500.html", _assigns) do
|
||||
# "Internal Server Error"
|
||||
# end
|
||||
|
||||
# By default, Phoenix returns the status message from
|
||||
# the template name. For example, "404.html" becomes
|
||||
# "Not Found".
|
||||
def template_not_found(template, _assigns) do
|
||||
Phoenix.Controller.status_message_from_template(template)
|
||||
end
|
||||
end
|
||||
3
lib/confient_web/views/layout_view.ex
Normal file
3
lib/confient_web/views/layout_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.LayoutView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
3
lib/confient_web/views/page_view.ex
Normal file
3
lib/confient_web/views/page_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.PageView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
3
lib/confient_web/views/session_view.ex
Normal file
3
lib/confient_web/views/session_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.SessionView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
3
lib/confient_web/views/student_view.ex
Normal file
3
lib/confient_web/views/student_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.StudentView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
3
lib/confient_web/views/user_view.ex
Normal file
3
lib/confient_web/views/user_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule ConfientWeb.UserView do
|
||||
use ConfientWeb, :view
|
||||
end
|
||||
58
lib/deposit.ex
Normal file
58
lib/deposit.ex
Normal file
@@ -0,0 +1,58 @@
|
||||
defmodule Confient.Deposit do
|
||||
alias Confient.Student.Work
|
||||
alias Confient.Repo
|
||||
import Ecto.Query
|
||||
|
||||
def list_works(), do: Repo.all(from w in Work, preload: [{:student, :class}, :assignment])
|
||||
|
||||
def verify_date(due) do
|
||||
date = DateTime.now!(Application.fetch_env!(:confient, :timezone)) |> DateTime.to_date()
|
||||
if Timex.before?(date, due), do: :ok, else: {:error, :date_too_late}
|
||||
end
|
||||
|
||||
def verify_file(file) do
|
||||
supported = [
|
||||
"application/pdf",
|
||||
"application/rtf",
|
||||
"application/vnd.oasis.opendocument.text",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/msword"
|
||||
]
|
||||
|
||||
if Enum.member?(supported, file.content_type) do
|
||||
{:ok, Path.extname(file.filename)}
|
||||
else
|
||||
{:error, :invalid_file_type}
|
||||
end
|
||||
end
|
||||
|
||||
def get_upload_dir(class_name, assignment_slug) do
|
||||
dir = Application.fetch_env!(:confient, :upload_dir)
|
||||
"#{dir}/#{class_name}/#{assignment_slug}"
|
||||
end
|
||||
|
||||
def ensure_upload_dir(dir) do
|
||||
File.mkdir_p!(dir)
|
||||
dir
|
||||
end
|
||||
|
||||
def gen_filename(
|
||||
class_name,
|
||||
assignment_slug,
|
||||
%Confient.School.Student{
|
||||
firstname: fname,
|
||||
lastname: lname
|
||||
},
|
||||
ext
|
||||
) do
|
||||
Enum.join(
|
||||
[
|
||||
class_name,
|
||||
assignment_slug,
|
||||
"#{String.downcase(String.at(fname, 0) <> String.replace(lname, " ", "-"))}",
|
||||
String.replace(DateTime.to_iso8601(DateTime.now!("Europe/Paris")), ":", "-")
|
||||
],
|
||||
"_"
|
||||
) <> ext
|
||||
end
|
||||
end
|
||||
20
lib/helpers/csv.ex
Normal file
20
lib/helpers/csv.ex
Normal file
@@ -0,0 +1,20 @@
|
||||
defmodule CSV do
|
||||
@moduledoc """
|
||||
Helper to interact with CSV
|
||||
"""
|
||||
|
||||
def parse(path) do
|
||||
content = File.read!(path)
|
||||
[_ | parts] = String.replace(content, "\r", "") |> String.split("\n") |> Enum.filter(fn v -> v != "" end)
|
||||
names = Enum.map(parts, fn v ->
|
||||
[name | _ ] = String.split(v, ";")
|
||||
String.trim(name, "\"")
|
||||
end)
|
||||
students = Enum.map(names, fn v ->
|
||||
IO.inspect(v)
|
||||
[_, lastname, firstname] = Regex.run(~r/(^[A-Z -]{2,}) (.+)/, v)
|
||||
{lastname, firstname}
|
||||
end)
|
||||
{:ok, students}
|
||||
end
|
||||
end
|
||||
69
mix.exs
Normal file
69
mix.exs
Normal file
@@ -0,0 +1,69 @@
|
||||
defmodule Confient.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :confient,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.7",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
# Configuration for the OTP application.
|
||||
#
|
||||
# Type `mix help compile.app` for more information.
|
||||
def application do
|
||||
[
|
||||
mod: {Confient.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools]
|
||||
]
|
||||
end
|
||||
|
||||
# Specifies which paths to compile per environment.
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
# Specifies your project dependencies.
|
||||
#
|
||||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.5.7"},
|
||||
{:phoenix_ecto, "~> 4.1"},
|
||||
{:ecto_sql, "~> 3.4"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_dashboard, "~> 0.4"},
|
||||
{:telemetry_metrics, "~> 0.4"},
|
||||
{:telemetry_poller, "~> 0.4"},
|
||||
{:gettext, "~> 0.11"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:comeonin, "~> 4.0"},
|
||||
{:bcrypt_elixir, "~> 1.0"},
|
||||
{:timex, "~> 3.0"},
|
||||
{:tzdata, "~> 1.0.5"}
|
||||
]
|
||||
end
|
||||
|
||||
# Aliases are shortcuts or tasks specific to the current project.
|
||||
# For example, to install project dependencies and perform other setup tasks, run:
|
||||
#
|
||||
# $ mix setup
|
||||
#
|
||||
# See the documentation for `Mix` for more info on aliases.
|
||||
defp aliases do
|
||||
[
|
||||
setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"],
|
||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
|
||||
]
|
||||
end
|
||||
end
|
||||
43
mix.lock
Normal file
43
mix.lock
Normal file
@@ -0,0 +1,43 @@
|
||||
%{
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "10f658be786bd2daaadcd45cc5b598da01d5bbc313da4d0e3efb2d6a511d896d"},
|
||||
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm", "d8700a0ca4dbb616c22c9b3f6dd539d88deaafec3efe66869d6370c9a559b3e9"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
|
||||
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
|
||||
"db_connection": {:hex, :db_connection, "2.3.0", "d56ef906956a37959bcb385704fc04035f4f43c0f560dd23e00740daf8028c49", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "dcc082b8f723de9a630451b49fdbd7a59b065c4b38176fb147aaf773574d4520"},
|
||||
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
|
||||
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
||||
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
|
||||
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"phoenix": {:hex, :phoenix, "1.5.7", "2923bb3af924f184459fe4fa4b100bd25fa6468e69b2803dfae82698269aa5e0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "774cd64417c5a3788414fdbb2be2eb9bcd0c048d9e6ad11a0c1fd67b7c0d0978"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.0", "f35f61c3f959c9a01b36defaa1f0624edd55b87e236b606664a556d6f72fd2e7", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "02c1007ae393f2b76ec61c1a869b1e617179877984678babde131d716f95b582"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.15.0", "1bb9b0c230a047cddc32b1708ec4cad4f91aaab0b4688c09702fcf78d7e2df94", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d42ad9b5fe9e874df3a12ea0a806e3f650db99774d597285c07df43f33cab486"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
||||
"plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
|
||||
"postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"},
|
||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.0", "da9d49ee7e6bb1c259d36ce6539cd45ae14d81247a2b0c90edf55e2b50507f7b", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5cfe67ad464b243835512aa44321cee91faed6ea868d7fb761d7016e02915c3d"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
|
||||
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
|
||||
}
|
||||
97
priv/gettext/en/LC_MESSAGES/errors.po
Normal file
97
priv/gettext/en/LC_MESSAGES/errors.po
Normal file
@@ -0,0 +1,97 @@
|
||||
## `msgid`s in this file come from POT (.pot) files.
|
||||
##
|
||||
## Do not add, change, or remove `msgid`s manually here as
|
||||
## they're tied to the ones in the corresponding POT file
|
||||
## (with the same domain).
|
||||
##
|
||||
## Use `mix gettext.extract --merge` or `mix gettext.merge`
|
||||
## to merge POT files into PO files.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Language: en\n"
|
||||
|
||||
## From Ecto.Changeset.cast/4
|
||||
msgid "can't be blank"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.unique_constraint/3
|
||||
msgid "has already been taken"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.put_change/3
|
||||
msgid "is invalid"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_acceptance/3
|
||||
msgid "must be accepted"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_format/3
|
||||
msgid "has invalid format"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_subset/3
|
||||
msgid "has an invalid entry"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_exclusion/3
|
||||
msgid "is reserved"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_confirmation/3
|
||||
msgid "does not match confirmation"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.no_assoc_constraint/3
|
||||
msgid "is still associated with this entry"
|
||||
msgstr ""
|
||||
|
||||
msgid "are still associated with this entry"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_length/3
|
||||
msgid "should be %{count} character(s)"
|
||||
msgid_plural "should be %{count} character(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should have %{count} item(s)"
|
||||
msgid_plural "should have %{count} item(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should be at least %{count} character(s)"
|
||||
msgid_plural "should be at least %{count} character(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should have at least %{count} item(s)"
|
||||
msgid_plural "should have at least %{count} item(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should be at most %{count} character(s)"
|
||||
msgid_plural "should be at most %{count} character(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should have at most %{count} item(s)"
|
||||
msgid_plural "should have at most %{count} item(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
## From Ecto.Changeset.validate_number/3
|
||||
msgid "must be less than %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be greater than %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be less than or equal to %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be greater than or equal to %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be equal to %{number}"
|
||||
msgstr ""
|
||||
95
priv/gettext/errors.pot
Normal file
95
priv/gettext/errors.pot
Normal file
@@ -0,0 +1,95 @@
|
||||
## This is a PO Template file.
|
||||
##
|
||||
## `msgid`s here are often extracted from source code.
|
||||
## Add new translations manually only if they're dynamic
|
||||
## translations that can't be statically extracted.
|
||||
##
|
||||
## Run `mix gettext.extract` to bring this file up to
|
||||
## date. Leave `msgstr`s empty as changing them here has no
|
||||
## effect: edit them in PO (`.po`) files instead.
|
||||
|
||||
## From Ecto.Changeset.cast/4
|
||||
msgid "can't be blank"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.unique_constraint/3
|
||||
msgid "has already been taken"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.put_change/3
|
||||
msgid "is invalid"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_acceptance/3
|
||||
msgid "must be accepted"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_format/3
|
||||
msgid "has invalid format"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_subset/3
|
||||
msgid "has an invalid entry"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_exclusion/3
|
||||
msgid "is reserved"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_confirmation/3
|
||||
msgid "does not match confirmation"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.no_assoc_constraint/3
|
||||
msgid "is still associated with this entry"
|
||||
msgstr ""
|
||||
|
||||
msgid "are still associated with this entry"
|
||||
msgstr ""
|
||||
|
||||
## From Ecto.Changeset.validate_length/3
|
||||
msgid "should be %{count} character(s)"
|
||||
msgid_plural "should be %{count} character(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should have %{count} item(s)"
|
||||
msgid_plural "should have %{count} item(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should be at least %{count} character(s)"
|
||||
msgid_plural "should be at least %{count} character(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should have at least %{count} item(s)"
|
||||
msgid_plural "should have at least %{count} item(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should be at most %{count} character(s)"
|
||||
msgid_plural "should be at most %{count} character(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "should have at most %{count} item(s)"
|
||||
msgid_plural "should have at most %{count} item(s)"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
## From Ecto.Changeset.validate_number/3
|
||||
msgid "must be less than %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be greater than %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be less than or equal to %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be greater than or equal to %{number}"
|
||||
msgstr ""
|
||||
|
||||
msgid "must be equal to %{number}"
|
||||
msgstr ""
|
||||
4
priv/repo/migrations/.formatter.exs
Normal file
4
priv/repo/migrations/.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
import_deps: [:ecto_sql],
|
||||
inputs: ["*.exs"]
|
||||
]
|
||||
14
priv/repo/migrations/20201124120633_create_classes.exs
Normal file
14
priv/repo/migrations/20201124120633_create_classes.exs
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule Confient.Repo.Migrations.CreateClasses do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:classes) do
|
||||
add :name, :string
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:classes, [:name])
|
||||
|
||||
end
|
||||
end
|
||||
15
priv/repo/migrations/20201128001745_create_students.exs
Normal file
15
priv/repo/migrations/20201128001745_create_students.exs
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule Confient.Repo.Migrations.CreateStudents do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:students) do
|
||||
add :lastname, :string
|
||||
add :firstname, :string
|
||||
add :class_id, references(:classes, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:students, [:lastname, :firstname, :class_id])
|
||||
end
|
||||
end
|
||||
16
priv/repo/migrations/20201128151154_create_assignments.exs
Normal file
16
priv/repo/migrations/20201128151154_create_assignments.exs
Normal file
@@ -0,0 +1,16 @@
|
||||
defmodule Confient.Repo.Migrations.CreateAssignments do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:assignments) do
|
||||
add :title, :string
|
||||
add :slug, :string
|
||||
add :due, :date
|
||||
add :class_id, references(:classes, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:assignments, [:title, :class_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
defmodule Confient.Repo.Migrations.CreateStudentsWorks do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:students_works) do
|
||||
add :date, :date
|
||||
add :path, :string
|
||||
|
||||
add :student_id, references(:students, on_delete: :delete_all)
|
||||
add :assignment_id, references(:assignments, on_delete: :delete_all)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:students_works, [:student_id, :assignment_id])
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user