- Install
docker
anddocker-compose
. - run
git submodule init && git submodule update
in root (for websockets) - run
make build && make run
in the root directory (this one) - In another window, run
make init_db
to set up some default values for everything - (opt) Run
make run_command "cmd=python src/manage.py populate_tables"
to load values into Major, Minor, Course tables
Simplified tree diagram
. # Contains docker setup and Makefile
├── docker-compose.yml # defines setup of PostgreSQL and Django
├── Dockerfile # defines pickmybruin/backend Docker image
├── initialize.sh # sets up Django container (runs migrations and then boots server)
├── Makefile # contains very useful helper commands
└── src # Contains all Django code
├── pickmybruin # Contains code relating to the entire website
│  ├── keys.py # put confidential info in here
│  ├── settings.py # global settings (please keep confidential info out of here)
│  ├── urls.py # global URLs (usually imports app URLs too)
└── users # One app for just users
├── admin.py # sets up /admin pages
├── models.py # contains class declarations and methods
├── serializers.py # contains serializers for classes
├── tests.py # necessary (pls)
├── urls.py # sets up app specific URLs
└── views.py # sets up responses to URLs
make build
creates thepickmybruin/backend
imagemake run
starts up the PostgreSQL and Django containersmake restart
restarts the Django container (useful when you edit code)make ssh
starts a bash session in the latest Django containermake run_command
runs a command inside the latest Django containermake run_command cmd="echo hi"
will runecho hi
inside the latest Django container
make shell
starts amanage.py shell_plus
inside the latest Django container- If you don't know what this means, that's fine
make test
runs test.py useargs=--keepdb
to use previous test database
- Run
make run_command cmd="src/manage.py startapp $APPNAME
- This creates a new skeleton folder for your new app
- YOU MUST ADD THIS APP TO
src/pickmybruin/settings.py
INSTALLED_APPS
FOR THE APP TO BE DISCOVEREDR'users',
adds theusers
app to the Django project. Big surprise.
- Add your code
- Add tests to
tests.py
- Add your models to
admin.py
- Import your urls.py in
src/pickmybruin/urls.py
- Run the tests
- Submit a PR
- Login
- Only the
username
andpassword
fields in the input should ever change - Only the
access_token
field in the output really matters
- Use the
access_token
to do requests
- MUST BE
Authorization: Bearer <ACCESS_TOKEN>
- Django will automatically identify the user with the token
POST /users/
{
"email": "<EMAIL>",
"password": "<PASSWORD>"
}
returns
{
"id": <PROFILE_ID>,
"user": {
"id" : <USER_ID>,
"first_name" : "<FIRST_NAME>",
"last_name" : "<LAST_NAME>",
"email" : "<EMAIL>"
},
"verfied" : <VERIFIED>
}
sends a verification email with a link: "https://bquest.ucladevx.com/verify?code=<VERIFICATION_CODE>" or "http://localhost:8000/users/verify?code=<VERIFICATION_CODE> in development
POST /verify/
{
"verification_code": "<VERIFICATION_CODE>"
}
returns
{
"profile_id": <PROFILE_ID>
}
GET /verify_link/ re-sends verification link returns
{
"profile_id": <PROFILE_ID>
}
POST /o/token/ (login)
grant_type:password
username:<USER_EMAIL>
password:<USER_PASSWORD>
client_id:web
client_secret:<CLIENT_SECRET> // Note: the CLIENT_SECRET is a hardcoded field you need to get from your backend
returns
{
"access_token": "<ACCESS_TOKEN>",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "read write groups",
"refresh_token": "<REFRESH_TOKEN>"
}
Authorization done over headers
Authorization: "Bearer <ACCESS_TOKEN>"
POST /password_link (get password reset link)
{
"username" : <USERNAME>
}
returns
HTTPResponse 200
sends a verification email with a link: "https://bquest.ucladevx.com/password?code=<PASSWORD_RESET_CODE>&userid=" or "http://localhost:8000/users/password?code=<PASSWORD_RESET_CODE>&userid=" in development
POST /password (reset password)
{
"userid" : <USERID>
"code" : <PASSWORD_RESET_CODE>
"password" : <NEW_PASSWORD>
}
returns
HTTPResponse 200
GET /users/me/
returns
{
"id": "<PROFILE_ID>",
"first_name": "<USER_FIRST_NAME>",
"last_name": "<USER_LAST_NAME>",
"email": "<USER_EMAIL>",
"year": "<YEAR>",
"verified": "<VERIFIED>",
"picture": <PICTURE>,
"notifications_enabled": "<NOTIFICATION_BOOL>
"phone_number": "<PHONE_NUMBER>"
"date_created": "<DATE_CREATED_ISO8601>"
}
GET /users/<PROFILE_ID>/
return is same as /users/me/
PATCH /users/me/
schema is same as /users/me/, but will update subfields (don't change the id please)
return is same as /users/me/
GET /mentors/me/
returns
{
"id": "<MENTOR_ID>",
"profile": <PROFILE>,
"active": "<MENTOR_STATE>",
"major": [
<MAJOR> ...
],
"minor": [
<MINOR> ...
],
"gpa": "<GPA",
"bio": <BIO>,
"clubs": "<CLUBS>"
"courses": [
<COURSES> ...
],
"pros": "<PROS>",
"cons": "<CONS>",
"date_created": "<DATE_CREATED_ISO8601>"
}
POST /mentors/me/ - create mentor it doesn't exist - activate mentor if it does exist - will always set active to true - takes no params returns same as /mentors/me/
PATCH /mentors/me/
schema is same as /mentors/me/, but will update subfields (don't change the id please)
Note: Use this when setting mentor to inactive
Note: Format to update major, minor, and courses is as follows:
(Majors are limited to Max 2 and Minors are limited to Max 3)
{
"major": [
{ "name" : "<MAJOR>" },
{ "name" : "<MAJOR2>" }
]
}
{
"minor": [
{ "name" : "<MINOR>" },
{ "name" : "<MINOR2>" }
]
}
{
"courses": [
{ "name" : "<COURSE>" },
{ "name" : "<COURSE2>" }
]
}
return is same as /mentors/me/
GET /majors/
returns
{
"majors": [
<MAJORS>...
]
}
GET /mentors/?query=<SPACE_SEPERATED_QUERY_STRINGS>&random=&name=&major=&bio=
-
if no query is given or no filters are checked, it defaults to return all (all params are optional)
-
checks user's name, major, and bio for Trigram Simularity (deprecated minor, courses, and year), and returns a sorted query based on similarity
-
random returns number of applicable mentors in a random order
-
case insentitive
-
only returns active mentors, excluding self
-
search term aliases, i.e ('cs' becomes 'computer science' and vice versa)
-
searching for multiple items allows more specific searches
- ex) cs Han returns mentors majoring in computer science with name of Han
returns
{ "count": <NUMBER OF RESULTS>, "next": null, "previous": null, "results": [ <MENTOR_PROFILE> ... // same as /mentor/me/ format ] }
-
sample GET request: GET /mentors/?query=computer science&major=True
POST /report_user/
- sends email to 'noreply@bquest.ucladevx.com'
returns
{ "reported_id": <REPORTED_USER_ID>, "reason": <MESSAGE> }
HTTPResponse 200
GET /mentors/<MENTOR_ID>/
return is same as /mentors/me/
POST /requests/<MENTOR_ID>/
{
"phone": "<OPTIONAL_PHONE>"
"preferred_mentee_email": "<REPLY_EMAIL>"
"message": "<EMAIL_BODY>"
}
returns a Request object (seen below)
GET /requests/list/me/
returns
{
"count": <NUMBER_OF_REQUESTS>
"next": null
"prev": null
"results": <LIST_OF_REQUESTS> [
{
"mentee": <MENTEE_INFO>
"mentor": <MENTOR_INFO>
"email_body": "<MESSAGE_FROM_MENTEE>"
"preferred_mentee_email": "<REPLY_EMAIL>"
"phone": "<OPTIONAL_MENTEE_PHONE>"
"date_created": "<DATE_OF_REQUEST>"
}
...
]
}
GET /messaging/me/ GET /messaging/
returns
{
"count": <NUMBER_OF_THREADS>
"next": null
"prev": null
"results": <LIST_OF_THREADS> [
{
'other_profile': <DATA_OF_OTHER_PROFILE>
'recent_message': <MOST_RECENT_MESSAGE_DATA>
}]
}
POST /messaging/<PROFILE_ID>/
{
"body": "<MESSAGE_BODY>"
}
returns a Message object
GET /messaging/<PROFILE_ID>/
returns
{
"count": <NUMBER_OF_MESSAGES>
"next": null
"prev": null
"results": <LIST_OF_MESSAGES> [
{
'id':'<MESSAGE_ID>'
'sender':<PROFILE_OF_SENDER>
'body':'<MESSAGE_BODY>'
'timestamp':'TIME_SENT'
'unread': <BOOLEAN>
}]
}
PATCH /messaging/read/<THREAD_ID>/
{
<DOESN'T MATTER>
}
returns the specified Thread object
GET /messaging/check/<PROFILE_ID>
returns
{
'exists': <True/False>
}
POST /blogs/<USER_NAME>/ - Make sure to include the proper username after blog (USERNAME@ucla.edu)
{
"title": <TITLE>,
"body": <BODY>,
"anonymous":<BOOLEAN>
"publish":<BOOLEAN>
<FILENAME>:<FILE>,
<FILE...
.
.
}
returns
{
"id": BLOG.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": BODY,
"title": TITLE,
"images": [
{
"id": IMAGE.ID,
"filename": FILENAME,
"blog": BLOG.ID,
"picture": FILEURL,
},
.
.
.
],
"published": time.current,
"created": time.current,
"updated": time.current,
"anonymous": BOOLEAN,
"publish": BOOLEAN,
"comments" : NUM_COMMENTS
}
GET /blogs/id/<BLOG_ID>/
returns
{
"id": BLOG.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": BODY,
"title": TITLE,
"images": [
{
"id": IMAGE.ID,
"filename": FILENAME,
"blog": BLOG.ID,
"picture": FILEURL,
},
.
.
.
],
"published": time.current,
"created": time.current,
"updated": time.current,
"anonymous": BOOLEAN,
"publish": BOOLEAN,
"comments" : NUM_COMMENTS
}
DELETE /blogs/id/<BLOG_ID>/
returns
HTTP_RESPONSE_200_OK
PATCH /blogs/id/<BLOG_ID>/
{
"title": <UPDATED_TITLE>,
"body": <UPDATED_BODY>,
"images": [IMAGE1.ID, IMAGE2.ID,...]
"anonymous" : BOOLEAN,
"publish" : BOOLEAN,
<FILENAME>:<NEW_FILE>,
<FILE...
.
.
}
returns
{
"id": BLOG.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": UPDATED_BODY,
"title": UPDATED_TITLE,
"images": [
{
"id": IMAGE.ID,
"filename": FILENAME,
"blog": BLOG.ID,
"picture": FILEURL,
},
.
.
.
],
"published": time.publish,
"created": time.created,
"updated": time.current,
"anonymous": BOOLEAN,
"publish": BOOLEAN,
"comments" : NUM_COMMENTS
}
GET /blogs/?query=&num=
-
if no query is given, it defaults to return all (all params are optional)
-
checks title and body for Trigram Simularity
-
case insentitive
returns
{ "count": <NUMBER OF RESULTS>, "next": null, "previous": null, "results": [ <BLOG_POST> ... // blogs/<blog_id>/ format ] }
POST /blogs/comment/
Enter blog or comment id to associate created comment with blog or comment. Enter type either 'blog' or 'post' Comments are connected like a single-directional tree with a blog root node.
{
"id": <BLOG_ID OR COMMENT_ID>,
"body": <BODY>,
"type":<'blog' OR 'comment'>
.
.
}
returns
{
"id": COMMENT.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": BODY,
"blog": BLOG_ID,
"published" : TIME.CURRENT(),
"likes" : NUM_LIKES,
"comments" : NUM_COMMENTS,
.
.
.
],
}
GET /blogs/comment/id/<COMMENT_ID>/?depth=
Depth is a parameter that sets the depth of the traversal if there are more nodes. Default is 0. For traversal, will give array of comments. At end of root will give num comments.
returns
{
"id": COMMENT.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": BODY,
"published": time.current,
"likes" : NUM_LIKES,
"comments" : NUM_COMMENTS or
[
{
"id": COMMENT.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": BODY,
"published": time.current,
"likes" : NUM_LIKES,
"comments" : NUM_COMMENTS or
[
...
]
},
{
...
}
]
}
DELETE /blogs/comment/id/<COMMENT_ID>/
returns
HTTP_RESPONSE_200_OK
PATCH /blogs/comment/id/<COMMENT_ID>/
{
"id": COMMENT_ID,
"user": USER_ID,
"author": FIRST_NAME + LAST_NAME,
"blog": BLOG_ID or null,
"published": TIMEZONE.NOW()
"body": "New updated blog",
"likes": NUM_LIKES,
"comments": NUM_COMMENTS
}
returns
{
"id": COMMENT.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": UPDATED_BODY,
"blog": BLOG_ID or null,
"published": TIMEZONE.NOW()
"likes": NUM_LIKES,
"comments": NUM_COMMENTS
}
GET /blogs/id/<BLOG_ID>/comments/?num=&depth=
-
If depth is not given, defaults to 0, surface level query
-
If num not given, default to return all
-
Typically set num from GET blog endpoint, user comment integer to set num
returns
{ "count": <NUMBER OF RESULTS>, "next": null, "previous": null, "results": [ <COMMENTS> ... // /blogs/comment/id/<COMMENT_ID>/ format ] }
PATCH /blogs/comment/id/<COMMENT_ID>/likes/
- Will increment or decrement like counter by 1
returns
{
"id": COMMENT.ID,
"author": "FIRST_NAME + LAST_NAME,
"user": USER.ID,
"body": BODY,
"published": time.current,
"likes" : NUM_LIKES +- 1,
"comments" : NUM_COMMENTS
}
(not implemented)
NOTE: you don't really need to understand this, but this is how Django will create tables for the models
id | first_name | last_name | (salted and hashed) password | |
---|---|---|---|---|
1 | me@marktai.com | Mark | Tai | verysecurepassword |
2 | you@marktai.com | John | Doe | corgisarecute |
id | user_id | bio |
---|---|---|
1 | 1 | Let me know if these examples suck |
2 | 2 | marktai.com/#corgis for all your corgi needs |
id | name |
---|---|
1 | CS |
id | profile_id | major_id | bio |
---|---|---|---|
1 | 2 | 1 | I will teach you the ways of corgis |
id | name |
---|---|
1 | CS31 |
- Identify bottlenecks
- Unearth insight into performance
- Future-proof code
- cProfile is a built in profiler for python
$ python cProfile method.py
- Using this, we can see the breakdown of function calls and dependencies across multiple files! Awesome!
- What about functions that aren't inherently run? For example, a class method that is only run when it's called dynamically?
- Profile hooks is the answer!
from profilehooks import profile
class ExampleClass():
@profile
def exampleFunc():
\\..
\\..
- We're set! Now we have an easy and succinct way to test performance of new features!
- Look for results in the docker terminal!