-
Notifications
You must be signed in to change notification settings - Fork 0
/
commands.txt
332 lines (298 loc) · 13.9 KB
/
commands.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
-- notes here should be all chronological, maybe not all, but almost --
conda
- conda create -n hidjango python=3.8
- conda activate hidjango (switching env actually do not require 'deactivate')
- conda install PACKAGE_NAME
initial setup
- django-admin startproject PROJECT
- django-admin startapp APP
- ./manage.py migrate
- ./manage.py createsuperuser (strong password recommended)
- ./manage.py runserver IP:PORT (e.g. localhost:8899)
main project vs apps
- a project is a folder containing all the stuff (apps, settings, templates ..)
- an app is a folder within a (django) project, it does one specific thing
template folder (where to find for Django)
- PROJECT/PROJECT/settings.py
- `TEMPLATES -> [ { DIRS: [ os.path.join(BASE_DIR, 'templates') ] } ]`
a sample index page (class-based view)
- views.py -> from django.views.generic import XxxView .. class YyyView(..): ..
- urls.py -> .. import the view, path('PATH', VIEW, name='index'),
- PLUS, path like ''(root) should be put to the last one, it's a MUST :P
passing context data to templates
- It's pretty obvious when using function-based views, you simply use a dict
- In class-based views, you need to write like this
```python3
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs):
context['VARIABLE_NAME'] = THE_VALUE # can be dynamic or else
return context
```
calling *view* methods in templates (steps below were based on class-based views)
- usage
- simply type `view.METHOD_NAME`, no `()` required :P
- why `view.`? see
- `lib/python3.8/site-packages/django/views/generic/base.py :: Line 72/73`
- how-to
- nothing special, `def METHOD(self): ... # return something`
----------------- THIS IS ANOTHER PROJECT: everycheese ------------------
initial setup
- create a new env, activate it, install packages
- conda create -n everycheese python=3.8
- conda install -c conda-forge cookiecutter
- mkdir everycheese && cd everycheese (in case other stuff messed up the root)
- generate a project using `cookiecutter`
- cookiecutter gh:roygreenfeld/django-crash-starter
> the name of the folder created'd be the *slug* part you specified
- database setup (postgreSQL)
- new role: createuser -dP everycheeseboss
- new database: createdb -E utf-8 -U everycheeseboss everycheese
- modify the default database URL
- first put the URL in a `.env` (for safety purposes)
- e.g. `postgres://ROLE:ROLE_PASSWORD@localhost:5432/DATABASE_NAME
- reference: https://stackoverflow.com/questions/3582552/postgresql-connection-url
- simply change the KEY (defined in the .env file)
- config/settings/base.py -> DATABASES -> { .. env.db("KEY", ..) }
- installing required packages
- first make sure the virutal environment is on (conda activate everycheese)
- pip install -r requirements/local.txt (it might take a while to install :P)
- finish up the basic part
- ./manage.py migrate
- ./manage.py createsuperuser
- ./manage.py runserver localhost:8899
- adding new field 'bio'
- models.py: `bio = models.TextField(_("Bio"), blank=True)`
- ./manage.py makemigrations users
- ./manage.py migrate users
- views.py
- add `'bio'` to the `fields` in the `UserUpdateView`, the rest would be handled by `UpdateView`
- templates: user_edit.html
```html
{% if object.name %}
<p>{{ object.name }}</p>
<p>{{ object.bio|linebreaksbr }}</p> <!-- the line we need to add -->
{% endif %}
```
- examine the pre-written tests
- tips
- write tests for **every single model methods** (code stability boosted!)
- commands that were used
- `coverage run -m pytest`
- `coverage report`
- `coverage html` (does the same as `report` but exists as HTML files)
- more
- almost all of the questions could answered in the docs & source code :P
- create a new app 'cheeses'
- .`/manage.py startapp cheeses`
- move it to `PROJECT_ROOT/everycheese/` (a bit different from I was used to, but okay)
- modify its `apps.py`: from `cheeses` to `everycheese.cheeses` (pretty noraml huh)
- register it
- `everycheese/config/settings/base.py` -> `LOCAL_APPS` -> `PROJECT.APP.apps.CheesesConfig`
- add new model 'Cheese' to app 'cheese'
- just remember to `makemigrations` & `migrate`!
- checking coverage status whenenver you add a new method to a model (or something else)
1. test current features: `coverage run -m pytest`
2. check coverage (it'll tell you exactly where wasn't covered)
- `coverage report`
- `coverage html` (=> `./html_cov/index.html`)
3. it's not 100%?
> Write tests! Normally you would need to delete the `tests.py`, then create a `tests` folder,
later put all the tests in there. Until now, we only added a `__str__` method,
it's extremely easy to test, let's see how.
```python3
# import stuff: pytest, model(aka. Cheese)
# import marker: powered by pytest-django (ensure the isolated transaction)
# create sample data & then `assert` them
```
4. now it should be 100%
- getting `Cheese` inside the admin!
- tips
- do not use `list_editable`, extremely bad for multi-user projects
- same as before
```python3
# import admin stuff
# import model
# admin stuff DOT site DOT register (model)
```
- adding some sample "cheeses" in case we'll use it later :)
- implementing *List* and *Detail* page for app ***Cheese***
- first of all, these are what you should think about before implementing it
- views: if using *class-based view*, see [https://ccbv.co.uk/](https://ccbv.co.uk/)
- routes: two parts, app-level (cheeses/urls.py) and project-level (config/urls.py)
- templates
1. the content page (follow the naming convensions if using class-based views)
2. the navbar (for easier access)
- writing factories for tests
- a note ahead
> Some libraries might synthesize different other libraries as some kind of *providers*, so if you can't find the explanations for a specific keyword, it might be in the providers' documentations!
- where
- normally it would be a `factories.py` file inside each apps' `tests` folder
- how
- The library we're using is `factory_boy` (aka. `factory` while importing).
- We only need to do two things, first: gen the data, second, name the model, done!
- notice
- While experimenting it, the result would be stored in the database, remember to delete it :P
- getting serious with *factories*
- You CAN generate not-so-random data using *factories*, just name the field
```python3
for cheese_id in range(1,3+1):
CheeseFactory(name=f"Sample cheese {cheese_id}")
# cleanup: Cheese.objects.filter(name__startswith="Sample").delete()
```
- plus
> I think in mosts cases we don't actually need to see what the test data is, we can simply use `CheeseFactory` to produce the sample data, and then write something like `assert FAC_INST.__str__() == FAC_INST.name` to test if it works.
- imagine we wanna add a new field called 'country' (where the cheese originates)
- two *immature* solutions
1. using CharField
> it's easy, but no validations (US ?= USA ?= U..of..A) whatsoever, big NO.
2. using CharField with Choices
> we CAN, indeed, follow the ISO 3166-1 standard (list of countries), but still, we have to update the list by ourselves, maybe not so often, but still sucks, big NO.
- what we should do is to find open-source packages (at least in this case)
> A great place would be [Django Packages](https://djangopackages.org/). Simply search the word you need. Note that there're two kinds of search results, one is the specific package, the other one is called as *Grid*, aka. side-by-side comparison between packages of the same kind. Basically you could choose which to use based on its maintain-status/contributors/stars(ordered by proorities).
- after finding what we need/want
- add it to the `requirements.txt` (in our case it'd be `requirements/base.txt`) and then install it
```bash
# we did add the requirement to the base.txt, but we also inherit it (base)
# in the local.txt, so you SHOULD install it from local.txt, therefore
# the package dependencies could be managed correctly (just my thought).
pip install -r requirements/local.txt
```
- use the package/module!
- config/settings/base.py
```python3
THIRD_PARTY_APPS = [
.. ,
'django_countries',
]
```
- models.py
```python3
from django_countries.fields import CountryField
country_of_origin = CountryField("Country of Origin", blank=True)
```
- then `makemigrations cheeses` and `migrate` it!
- finally, we can make some changes to the old data, i.e. edit the country field!
- display the country in the detail page
```html
<!-- everycheese/templates/cheeses/cheese_detail.html -->
{% if cheese.country_of_origin %}
<p>
Country of Origin: {{ cheese.country_of_origin.name }}
<img src="{{ cheese.country_of_origin.flag }}"
alt="photo of {{ cheese.country_of_origin.name }}" />
</p>
{% endif %}
```
- one last thing
- add the new field to `factories.py`, i.e. `factory.Faker("country_code")`
- edit the cheeses
- two parts basically, how-and-what-to-EDIT and where-to-go-after-EDITed
1. the first part was largely handled by `CreateView`
2. we only need to do the 2nd part, tell Django where it'd go after submited the form :P
- make the forms great again
- install `django-crispy-forms`
- edit the templates
```html
<!-- put this under the `extends` command -->
{% load crispy_forms_tags %}
<!-- before: .as_p -->
{{ forms|crispy }}
```
- add a `creator` field
- after `makemigrations` & `migrate` it, give previous cheeses a default creator
```python3
jane = User.objects.get(username="jane")
for j in Cheese.objects.all():
j.creator = jane
j.save()
# test
for c in Cheese.objects.all():
c, c.creator
```
- assign `creator` attr to users that currently logged in when adding cheeses
- cheeses/views.py
```python
CheeseCreateView(.., ..):
..
..
def form_valid(.., ..):
form.instance.creator = self.request.user
super().form_valid(form)
```
- also displaying it in the templates (cheese_detail.html)
```
<!-- add this to the bottom of this file -->
<hr/>
<h3>
Submitted by
<a href="{% url 'users:detail' cheese.creator.username %}">
{{ cheese.creator.username }}
</a>
</h3>
{% if cheese.creator.name %}
{{ cheese.creator.name }}
{% endif %}
```
- make the correspond changes to *cheeses/tests/factories.py*
```
from everycheese.users.tests.factories import UserFactory
# add this under `country_of_origin`
creator = factory.SubFactory(UserFactory)
# ---- have a little test with it ----
# 1) import it first
# 2) create
cheese = CheeseFactory()
cheese.creator
# 3) clean up
newbie = User.objects.last()
newbie.delete()
```
- add more tests (for models & views)
- there's not much to talk about, notes were all inside the test files.
- let users able to update the cheeses they've added
- views.py
```python3
# 1. inherit UpdateView & LoginRequiredMixin
# 2. specify the model, fields and data (the name 'action' is not special!)
```
- urls.py
```python3
# nothing special, do put this before the '<slug:slug>' (detail),
# if `update` was put after it, it'll be a dead link for sure :)
```
- cheese_form.html
```html
<!--
EDIT
> Actually it's not "Django being smart", their URLs differed with each
each other, the reason why Django could do the right thing is because
its URLs are correspondent to the views. We basically just display the
right thing and modify the `action` to make it call the view they're on.
-->
<!--
Since both the XxCreateView and XxUpdateView share the same template,
we have to make the templates adapt to it. It seems that Django is
smart enough to use the right view when it comes to decide whether is
to add or update, the only thing we need to concern is the text. So
we added `action = "Update"` to our view then referenced it in the
templates via the `with` keyword with default values, therefore, the
right text (Add/Update) will be displayed at the right time :)
-->
<!--
This is the only and most important part, writing '.' means let the
current view handle the form (that's why I call "smart enough to ..").
-->
<form method="post" action="{% url 'cheeses:add' %}">
<form method="post" action=".">
```
- cheese_detail.html
```html
<!-- Right after the header inside the content block -->
<p>
<a href="{% url 'cheeses:update' cheese.slug %}"
class="btn btn-primary"
role="button">
Update
</a>
</p>
```