Skip to content

Commit

Permalink
feat: add quiz module (#188)
Browse files Browse the repository at this point in the history
* refactor:create dto music_all (#132)

* refactor:create dto music_all

* refactor:create dto music_all

* ci: fix lint

* test: add test for one music

* test: beautify test config

* test: fix test music

* feat: add MC0105

PUT /music/<int:id>/visibility

* ci: use black instead of pylint

[skip ci]

* style: reformat with black

* chore: update .gitignore

* docs: add LICENSE

[skip ci]

* refactor: create dto character_normal

* refactor: create dto character_all

* style: make some notes in views.py and urls.py

make it easy to find apis

* refactor: make music_normal dto model

this dto model can be used in MC0202 api

* fix: delete "import imp" in views.py

I dont know how it is in the scripts, but it do

* refactor: add dto music_normal (#141)

* style: make some notes in views.py and urls.py

make it easy to find apis

* refactor: make music_normal dto model

this dto model can be used in MC0202 api

* fix: delete "import imp" in views.py

I dont know how it is in the scripts, but it do

Co-authored-by: lll123github <[email protected]>

* fix: update default avatar

* refactor:create dto

create dto
article_all,
article_normal,
comment_normal

* style: make some notes in HinghwaDict/urls.py and word/word.py

make it easy to find apis about word

* refactor: build word2pronunciation.py

move the function word2pronunciation into word2pronunciation.py

* refactor: create dto word_all

create word_all.py and delete function word2pronunciation from the word.py completely, meanwhile import word2pronunciation.py in word.py

* rafactor: create dto word_normal

create dto word_normal in word_normal.py. this model is not used

* refactor: create dto word_quick

this model can be used in WD0201, word.py is modified, too

* refactor: create dto word_simple

create word_simple.py, which has not been used

* style: style urls.py in HinghwaDict

style urls.py in HinghwaDict

* fix: delete "import imp" in views.py

I dont know how it is in the scripts, but it do

* fix: delete import response and Docs (blacked)

we should catch sight on how they appeared

* fix: return response in word_normal and word_simple

linzichuan is not a qualified programer

* fix: delete import User in word_simple.py

delete import User in word_simple.py, which is nothing to explain

* refactor: use word_simple in WD0202

modify woed.py and note in word_simple.py

* fix: delete import Pronunciation from .models

in word.py, which is not used

* refactor: create dto

create dto
pronunciation_normal
pronunciation_all
pronunciation_simple already exists,and this model is not used

* build: change dto used in WD0201 from word_quick to word_all

word_quick will be changed to application_content which will be used with data type Application. the reference will be noted in word_all.py

* style: note in models.py

explain the relation between word and application

* refactor: delete word_quick and make a dto named application_simple

dto application_simple_content is used in application_simple's content

* refactor: make a dto named application_all

application_all_content is used in application_all's content

* fix: return response in application_all.py

return response in application_all.py

* ci: add test word-creation (#161)

* ci: add word-creation

* test: rename some tests

* fix: fix error filed name

* docs: add TESTING.md (#162)

* docs: rename TESTING.md

* docs: update TESTING.md (#164)

* refactor: refactor directories in module word  (#166)

* refactor: create word.character

* refactor: create word.pronunciation

* fix: fix response error

* refactor: create word.word and word.application

* refactor: move translate.py

* fix: fix calling error

* refactor: move word2pronunciation.py

* style: beautify with black

* refactor: refactor MultiApplication (#167)

* feat: add ExceptionMiddleware

* feat: add InvalidTokenException

* fix: import ExceptionMiddleware

* fix: fix CommonException

* fix: fix ExceptionMiddleware

* feat: add token checking

* refactor: adjust output logs

* refactor: refactor MultiApplication

/words/applications

* style: beautify with black

* refactor: organize utils (#168)

* refactor: rename islist

* refactor: move TokenChecking

* test: add new testing for word-creation (#169)

* test: add new testing for word-creation

* test: fix test script

* fix: fix error response

* Update word-creation.json

* Update word-creation.json

* feat: add quiz module (#160)

* feat: add quiz modular

* fix: fix formatting problems

* test: add the test of apifox

* fix: add urls.py

* fix:fix quiz.json

* fix : fix the problem of urls.py

* Update apps.py

* update urls.py、views.py and quiz.json

* Update quiz.json

* fix: the problem of url

* 试一下ci

* 试一下ci

* fix: fix the problems of test

* fix: quiz.json format

* fix: add for migrations

* 加错路径了 尴尬

* Create __init__.py

* fix: optimize code structure

* fix: fix the problem of quiz

* fix: fixed the display problem

Fixed the display problem of background management

Co-authored-by: sheeplin <[email protected]>

* test: add enhancement tests for module quiz (#172)

* ci: add test quiz_401,quiz_404

* fix :fix the format of json

* ci :add test quiz_403

* Update quiz_403.json

* test: update quiz_403.json

* ci: add common user

* ci : update e2e.yml

* ci: add user_test

* ci: fix user_test

Co-authored-by: Charlie Chiang <[email protected]>

* ci: fix single quote

* style: update quiz.json

* style: modify the test folder structure

* Revert "style: modify the test folder structure"

This reverts commit ce38918.

Co-authored-by: sheeplin <[email protected]>
Co-authored-by: Charlie Chiang <[email protected]>

* refactor: refactor music module (#177)

* refactor: refactor music module

* style: fix the problem of test

* fix: fix the token check of MC0105

* style: update the music_403.json

* style: update the music module

* sytle: update the music module

* style: format music_403.json,music_404.json

* feat: check the password valid or not (#173)

* fix: make the length of passwords in wechat program adaptable

make the length of passwords in wechat program adaptable

* feat: add a child exception 400 named InvalidPassword

add a child exception of BadRequestException 400 named InvalidPassword

* feat: create password_validator and class MaximumLengthValidator

create password_validator and class MaximumLengthValidator in password_validation.py

* feat: check the new passwords valid or not

check the new passwords valid or not when retrieving updating or creating passwords

* fix: import user.password_validation

instead of password_validation

* fix: deal with the exception in password_validation

deal with the exception in password_validation

* feat: create UserNotFoundException

in NotFoundException(404)

* style: make a note on updatepassword

make a note on updatepassword

* style: format urls.py

format urls.py

* style: delete "as e" which is not used

when catch error ValidationError

* perf: abolish the exception "WhenUpdatePassword" in WrongPassword

this can be used in normal password  checking

* style: add some explaination on password_validator function

add some explanation on password_validator function

* fix: wrong word: UnauthorizedException

add 'e'

* refactor: new class UpdatePassword

to replace function updatePassword

* refactor: use new class UpdatePassword formally

function updatePassword has been deleted and urls.py has been changed

* style: format views.py with black

format views.py with black

* style:delete some excepting and raising

without any significance

* fix: delete "password" in MaximumLengthValidator(password).validator

delete "password" in MaximumLengthValidator(password).validator

* style: delete "(旧)" in a note

delete "(旧)" in a note

* refactor: arrange code about class UpdatePassword

arrange code about class UpdatePassword

* fix: add "id" in put(self,request)

which is necessary for django

* refactor: simplify code for the limitation of the length of new password

simplify code for the limitation of the length of new password

* fix: add returning CommenException

add returning CommonException

* fix: fix the condition of validating a password

fix the condition of validating a password

* style: move the password_validation to the filter utils and rename it

and delete someting not used in it

* style: black and delete something not used

black and delete something not used in views.py

* ci: update e2e.yml

add a user for test

* style: delete try except in UpdatePassword

which is not useful

* ci: update information about a user for test

update password about a user for test

* test: add two tests

which are about the update-password actions

* style: modify the format of the tests

which are two commited before

* test: try some tests in ci

change the id of the user in tests

* test: try some tests in ci

change the ids of the user in tests

* test: correct the password

which may cause unauthorized occation

* test: try some tests in ci

change the id of the user in tests

* test: try some tests in ci

change the id of the user in tests

* test: try some tests in ci

change the id of the user in tests

* ci: update e2e.yml

update e2e.yml and set the password

Co-authored-by: sheeplin <[email protected]>

* ci: update e2e.yml

add the ids of the users for test

* test: try some tests in ci

change the id of the user in tests

* ci: fix the bug

without checking before

* ci: test somthing

add a space

* test: add the actions after logining

catch tokens

* test: add actions after logining

catch ids

* test: change the order of tests

change the order of tests

* style: style not_found.py

black not_found.py

* test: try some tests in ci

change the id of users and fix the test suite

Co-authored-by: lll123github <[email protected]>
Co-authored-by: sheeplin <[email protected]>
Co-authored-by: sheeplin <[email protected]>

* refactor: refactor article module (#181)

* refactor: refactor article module

* fix: fix the known problems

* refactor: refactor article.json

* Update article.json

* refactor: delete old code and fix known problems

* refactor:delete invalid judgment

* feat: add admin feature of quiz module (#185)

* refactor: refactor article module

* fix: fix the known problems

* refactor: refactor article.json

* Update article.json

* refactor: delete old code and fix known problems

* refactor:delete invalid judgment

* feat: add admin review the quiz

* refactor: format json

Co-authored-by: Norton-Lin <[email protected]>
Co-authored-by: lll123github <[email protected]>
Co-authored-by: 林子川 <[email protected]>
Co-authored-by: Charlie Chiang <[email protected]>
  • Loading branch information
5 people authored Nov 6, 2022
1 parent 584365e commit 60f3763
Show file tree
Hide file tree
Showing 113 changed files with 18,458 additions and 2,039 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
python3 manage.py makemigrations
python3 manage.py migrate
DJANGO_SUPERUSER_PASSWORD=testtest123 python manage.py createsuperuser --username admin --email [email protected] --no-input
DJANGO_SUPERUSER_PASSWORD=testtest222 python manage.py createsuperuser --username admin2 --email [email protected] --no-input
echo -e "from django.contrib.auth.models import User\nuser=User.objects.create_user('user_test','user_test@user_test.com','123456')\nuser.set_password('123456')\nuser.save()\nexit()" | python manage.py shell
echo -e "from django.contrib.auth.models import User\nuser=User.objects.create_user('user_test_old_password','user_test_old_password@user_test_old_password.com','12')\nuser.set_password('12')\nuser.save()\nexit()"| python manage.py shell
python3 manage.py runserver &
- uses: actions/setup-node@v3
with:
Expand All @@ -44,6 +47,9 @@ jobs:
- name: Run tests
run: |
curl 127.0.0.1:8000/users/1
curl 127.0.0.1:8000/users/2
curl 127.0.0.1:8000/users/3
curl 127.0.0.1:8000/users/4
cd tests
for test in `ls .`
do
Expand Down
9 changes: 2 additions & 7 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
pip install pylint-django
cd hinghwa-dict-backend
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
sudo apt-get update
sudo apt-get install ffmpeg -y
pip install black
- name: Get changed files
id: changed-files
Expand All @@ -38,6 +33,6 @@ jobs:
for file in ${{ steps.changed-files.outputs.all_changed_files }} ; do
# check if file is python file
if [[ ${file} == *.py ]]; then
DJANGO_SETTINGS_MODULE=HinghwaDict.settings pylint --load-plugins pylint_django ${file}
black --check ${file}
fi
done
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ material
logs
.env
db.sqlite3
migrations
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## 测试说明

### 原则

理论上的测试类型包括:

- 单元测试(Unit test)
- 集成测试(Integration test)
- 端到端测试(End-to-End test, e2e test)

然而在这个项目中,由于时间精力和函数的实际复杂程度,我们直接使用端到端测试检验函数的正确性。(正常来说,应该是金字塔模型: 底部是单元测试,中间是集成测试,最上层是端到端测试)

一个功能的上线要经过下列流程:

- 发起 Pull Request 提交代码(具体见 [CONTRIBUTING.md](CONTRIBUTING.md)
- 运行 [CI](./.github/workflows) 进行 `Github Actions` 进行一些相应的检查
- 部署到测试服务器进行测试
- 上线服务至正式服务器

### CI

CI,(*Continuous Integration*,持续集成),是一个可以帮助我们控制代码质量的好方法。

在本项目目前的 CI 包括:

- e2e:运行 [`tests`](./tests) 文件夹中的 `Apifox` 自动化测试脚本进行端到端测试
- lint:通过 `blank` 进行代码格式化检查

在运行端到端测试时,在会通过以下命令创建初始数据库:


```shell
python3 manage.py makemigrations
python3 manage.py migrate
DJANGO_SUPERUSER_PASSWORD=testtest123 python manage.py createsuperuser --username admin --email [email protected] --no-input
DJANGO_SUPERUSER_PASSWORD=testtest222 python manage.py createsuperuser --username admin2 --email [email protected] --no-input
```

> 创建两个用户的原因是在目前的代码中,发送邮件默认是由2号用户进行发送。
而在启动服务之后,你需要首先运行:

```shell
curl 127.0.0.1:8000/users/1
curl 127.0.0.1:8000/users/2
```

其原因是:直接创建的用户只有 Django 默认的用户模型,不包括我们自定义的 `UserInfo` 模型,在获取用户模块中实现了自动创建功能,否则在其他模块调用时会出错。

#### Apifox 自动化测试

-`Apifox` - `自动化测试` - `测试用例` 中添加相应的测试用例或测试套件。
- 选择 `测试环境` 导出 `Apifox CLI` 格式的 `json` 源文件,格式化后放入 `tests` 文件夹即可。
- 测试套件或测试用例可以自己根据需要进行组合、拼装。
11 changes: 7 additions & 4 deletions hinghwa-dict-backend/AudioCompare/FFT.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ def series(self, chunks=-1):
samples = self.input_file.get_audio_samples(chunks * self.chunk_size)
# mix those samples down into one channel
samples = samples.mean(axis=0)
result = self.specgram(samples, NFFT=self.chunk_size,
window=FFT.__window_hanning,
noverlap=int(self.chunk_size / self.overlap_ratio))
result = self.specgram(
samples,
NFFT=self.chunk_size,
window=FFT.__window_hanning,
noverlap=int(self.chunk_size / self.overlap_ratio),
)
result = result.transpose()
return result

Expand Down Expand Up @@ -65,7 +68,7 @@ def specgram(self, x, NFFT, window, noverlap):

# do the ffts of the slices
for i in range(n):
thisX = x[ind[i]:ind[i] + NFFT]
thisX = x[ind[i] : ind[i] + NFFT]
thisX = windowVals * thisX
fx = np.fft.fft(thisX, n=NFFT)
Pxx[:, i] = np.conjugate(fx[:numFreqs]) * fx[:numFreqs]
Expand Down
23 changes: 12 additions & 11 deletions hinghwa-dict-backend/AudioCompare/InputFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@


class InputFile:

def __init__(self, filename):
"""Open an Audio file with the given file path.
Supported formats: WAVE, MP3.
Expand All @@ -27,7 +26,7 @@ def __init__(self, filename):
original_name = filename
self.wav_file = open(filename, "rb")
# try to use lame to convert
self.workingdir = os.path.join(os.getcwd(), 'temp')
self.workingdir = os.path.join(os.getcwd(), "temp")
if not os.path.exists(self.workingdir):
os.makedirs(self.workingdir)
if not self.__is_wave_file(self.wav_file):
Expand All @@ -37,14 +36,16 @@ def __init__(self, filename):
# make sure the filename has a ".mp3" extension before sending to lame
if filename[-4:] != ".mp3":
# create a copy of the file in
temp_file_name = os.path.join(self.workingdir, str(time.time()) + ".mp3")
temp_file_name = os.path.join(
self.workingdir, str(time.time()) + ".mp3"
)
shutil.copyfile(filename, temp_file_name)
filename = temp_file_name
# Use lame to make a wav representation of the mp3 file to be analyzed
lame = [lame, '--silent', '--decode', filename, canonical_form]
lame = [lame, "--silent", "--decode", filename, canonical_form]

music = pydub.AudioSegment.from_file(filename)
music.export(canonical_form, format='wav')
music.export(canonical_form, format="wav")

if not os.path.exists(canonical_form):
raise IOError("{f} 's format is not supported".format(f=original_name))
Expand All @@ -67,22 +68,22 @@ def __init__(self, filename):

self.wav_file.seek(40, 0)
self.data_chunk_size = InputFile.__read_size(self.wav_file)
self.total_samples = (self.data_chunk_size / self.block_align)
self.total_samples = self.data_chunk_size / self.block_align

@staticmethod
def __is_wave_file(file):

if (not InputFile.__check_riff_format(file)):
if not InputFile.__check_riff_format(file):
return False
if (not InputFile.__check_wave_id(file)):
if not InputFile.__check_wave_id(file):
return False
if (not InputFile.__check_fmt(file)):
if not InputFile.__check_fmt(file):
return False

file.seek(20)
data = file.read(2)
file.seek(0)
if (not InputFile.__check_fmt_valid(data)):
if not InputFile.__check_fmt_valid(data):
return False
return InputFile.__check_data(file)

Expand Down Expand Up @@ -146,7 +147,7 @@ def get_audio_samples(self, n):
data = np.fromfile(self.wav_file, dtype=np.int16, count=n * self.channels)
result = np.zeros((self.channels, n), dtype=int)
for c in range(self.channels):
result[c] = data[c::self.channels]
result[c] = data[c :: self.channels]

return result

Expand Down
48 changes: 33 additions & 15 deletions hinghwa-dict-backend/AudioCompare/Matcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
BUCKET_SIZE = 20
BUCKETS = 4
BITS_PER_NUMBER = int(math.ceil(math.log(BUCKET_SIZE, 2)))
assert ((BITS_PER_NUMBER * BUCKETS) <= 32)
assert (BITS_PER_NUMBER * BUCKETS) <= 32

NORMAL_CHUNK_SIZE = 1024
NORMAL_SAMPLE_RATE = 44100.0
Expand Down Expand Up @@ -67,9 +67,13 @@ def __str__(self):
short_file2 = os.path.basename(self.file2)
if self.score > SCORE_THRESHOLD:
if self.file1_len < self.file2_len:
return "MATCH {f1} {f2} ({s})".format(f1=short_file1, f2=short_file2, s=self.score)
return "MATCH {f1} {f2} ({s})".format(
f1=short_file1, f2=short_file2, s=self.score
)
else:
return "MATCH {f2} {f1} ({s})".format(f1=short_file1, f2=short_file2, s=self.score)
return "MATCH {f2} {f1} ({s})".format(
f1=short_file1, f2=short_file2, s=self.score
)
else:
return "NO MATCH"

Expand All @@ -91,7 +95,7 @@ def _to_fingerprints(freq_chunks):
end_index = (bucket + 1) * BUCKET_SIZE
bucket_vals = freq_chunks[chunk][start_index:end_index]
max_index = bucket_vals.argmax()
fingerprint += (max_index << (bucket * BITS_PER_NUMBER))
fingerprint += max_index << (bucket * BITS_PER_NUMBER)
fingerprints[chunk] = fingerprint

# return the indexes of the loudest frequencies
Expand All @@ -116,7 +120,7 @@ def _file_fingerprint(filename):
# file. It is important that each chunk represent the
# same amount of time, regardless of the sample
# rate of the file.
chunk_size_adjust_factor = (NORMAL_SAMPLE_RATE / file.get_sample_rate())
chunk_size_adjust_factor = NORMAL_SAMPLE_RATE / file.get_sample_rate()
fft = FFT(file, int(NORMAL_CHUNK_SIZE / chunk_size_adjust_factor))
series = fft.series()

Expand Down Expand Up @@ -153,7 +157,7 @@ def __init__(self, dir1, dir2):
them for files."""
self.dir1 = dir1
self.dir2 = dir2
if os.path.split(dir2)[1] != 'submit':
if os.path.split(dir2)[1] != "submit":
self.change = True
else:
self.change = False
Expand Down Expand Up @@ -196,7 +200,11 @@ def __search_dir(dir):
if stat.S_ISREG(node_stat.st_mode):
results.append(abs_node)
else:
warn("An inode that is not a regular file was found at {f}".format(abs_node))
warn(
"An inode that is not a regular file was found at {f}".format(
abs_node
)
)

return results

Expand Down Expand Up @@ -288,7 +296,9 @@ def __report_file_matches(file, master_hash, file_lengths):
else:
score = 0

results.append(MatchResult(file.filename, f, file.file_len, file_lengths[f], score))
results.append(
MatchResult(file.filename, f, file.file_len, file_lengths[f], score)
)

return results

Expand Down Expand Up @@ -321,13 +331,15 @@ def match(self):
# map2_result = [_file_fingerprint(item) for item in dir2_files]
# map1_result = pool.map_async(_file_fingerprint, dir1_files)
dir1_results = map1_result
if self.change or not os.path.exists(os.path.join(os.getcwd(), 'submit结果存储.pkl')):
if self.change or not os.path.exists(
os.path.join(os.getcwd(), "submit结果存储.pkl")
):
map2_result = [_file_fingerprint(item) for item in dir2_files]
dir2_results = map2_result
with open(os.path.join(os.getcwd(), 'submit结果存储.pkl'), 'wb') as f:
with open(os.path.join(os.getcwd(), "submit结果存储.pkl"), "wb") as f:
pickle.dump(dir2_results, f)
else:
with open(os.path.join(os.getcwd(), 'submit结果存储.pkl'), 'rb') as f:
with open(os.path.join(os.getcwd(), "submit结果存储.pkl"), "rb") as f:
dir2_results = pickle.load(f)
print(len(dir2_results))
# Wait for pool to finish processing
Expand All @@ -337,7 +349,7 @@ def match(self):
# dir2_results = map2_result
# Get results from process pool

shutil.rmtree(os.path.join(os.getcwd(), 'temp'))
shutil.rmtree(os.path.join(os.getcwd(), "temp"))
except KeyboardInterrupt:
# pool.terminate()
raise
Expand All @@ -351,8 +363,12 @@ def match(self):

# Proceed only with fingerprints that were computed
# successfully
dir1_successes = list(filter(lambda x: x.success and x.file_len > 0, dir1_results))
dir2_successes = list(filter(lambda x: x.success and x.file_len > 0, dir2_results))
dir1_successes = list(
filter(lambda x: x.success and x.file_len > 0, dir1_results)
)
dir2_successes = list(
filter(lambda x: x.success and x.file_len > 0, dir2_results)
)

# Empty files should match other empty files
# Our matching algorithm will not report these as a match,
Expand Down Expand Up @@ -400,7 +416,9 @@ def match(self):
# same time difference relative to each
# other. This indicates that the two files
# contain similar audio.
file_matches = Matcher.__report_file_matches(file, master_hash, file_lengths)
file_matches = Matcher.__report_file_matches(
file, master_hash, file_lengths
)
results.extend(file_matches)

return results
3 changes: 3 additions & 0 deletions hinghwa-dict-backend/AudioCompare/common.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
class BaseResult(object):
"""Used to report results of operations
performed in our algorithms."""

def __init__(self, success, message):
self.success = success
self.message = message

def __str__(self):
return self.message


class FileErrorResult(BaseResult):
"""A result that indicates there was an error
operating on a file."""

def __init__(self, message):
super(FileErrorResult, self).__init__(False, message)
2 changes: 0 additions & 2 deletions hinghwa-dict-backend/AudioCompare/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@ def die(msg):

def warn(msg):
print(sys.stderr, "ERROR: {e}".format(e=msg))


2 changes: 1 addition & 1 deletion hinghwa-dict-backend/HinghwaDict/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'HinghwaDict.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "HinghwaDict.settings")

application = get_asgi_application()
Loading

0 comments on commit 60f3763

Please sign in to comment.