Merge branch 'master' of https://github.com/dae/anki
|
@ -1,7 +1,11 @@
|
||||||
Please see the README file for basic requirements.
|
Please see the README file for basic requirements.
|
||||||
|
|
||||||
You also need to have the python-pyqt development packages installed
|
In addition to the basic requirements, you also need the PyQt development
|
||||||
(specifically, you need the binary pyuic4).
|
tools (specifically pyrcc4 and pyuic4). These are often contained in a
|
||||||
|
separate package on Linux, such as 'pyqt4-dev-tools' on Debian/Ubuntu. On a Mac
|
||||||
|
they are part of the PyQt source install.
|
||||||
|
|
||||||
|
Windows users, please see the note at the bottom of this file before proceeding.
|
||||||
|
|
||||||
To use the development version:
|
To use the development version:
|
||||||
|
|
||||||
|
@ -9,7 +13,11 @@ $ git clone https://github.com/dae/anki.git
|
||||||
$ cd anki
|
$ cd anki
|
||||||
$ ./tools/build_ui.sh
|
$ ./tools/build_ui.sh
|
||||||
|
|
||||||
Make sure you rebuild the UI every time you update the sources.
|
If you get any errors, you will not be able to proceed, so please return to
|
||||||
|
the top and check the requirements again.
|
||||||
|
|
||||||
|
ALL USERS: Make sure you rebuild the UI every time you git pull, otherwise you
|
||||||
|
will get errors down the road.
|
||||||
|
|
||||||
The translations are stored in a bazaar repo for integration with Launchpad's
|
The translations are stored in a bazaar repo for integration with Launchpad's
|
||||||
translation services. If you want to use a language other than English:
|
translation services. If you want to use a language other than English:
|
||||||
|
@ -31,3 +39,20 @@ Before contributing code, please read the LICENSE file.
|
||||||
|
|
||||||
If you'd like to contribute translations, please see the translations section
|
If you'd like to contribute translations, please see the translations section
|
||||||
of http://ankisrs.net/docs/manual.html#_contributing
|
of http://ankisrs.net/docs/manual.html#_contributing
|
||||||
|
|
||||||
|
WINDOWS USERS:
|
||||||
|
|
||||||
|
I have not tested the build scripts on Windows, so you'll need to solve any
|
||||||
|
problems you encounter on your own. The easiest way is to use a source
|
||||||
|
tarball instead of git, as that way you don't need to build the UI yourself.
|
||||||
|
If you do want to use git, a user contributed the following, which should get
|
||||||
|
you most of the way there:
|
||||||
|
|
||||||
|
1) Install "git bash".
|
||||||
|
2) In the tools directory, modify build_ui.sh. Locate the line that reads
|
||||||
|
"pyuic4 $i -o $py" and alter it to be of the following form:
|
||||||
|
"<python-path-string>" "<pyuic-path-string>" $i -o $py
|
||||||
|
These two paths must point to your python executable, and to pyuic.py, on your
|
||||||
|
system. Typical paths would be:
|
||||||
|
<python-path> = C:\\Python27\\python.exe
|
||||||
|
<pyuic-path-string> = C:\\Python27\\Lib\\site-packages\\PyQt4\\uic\\pyuic.py
|
||||||
|
|
|
@ -30,6 +30,6 @@ if arch[1] == "ELF":
|
||||||
sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % (
|
sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % (
|
||||||
sys.version_info[1], arch[0][0:2])))
|
sys.version_info[1], arch[0][0:2])))
|
||||||
|
|
||||||
version="2.0.19" # build scripts grep this line, so preserve formatting
|
version="2.0.26" # build scripts grep this line, so preserve formatting
|
||||||
from anki.storage import Collection
|
from anki.storage import Collection
|
||||||
__all__ = ["Collection"]
|
__all__ = ["Collection"]
|
||||||
|
|
|
@ -1,4 +1,142 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFFjCCA/6gAwIBAgIQRi3682cfg0pV4BcKiOriGTANBgkqhkiG9w0BAQUFADBy
|
||||||
|
MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
|
||||||
|
VQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEYMBYGA1UE
|
||||||
|
AxMPRXNzZW50aWFsU1NMIENBMB4XDTE0MDQxMDAwMDAwMFoXDTE3MDQwOTIzNTk1
|
||||||
|
OVowWzEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMR4wHAYDVQQL
|
||||||
|
ExVFc3NlbnRpYWxTU0wgV2lsZGNhcmQxFjAUBgNVBAMUDSouYW5raXdlYi5uZXQw
|
||||||
|
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+A1hdxZ8NNorbzRF9qutY
|
||||||
|
7DNuKQdXRjMuRszAeAHI8Ln6+zfvMvLPvCHrZ9aDNTpKhAQBuE7NrFkR9oR0xHT8
|
||||||
|
JtgVQKJ5zvVFfZmNoHyWYW0+dj8ay1Y/74V/I4Xhb43Fk4Q7UBgAl0kwm5vDWC6U
|
||||||
|
HsA9/KPfEbWCOLSVoA1sU60viggnfbIl0XmNkkt355KvUdJgT5LisOfi8KvH58RN
|
||||||
|
X8RHDsLI+Kv3rc11hLxwJ171NC0bPvjOIvQ5NKlNeDFZASx00kaGE3H26r1it2Gr
|
||||||
|
hrR8IlTSV967JDpYi1FO+Aom54/OZ6ozE/JNsbKlcwmdH2CO2aMizFZfoQmYEsyr
|
||||||
|
AgMBAAGjggG9MIIBuTAfBgNVHSMEGDAWgBTay+qtWwhdzP/8JlTOSeVVxjj0+DAd
|
||||||
|
BgNVHQ4EFgQU8P8DDEM/4k6bCQr8Z9BeoGw5ANgwDgYDVR0PAQH/BAQDAgWgMAwG
|
||||||
|
A1UdEwEB/wQCMAAwNAYDVR0lBC0wKwYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEE
|
||||||
|
AYI3CgMDBglghkgBhvhCBAEwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQICBzArMCkG
|
||||||
|
CCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAIBgZngQwB
|
||||||
|
AgEwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5jb21vZG9jYS5jb20vRXNz
|
||||||
|
ZW50aWFsU1NMQ0EuY3JsMG4GCCsGAQUFBwEBBGIwYDA4BggrBgEFBQcwAoYsaHR0
|
||||||
|
cDovL2NydC5jb21vZG9jYS5jb20vRXNzZW50aWFsU1NMQ0FfMi5jcnQwJAYIKwYB
|
||||||
|
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTAlBgNVHREEHjAcgg0qLmFu
|
||||||
|
a2l3ZWIubmV0ggthbmtpd2ViLm5ldDANBgkqhkiG9w0BAQUFAAOCAQEAR2JPdym8
|
||||||
|
MsSqmi2EyB4zR/UZH7XQUdIwx/1NhKf1XTN9akTNsS2y6Vp+TvaRVknEm7Z1i8CU
|
||||||
|
xiSZicsUOUr8MCzDVtTl3KUuYNUdsv0yXwTvGc01xJ26ix+KTmmQVKBq86gYGzXI
|
||||||
|
pLG0mfG1UAdZc6MxhcPXDROppvGXWk2vb6xYryVQiqV45SCiX2a4PtIf8zqO62eD
|
||||||
|
Xqazh3fpJ685rBnvWvpi8teYlYbJpJoaFEHa5ime2VixEYPVnfY2DBbiLQVImCgF
|
||||||
|
4NLwbCh79uD90CiGXHZbVUCq8fNf2gYzhK08erbDWhK39DzkBcE1XlbsPYzN9fUL
|
||||||
|
E1Re6AD7oBwALQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFAzCCA+ugAwIBAgIQGLLLuqME8aAPwfLzJkYqSjANBgkqhkiG9w0BAQUFADCB
|
||||||
|
gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||||
|
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
|
||||||
|
BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
|
||||||
|
MDBaFw0xOTEyMzEyMzU5NTlaMHIxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVh
|
||||||
|
dGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9E
|
||||||
|
TyBDQSBMaW1pdGVkMRgwFgYDVQQDEw9Fc3NlbnRpYWxTU0wgQ0EwggEiMA0GCSqG
|
||||||
|
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt8AiwcsargxIxF3CJhakgEtSYau2A1NHf
|
||||||
|
5I5ZLdOWIY120j8YC0YZYwvHIPPlC92AGvFaoL0dds23Izp0XmEbdaqb1IX04XiR
|
||||||
|
0y3hr/yYLgbSeT1awB8hLRyuIVPGOqchfr7tZ291HRqfalsGs2rjsQuqag7nbWzD
|
||||||
|
ypWMN84hHzWQfdvaGlyoiBSyD8gSIF/F03/o4Tjg27z5H6Gq1huQByH6RSRQXScq
|
||||||
|
oChBRVt9vKCiL6qbfltTxfEFFld+Edc7tNkBdtzffRDPUanlOPJ7FAB1WfnwWdsX
|
||||||
|
Pvev5gItpHnBXaIcw5rIp6gLSApqLn8tl2X2xQScRMiZln5+pN0vAgMBAAGjggGD
|
||||||
|
MIIBfzAfBgNVHSMEGDAWgBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAdBgNVHQ4EFgQU
|
||||||
|
2svqrVsIXcz//CZUzknlVcY49PgwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI
|
||||||
|
MAYBAf8CAQAwIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMD4GA1Ud
|
||||||
|
IAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21v
|
||||||
|
ZG8uY29tL0NQUzBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2Nh
|
||||||
|
LmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBsBggrBgEFBQcB
|
||||||
|
AQRgMF4wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NvbW9k
|
||||||
|
b1VUTlNHQ0NBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2Eu
|
||||||
|
Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAtlzR6QDLqcJcvgTtLeRJ3rvuq1xqo2l/z
|
||||||
|
odueTZbLN3qo6u6bldudu+Ennv1F7Q5Slqz0J790qpL0pcRDAB8OtXj5isWMcL2a
|
||||||
|
ejGjKdBZa0wztSz4iw+SY1dWrCRnilsvKcKxudokxeRiDn55w/65g+onO7wdQ7Vu
|
||||||
|
F6r7yJiIatnyfKH2cboZT7g440LX8NqxwCPf3dfxp+0Jj1agq8MLy6SSgIGSH6lv
|
||||||
|
+Wwz3D5XxqfyH8wqfOQsTEZf6/Nh9yvENZ+NWPU6g0QO2JOsTGvMd/QDzczc4BxL
|
||||||
|
XSXaPV7Od4rhPsbXlM1wSTz/Dr0ISKvlUhQVnQ6cGodWaK2cCQBk
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEqzCCA5OgAwIBAgIQLnmDLpCIh+qLjvMabuZ6RDANBgkqhkiG9w0BAQUFADCB
|
||||||
|
kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
|
||||||
|
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
|
||||||
|
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw
|
||||||
|
IFNHQzAeFw0wNjEyMDEwMDAwMDBaFw0yMDA1MzAxMDQ4MzhaMIGBMQswCQYDVQQG
|
||||||
|
EwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm
|
||||||
|
b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RP
|
||||||
|
IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||||
|
MIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZ
|
||||||
|
rts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAh
|
||||||
|
TaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23Iw
|
||||||
|
ambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVD
|
||||||
|
iOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ
|
||||||
|
0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4IBCTCCAQUwHwYDVR0jBBgw
|
||||||
|
FoAUUzLRs89/+uDxoF2FTpLSnkUdtE8wHQYDVR0OBBYEFAtY5YvGTBU3pECpMKkh
|
||||||
|
vkc2Wlb/MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MCAGA1UdJQQZ
|
||||||
|
MBcGCisGAQQBgjcKAwMGCWCGSAGG+EIEATARBgNVHSAECjAIMAYGBFUdIAAwbQYD
|
||||||
|
VR0fBGYwZDAxoC+gLYYraHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLURBVEFD
|
||||||
|
b3JwU0dDLmNybDAvoC2gK4YpaHR0cDovL2NybC5jb21vZG8ubmV0L1VUTi1EQVRB
|
||||||
|
Q29ycFNHQy5jcmwwDQYJKoZIhvcNAQEFBQADggEBANheksSuFNxDrcKkw2dFBx35
|
||||||
|
N6IZxxw3NZETHAfEfUKmDvCGXENrDkTPviRhOkKpzp1Mr3k5cN0OBCBOlZw83rdg
|
||||||
|
umNDQO1qD4FJRrsek8BL8/jhNkkbb7YMDfKQV4r8bZPyKMf6hgoosxcOWYoutr/N
|
||||||
|
4axMZmzyVZFWtzK/seR9teg6ti/bspzaUJOOTsWsmn5cnhI8O03GUHCzZSuO92uh
|
||||||
|
uyXAALv17BZlgQ771KMhlneaqHS8U6rCOVD/CwIJYcyVt9eIavZcxWjTFJUaR1/Z
|
||||||
|
+y3kL48ThqsxE0ATrG7ttRAwixtQqc7ujMrrfLW5Fj3U+m+SbR6ivfsCSsVwvvE=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEpjCCA46gAwIBAgIQRurwlgVMxeP6Zepun0LGZDANBgkqhkiG9w0BAQUFADBv
|
||||||
|
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
|
||||||
|
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
|
||||||
|
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
|
||||||
|
gZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
|
||||||
|
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
|
||||||
|
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMRswGQYDVQQDExJVVE4gLSBEQVRBQ29y
|
||||||
|
cCBTR0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDf7lgQoituVcSO
|
||||||
|
vy5GCefgCA8uK3oTlBu99raAjmUFkwAevK/iD44ZDRJH7Kyto/oucPjebvtWQhWe
|
||||||
|
LlzvI94huQV2JxkPT9bDnLS+lBlj8qYRCutTSJy+8ik7FugaoEymyfQYWWjAcPJT
|
||||||
|
AMBeUIKlVm82+UrgRIagTU7WR25JSstn16bEBbmOHvT8/83nNuCcBWyyMyIV0LTg
|
||||||
|
zBfAssD0/jI/KSqVe9jyp04PVHyhDYCzCQPB/1zdXpo+vK68R4pqrnHKH7EquF9C
|
||||||
|
BQvsRjDRcgvK6VZt9e/feL5hurKlrgRMvKisaRWXve/rtIy/NfjUw9EoDlw6n3AY
|
||||||
|
MyB3xKKvAgMBAAGjggEXMIIBEzAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
|
||||||
|
JMtUGjAdBgNVHQ4EFgQUUzLRs89/+uDxoF2FTpLSnkUdtE8wDgYDVR0PAQH/BAQD
|
||||||
|
AgEGMA8GA1UdEwEB/wQFMAMBAf8wIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZI
|
||||||
|
AYb4QgQBMBEGA1UdIAQKMAgwBgYEVR0gADB7BgNVHR8EdDByMDigNqA0hjJodHRw
|
||||||
|
Oi8vY3JsLmNvbW9kb2NhLmNvbS9BZGRUcnVzdEV4dGVybmFsQ0FSb290LmNybDA2
|
||||||
|
oDSgMoYwaHR0cDovL2NybC5jb21vZG8ubmV0L0FkZFRydXN0RXh0ZXJuYWxDQVJv
|
||||||
|
b3QuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBjhpIQsRP6N76OKrYbikP1XK4OFN/3
|
||||||
|
aUB/vxpxAAnYv9QkSr/gk/8B2AvGD+x+R5ywXfd8FJ38wDOShFvSg/RS4iJYdPxD
|
||||||
|
Gz+no1jaA/288Drk7cwSu8m5rnsEoARyv+neLdKnUWYAc9K9fqqeU5Z9abIYPo6t
|
||||||
|
VlB+99Ww/zliZYKMllfDj/dg9sKNNIf8T0Pl278cqvaGzebfET+NB/dtgxPAOIg5
|
||||||
|
YKF+MOHjiD6ku2NvLOmKaCzulmmsBGHhT04OnXJM9nk4yMdIaW+UD3S0vMjPV025
|
||||||
|
dXGWDYoGC+vd0PA8fcYumEZqOMcCtci4smV13tqQCLZ3uFMAJctHynNf
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
|
||||||
|
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
|
||||||
|
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
|
||||||
|
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
|
||||||
|
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
|
||||||
|
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
|
||||||
|
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
|
||||||
|
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
|
||||||
|
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
|
||||||
|
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
|
||||||
|
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
|
||||||
|
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
|
||||||
|
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
|
||||||
|
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
|
||||||
|
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
|
||||||
|
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
|
||||||
|
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
|
||||||
|
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
|
||||||
|
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
|
||||||
|
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
|
||||||
|
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
||||||
|
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
||||||
|
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw
|
MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw
|
||||||
cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||||
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV
|
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV
|
||||||
|
@ -53,3 +191,59 @@ Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
||||||
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
||||||
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEhjCCA26gAwIBAgIQUkIGSk83/kNpSHqWZ/9dJzANBgkqhkiG9w0BAQUFADBv
|
||||||
|
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
|
||||||
|
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
|
||||||
|
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
|
||||||
|
gZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
|
||||||
|
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
|
||||||
|
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR8wHQYDVQQDExZVVE4tVVNFUkZpcnN0
|
||||||
|
LUhhcmR3YXJlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsffDOD+0
|
||||||
|
qH/POYJRZ9Btn9L/WPPnnyvsDYlUmbk4mRb34CF5SMK7YXQSlh08anLVPBBnOjnt
|
||||||
|
KxPNZuuVCTOkbJex6MbswXV5nEZejavQav25KlUXEFSzGfCa9vGxXbanbfvgcRdr
|
||||||
|
ooj7AN/+GjF3DJoBerEy4ysBBzhuw6VeI7xFm3tQwckwj9vlK3rTW/szQB6g1ZgX
|
||||||
|
vIuHw4nTXaCOsqqq9o5piAbF+okh8widaS4JM5spDUYPjMxJNLBpUb35Bs1orWZM
|
||||||
|
vD6sYb0KiA7I3z3ufARMnQpea5HW7sftKI2rTYeJc9BupNAeFosU4XZEA39jrOTN
|
||||||
|
SZzFkvSrMqFIWwIDAQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8D
|
||||||
|
veAky1QaMB0GA1UdDgQWBBShcl8mGyiYQ5VdBzfVhZadS9LDRTAOBgNVHQ8BAf8E
|
||||||
|
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwewYDVR0f
|
||||||
|
BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQWRkVHJ1c3RFeHRl
|
||||||
|
cm5hbENBUm9vdC5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BZGRU
|
||||||
|
cnVzdEV4dGVybmFsQ0FSb290LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAYGQ5WaJD
|
||||||
|
ZS79+R/WrjO76FMTxIjuIxpszthkWVNTkOg239T88055L9XmjwzvKkFtcb2beDgj
|
||||||
|
03BLhgz9EqciYhLYzOBR7y3lzQxFoura7X7s9zKa5wU1Xm7CLGhonf+M8cpVh8Qv
|
||||||
|
sUAG3IQiXG2zzdGbGgozKGYWDL0zwvYH8eOheZTg+NDQ099Shj+p4ckdPoaEsdtf
|
||||||
|
7uRJQ8E5fc8vlqd1XX5nZ4TlWSBAvzcivwdDtDDhQ4rNA11tuSnZhKf1YmOEhtY3
|
||||||
|
vm9nu/9iVzmdDE2yKmE9HZzvmncgoC/uGnKdsJ2/eBMnBwpgEZP1Dy7J72skg/6b
|
||||||
|
kLRLaIHQwvrgPw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFAzCCA+ugAwIBAgIQTM1KmltFEyGMz5AviytRcTANBgkqhkiG9w0BAQUFADCB
|
||||||
|
lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
|
||||||
|
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
|
||||||
|
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt
|
||||||
|
SGFyZHdhcmUwHhcNMDYwOTE4MDAwMDAwWhcNMjAwNTMwMTA0ODM4WjBxMQswCQYD
|
||||||
|
VQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdT
|
||||||
|
YWxmb3JkMRowGAYDVQQKExFDb21vZG8gQ0EgTGltaXRlZDEXMBUGA1UEAxMOUG9z
|
||||||
|
aXRpdmVTU0wgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9T3lY
|
||||||
|
IpPJKD5SEQAvwKkgitctVR4Q57h/4oYqpOxe6eSSWJZUDfMXukGeFZFV78LuACAY
|
||||||
|
RYMm3yDMPbOhEzEKIVx5g3mrJBVcVvC0lZih2tIb6ha1y7ewwVP5pEba8C4kuGKe
|
||||||
|
joteK1qWoOpQ6Yj7KCpNmpxIT4O2h65Pxci12f2+P9GnncYsEw3AAcezcPOPabuw
|
||||||
|
PBDf6wkAhD9u7/zjLbTHXRHM9/Lx9uLjAH4SDt6NfQDKOj32cuh5JaYIFveriP9W
|
||||||
|
XgkXwFqCBWI0KyhIMpfQhAysExjbnmbHqhSLEWlN8QnTul2piDdi2L8Dm53X5gV+
|
||||||
|
wmpSqo0HgOqODvMdAgMBAAGjggFuMIIBajAfBgNVHSMEGDAWgBShcl8mGyiYQ5Vd
|
||||||
|
BzfVhZadS9LDRTAdBgNVHQ4EFgQUuMoR6QYxedvDlMboGSq8uzUWMaQwDgYDVR0P
|
||||||
|
AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwewYDVR0fBHQwcjA4oDagNIYy
|
||||||
|
aHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLVVTRVJGaXJzdC1IYXJkd2FyZS5j
|
||||||
|
cmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9VVE4tVVNFUkZpcnN0LUhh
|
||||||
|
cmR3YXJlLmNybDCBhgYIKwYBBQUHAQEEejB4MDsGCCsGAQUFBzAChi9odHRwOi8v
|
||||||
|
Y3J0LmNvbW9kb2NhLmNvbS9VVE5BZGRUcnVzdFNlcnZlckNBLmNydDA5BggrBgEF
|
||||||
|
BQcwAoYtaHR0cDovL2NydC5jb21vZG8ubmV0L1VUTkFkZFRydXN0U2VydmVyQ0Eu
|
||||||
|
Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQAdtOf5GEhd7fpawx3jt++GFclsE0kWDTGM
|
||||||
|
MVzn2odkjq8SFqRaLZIaOz4hZaoXw5V+QBz9FGkGGM2sMexq8RaeiSY9WyGN6Oj5
|
||||||
|
qz2qPMuZ8oZfiFMVBRflqNKFp05Jfdbdx4/OiL9lBeAUtTF37r0qhujop2ot2mUZ
|
||||||
|
jGfibfZKhWaDtjJNn0IjF9dFQWp2BNStuY9u3MI+6VHyntjzf/tQKvCL/W8NIjYu
|
||||||
|
zg5G8t6P2jt9HpOs/PQyKw+rAR+lQI/jJJkfXbKqDLnioeeSDJBLU30fKO5WPa8Y
|
||||||
|
Z0nf1R7CqJgrTEeDgUwuRMLvyGPui3tbMfYmYb95HLCpTqnJUHvi
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
|
@ -73,9 +73,8 @@ class _Collection(object):
|
||||||
self.crt = int(time.mktime(d.timetuple()))
|
self.crt = int(time.mktime(d.timetuple()))
|
||||||
self.sched = Scheduler(self)
|
self.sched = Scheduler(self)
|
||||||
if not self.conf.get("newBury", False):
|
if not self.conf.get("newBury", False):
|
||||||
mod = self.db.mod
|
self.conf['newBury'] = True
|
||||||
self.sched.unburyCards()
|
self.setMod()
|
||||||
self.db.mod = mod
|
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
n = os.path.splitext(os.path.basename(self.path))[0]
|
n = os.path.splitext(os.path.basename(self.path))[0]
|
||||||
|
@ -148,10 +147,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
|
||||||
def close(self, save=True):
|
def close(self, save=True):
|
||||||
"Disconnect from DB."
|
"Disconnect from DB."
|
||||||
if self.db:
|
if self.db:
|
||||||
if not self.conf.get("newBury", False):
|
|
||||||
mod = self.db.mod
|
|
||||||
self.sched.unburyCards()
|
|
||||||
self.db.mod = mod
|
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
else:
|
else:
|
||||||
|
@ -515,13 +510,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
|
||||||
afmt = afmt or template['afmt']
|
afmt = afmt or template['afmt']
|
||||||
for (type, format) in (("q", qfmt), ("a", afmt)):
|
for (type, format) in (("q", qfmt), ("a", afmt)):
|
||||||
if type == "q":
|
if type == "q":
|
||||||
format = format.replace("{{cloze:", "{{cq:%d:" % (
|
format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format)
|
||||||
data[4]+1))
|
|
||||||
format = format.replace("<%cloze:", "<%%cq:%d:" % (
|
format = format.replace("<%cloze:", "<%%cq:%d:" % (
|
||||||
data[4]+1))
|
data[4]+1))
|
||||||
else:
|
else:
|
||||||
format = format.replace("{{cloze:", "{{ca:%d:" % (
|
format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format)
|
||||||
data[4]+1))
|
|
||||||
format = format.replace("<%cloze:", "<%%ca:%d:" % (
|
format = format.replace("<%cloze:", "<%%ca:%d:" % (
|
||||||
data[4]+1))
|
data[4]+1))
|
||||||
fields['FrontSide'] = stripSounds(d['q'])
|
fields['FrontSide'] = stripSounds(d['q'])
|
||||||
|
@ -613,13 +606,19 @@ where c.nid == f.id
|
||||||
if self._undo[0] == 1:
|
if self._undo[0] == 1:
|
||||||
old = self._undo[2]
|
old = self._undo[2]
|
||||||
self.clearUndo()
|
self.clearUndo()
|
||||||
self._undo = [1, _("Review"), old + [copy.copy(card)]]
|
wasLeech = card.note().hasTag("leech") or False
|
||||||
|
self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
|
||||||
|
|
||||||
def _undoReview(self):
|
def _undoReview(self):
|
||||||
data = self._undo[2]
|
data = self._undo[2]
|
||||||
|
wasLeech = self._undo[3]
|
||||||
c = data.pop()
|
c = data.pop()
|
||||||
if not data:
|
if not data:
|
||||||
self.clearUndo()
|
self.clearUndo()
|
||||||
|
# remove leech tag if it didn't have it before
|
||||||
|
if not wasLeech and c.note().hasTag("leech"):
|
||||||
|
c.note().delTag("leech")
|
||||||
|
c.note().flush()
|
||||||
# write old data
|
# write old data
|
||||||
c.flush()
|
c.flush()
|
||||||
# and delete revlog entry
|
# and delete revlog entry
|
||||||
|
@ -696,6 +695,10 @@ select id from notes where mid not in """ + ids2str(self.models.ids()))
|
||||||
self.remNotes(ids)
|
self.remNotes(ids)
|
||||||
# for each model
|
# for each model
|
||||||
for m in self.models.all():
|
for m in self.models.all():
|
||||||
|
for t in m['tmpls']:
|
||||||
|
if t['did'] == "None":
|
||||||
|
t['did'] = None
|
||||||
|
problems.append(_("Fixed AnkiDroid deck override bug."))
|
||||||
if m['type'] == MODEL_STD:
|
if m['type'] == MODEL_STD:
|
||||||
# model with missing req specification
|
# model with missing req specification
|
||||||
if 'req' not in m:
|
if 'req' not in m:
|
||||||
|
@ -753,6 +756,17 @@ select id from cards where odue > 0 and (type=1 or queue=2) and not odid""")
|
||||||
"Fixed %d cards with invalid properties.", cnt) % cnt)
|
"Fixed %d cards with invalid properties.", cnt) % cnt)
|
||||||
self.db.execute("update cards set odue=0 where id in "+
|
self.db.execute("update cards set odue=0 where id in "+
|
||||||
ids2str(ids))
|
ids2str(ids))
|
||||||
|
# cards with odid set when not in a dyn deck
|
||||||
|
dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)]
|
||||||
|
ids = self.db.list("""
|
||||||
|
select id from cards where odid > 0 and did in %s""" % ids2str(dids))
|
||||||
|
if ids:
|
||||||
|
cnt = len(ids)
|
||||||
|
problems.append(
|
||||||
|
ngettext("Fixed %d card with invalid properties.",
|
||||||
|
"Fixed %d cards with invalid properties.", cnt) % cnt)
|
||||||
|
self.db.execute("update cards set odid=0, odue=0 where id in "+
|
||||||
|
ids2str(ids))
|
||||||
# tags
|
# tags
|
||||||
self.tags.registerNotes()
|
self.tags.registerNotes()
|
||||||
# field cache
|
# field cache
|
||||||
|
|
|
@ -38,6 +38,8 @@ DYN_DUE = 6
|
||||||
DYN_REVADDED = 7
|
DYN_REVADDED = 7
|
||||||
DYN_DUEPRIORITY = 8
|
DYN_DUEPRIORITY = 8
|
||||||
|
|
||||||
|
DYN_MAX_SIZE = 99999
|
||||||
|
|
||||||
# model types
|
# model types
|
||||||
MODEL_STD = 0
|
MODEL_STD = 0
|
||||||
MODEL_CLOZE = 1
|
MODEL_CLOZE = 1
|
||||||
|
|
|
@ -197,6 +197,12 @@ class DeckManager(object):
|
||||||
deck['collapsed'] = not deck['collapsed']
|
deck['collapsed'] = not deck['collapsed']
|
||||||
self.save(deck)
|
self.save(deck)
|
||||||
|
|
||||||
|
def collapseBrowser(self, did):
|
||||||
|
deck = self.get(did)
|
||||||
|
collapsed = deck.get('browserCollapsed', False)
|
||||||
|
deck['browserCollapsed'] = not collapsed
|
||||||
|
self.save(deck)
|
||||||
|
|
||||||
def count(self):
|
def count(self):
|
||||||
return len(self.decks)
|
return len(self.decks)
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,12 @@ class Exporter(object):
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
def escapeText(self, text):
|
def escapeText(self, text):
|
||||||
"Escape newlines, tabs and CSS."
|
"Escape newlines, tabs, CSS and quotechar."
|
||||||
text = text.replace("\n", "<br>")
|
text = text.replace("\n", "<br>")
|
||||||
text = text.replace("\t", " " * 8)
|
text = text.replace("\t", " " * 8)
|
||||||
text = re.sub("(?i)<style>.*?</style>", "", text)
|
text = re.sub("(?i)<style>.*?</style>", "", text)
|
||||||
|
if "\"" in text:
|
||||||
|
text = "\"" + text.replace("\"", "\"\"") + "\""
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def cardIds(self):
|
def cardIds(self):
|
||||||
|
@ -134,8 +136,14 @@ class AnkiExporter(Exporter):
|
||||||
data)
|
data)
|
||||||
# notes
|
# notes
|
||||||
strnids = ids2str(nids.keys())
|
strnids = ids2str(nids.keys())
|
||||||
notedata = self.src.db.all("select * from notes where id in "+
|
notedata = []
|
||||||
strnids)
|
for row in self.src.db.all(
|
||||||
|
"select * from notes where id in "+strnids):
|
||||||
|
# remove system tags if not exporting scheduling info
|
||||||
|
if not self.includeSched:
|
||||||
|
row = list(row)
|
||||||
|
row[5] = self.removeSystemTags(row[5])
|
||||||
|
notedata.append(row)
|
||||||
self.dst.db.executemany(
|
self.dst.db.executemany(
|
||||||
"insert into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
"insert into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
notedata)
|
notedata)
|
||||||
|
@ -206,6 +214,9 @@ class AnkiExporter(Exporter):
|
||||||
# overwrite to apply customizations to the deck before it's closed,
|
# overwrite to apply customizations to the deck before it's closed,
|
||||||
# such as update the deck description
|
# such as update the deck description
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def removeSystemTags(self, tags):
|
||||||
|
return self.src.tags.remFromStr("marked leech", tags)
|
||||||
|
|
||||||
# Packaged Anki decks
|
# Packaged Anki decks
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -18,7 +18,7 @@ class MnemosyneImporter(NoteImporter):
|
||||||
db = DB(self.file)
|
db = DB(self.file)
|
||||||
ver = db.scalar(
|
ver = db.scalar(
|
||||||
"select value from global_variables where key='version'")
|
"select value from global_variables where key='version'")
|
||||||
assert ver.startswith('Mnemosyne SQL 1')
|
assert ver.startswith('Mnemosyne SQL 1') or ver == "2"
|
||||||
# gather facts into temp objects
|
# gather facts into temp objects
|
||||||
curid = None
|
curid = None
|
||||||
notes = {}
|
notes = {}
|
||||||
|
|
|
@ -140,65 +140,29 @@ class SupermemoXmlImporter(NoteImporter):
|
||||||
#s = re.sub(u'>',u'>',s)
|
#s = re.sub(u'>',u'>',s)
|
||||||
#s = re.sub(u'<',u'<',s)
|
#s = re.sub(u'<',u'<',s)
|
||||||
|
|
||||||
return unicode(btflsoup(s,convertEntities=btflsoup.HTML_ENTITIES ))
|
return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES))
|
||||||
|
|
||||||
|
def _afactor2efactor(self, af):
|
||||||
|
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
|
||||||
|
|
||||||
def _unescape(self,s,initilize):
|
# Ranges for A-factors and E-factors
|
||||||
"""Note: This method is not used, BeautifulSoup does better job.
|
af_min = 1.2
|
||||||
"""
|
af_max = 6.9
|
||||||
|
ef_min = 1.3
|
||||||
|
ef_max = 3.3
|
||||||
|
|
||||||
if self._unescape_trtable == None:
|
# Sanity checks for the A-factor
|
||||||
self._unescape_trtable = (
|
if af < af_min:
|
||||||
('€',u'€'), (' ',u' '), ('!',u'!'), ('"',u'"'), ('#',u'#'), ('$',u'$'), ('%',u'%'), ('&',u'&'), (''',u"'"),
|
af = af_min
|
||||||
('(',u'('), (')',u')'), ('*',u'*'), ('+',u'+'), (',',u','), ('-',u'-'), ('.',u'.'), ('/',u'/'), ('0',u'0'),
|
elif af > af_max:
|
||||||
('1',u'1'), ('2',u'2'), ('3',u'3'), ('4',u'4'), ('5',u'5'), ('6',u'6'), ('7',u'7'), ('8',u'8'), ('9',u'9'),
|
af = af_max
|
||||||
(':',u':'), (';',u';'), ('<',u'<'), ('=',u'='), ('>',u'>'), ('?',u'?'), ('@',u'@'), ('A',u'A'), ('B',u'B'),
|
|
||||||
('C',u'C'), ('D',u'D'), ('E',u'E'), ('F',u'F'), ('G',u'G'), ('H',u'H'), ('I',u'I'), ('J',u'J'), ('K',u'K'),
|
|
||||||
('L',u'L'), ('M',u'M'), ('N',u'N'), ('O',u'O'), ('P',u'P'), ('Q',u'Q'), ('R',u'R'), ('S',u'S'), ('T',u'T'),
|
|
||||||
('U',u'U'), ('V',u'V'), ('W',u'W'), ('X',u'X'), ('Y',u'Y'), ('Z',u'Z'), ('[',u'['), ('\',u'\\'), (']',u']'),
|
|
||||||
('^',u'^'), ('_',u'_'), ('`',u'`'), ('a',u'a'), ('b',u'b'), ('c',u'c'), ('d',u'd'), ('e',u'e'), ('f',u'f'),
|
|
||||||
('g',u'g'), ('h',u'h'), ('i',u'i'), ('j',u'j'), ('k',u'k'), ('l',u'l'), ('m',u'm'), ('n',u'n'),
|
|
||||||
('o',u'o'), ('p',u'p'), ('q',u'q'), ('r',u'r'), ('s',u's'), ('t',u't'), ('u',u'u'), ('v',u'v'),
|
|
||||||
('w',u'w'), ('x',u'x'), ('y',u'y'), ('z',u'z'), ('{',u'{'), ('|',u'|'), ('}',u'}'), ('~',u'~'),
|
|
||||||
(' ',u' '), ('¡',u'¡'), ('¢',u'¢'), ('£',u'£'), ('¤',u'¤'), ('¥',u'¥'), ('¦',u'¦'), ('§',u'§'),
|
|
||||||
('¨',u'¨'), ('©',u'©'), ('ª',u'ª'), ('«',u'«'), ('¬',u'¬'), ('­',u''), ('®',u'®'), ('¯',u'¯'),
|
|
||||||
('°',u'°'), ('±',u'±'), ('²',u'²'), ('³',u'³'), ('´',u'´'), ('µ',u'µ'), ('¶',u'¶'), ('·',u'·'),
|
|
||||||
('¸',u'¸'), ('¹',u'¹'), ('º',u'º'), ('»',u'»'), ('¼',u'¼'), ('½',u'½'), ('¾',u'¾'), ('¿',u'¿'),
|
|
||||||
('À',u'À'), ('Á',u'Á'), ('Â',u'Â'), ('Ã',u'Ã'), ('Ä',u'Ä'), ('Å',u'Å'), ('Å',u'Å'), ('Æ',u'Æ'),
|
|
||||||
('Ç',u'Ç'), ('È',u'È'), ('É',u'É'), ('Ê',u'Ê'), ('Ë',u'Ë'), ('Ì',u'Ì'), ('Í',u'Í'), ('Î',u'Î'),
|
|
||||||
('Ï',u'Ï'), ('Ð',u'Ð'), ('Ñ',u'Ñ'), ('Ò',u'Ò'), ('Ó',u'Ó'), ('Ô',u'Ô'), ('Õ',u'Õ'), ('Ö',u'Ö'),
|
|
||||||
('×',u'×'), ('Ø',u'Ø'), ('Ù',u'Ù'), ('Ú',u'Ú'), ('Û',u'Û'), ('Ü',u'Ü'), ('Ý',u'Ý'), ('Þ',u'Þ'),
|
|
||||||
('ß',u'ß'), ('à',u'à'), ('á',u'á'), ('â',u'â'), ('ã',u'ã'), ('ä',u'ä'), ('å',u'å'), ('æ',u'æ'),
|
|
||||||
('ç',u'ç'), ('è',u'è'), ('é',u'é'), ('ê',u'ê'), ('ë',u'ë'), ('ì',u'ì'), ('í',u'í'), ('í',u'í'),
|
|
||||||
('î',u'î'), ('ï',u'ï'), ('ð',u'ð'), ('ñ',u'ñ'), ('ò',u'ò'), ('ó',u'ó'), ('ô',u'ô'), ('õ',u'õ'),
|
|
||||||
('ö',u'ö'), ('÷',u'÷'), ('ø',u'ø'), ('ù',u'ù'), ('ú',u'ú'), ('û',u'û'), ('ü',u'ü'), ('ý',u'ý'),
|
|
||||||
('þ',u'þ'), ('ÿ',u'ÿ'), ('Ā',u'Ā'), ('ā',u'ā'), ('Ă',u'Ă'), ('ă',u'ă'), ('Ą',u'Ą'), ('ą',u'ą'),
|
|
||||||
('Ć',u'Ć'), ('ć',u'ć'), ('Ĉ',u'Ĉ'), ('ĉ',u'ĉ'), ('Ċ',u'Ċ'), ('ċ',u'ċ'), ('Č',u'Č'), ('č',u'č'),
|
|
||||||
('Ď',u'Ď'), ('ď',u'ď'), ('Đ',u'Đ'), ('đ',u'đ'), ('Ē',u'Ē'), ('ē',u'ē'), ('Ĕ',u'Ĕ'), ('ĕ',u'ĕ'),
|
|
||||||
('Ė',u'Ė'), ('ė',u'ė'), ('Ę',u'Ę'), ('ę',u'ę'), ('Ě',u'Ě'), ('ě',u'ě'), ('Ĝ',u'Ĝ'), ('ĝ',u'ĝ'),
|
|
||||||
('Ğ',u'Ğ'), ('ğ',u'ğ'), ('Ġ',u'Ġ'), ('ġ',u'ġ'), ('Ģ',u'Ģ'), ('ģ',u'ģ'), ('Ĥ',u'Ĥ'), ('ĥ',u'ĥ'),
|
|
||||||
('Ħ',u'Ħ'), ('ħ',u'ħ'), ('Ĩ',u'Ĩ'), ('ĩ',u'ĩ'), ('Ī',u'Ī'), ('ī',u'ī'), ('Ĭ',u'Ĭ'), ('ĭ',u'ĭ'),
|
|
||||||
('Į',u'Į'), ('į',u'į'), ('İ',u'İ'), ('ı',u'ı'), ('IJ',u'IJ'), ('ij',u'ij'), ('Ĵ',u'Ĵ'), ('ĵ',u'ĵ'),
|
|
||||||
('Ķ',u'Ķ'), ('ķ',u'ķ'), ('ĸ',u'ĸ'), ('Ĺ',u'Ĺ'), ('ĺ',u'ĺ'), ('Ļ',u'Ļ'), ('ļ',u'ļ'), ('Ľ',u'Ľ'),
|
|
||||||
('ľ',u'ľ'), ('Ŀ',u'Ŀ'), ('ŀ',u'ŀ'), ('Ł',u'Ł'), ('ł',u'ł'), ('Ń',u'Ń'), ('ń',u'ń'), ('Ņ',u'Ņ'),
|
|
||||||
('ņ',u'ņ'), ('Ň',u'Ň'), ('ň',u'ň'), ('ʼn',u'ʼn'), ('Ŋ',u'Ŋ'), ('ŋ',u'ŋ'), ('Ō',u'Ō'), ('ō',u'ō'),
|
|
||||||
('Ŏ',u'Ŏ'), ('ŏ',u'ŏ'), ('Ő',u'Ő'), ('ő',u'ő'), ('Œ',u'Œ'), ('œ',u'œ'), ('Ŕ',u'Ŕ'), ('ŕ',u'ŕ'),
|
|
||||||
('Ŗ',u'Ŗ'), ('ŗ',u'ŗ'), ('Ř',u'Ř'), ('ř',u'ř'), ('Ś',u'Ś'), ('ś',u'ś'), ('Ŝ',u'Ŝ'), ('ŝ',u'ŝ'),
|
|
||||||
('Ş',u'Ş'), ('ş',u'ş'), ('Š',u'Š'), ('š',u'š'), ('Ţ',u'Ţ'), ('ţ',u'ţ'), ('Ť',u'Ť'), ('ť',u'ť'),
|
|
||||||
('Ŧ',u'Ŧ'), ('ŧ',u'ŧ'), ('Ũ',u'Ũ'), ('ũ',u'ũ'), ('Ū',u'Ū'), ('ū',u'ū'), ('Ŭ',u'Ŭ'), ('ŭ',u'ŭ'),
|
|
||||||
('Ů',u'Ů'), ('ů',u'ů'), ('Ű',u'Ű'), ('ű',u'ű'), ('Ų',u'Ų'), ('ų',u'ų'), ('Ŵ',u'Ŵ'), ('ŵ',u'ŵ'),
|
|
||||||
('Ŷ',u'Ŷ'), ('ŷ',u'ŷ'), ('Ÿ',u'Ÿ'), ('Ź',u'Ź'), ('ź',u'ź'), ('Ż',u'Ż'), ('ż',u'ż'), ('Ž',u'Ž'),
|
|
||||||
('ž',u'ž'), ('ſ',u'ſ'), ('Ŕ',u'Ŕ'), ('ŕ',u'ŕ'), ('Ŗ',u'Ŗ'), ('ŗ',u'ŗ'), ('Ř',u'Ř'), ('ř',u'ř'),
|
|
||||||
('Ś',u'Ś'), ('ś',u'ś'), ('Ŝ',u'Ŝ'), ('ŝ',u'ŝ'), ('Ş',u'Ş'), ('ş',u'ş'), ('Š',u'Š'), ('š',u'š'),
|
|
||||||
('Ţ',u'Ţ'), ('ţ',u'ţ'), ('Ť',u'Ť'), ('Ɂ',u'ť'), ('Ŧ',u'Ŧ'), ('ŧ',u'ŧ'), ('Ũ',u'Ũ'), ('ũ',u'ũ'),
|
|
||||||
('Ū',u'Ū'), ('ū',u'ū'), ('Ŭ',u'Ŭ'), ('ŭ',u'ŭ'), ('Ů',u'Ů'), ('ů',u'ů'), ('Ű',u'Ű'), ('ű',u'ű'),
|
|
||||||
('Ų',u'Ų'), ('ų',u'ų'), ('Ŵ',u'Ŵ'), ('ŵ',u'ŵ'), ('Ŷ',u'Ŷ'), ('ŷ',u'ŷ'), ('Ÿ',u'Ÿ'), ('Ź',u'Ź'),
|
|
||||||
('ź',u'ź'), ('Ż',u'Ż'), ('ż',u'ż'), ('Ž',u'Ž'), ('ž',u'ž'), ('ſ',u'ſ'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Scale af to the range 0..1
|
||||||
|
af_scaled = (af - af_min) / (af_max - af_min)
|
||||||
|
# Rescale to the interval ef_min..ef_max
|
||||||
|
ef = ef_min + af_scaled * (ef_max - ef_min)
|
||||||
|
|
||||||
#m = re.match()
|
return ef
|
||||||
#s = s.replace(code[0], code[1])
|
|
||||||
|
|
||||||
## DEFAULT IMPORTER METHODS
|
## DEFAULT IMPORTER METHODS
|
||||||
|
|
||||||
|
@ -249,7 +213,7 @@ class SupermemoXmlImporter(NoteImporter):
|
||||||
nextDue = tLastrep + (float(item.Interval) * 86400.0)
|
nextDue = tLastrep + (float(item.Interval) * 86400.0)
|
||||||
remDays = int((nextDue - time.time())/86400)
|
remDays = int((nextDue - time.time())/86400)
|
||||||
card.due = self.col.sched.today+remDays
|
card.due = self.col.sched.today+remDays
|
||||||
card.factor = int(float(item.AFactor.replace(',','.'))*1000)
|
card.factor = int(self._afactor2efactor(float(item.AFactor.replace(',','.')))*1000)
|
||||||
note.cards[0] = card
|
note.cards[0] = card
|
||||||
|
|
||||||
# categories & tags
|
# categories & tags
|
||||||
|
|
|
@ -7,8 +7,13 @@ from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML
|
||||||
from anki.hooks import addHook
|
from anki.hooks import addHook
|
||||||
from anki.lang import _
|
from anki.lang import _
|
||||||
|
|
||||||
latexCmd = ["latex", "-interaction=nonstopmode"]
|
# if you modify these in an add-on, you must make sure to take tmp.tex as the
|
||||||
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"]
|
# input, and output tmp.png as the output file
|
||||||
|
latexCmds = [
|
||||||
|
["latex", "-interaction=nonstopmode", "tmp.tex"],
|
||||||
|
["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"]
|
||||||
|
]
|
||||||
|
|
||||||
build = True # if off, use existing media but don't create new
|
build = True # if off, use existing media but don't create new
|
||||||
regexps = {
|
regexps = {
|
||||||
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
|
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
|
||||||
|
@ -89,14 +94,11 @@ package in the LaTeX header instead.""") % bad
|
||||||
oldcwd = os.getcwd()
|
oldcwd = os.getcwd()
|
||||||
png = namedtmp("tmp.png")
|
png = namedtmp("tmp.png")
|
||||||
try:
|
try:
|
||||||
# generate dvi
|
# generate png
|
||||||
os.chdir(tmpdir())
|
os.chdir(tmpdir())
|
||||||
if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log):
|
for latexCmd in latexCmds:
|
||||||
return _errMsg("latex", texpath)
|
if call(latexCmd, stdout=log, stderr=log):
|
||||||
# and png
|
return _errMsg(latexCmd[0], texpath)
|
||||||
if call(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"],
|
|
||||||
stdout=log, stderr=log):
|
|
||||||
return _errMsg("dvipng", texpath)
|
|
||||||
# add to media
|
# add to media
|
||||||
shutil.copyfile(png, os.path.join(mdir, fname))
|
shutil.copyfile(png, os.path.join(mdir, fname))
|
||||||
return
|
return
|
||||||
|
|
|
@ -217,6 +217,7 @@ class MediaManager(object):
|
||||||
files = os.listdir(mdir)
|
files = os.listdir(mdir)
|
||||||
else:
|
else:
|
||||||
files = local
|
files = local
|
||||||
|
renamedFiles = False
|
||||||
for file in files:
|
for file in files:
|
||||||
if not local:
|
if not local:
|
||||||
if not os.path.isfile(file):
|
if not os.path.isfile(file):
|
||||||
|
@ -236,14 +237,20 @@ class MediaManager(object):
|
||||||
# delete if we already have the NFC form, otherwise rename
|
# delete if we already have the NFC form, otherwise rename
|
||||||
if os.path.exists(nfcFile):
|
if os.path.exists(nfcFile):
|
||||||
os.unlink(file)
|
os.unlink(file)
|
||||||
|
renamedFiles = True
|
||||||
else:
|
else:
|
||||||
os.rename(file, nfcFile)
|
os.rename(file, nfcFile)
|
||||||
|
renamedFiles = True
|
||||||
file = nfcFile
|
file = nfcFile
|
||||||
# compare
|
# compare
|
||||||
if nfcFile not in allRefs:
|
if nfcFile not in allRefs:
|
||||||
unused.append(file)
|
unused.append(file)
|
||||||
else:
|
else:
|
||||||
allRefs.discard(nfcFile)
|
allRefs.discard(nfcFile)
|
||||||
|
# if we renamed any files to nfc format, we must rerun the check
|
||||||
|
# to make sure the renamed files are not marked as unused
|
||||||
|
if renamedFiles:
|
||||||
|
return self.check(local=local)
|
||||||
nohave = [x for x in allRefs if not x.startswith("_")]
|
nohave = [x for x in allRefs if not x.startswith("_")]
|
||||||
return (nohave, unused, invalid)
|
return (nohave, unused, invalid)
|
||||||
|
|
||||||
|
@ -333,7 +340,7 @@ class MediaManager(object):
|
||||||
# Illegal characters
|
# Illegal characters
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0]')
|
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
|
||||||
|
|
||||||
def stripIllegal(self, str):
|
def stripIllegal(self, str):
|
||||||
return re.sub(self._illegalCharReg, "", str)
|
return re.sub(self._illegalCharReg, "", str)
|
||||||
|
|
|
@ -569,7 +569,7 @@ select id from notes where mid = ?)""" % " ".join(map),
|
||||||
sflds = splitFields(flds)
|
sflds = splitFields(flds)
|
||||||
map = self.fieldMap(m)
|
map = self.fieldMap(m)
|
||||||
ords = set()
|
ords = set()
|
||||||
matches = re.findall("{{cloze:(.+?)}}", m['tmpls'][0]['qfmt'])
|
matches = re.findall("{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}", m['tmpls'][0]['qfmt'])
|
||||||
matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt'])
|
matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt'])
|
||||||
for fname in matches:
|
for fname in matches:
|
||||||
if fname not in map:
|
if fname not in map:
|
||||||
|
|
|
@ -954,7 +954,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
|
||||||
def _fillDyn(self, deck):
|
def _fillDyn(self, deck):
|
||||||
search, limit, order = deck['terms'][0]
|
search, limit, order = deck['terms'][0]
|
||||||
orderlimit = self._dynOrder(order, limit)
|
orderlimit = self._dynOrder(order, limit)
|
||||||
search += " -is:suspended -is:buried -deck:filtered"
|
if search.strip():
|
||||||
|
search = "(%s)" % search
|
||||||
|
search = "%s -is:suspended -is:buried -deck:filtered" % search
|
||||||
try:
|
try:
|
||||||
ids = self.col.findCards(search, order=orderlimit)
|
ids = self.col.findCards(search, order=orderlimit)
|
||||||
except:
|
except:
|
||||||
|
@ -997,7 +999,7 @@ due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim,
|
||||||
elif o == DYN_DUE:
|
elif o == DYN_DUE:
|
||||||
t = "c.due"
|
t = "c.due"
|
||||||
elif o == DYN_DUEPRIORITY:
|
elif o == DYN_DUEPRIORITY:
|
||||||
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 10000+due end)" % (
|
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (
|
||||||
self.today, self.today)
|
self.today, self.today)
|
||||||
else:
|
else:
|
||||||
# if we don't understand the term, default to due order
|
# if we don't understand the term, default to due order
|
||||||
|
|
|
@ -204,12 +204,12 @@ addHook("unloadProfile", stopMplayer)
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
import pyaudio
|
import pyaudio
|
||||||
import wave
|
import wave
|
||||||
|
|
||||||
PYAU_FORMAT = pyaudio.paInt16
|
PYAU_FORMAT = pyaudio.paInt16
|
||||||
PYAU_CHANNELS = 1
|
PYAU_CHANNELS = 1
|
||||||
PYAU_RATE = 44100
|
|
||||||
PYAU_INPUT_INDEX = None
|
PYAU_INPUT_INDEX = None
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -244,12 +244,16 @@ class PyAudioThreadedRecorder(threading.Thread):
|
||||||
except NameError:
|
except NameError:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Pyaudio not installed (recording not supported on OSX10.3)")
|
"Pyaudio not installed (recording not supported on OSX10.3)")
|
||||||
|
|
||||||
|
rate = int(p.get_default_input_device_info()['defaultSampleRate'])
|
||||||
|
|
||||||
stream = p.open(format=PYAU_FORMAT,
|
stream = p.open(format=PYAU_FORMAT,
|
||||||
channels=PYAU_CHANNELS,
|
channels=PYAU_CHANNELS,
|
||||||
rate=PYAU_RATE,
|
rate=rate,
|
||||||
input=True,
|
input=True,
|
||||||
input_device_index=PYAU_INPUT_INDEX,
|
input_device_index=PYAU_INPUT_INDEX,
|
||||||
frames_per_buffer=chunk)
|
frames_per_buffer=chunk)
|
||||||
|
|
||||||
all = []
|
all = []
|
||||||
while not self.finish:
|
while not self.finish:
|
||||||
try:
|
try:
|
||||||
|
@ -267,7 +271,7 @@ class PyAudioThreadedRecorder(threading.Thread):
|
||||||
wf = wave.open(processingSrc, 'wb')
|
wf = wave.open(processingSrc, 'wb')
|
||||||
wf.setnchannels(PYAU_CHANNELS)
|
wf.setnchannels(PYAU_CHANNELS)
|
||||||
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
|
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
|
||||||
wf.setframerate(PYAU_RATE)
|
wf.setframerate(rate)
|
||||||
wf.writeframes(data)
|
wf.writeframes(data)
|
||||||
wf.close()
|
wf.close()
|
||||||
|
|
||||||
|
|
|
@ -353,7 +353,7 @@ group by day order by day""" % (self._limit(), lim),
|
||||||
tot, period, unit))
|
tot, period, unit))
|
||||||
if total and tot:
|
if total and tot:
|
||||||
perMin = total / float(tot)
|
perMin = total / float(tot)
|
||||||
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % perMin
|
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % round(perMin)
|
||||||
self._line(
|
self._line(
|
||||||
i, _("Average answer time"),
|
i, _("Average answer time"),
|
||||||
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin))
|
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin))
|
||||||
|
@ -705,7 +705,7 @@ select
|
||||||
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
|
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
|
||||||
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
|
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
|
||||||
sum(case when queue=0 then 1 else 0 end), -- new
|
sum(case when queue=0 then 1 else 0 end), -- new
|
||||||
sum(case when queue=-1 then 1 else 0 end) -- susp
|
sum(case when queue<0 then 1 else 0 end) -- susp
|
||||||
from cards where did in %s""" % self._limit())
|
from cards where did in %s""" % self._limit())
|
||||||
|
|
||||||
# Footer
|
# Footer
|
||||||
|
|
|
@ -48,10 +48,11 @@ def addForwardOptionalReverse(col):
|
||||||
mm = col.models
|
mm = col.models
|
||||||
m = addBasicModel(col)
|
m = addBasicModel(col)
|
||||||
m['name'] = _("Basic (optional reversed card)")
|
m['name'] = _("Basic (optional reversed card)")
|
||||||
fm = mm.newField(_("Add Reverse"))
|
av = _("Add Reverse")
|
||||||
|
fm = mm.newField(av)
|
||||||
mm.addField(m, fm)
|
mm.addField(m, fm)
|
||||||
t = mm.newTemplate(_("Card 2"))
|
t = mm.newTemplate(_("Card 2"))
|
||||||
t['qfmt'] = "{{#Add Reverse}}{{"+_("Back")+"}}{{/Add Reverse}}"
|
t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av)
|
||||||
t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n"+"{{"+_("Front")+"}}"
|
t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n"+"{{"+_("Front")+"}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
return m
|
return m
|
||||||
|
|
|
@ -757,6 +757,7 @@ class MediaSyncer(object):
|
||||||
# if the sanity check failed, force a resync
|
# if the sanity check failed, force a resync
|
||||||
self.col.media.forceResync()
|
self.col.media.forceResync()
|
||||||
return "sanityCheckFailed"
|
return "sanityCheckFailed"
|
||||||
|
return "success"
|
||||||
|
|
||||||
def removed(self):
|
def removed(self):
|
||||||
return self.col.media.removed()
|
return self.col.media.removed()
|
||||||
|
|
|
@ -4,7 +4,7 @@ from anki.hooks import runFilter
|
||||||
from anki.template import furigana; furigana.install()
|
from anki.template import furigana; furigana.install()
|
||||||
from anki.template import hint; hint.install()
|
from anki.template import hint; hint.install()
|
||||||
|
|
||||||
clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}"
|
clozeReg = r"(?s)\{\{c%s::(.*?)(::(.*?))?\}\}"
|
||||||
|
|
||||||
modifiers = {}
|
modifiers = {}
|
||||||
def modifier(symbol):
|
def modifier(symbol):
|
||||||
|
@ -158,40 +158,45 @@ class Template(object):
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
# field modifiers
|
# field modifiers
|
||||||
parts = tag_name.split(':',2)
|
parts = tag_name.split(':')
|
||||||
extra = None
|
extra = None
|
||||||
if len(parts) == 1 or parts[0] == '':
|
if len(parts) == 1 or parts[0] == '':
|
||||||
return '{unknown field %s}' % tag_name
|
return '{unknown field %s}' % tag_name
|
||||||
elif len(parts) == 2:
|
else:
|
||||||
(mod, tag) = parts
|
mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts
|
||||||
elif len(parts) == 3:
|
|
||||||
(mod, extra, tag) = parts
|
|
||||||
|
|
||||||
txt = get_or_attr(context, tag)
|
txt = get_or_attr(context, tag)
|
||||||
|
|
||||||
|
#Since 'text:' and other mods can affect html on which Anki relies to
|
||||||
|
#process clozes, we need to make sure clozes are always
|
||||||
|
#treated after all the other mods, regardless of how they're specified
|
||||||
|
#in the template, so that {{cloze:text: == {{text:cloze:
|
||||||
|
#For type:, we return directly since no other mod than cloze (or other
|
||||||
|
#pre-defined mods) can be present and those are treated separately
|
||||||
|
mods.reverse()
|
||||||
|
mods.sort(key=lambda s: not s=="type")
|
||||||
|
|
||||||
# built-in modifiers
|
for mod in mods:
|
||||||
if mod == 'text':
|
# built-in modifiers
|
||||||
# strip html
|
if mod == 'text':
|
||||||
if txt:
|
# strip html
|
||||||
return stripHTML(txt)
|
txt = stripHTML(txt) if txt else ""
|
||||||
return ""
|
elif mod == 'type':
|
||||||
elif mod == 'type':
|
# type answer field; convert it to [[type:...]] for the gui code
|
||||||
# type answer field; convert it to [[type:...]] for the gui code
|
# to process
|
||||||
# to process
|
return "[[%s]]" % tag_name
|
||||||
return "[[%s]]" % tag_name
|
elif mod.startswith('cq-') or mod.startswith('ca-'):
|
||||||
elif mod == 'cq' or mod == 'ca':
|
# cloze deletion
|
||||||
# cloze deletion
|
mod, extra = mod.split("-")
|
||||||
if txt and extra:
|
txt = self.clozeText(txt, extra, mod[1]) if txt and extra else ""
|
||||||
return self.clozeText(txt, extra, mod[1])
|
|
||||||
else:
|
else:
|
||||||
return ""
|
# hook-based field modifier
|
||||||
else:
|
mod, extra = re.search("^(.*?)(?:\((.*)\))?$", mod).groups()
|
||||||
# hook-based field modifier
|
txt = runFilter('fmod_' + mod, txt or '', extra or '', context,
|
||||||
txt = runFilter('fmod_' + mod, txt or '', extra, context,
|
tag, tag_name);
|
||||||
tag, tag_name);
|
if txt is None:
|
||||||
if txt is None:
|
return '{unknown field %s}' % tag_name
|
||||||
return '{unknown field %s}' % tag_name
|
return txt
|
||||||
return txt
|
|
||||||
|
|
||||||
def clozeText(self, txt, ord, type):
|
def clozeText(self, txt, ord, type):
|
||||||
reg = clozeReg
|
reg = clozeReg
|
||||||
|
|
|
@ -17,6 +17,7 @@ import sys
|
||||||
import locale
|
import locale
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
import platform
|
import platform
|
||||||
|
import traceback
|
||||||
|
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
|
|
||||||
|
@ -360,6 +361,8 @@ def invalidFilename(str, dirsep=True):
|
||||||
return "/"
|
return "/"
|
||||||
elif (dirsep or not isWin) and "\\" in str:
|
elif (dirsep or not isWin) and "\\" in str:
|
||||||
return "\\"
|
return "\\"
|
||||||
|
elif str.strip().startswith("."):
|
||||||
|
return "."
|
||||||
|
|
||||||
def platDesc():
|
def platDesc():
|
||||||
# we may get an interrupted system call, so try this in a loop
|
# we may get an interrupted system call, so try this in a loop
|
||||||
|
@ -382,3 +385,15 @@ def platDesc():
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
return theos
|
return theos
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
class TimedLog(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._last = time.time()
|
||||||
|
def log(self, s):
|
||||||
|
path, num, fn, y = traceback.extract_stack(limit=2)[0]
|
||||||
|
sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s))
|
||||||
|
self._last = time.time()
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ system. It's free and open source.")
|
||||||
Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina,
|
Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina,
|
||||||
Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen,
|
Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen,
|
||||||
Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail,
|
Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail,
|
||||||
Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
|
Houssam Salem, Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
|
||||||
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Kieran Clancy, LaC, Laurent Steffan,
|
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Julien Baley, Kieran Clancy, LaC, Laurent Steffan,
|
||||||
Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur,
|
Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur,
|
||||||
Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael
|
Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael
|
||||||
Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas
|
Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas
|
||||||
|
|
145
aqt/browser.py
|
@ -96,10 +96,14 @@ class DataModel(QAbstractTableModel):
|
||||||
return
|
return
|
||||||
elif role == Qt.DisplayRole and section < len(self.activeCols):
|
elif role == Qt.DisplayRole and section < len(self.activeCols):
|
||||||
type = self.columnType(section)
|
type = self.columnType(section)
|
||||||
|
txt = None
|
||||||
for stype, name in self.browser.columns:
|
for stype, name in self.browser.columns:
|
||||||
if type == stype:
|
if type == stype:
|
||||||
txt = name
|
txt = name
|
||||||
break
|
break
|
||||||
|
# handle case where extension has set an invalid column type
|
||||||
|
if not txt:
|
||||||
|
txt = self.browser.columns[0][1]
|
||||||
return txt
|
return txt
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
@ -500,15 +504,18 @@ class Browser(QMainWindow):
|
||||||
|
|
||||||
def setupSearch(self):
|
def setupSearch(self):
|
||||||
self.filterTimer = None
|
self.filterTimer = None
|
||||||
|
self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self))
|
||||||
self.connect(self.form.searchButton,
|
self.connect(self.form.searchButton,
|
||||||
SIGNAL("clicked()"),
|
SIGNAL("clicked()"),
|
||||||
self.onSearch)
|
self.onSearch)
|
||||||
self.connect(self.form.searchEdit.lineEdit(),
|
self.connect(self.form.searchEdit.lineEdit(),
|
||||||
SIGNAL("returnPressed()"),
|
SIGNAL("returnPressed()"),
|
||||||
self.onSearch)
|
self.onSearch)
|
||||||
self.setTabOrder(self.form.searchEdit, self.form.tableView)
|
|
||||||
self.form.searchEdit.setCompleter(None)
|
self.form.searchEdit.setCompleter(None)
|
||||||
self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory'])
|
self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory'])
|
||||||
|
self.connect(self.form.searchEdit.lineEdit(),
|
||||||
|
SIGNAL("returnPressed()"),
|
||||||
|
self.onSearch)
|
||||||
|
|
||||||
def onSearch(self, reset=True):
|
def onSearch(self, reset=True):
|
||||||
"Careful: if reset is true, the current note is saved."
|
"Careful: if reset is true, the current note is saved."
|
||||||
|
@ -712,11 +719,9 @@ by clicking on one on the left."""))
|
||||||
|
|
||||||
def setColumnSizes(self):
|
def setColumnSizes(self):
|
||||||
hh = self.form.tableView.horizontalHeader()
|
hh = self.form.tableView.horizontalHeader()
|
||||||
for i in range(len(self.model.activeCols)):
|
hh.setResizeMode(QHeaderView.Interactive)
|
||||||
if hh.visualIndex(i) == len(self.model.activeCols) - 1:
|
hh.setResizeMode(hh.logicalIndex(len(self.model.activeCols)-1),
|
||||||
hh.setResizeMode(i, QHeaderView.Stretch)
|
QHeaderView.Stretch)
|
||||||
else:
|
|
||||||
hh.setResizeMode(i, QHeaderView.Interactive)
|
|
||||||
# this must be set post-resize or it doesn't work
|
# this must be set post-resize or it doesn't work
|
||||||
hh.setCascadingSectionResizes(False)
|
hh.setCascadingSectionResizes(False)
|
||||||
|
|
||||||
|
@ -727,9 +732,10 @@ by clicking on one on the left."""))
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
class CallbackItem(QTreeWidgetItem):
|
class CallbackItem(QTreeWidgetItem):
|
||||||
def __init__(self, root, name, onclick):
|
def __init__(self, root, name, onclick, oncollapse=None):
|
||||||
QTreeWidgetItem.__init__(self, root, [name])
|
QTreeWidgetItem.__init__(self, root, [name])
|
||||||
self.onclick = onclick
|
self.onclick = onclick
|
||||||
|
self.oncollapse = oncollapse
|
||||||
|
|
||||||
def setupTree(self):
|
def setupTree(self):
|
||||||
self.connect(
|
self.connect(
|
||||||
|
@ -739,22 +745,31 @@ by clicking on one on the left."""))
|
||||||
p.setColor(QPalette.Base, QColor("#d6dde0"))
|
p.setColor(QPalette.Base, QColor("#d6dde0"))
|
||||||
self.form.tree.setPalette(p)
|
self.form.tree.setPalette(p)
|
||||||
self.buildTree()
|
self.buildTree()
|
||||||
|
self.connect(
|
||||||
|
self.form.tree, SIGNAL("itemExpanded(QTreeWidgetItem*)"),
|
||||||
|
lambda item: self.onTreeCollapse(item))
|
||||||
|
self.connect(
|
||||||
|
self.form.tree, SIGNAL("itemCollapsed(QTreeWidgetItem*)"),
|
||||||
|
lambda item: self.onTreeCollapse(item))
|
||||||
|
|
||||||
def buildTree(self):
|
def buildTree(self):
|
||||||
self.form.tree.clear()
|
self.form.tree.clear()
|
||||||
root = self.form.tree
|
root = self.form.tree
|
||||||
self._systemTagTree(root)
|
self._systemTagTree(root)
|
||||||
|
self._favTree(root)
|
||||||
self._decksTree(root)
|
self._decksTree(root)
|
||||||
self._modelTree(root)
|
self._modelTree(root)
|
||||||
self._userTagTree(root)
|
self._userTagTree(root)
|
||||||
self.form.tree.expandAll()
|
|
||||||
self.form.tree.setItemsExpandable(False)
|
|
||||||
self.form.tree.setIndentation(15)
|
self.form.tree.setIndentation(15)
|
||||||
|
|
||||||
def onTreeClick(self, item, col):
|
def onTreeClick(self, item, col):
|
||||||
if getattr(item, 'onclick', None):
|
if getattr(item, 'onclick', None):
|
||||||
item.onclick()
|
item.onclick()
|
||||||
|
|
||||||
|
def onTreeCollapse(self, item):
|
||||||
|
if getattr(item, 'oncollapse', None):
|
||||||
|
item.oncollapse()
|
||||||
|
|
||||||
def setFilter(self, *args):
|
def setFilter(self, *args):
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
txt = args[0]
|
txt = args[0]
|
||||||
|
@ -804,6 +819,18 @@ by clicking on one on the left."""))
|
||||||
item.setIcon(0, QIcon(":/icons/" + icon))
|
item.setIcon(0, QIcon(":/icons/" + icon))
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
def _favTree(self, root):
|
||||||
|
saved = self.col.conf.get('savedFilters', [])
|
||||||
|
if not saved:
|
||||||
|
# Don't add favourites to tree if none saved
|
||||||
|
return
|
||||||
|
root = self.CallbackItem(root, _("My Searches"), None)
|
||||||
|
root.setExpanded(True)
|
||||||
|
root.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
|
||||||
|
for name, filt in saved.items():
|
||||||
|
item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s))
|
||||||
|
item.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
|
||||||
|
|
||||||
def _userTagTree(self, root):
|
def _userTagTree(self, root):
|
||||||
for t in sorted(self.col.tags.all()):
|
for t in sorted(self.col.tags.all()):
|
||||||
if t.lower() == "marked" or t.lower() == "leech":
|
if t.lower() == "marked" or t.lower() == "leech":
|
||||||
|
@ -817,10 +844,13 @@ by clicking on one on the left."""))
|
||||||
def fillGroups(root, grps, head=""):
|
def fillGroups(root, grps, head=""):
|
||||||
for g in grps:
|
for g in grps:
|
||||||
item = self.CallbackItem(
|
item = self.CallbackItem(
|
||||||
root, g[0], lambda g=g: self.setFilter(
|
root, g[0],
|
||||||
"deck", head+g[0]))
|
lambda g=g: self.setFilter("deck", head+g[0]),
|
||||||
|
lambda g=g: self.mw.col.decks.collapseBrowser(g[1]))
|
||||||
item.setIcon(0, QIcon(":/icons/deck16.png"))
|
item.setIcon(0, QIcon(":/icons/deck16.png"))
|
||||||
newhead = head + g[0]+"::"
|
newhead = head + g[0]+"::"
|
||||||
|
collapsed = self.mw.col.decks.get(g[1]).get('browserCollapsed', False)
|
||||||
|
item.setExpanded(not collapsed)
|
||||||
fillGroups(item, g[5], newhead)
|
fillGroups(item, g[5], newhead)
|
||||||
fillGroups(root, grps)
|
fillGroups(root, grps)
|
||||||
|
|
||||||
|
@ -847,7 +877,7 @@ by clicking on one on the left."""))
|
||||||
d = QDialog(self)
|
d = QDialog(self)
|
||||||
l = QVBoxLayout()
|
l = QVBoxLayout()
|
||||||
l.setMargin(0)
|
l.setMargin(0)
|
||||||
w = AnkiWebView()
|
w = AnkiWebView(canCopy=True)
|
||||||
l.addWidget(w)
|
l.addWidget(w)
|
||||||
w.stdHtml(info + "<p>" + reps)
|
w.stdHtml(info + "<p>" + reps)
|
||||||
bb = QDialogButtonBox(QDialogButtonBox.Close)
|
bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||||
|
@ -982,8 +1012,7 @@ where id in %s""" % ids2str(sf))
|
||||||
c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished)
|
c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished)
|
||||||
vbox = QVBoxLayout()
|
vbox = QVBoxLayout()
|
||||||
vbox.setMargin(0)
|
vbox.setMargin(0)
|
||||||
self._previewWeb = AnkiWebView()
|
self._previewWeb = AnkiWebView(True)
|
||||||
self._previewWeb.setFocusPolicy(Qt.NoFocus)
|
|
||||||
vbox.addWidget(self._previewWeb)
|
vbox.addWidget(self._previewWeb)
|
||||||
bbox = QDialogButtonBox()
|
bbox = QDialogButtonBox()
|
||||||
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
|
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
|
||||||
|
@ -1193,7 +1222,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
|
||||||
frm = aqt.forms.reposition.Ui_Dialog()
|
frm = aqt.forms.reposition.Ui_Dialog()
|
||||||
frm.setupUi(d)
|
frm.setupUi(d)
|
||||||
(pmin, pmax) = self.col.db.first(
|
(pmin, pmax) = self.col.db.first(
|
||||||
"select min(due), max(due) from cards where type=0")
|
"select min(due), max(due) from cards where type=0 and odid=0")
|
||||||
|
pmin = pmin or 0
|
||||||
|
pmax = pmax or 0
|
||||||
txt = _("Queue top: %d") % pmin
|
txt = _("Queue top: %d") % pmin
|
||||||
txt += "\n" + _("Queue bottom: %d") % pmax
|
txt += "\n" + _("Queue bottom: %d") % pmax
|
||||||
frm.label.setText(txt)
|
frm.label.setText(txt)
|
||||||
|
@ -1708,3 +1739,87 @@ a { margin-right: 1em; }
|
||||||
self.browser.addTags()
|
self.browser.addTags()
|
||||||
elif l == "deletetag":
|
elif l == "deletetag":
|
||||||
self.browser.deleteTags()
|
self.browser.deleteTags()
|
||||||
|
|
||||||
|
|
||||||
|
# Favourites button
|
||||||
|
######################################################################
|
||||||
|
class FavouritesLineEdit(QLineEdit):
|
||||||
|
buttonClicked = pyqtSignal(bool)
|
||||||
|
|
||||||
|
def __init__(self, mw, browser, parent=None):
|
||||||
|
super(FavouritesLineEdit, self).__init__(parent)
|
||||||
|
self.mw = mw
|
||||||
|
self.browser = browser
|
||||||
|
# add conf if missing
|
||||||
|
if not self.mw.col.conf.has_key('savedFilters'):
|
||||||
|
self.mw.col.conf['savedFilters'] = {}
|
||||||
|
self.button = QToolButton(self)
|
||||||
|
self.button.setStyleSheet('border: 0px;')
|
||||||
|
self.button.setCursor(Qt.ArrowCursor)
|
||||||
|
self.button.clicked.connect(self.buttonClicked.emit)
|
||||||
|
self.setIcon(':/icons/emblem-favorite-off.png')
|
||||||
|
# flag to raise save or delete dialog on button click
|
||||||
|
self.doSave = True
|
||||||
|
# name of current saved filter (if query matches)
|
||||||
|
self.name = None
|
||||||
|
self.buttonClicked.connect(self.onClicked)
|
||||||
|
self.connect(self, SIGNAL("textChanged(QString)"), self.updateButton)
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
buttonSize = self.button.sizeHint()
|
||||||
|
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
|
||||||
|
self.button.move(self.rect().right() - frameWidth - buttonSize.width(),
|
||||||
|
(self.rect().bottom() - buttonSize.height() + 1) / 2)
|
||||||
|
super(FavouritesLineEdit, self).resizeEvent(event)
|
||||||
|
|
||||||
|
def setIcon(self, path):
|
||||||
|
self.button.setIcon(QIcon(path))
|
||||||
|
|
||||||
|
def setText(self, txt):
|
||||||
|
super(FavouritesLineEdit, self).setText(txt)
|
||||||
|
self.updateButton()
|
||||||
|
|
||||||
|
def updateButton(self, reset=True):
|
||||||
|
# If search text is a saved query, switch to the delete button.
|
||||||
|
# Otherwise show save button.
|
||||||
|
txt = unicode(self.text()).strip()
|
||||||
|
for key, value in self.mw.col.conf['savedFilters'].items():
|
||||||
|
if txt == value:
|
||||||
|
self.doSave = False
|
||||||
|
self.name = key
|
||||||
|
self.setIcon(QIcon(":/icons/emblem-favorite.png"))
|
||||||
|
return
|
||||||
|
self.doSave = True
|
||||||
|
self.setIcon(QIcon(":/icons/emblem-favorite-off.png"))
|
||||||
|
|
||||||
|
def onClicked(self):
|
||||||
|
if self.doSave:
|
||||||
|
self.saveClicked()
|
||||||
|
else:
|
||||||
|
self.deleteClicked()
|
||||||
|
|
||||||
|
def saveClicked(self):
|
||||||
|
txt = unicode(self.text()).strip()
|
||||||
|
dlg = QInputDialog(self)
|
||||||
|
dlg.setInputMode(QInputDialog.TextInput)
|
||||||
|
dlg.setLabelText(_("The current search terms will be added as a new "
|
||||||
|
"item in the sidebar.\n"
|
||||||
|
"Search name:"))
|
||||||
|
dlg.setWindowTitle(_("Save search"))
|
||||||
|
ok = dlg.exec_()
|
||||||
|
name = dlg.textValue()
|
||||||
|
if ok:
|
||||||
|
self.mw.col.conf['savedFilters'][name] = txt
|
||||||
|
|
||||||
|
self.updateButton()
|
||||||
|
self.browser.setupTree()
|
||||||
|
|
||||||
|
def deleteClicked(self):
|
||||||
|
msg = _('Remove "%s" from your saved searches?') % self.name
|
||||||
|
ok = QMessageBox.question(self, _('Remove search'),
|
||||||
|
msg, QMessageBox.Yes, QMessageBox.No)
|
||||||
|
|
||||||
|
if ok == QMessageBox.Yes:
|
||||||
|
self.mw.col.conf['savedFilters'].pop(self.name, None)
|
||||||
|
self.updateButton()
|
||||||
|
self.browser.setupTree()
|
||||||
|
|
|
@ -120,9 +120,9 @@ class CardLayout(QDialog):
|
||||||
self.model, joinFields(self.note.fields)))
|
self.model, joinFields(self.note.fields)))
|
||||||
for g in pform.groupBox, pform.groupBox_2:
|
for g in pform.groupBox, pform.groupBox_2:
|
||||||
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
|
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
|
||||||
pform.frontWeb = AnkiWebView()
|
pform.frontWeb = AnkiWebView(True)
|
||||||
pform.frontPrevBox.addWidget(pform.frontWeb)
|
pform.frontPrevBox.addWidget(pform.frontWeb)
|
||||||
pform.backWeb = AnkiWebView()
|
pform.backWeb = AnkiWebView(True)
|
||||||
pform.backPrevBox.addWidget(pform.backWeb)
|
pform.backPrevBox.addWidget(pform.backWeb)
|
||||||
for wig in pform.frontWeb, pform.backWeb:
|
for wig in pform.frontWeb, pform.backWeb:
|
||||||
wig.page().setLinkDelegationPolicy(
|
wig.page().setLinkDelegationPolicy(
|
||||||
|
|
|
@ -41,7 +41,7 @@ class CustomStudy(QDialog):
|
||||||
|
|
||||||
def onRadioChange(self, idx):
|
def onRadioChange(self, idx):
|
||||||
f = self.form; sp = f.spin
|
f = self.form; sp = f.spin
|
||||||
smin = 1; smax = 9999; sval = 1
|
smin = 1; smax = DYN_MAX_SIZE; sval = 1
|
||||||
post = _("cards")
|
post = _("cards")
|
||||||
tit = ""
|
tit = ""
|
||||||
spShow = True
|
spShow = True
|
||||||
|
@ -127,15 +127,15 @@ class CustomStudy(QDialog):
|
||||||
# and then set various options
|
# and then set various options
|
||||||
if i == RADIO_FORGOT:
|
if i == RADIO_FORGOT:
|
||||||
dyn['delays'] = [1]
|
dyn['delays'] = [1]
|
||||||
dyn['terms'][0] = ['rated:%d:1' % spin, 9999, DYN_RANDOM]
|
dyn['terms'][0] = ['rated:%d:1' % spin, DYN_MAX_SIZE, DYN_RANDOM]
|
||||||
dyn['resched'] = False
|
dyn['resched'] = False
|
||||||
elif i == RADIO_AHEAD:
|
elif i == RADIO_AHEAD:
|
||||||
dyn['delays'] = None
|
dyn['delays'] = None
|
||||||
dyn['terms'][0] = ['prop:due<=%d' % spin, 9999, DYN_DUE]
|
dyn['terms'][0] = ['prop:due<=%d' % spin, DYN_MAX_SIZE, DYN_DUE]
|
||||||
dyn['resched'] = True
|
dyn['resched'] = True
|
||||||
elif i == RADIO_PREVIEW:
|
elif i == RADIO_PREVIEW:
|
||||||
dyn['delays'] = None
|
dyn['delays'] = None
|
||||||
dyn['terms'][0] = ['is:new added:%s'%spin, 9999, DYN_OLDEST]
|
dyn['terms'][0] = ['is:new added:%s'%spin, DYN_MAX_SIZE, DYN_OLDEST]
|
||||||
dyn['resched'] = False
|
dyn['resched'] = False
|
||||||
elif i == RADIO_CRAM:
|
elif i == RADIO_CRAM:
|
||||||
dyn['delays'] = None
|
dyn['delays'] = None
|
||||||
|
|
|
@ -17,6 +17,7 @@ class DeckBrowser(object):
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.web = mw.web
|
self.web = mw.web
|
||||||
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
|
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
|
||||||
|
self.scrollPos = QPoint(0, 0)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
clearAudioQueue()
|
clearAudioQueue()
|
||||||
|
@ -65,6 +66,7 @@ class DeckBrowser(object):
|
||||||
key = unicode(evt.text())
|
key = unicode(evt.text())
|
||||||
|
|
||||||
def _selDeck(self, did):
|
def _selDeck(self, did):
|
||||||
|
self.scrollPos = self.web.page().mainFrame().scrollPosition()
|
||||||
self.mw.col.decks.select(did)
|
self.mw.col.decks.select(did)
|
||||||
self.mw.onOverview()
|
self.mw.onOverview()
|
||||||
|
|
||||||
|
@ -152,7 +154,7 @@ body { margin: 1em; -webkit-user-select: none; }
|
||||||
if self.web.key == "deckBrowser":
|
if self.web.key == "deckBrowser":
|
||||||
return self.web.page().mainFrame().scrollPosition()
|
return self.web.page().mainFrame().scrollPosition()
|
||||||
else:
|
else:
|
||||||
return QPoint(0,0)
|
return self.scrollPos
|
||||||
|
|
||||||
def _renderStats(self):
|
def _renderStats(self):
|
||||||
cards, thetime = self.mw.col.db.first("""
|
cards, thetime = self.mw.col.db.first("""
|
||||||
|
|
|
@ -21,7 +21,7 @@ import anki.js
|
||||||
from BeautifulSoup import BeautifulSoup
|
from BeautifulSoup import BeautifulSoup
|
||||||
|
|
||||||
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg")
|
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg")
|
||||||
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv")
|
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a")
|
||||||
|
|
||||||
_html = """
|
_html = """
|
||||||
<html><head>%s<style>
|
<html><head>%s<style>
|
||||||
|
@ -690,7 +690,7 @@ class Editor(object):
|
||||||
|
|
||||||
def onCloze(self):
|
def onCloze(self):
|
||||||
# check that the model is set up for cloze deletion
|
# check that the model is set up for cloze deletion
|
||||||
if '{{cloze:' not in self.note.model()['tmpls'][0]['qfmt']:
|
if not re.search('{{(.*:)*cloze:',self.note.model()['tmpls'][0]['qfmt']):
|
||||||
if self.addMode:
|
if self.addMode:
|
||||||
tooltip(_("Warning, cloze deletions will not work until "
|
tooltip(_("Warning, cloze deletions will not work until "
|
||||||
"you switch the type at the top to Cloze."))
|
"you switch the type at the top to Cloze."))
|
||||||
|
@ -988,7 +988,7 @@ to a cloze type first, via Edit>Change Note Type."""))
|
||||||
class EditorWebView(AnkiWebView):
|
class EditorWebView(AnkiWebView):
|
||||||
|
|
||||||
def __init__(self, parent, editor):
|
def __init__(self, parent, editor):
|
||||||
AnkiWebView.__init__(self)
|
AnkiWebView.__init__(self, canFocus=True)
|
||||||
self.editor = editor
|
self.editor = editor
|
||||||
self.strip = self.editor.mw.pm.profile['stripHTML']
|
self.strip = self.editor.mw.pm.profile['stripHTML']
|
||||||
|
|
||||||
|
@ -1117,13 +1117,18 @@ class EditorWebView(AnkiWebView):
|
||||||
url = mime.urls()[0].toString()
|
url = mime.urls()[0].toString()
|
||||||
# chrome likes to give us the URL twice with a \n
|
# chrome likes to give us the URL twice with a \n
|
||||||
url = url.splitlines()[0]
|
url = url.splitlines()[0]
|
||||||
mime = QMimeData()
|
newmime = QMimeData()
|
||||||
link = self.editor.urlToLink(url)
|
link = self.editor.urlToLink(url)
|
||||||
if link:
|
if link:
|
||||||
mime.setHtml(link)
|
newmime.setHtml(link)
|
||||||
|
elif mime.hasImage():
|
||||||
|
# if we couldn't convert the url to a link and there's an
|
||||||
|
# image on the clipboard (such as copy&paste from
|
||||||
|
# google images in safari), use that instead
|
||||||
|
return self._processImage(mime)
|
||||||
else:
|
else:
|
||||||
mime.setText(url)
|
newmime.setText(url)
|
||||||
return mime
|
return newmime
|
||||||
|
|
||||||
# if the user has used 'copy link location' in the browser, the clipboard
|
# if the user has used 'copy link location' in the browser, the clipboard
|
||||||
# will contain the URL as text, and no URLs or HTML. the URL will already
|
# will contain the URL as text, and no URLs or HTML. the URL will already
|
||||||
|
|
|
@ -75,7 +75,7 @@ into a bug report:""")
|
||||||
pluginText = _("""\
|
pluginText = _("""\
|
||||||
An error occurred in an add-on.<br>
|
An error occurred in an add-on.<br>
|
||||||
Please post on the add-on forum:<br>%s<br>""")
|
Please post on the add-on forum:<br>%s<br>""")
|
||||||
pluginText %= "https://groups.google.com/forum/#!forum/anki-addons"
|
pluginText %= "https://anki.tenderapp.com/discussions/add-ons"
|
||||||
if "addon" in error:
|
if "addon" in error:
|
||||||
txt = pluginText
|
txt = pluginText
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -17,7 +17,6 @@ import aqt.forms
|
||||||
import aqt.modelchooser
|
import aqt.modelchooser
|
||||||
import aqt.deckchooser
|
import aqt.deckchooser
|
||||||
|
|
||||||
|
|
||||||
class ChangeMap(QDialog):
|
class ChangeMap(QDialog):
|
||||||
def __init__(self, mw, model, current):
|
def __init__(self, mw, model, current):
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
|
@ -80,6 +79,9 @@ class ImportDialog(QDialog):
|
||||||
self.updateDelimiterButtonText()
|
self.updateDelimiterButtonText()
|
||||||
self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True))
|
self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True))
|
||||||
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1))
|
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1))
|
||||||
|
# import button
|
||||||
|
b = QPushButton(_("Import"))
|
||||||
|
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def setupOptions(self):
|
def setupOptions(self):
|
||||||
|
@ -88,8 +90,6 @@ class ImportDialog(QDialog):
|
||||||
self.mw, self.frm.modelArea, label=False)
|
self.mw, self.frm.modelArea, label=False)
|
||||||
self.deck = aqt.deckchooser.DeckChooser(
|
self.deck = aqt.deckchooser.DeckChooser(
|
||||||
self.mw, self.frm.deckArea, label=False)
|
self.mw, self.frm.deckArea, label=False)
|
||||||
self.connect(self.frm.importButton, SIGNAL("clicked()"),
|
|
||||||
self.doImport)
|
|
||||||
|
|
||||||
def modelChanged(self):
|
def modelChanged(self):
|
||||||
self.importer.model = self.mw.col.models.current()
|
self.importer.model = self.mw.col.models.current()
|
||||||
|
@ -139,8 +139,8 @@ you can enter it here. Use \\t to represent tab."""),
|
||||||
d = `d`
|
d = `d`
|
||||||
txt = _("Fields separated by: %s") % d
|
txt = _("Fields separated by: %s") % d
|
||||||
self.frm.autoDetect.setText(txt)
|
self.frm.autoDetect.setText(txt)
|
||||||
|
|
||||||
def doImport(self, update=False):
|
def accept(self):
|
||||||
self.importer.mapping = self.mapping
|
self.importer.mapping = self.mapping
|
||||||
if not self.importer.mappingOk():
|
if not self.importer.mappingOk():
|
||||||
showWarning(
|
showWarning(
|
||||||
|
@ -250,7 +250,7 @@ you can enter it here. Use \\t to represent tab."""),
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def helpRequested(self):
|
def helpRequested(self):
|
||||||
openHelp("FileImport")
|
openHelp("importing")
|
||||||
|
|
||||||
|
|
||||||
def showUnicodeWarning():
|
def showUnicodeWarning():
|
||||||
|
@ -338,6 +338,8 @@ with a different browser.""")
|
||||||
msg = _("""\
|
msg = _("""\
|
||||||
Invalid file. Please restore from backup.""")
|
Invalid file. Please restore from backup.""")
|
||||||
showWarning(msg)
|
showWarning(msg)
|
||||||
|
elif "invalidTempFolder" in err:
|
||||||
|
showWarning(mw.errorHandler.tempFolderMsg())
|
||||||
elif "readonly" in err:
|
elif "readonly" in err:
|
||||||
showWarning(_("""\
|
showWarning(_("""\
|
||||||
Unable to import from a read-only file."""))
|
Unable to import from a read-only file."""))
|
||||||
|
|
22
aqt/main.py
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from send2trash import send2trash
|
from send2trash import send2trash
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -63,6 +63,8 @@ class AnkiQt(QMainWindow):
|
||||||
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore"))
|
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore"))
|
||||||
# Load profile in a timer so we can let the window finish init and not
|
# Load profile in a timer so we can let the window finish init and not
|
||||||
# close on profile load error.
|
# close on profile load error.
|
||||||
|
if isMac and qtmajor >= 5:
|
||||||
|
self.show()
|
||||||
self.progress.timer(10, self.setupProfile, False)
|
self.progress.timer(10, self.setupProfile, False)
|
||||||
|
|
||||||
def setupUI(self):
|
def setupUI(self):
|
||||||
|
@ -264,15 +266,19 @@ To import into a password protected profile, please open the profile before atte
|
||||||
|
|
||||||
def loadCollection(self):
|
def loadCollection(self):
|
||||||
self.hideSchemaMsg = True
|
self.hideSchemaMsg = True
|
||||||
|
cpath = self.pm.collectionPath()
|
||||||
try:
|
try:
|
||||||
self.col = Collection(self.pm.collectionPath(), log=True)
|
self.col = Collection(cpath, log=True)
|
||||||
except anki.db.Error:
|
except anki.db.Error:
|
||||||
# move back to profile manager
|
# warn user
|
||||||
showWarning("""\
|
showWarning("""\
|
||||||
Your collection is corrupt. Please see the manual for \
|
Your collection is corrupt. Please see the manual for \
|
||||||
how to restore from a backup.""")
|
how to restore from a backup.""")
|
||||||
self.unloadProfile()
|
# move it out of the way so the profile can be used again
|
||||||
raise
|
newpath = cpath+str(intTime())
|
||||||
|
os.rename(cpath, newpath)
|
||||||
|
# then close
|
||||||
|
sys.exit(1)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
# the custom exception handler won't catch this if we immediately
|
# the custom exception handler won't catch this if we immediately
|
||||||
# unload, so we have to manually handle it
|
# unload, so we have to manually handle it
|
||||||
|
@ -377,7 +383,9 @@ the manual for information on how to restore from an automatic backup."))
|
||||||
if cleanup:
|
if cleanup:
|
||||||
cleanup(state)
|
cleanup(state)
|
||||||
self.state = state
|
self.state = state
|
||||||
|
runHook('beforeStateChange', state, oldState, *args)
|
||||||
getattr(self, "_"+state+"State")(oldState, *args)
|
getattr(self, "_"+state+"State")(oldState, *args)
|
||||||
|
runHook('afterStateChange', state, oldState, *args)
|
||||||
|
|
||||||
def _deckBrowserState(self, oldState):
|
def _deckBrowserState(self, oldState):
|
||||||
self.deckBrowser.show()
|
self.deckBrowser.show()
|
||||||
|
@ -405,10 +413,12 @@ the manual for information on how to restore from an automatic backup."))
|
||||||
|
|
||||||
def _reviewState(self, oldState):
|
def _reviewState(self, oldState):
|
||||||
self.reviewer.show()
|
self.reviewer.show()
|
||||||
|
self.web.setCanFocus(True)
|
||||||
|
|
||||||
def _reviewCleanup(self, newState):
|
def _reviewCleanup(self, newState):
|
||||||
if newState != "resetRequired" and newState != "review":
|
if newState != "resetRequired" and newState != "review":
|
||||||
self.reviewer.cleanup()
|
self.reviewer.cleanup()
|
||||||
|
self.web.setCanFocus(False)
|
||||||
|
|
||||||
def noteChanged(self, nid):
|
def noteChanged(self, nid):
|
||||||
"Called when a card or note is edited (but not deleted)."
|
"Called when a card or note is edited (but not deleted)."
|
||||||
|
@ -504,7 +514,7 @@ title="%s">%s</button>''' % (
|
||||||
self.toolbar = aqt.toolbar.Toolbar(self, tweb)
|
self.toolbar = aqt.toolbar.Toolbar(self, tweb)
|
||||||
self.toolbar.draw()
|
self.toolbar.draw()
|
||||||
# main area
|
# main area
|
||||||
self.web = aqt.webview.AnkiWebView()
|
self.web = aqt.webview.AnkiWebView(canFocus=True)
|
||||||
self.web.setObjectName("mainText")
|
self.web.setObjectName("mainText")
|
||||||
self.web.setFocusPolicy(Qt.WheelFocus)
|
self.web.setFocusPolicy(Qt.WheelFocus)
|
||||||
self.web.setMinimumWidth(400)
|
self.web.setMinimumWidth(400)
|
||||||
|
|
|
@ -123,7 +123,7 @@ to their original deck.""")
|
||||||
counts = list(self.mw.col.sched.counts())
|
counts = list(self.mw.col.sched.counts())
|
||||||
finished = not sum(counts)
|
finished = not sum(counts)
|
||||||
for n in range(len(counts)):
|
for n in range(len(counts)):
|
||||||
if counts[n] == 1000:
|
if counts[n] >= 1000:
|
||||||
counts[n] = "1000+"
|
counts[n] = "1000+"
|
||||||
but = self.mw.button
|
but = self.mw.button
|
||||||
if finished:
|
if finished:
|
||||||
|
|
|
@ -18,6 +18,8 @@ class Preferences(QDialog):
|
||||||
self.prof = self.mw.pm.profile
|
self.prof = self.mw.pm.profile
|
||||||
self.form = aqt.forms.preferences.Ui_Preferences()
|
self.form = aqt.forms.preferences.Ui_Preferences()
|
||||||
self.form.setupUi(self)
|
self.form.setupUi(self)
|
||||||
|
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
|
||||||
|
self.form.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False)
|
||||||
self.connect(self.form.buttonBox, SIGNAL("helpRequested()"),
|
self.connect(self.form.buttonBox, SIGNAL("helpRequested()"),
|
||||||
lambda: openHelp("profileprefs"))
|
lambda: openHelp("profileprefs"))
|
||||||
self.setupCollection()
|
self.setupCollection()
|
||||||
|
|
|
@ -179,7 +179,7 @@ documentation for information on using a flash drive.""")
|
||||||
|
|
||||||
def _defaultBase(self):
|
def _defaultBase(self):
|
||||||
if isWin:
|
if isWin:
|
||||||
if qtmajor >= 5:
|
if False: #qtmajor >= 5:
|
||||||
loc = QStandardPaths.writeableLocation(QStandardPaths.DocumentsLocation)
|
loc = QStandardPaths.writeableLocation(QStandardPaths.DocumentsLocation)
|
||||||
else:
|
else:
|
||||||
loc = QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation)
|
loc = QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation)
|
||||||
|
@ -284,7 +284,15 @@ please see:
|
||||||
|
|
||||||
def _onLangSelected(self):
|
def _onLangSelected(self):
|
||||||
f = self.langForm
|
f = self.langForm
|
||||||
code = langs[f.lang.currentRow()][1]
|
obj = langs[f.lang.currentRow()]
|
||||||
|
code = obj[1]
|
||||||
|
name = obj[0]
|
||||||
|
en = "Are you sure you wish to display Anki's interface in %s?"
|
||||||
|
r = QMessageBox.question(
|
||||||
|
None, "Anki", en%name, QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No)
|
||||||
|
if r != QMessageBox.Yes:
|
||||||
|
return self._setDefaultLang()
|
||||||
self.meta['defaultLang'] = code
|
self.meta['defaultLang'] = code
|
||||||
sql = "update profiles set data = ? where name = ?"
|
sql = "update profiles set data = ? where name = ?"
|
||||||
self.db.execute(sql, cPickle.dumps(self.meta), "_global")
|
self.db.execute(sql, cPickle.dumps(self.meta), "_global")
|
||||||
|
|
15
aqt/qt.py
|
@ -2,9 +2,13 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
# imports are all in this file to make moving to pyside easier in the future
|
# imports are all in this file to make moving to pyside easier in the future
|
||||||
|
# fixme: make sure not to optimize imports on this file
|
||||||
|
|
||||||
|
import sip
|
||||||
|
import os
|
||||||
|
|
||||||
import sip, os
|
|
||||||
from anki.utils import isWin, isMac
|
from anki.utils import isWin, isMac
|
||||||
|
|
||||||
sip.setapi('QString', 2)
|
sip.setapi('QString', 2)
|
||||||
sip.setapi('QVariant', 2)
|
sip.setapi('QVariant', 2)
|
||||||
sip.setapi('QUrl', 2)
|
sip.setapi('QUrl', 2)
|
||||||
|
@ -18,14 +22,16 @@ from PyQt4.QtGui import *
|
||||||
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
||||||
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
|
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
|
|
||||||
def debug():
|
def debug():
|
||||||
from PyQt4.QtCore import pyqtRemoveInputHook
|
from PyQt4.QtCore import pyqtRemoveInputHook
|
||||||
from pdb import set_trace
|
from pdb import set_trace
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
set_trace()
|
set_trace()
|
||||||
|
|
||||||
|
import sys, traceback
|
||||||
|
|
||||||
if os.environ.get("DEBUG"):
|
if os.environ.get("DEBUG"):
|
||||||
import sys, traceback
|
|
||||||
def info(type, value, tb):
|
def info(type, value, tb):
|
||||||
from PyQt4.QtCore import pyqtRemoveInputHook
|
from PyQt4.QtCore import pyqtRemoveInputHook
|
||||||
for line in traceback.format_exception(type, value, tb):
|
for line in traceback.format_exception(type, value, tb):
|
||||||
|
@ -43,8 +49,3 @@ if qtmajor <= 4 and qtminor <= 6:
|
||||||
import anki.template.furigana
|
import anki.template.furigana
|
||||||
anki.template.furigana.ruby = r'<span style="display: inline-block; text-align: center; line-height: 1; white-space: nowrap; vertical-align: baseline; margin: 0; padding: 0"><span style="display: block; text-decoration: none; line-height: 1.2; font-weight: normal; font-size: 0.64em">\2</span>\1</span>'
|
anki.template.furigana.ruby = r'<span style="display: inline-block; text-align: center; line-height: 1; white-space: nowrap; vertical-align: baseline; margin: 0; padding: 0"><span style="display: block; text-decoration: none; line-height: 1.2; font-weight: normal; font-size: 0.64em">\2</span>\1</span>'
|
||||||
|
|
||||||
if isWin or isMac:
|
|
||||||
# we no longer use this, but want it included in the mac+win builds
|
|
||||||
# so we don't break add-ons that use it. any new add-ons should use
|
|
||||||
# the above variables instead
|
|
||||||
from PyQt4 import pyqtconfig
|
|
||||||
|
|
|
@ -134,7 +134,8 @@ function _updateQA (q, answerMode, klass) {
|
||||||
typeans.focus();
|
typeans.focus();
|
||||||
}
|
}
|
||||||
if (answerMode) {
|
if (answerMode) {
|
||||||
window.location = "#answer";
|
var e = $("#answer");
|
||||||
|
if (e[0]) { e[0].scrollIntoView(); }
|
||||||
} else {
|
} else {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
@ -433,10 +434,11 @@ Please run Tools>Empty Cards""")
|
||||||
return txt.split("::")[0]
|
return txt.split("::")[0]
|
||||||
return txt
|
return txt
|
||||||
matches = [noHint(txt) for txt in matches]
|
matches = [noHint(txt) for txt in matches]
|
||||||
if len(matches) > 1:
|
uniqMatches = set(matches)
|
||||||
txt = ", ".join(matches)
|
if len(uniqMatches) == 1:
|
||||||
else:
|
|
||||||
txt = matches[0]
|
txt = matches[0]
|
||||||
|
else:
|
||||||
|
txt = ", ".join(matches)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def tokenizeComparison(self, given, correct):
|
def tokenizeComparison(self, given, correct):
|
||||||
|
|
|
@ -170,6 +170,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
|
||||||
elif "10061" in err or "10013" in err or "10053" in err:
|
elif "10061" in err or "10013" in err or "10053" in err:
|
||||||
return _(
|
return _(
|
||||||
"Antivirus or firewall software is preventing Anki from connecting to the internet.")
|
"Antivirus or firewall software is preventing Anki from connecting to the internet.")
|
||||||
|
elif "10054" in err or "Broken pipe" in err:
|
||||||
|
return _("Connection timed out. Either your internet connection is experiencing problems, or you have a very large file in your media folder.")
|
||||||
elif "Unable to find the server" in err:
|
elif "Unable to find the server" in err:
|
||||||
return _(
|
return _(
|
||||||
"Server not found. Either your connection is down, or antivirus/firewall "
|
"Server not found. Either your connection is down, or antivirus/firewall "
|
||||||
|
@ -178,6 +180,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
|
||||||
return _("Proxy authentication required.")
|
return _("Proxy authentication required.")
|
||||||
elif "code: 413" in err:
|
elif "code: 413" in err:
|
||||||
return _("Your collection or a media file is too large to sync.")
|
return _("Your collection or a media file is too large to sync.")
|
||||||
|
elif "EOF occurred in violation of protocol" in err:
|
||||||
|
return _("Error establishing a secure connection. This is usually caused by filtering software, or problems with your ISP.")
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def _getUserPass(self):
|
def _getUserPass(self):
|
||||||
|
|
|
@ -240,7 +240,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
|
||||||
ret = []
|
ret = []
|
||||||
def accept():
|
def accept():
|
||||||
# work around an osx crash
|
# work around an osx crash
|
||||||
aqt.mw.app.processEvents()
|
#aqt.mw.app.processEvents()
|
||||||
file = unicode(list(d.selectedFiles())[0])
|
file = unicode(list(d.selectedFiles())[0])
|
||||||
if dirkey:
|
if dirkey:
|
||||||
dir = os.path.dirname(file)
|
dir = os.path.dirname(file)
|
||||||
|
@ -394,6 +394,7 @@ def tooltip(msg, period=3000, parent=None):
|
||||||
lab.setWindowFlags(Qt.ToolTip)
|
lab.setWindowFlags(Qt.ToolTip)
|
||||||
p = QPalette()
|
p = QPalette()
|
||||||
p.setColor(QPalette.Window, QColor("#feffc4"))
|
p.setColor(QPalette.Window, QColor("#feffc4"))
|
||||||
|
p.setColor(QPalette.WindowText, QColor("#000000"))
|
||||||
lab.setPalette(p)
|
lab.setPalette(p)
|
||||||
lab.move(
|
lab.move(
|
||||||
aw.mapToGlobal(QPoint(0, -100 + aw.height())))
|
aw.mapToGlobal(QPoint(0, -100 + aw.height())))
|
||||||
|
|
|
@ -40,7 +40,8 @@ class AnkiWebPage(QWebPage):
|
||||||
|
|
||||||
class AnkiWebView(QWebView):
|
class AnkiWebView(QWebView):
|
||||||
|
|
||||||
def __init__(self):
|
# canFocus implies canCopy
|
||||||
|
def __init__(self, canFocus=False, canCopy=False):
|
||||||
QWebView.__init__(self)
|
QWebView.__init__(self)
|
||||||
self.setRenderHints(
|
self.setRenderHints(
|
||||||
QPainter.TextAntialiasing |
|
QPainter.TextAntialiasing |
|
||||||
|
@ -59,6 +60,8 @@ class AnkiWebView(QWebView):
|
||||||
self.allowDrops = False
|
self.allowDrops = False
|
||||||
# reset each time new html is set; used to detect if still in same state
|
# reset each time new html is set; used to detect if still in same state
|
||||||
self.key = None
|
self.key = None
|
||||||
|
self.setCanFocus(canFocus)
|
||||||
|
self._canCopy = canCopy or canFocus
|
||||||
|
|
||||||
def keyPressEvent(self, evt):
|
def keyPressEvent(self, evt):
|
||||||
if evt.matches(QKeySequence.Copy):
|
if evt.matches(QKeySequence.Copy):
|
||||||
|
@ -78,9 +81,7 @@ class AnkiWebView(QWebView):
|
||||||
QWebView.keyReleaseEvent(self, evt)
|
QWebView.keyReleaseEvent(self, evt)
|
||||||
|
|
||||||
def contextMenuEvent(self, evt):
|
def contextMenuEvent(self, evt):
|
||||||
# lazy: only run in reviewer
|
if not self._canCopy:
|
||||||
import aqt
|
|
||||||
if aqt.mw.state != "review":
|
|
||||||
return
|
return
|
||||||
m = QMenu(self)
|
m = QMenu(self)
|
||||||
a = m.addAction(_("Copy"))
|
a = m.addAction(_("Copy"))
|
||||||
|
@ -131,6 +132,13 @@ button {
|
||||||
def setBridge(self, bridge):
|
def setBridge(self, bridge):
|
||||||
self._bridge.setBridge(bridge)
|
self._bridge.setBridge(bridge)
|
||||||
|
|
||||||
|
def setCanFocus(self, canFocus=False):
|
||||||
|
self._canFocus = canFocus
|
||||||
|
if self._canFocus:
|
||||||
|
self.setFocusPolicy(Qt.WheelFocus)
|
||||||
|
else:
|
||||||
|
self.setFocusPolicy(Qt.NoFocus)
|
||||||
|
|
||||||
def eval(self, js):
|
def eval(self, js):
|
||||||
self.page().mainFrame().evaluateJavaScript(js)
|
self.page().mainFrame().evaluateJavaScript(js)
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<number>1</number>
|
<number>1</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>9999</number>
|
<number>99999</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
<file>icons/editclear.png</file>
|
<file>icons/editclear.png</file>
|
||||||
<file>icons/view-statistics.png</file>
|
<file>icons/view-statistics.png</file>
|
||||||
<file>icons/emblem-favorite.png</file>
|
<file>icons/emblem-favorite.png</file>
|
||||||
|
<file>icons/emblem-favorite-dark.png</file>
|
||||||
|
<file>icons/emblem-favorite-off.png</file>
|
||||||
<file>icons/view-pim-calendar.png</file>
|
<file>icons/view-pim-calendar.png</file>
|
||||||
<file>icons/anki-tag.png</file>
|
<file>icons/anki-tag.png</file>
|
||||||
<file>icons/edit-redo.png</file>
|
<file>icons/edit-redo.png</file>
|
||||||
|
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 545 B |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 786 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 440 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 537 B |
BIN
designer/icons/emblem-favorite-dark.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
designer/icons/emblem-favorite-off.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 802 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 502 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 482 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 517 B |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 457 B |
|
@ -94,20 +94,6 @@
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="1">
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="importButton">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Import</string>
|
|
||||||
</property>
|
|
||||||
<property name="default">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QScrollArea" name="mappingArea">
|
<widget class="QScrollArea" name="mappingArea">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
|
@ -133,8 +119,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>402</width>
|
<width>529</width>
|
||||||
<height>206</height>
|
<height>251</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -158,7 +144,6 @@
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<tabstops>
|
<tabstops>
|
||||||
<tabstop>importButton</tabstop>
|
|
||||||
<tabstop>buttonBox</tabstop>
|
<tabstop>buttonBox</tabstop>
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources/>
|
<resources/>
|
||||||
|
|
|
@ -9,8 +9,27 @@ def assertException(exception, func):
|
||||||
found = True
|
found = True
|
||||||
assert found
|
assert found
|
||||||
|
|
||||||
def getEmptyDeck(**kwargs):
|
|
||||||
|
# Creating new decks is expensive. Just do it once, and then spin off
|
||||||
|
# copies from the master.
|
||||||
|
def getEmptyCol():
|
||||||
|
if len(getEmptyCol.master) == 0:
|
||||||
|
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
||||||
|
os.close(fd)
|
||||||
|
os.unlink(nam)
|
||||||
|
col = aopen(nam)
|
||||||
|
col.db.close()
|
||||||
|
getEmptyCol.master = nam
|
||||||
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
||||||
|
shutil.copy(getEmptyCol.master, nam)
|
||||||
|
return aopen(nam)
|
||||||
|
|
||||||
|
getEmptyCol.master = ""
|
||||||
|
|
||||||
|
# Fallback for when the DB needs options passed in.
|
||||||
|
def getEmptyDeckWith(**kwargs):
|
||||||
|
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
||||||
|
os.close(fd)
|
||||||
os.unlink(nam)
|
os.unlink(nam)
|
||||||
return aopen(nam, **kwargs)
|
return aopen(nam, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
def test_previewCards():
|
def test_previewCards():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u'1'
|
f['Front'] = u'1'
|
||||||
f['Back'] = u'2'
|
f['Back'] = u'2'
|
||||||
|
@ -23,7 +23,7 @@ def test_previewCards():
|
||||||
assert deck.cardCount() == 1
|
assert deck.cardCount() == 1
|
||||||
|
|
||||||
def test_delete():
|
def test_delete():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u'1'
|
f['Front'] = u'1'
|
||||||
f['Back'] = u'2'
|
f['Back'] = u'2'
|
||||||
|
@ -39,7 +39,7 @@ def test_delete():
|
||||||
assert deck.db.scalar("select count() from graves") == 2
|
assert deck.db.scalar("select count() from graves") == 2
|
||||||
|
|
||||||
def test_misc():
|
def test_misc():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u'1'
|
f['Front'] = u'1'
|
||||||
f['Back'] = u'2'
|
f['Back'] = u'2'
|
||||||
|
@ -49,7 +49,7 @@ def test_misc():
|
||||||
assert c.template()['ord'] == 0
|
assert c.template()['ord'] == 0
|
||||||
|
|
||||||
def test_genrem():
|
def test_genrem():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u'1'
|
f['Front'] = u'1'
|
||||||
f['Back'] = u''
|
f['Back'] = u''
|
||||||
|
@ -76,7 +76,7 @@ def test_genrem():
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
|
|
||||||
def test_gendeck():
|
def test_gendeck():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
cloze = d.models.byName("Cloze")
|
cloze = d.models.byName("Cloze")
|
||||||
d.models.setCurrent(cloze)
|
d.models.setCurrent(cloze)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os, tempfile
|
||||||
from tests.shared import assertException, getEmptyDeck
|
from tests.shared import assertException, getEmptyCol
|
||||||
from anki.stdmodels import addBasicModel
|
from anki.stdmodels import addBasicModel
|
||||||
|
|
||||||
from anki import Collection as aopen
|
from anki import Collection as aopen
|
||||||
|
@ -11,8 +11,9 @@ newMod = None
|
||||||
|
|
||||||
def test_create():
|
def test_create():
|
||||||
global newPath, newMod
|
global newPath, newMod
|
||||||
path = "/tmp/test_attachNew.anki2"
|
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
|
||||||
try:
|
try:
|
||||||
|
os.close(fd)
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
@ -40,7 +41,7 @@ def test_openReadOnly():
|
||||||
os.unlink(newPath)
|
os.unlink(newPath)
|
||||||
|
|
||||||
def test_noteAddDelete():
|
def test_noteAddDelete():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u"one"; f['Back'] = u"two"
|
f['Front'] = u"one"; f['Back'] = u"two"
|
||||||
|
@ -79,7 +80,7 @@ def test_noteAddDelete():
|
||||||
assert f2.dupeOrEmpty()
|
assert f2.dupeOrEmpty()
|
||||||
|
|
||||||
def test_fieldChecksum():
|
def test_fieldChecksum():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u"new"; f['Back'] = u"new2"
|
f['Front'] = u"new"; f['Back'] = u"new2"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
|
@ -92,7 +93,7 @@ def test_fieldChecksum():
|
||||||
"select csum from notes") == int("302811ae", 16)
|
"select csum from notes") == int("302811ae", 16)
|
||||||
|
|
||||||
def test_addDelTags():
|
def test_addDelTags():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u"1"
|
f['Front'] = u"1"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
|
@ -111,14 +112,14 @@ def test_addDelTags():
|
||||||
assert len(f.tags) == 2
|
assert len(f.tags) == 2
|
||||||
|
|
||||||
def test_timestamps():
|
def test_timestamps():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
assert len(deck.models.models) == 4
|
assert len(deck.models.models) == 4
|
||||||
for i in range(100):
|
for i in range(100):
|
||||||
addBasicModel(deck)
|
addBasicModel(deck)
|
||||||
assert len(deck.models.models) == 104
|
assert len(deck.models.models) == 104
|
||||||
|
|
||||||
def test_furigana():
|
def test_furigana():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
mm = deck.models
|
mm = deck.models
|
||||||
m = mm.current()
|
m = mm.current()
|
||||||
# filter should work
|
# filter should work
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from tests.shared import assertException, getEmptyDeck
|
from tests.shared import assertException, getEmptyCol
|
||||||
|
|
||||||
def test_basic():
|
def test_basic():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
# we start with a standard deck
|
# we start with a standard deck
|
||||||
assert len(deck.decks.decks) == 1
|
assert len(deck.decks.decks) == 1
|
||||||
# it should have an id of 1
|
# it should have an id of 1
|
||||||
|
@ -42,7 +42,7 @@ def test_basic():
|
||||||
deck.sched.deckDueList()
|
deck.sched.deckDueList()
|
||||||
|
|
||||||
def test_remove():
|
def test_remove():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
# create a new deck, and add a note/card to it
|
# create a new deck, and add a note/card to it
|
||||||
g1 = deck.decks.id("g1")
|
g1 = deck.decks.id("g1")
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
|
@ -68,7 +68,7 @@ def test_remove():
|
||||||
assert deck.noteCount() == 0
|
assert deck.noteCount() == 0
|
||||||
|
|
||||||
def test_rename():
|
def test_rename():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
id = d.decks.id("hello::world")
|
id = d.decks.id("hello::world")
|
||||||
# should be able to rename into a completely different branch, creating
|
# should be able to rename into a completely different branch, creating
|
||||||
# parents as necessary
|
# parents as necessary
|
||||||
|
@ -89,7 +89,7 @@ def test_rename():
|
||||||
assert n in d.decks.allNames()
|
assert n in d.decks.allNames()
|
||||||
|
|
||||||
def test_renameForDragAndDrop():
|
def test_renameForDragAndDrop():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
|
|
||||||
def deckNames():
|
def deckNames():
|
||||||
return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ]
|
return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ]
|
||||||
|
|
|
@ -4,7 +4,7 @@ import nose, os, tempfile
|
||||||
from anki import Collection as aopen
|
from anki import Collection as aopen
|
||||||
from anki.exporting import *
|
from anki.exporting import *
|
||||||
from anki.importing import Anki2Importer
|
from anki.importing import Anki2Importer
|
||||||
from shared import getEmptyDeck
|
from shared import getEmptyCol
|
||||||
|
|
||||||
deck = None
|
deck = None
|
||||||
ds = None
|
ds = None
|
||||||
|
@ -12,7 +12,7 @@ testDir = os.path.dirname(__file__)
|
||||||
|
|
||||||
def setup1():
|
def setup1():
|
||||||
global deck
|
global deck
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"]
|
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"]
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
|
@ -36,7 +36,9 @@ def test_export_anki():
|
||||||
deck.decks.setConf(dobj, confId)
|
deck.decks.setConf(dobj, confId)
|
||||||
# export
|
# export
|
||||||
e = AnkiExporter(deck)
|
e = AnkiExporter(deck)
|
||||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
|
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||||
|
newname = unicode(newname)
|
||||||
|
os.close(fd)
|
||||||
os.unlink(newname)
|
os.unlink(newname)
|
||||||
e.exportInto(newname)
|
e.exportInto(newname)
|
||||||
# exporting should not have changed conf for original deck
|
# exporting should not have changed conf for original deck
|
||||||
|
@ -54,7 +56,9 @@ def test_export_anki():
|
||||||
# conf should be 1
|
# conf should be 1
|
||||||
assert dobj['conf'] == 1
|
assert dobj['conf'] == 1
|
||||||
# try again, limited to a deck
|
# try again, limited to a deck
|
||||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
|
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||||
|
newname = unicode(newname)
|
||||||
|
os.close(fd)
|
||||||
os.unlink(newname)
|
os.unlink(newname)
|
||||||
e.did = 1
|
e.did = 1
|
||||||
e.exportInto(newname)
|
e.exportInto(newname)
|
||||||
|
@ -69,13 +73,15 @@ def test_export_ankipkg():
|
||||||
n['Front'] = u'[sound:今日.mp3]'
|
n['Front'] = u'[sound:今日.mp3]'
|
||||||
deck.addNote(n)
|
deck.addNote(n)
|
||||||
e = AnkiPackageExporter(deck)
|
e = AnkiPackageExporter(deck)
|
||||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".apkg")[1])
|
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
|
||||||
|
newname = unicode(newname)
|
||||||
|
os.close(fd)
|
||||||
os.unlink(newname)
|
os.unlink(newname)
|
||||||
e.exportInto(newname)
|
e.exportInto(newname)
|
||||||
|
|
||||||
@nose.with_setup(setup1)
|
@nose.with_setup(setup1)
|
||||||
def test_export_anki_due():
|
def test_export_anki_due():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u"foo"
|
f['Front'] = u"foo"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
|
@ -92,11 +98,13 @@ def test_export_anki_due():
|
||||||
# export
|
# export
|
||||||
e = AnkiExporter(deck)
|
e = AnkiExporter(deck)
|
||||||
e.includeSched = True
|
e.includeSched = True
|
||||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
|
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||||
|
newname = unicode(newname)
|
||||||
|
os.close(fd)
|
||||||
os.unlink(newname)
|
os.unlink(newname)
|
||||||
e.exportInto(newname)
|
e.exportInto(newname)
|
||||||
# importing into a new deck, the due date should be equivalent
|
# importing into a new deck, the due date should be equivalent
|
||||||
deck2 = getEmptyDeck()
|
deck2 = getEmptyCol()
|
||||||
imp = Anki2Importer(deck2, newname)
|
imp = Anki2Importer(deck2, newname)
|
||||||
imp.run()
|
imp.run()
|
||||||
c = deck2.getCard(c.id)
|
c = deck2.getCard(c.id)
|
||||||
|
@ -115,7 +123,9 @@ def test_export_anki_due():
|
||||||
@nose.with_setup(setup1)
|
@nose.with_setup(setup1)
|
||||||
def test_export_textnote():
|
def test_export_textnote():
|
||||||
e = TextNoteExporter(deck)
|
e = TextNoteExporter(deck)
|
||||||
f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
|
fd, f = tempfile.mkstemp(prefix="ankitest")
|
||||||
|
f = unicode(f)
|
||||||
|
os.close(fd)
|
||||||
os.unlink(f)
|
os.unlink(f)
|
||||||
e.exportInto(f)
|
e.exportInto(f)
|
||||||
e.includeTags = True
|
e.includeTags = True
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from anki.find import Finder
|
from anki.find import Finder
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
def test_parse():
|
def test_parse():
|
||||||
f = Finder(None)
|
f = Finder(None)
|
||||||
|
@ -20,7 +20,7 @@ def test_parse():
|
||||||
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
||||||
|
|
||||||
def test_findCards():
|
def test_findCards():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u'dog'
|
f['Front'] = u'dog'
|
||||||
f['Back'] = u'cat'
|
f['Back'] = u'cat'
|
||||||
|
@ -216,7 +216,7 @@ def test_findCards():
|
||||||
assert len(deck.findCards("added:2")) == deck.cardCount()
|
assert len(deck.findCards("added:2")) == deck.cardCount()
|
||||||
|
|
||||||
def test_findReplace():
|
def test_findReplace():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u'foo'
|
f['Front'] = u'foo'
|
||||||
f['Back'] = u'bar'
|
f['Back'] = u'bar'
|
||||||
|
@ -243,7 +243,7 @@ def test_findReplace():
|
||||||
f.load(); assert f['Back'] == "reg"
|
f.load(); assert f['Back'] == "reg"
|
||||||
|
|
||||||
def test_findDupes():
|
def test_findDupes():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u'foo'
|
f['Front'] = u'foo'
|
||||||
f['Back'] = u'bar'
|
f['Back'] = u'bar'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from tests.shared import getUpgradeDeckPath, getEmptyDeck
|
from tests.shared import getUpgradeDeckPath, getEmptyCol
|
||||||
from anki.upgrade import Upgrader
|
from anki.upgrade import Upgrader
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \
|
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \
|
||||||
|
@ -27,7 +27,7 @@ def test_anki2():
|
||||||
open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo")
|
open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo")
|
||||||
src.close()
|
src.close()
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
dst = getEmptyDeck()
|
dst = getEmptyCol()
|
||||||
# import src into dst
|
# import src into dst
|
||||||
imp = Anki2Importer(dst, srcpath)
|
imp = Anki2Importer(dst, srcpath)
|
||||||
imp.run()
|
imp.run()
|
||||||
|
@ -50,7 +50,7 @@ def test_anki2():
|
||||||
assert len(os.listdir(dst.media.dir())) == 1
|
assert len(os.listdir(dst.media.dir())) == 1
|
||||||
|
|
||||||
def test_anki2_mediadupes():
|
def test_anki2_mediadupes():
|
||||||
tmp = getEmptyDeck()
|
tmp = getEmptyCol()
|
||||||
# add a note that references a sound
|
# add a note that references a sound
|
||||||
n = tmp.newNote()
|
n = tmp.newNote()
|
||||||
n['Front'] = "[sound:foo.mp3]"
|
n['Front'] = "[sound:foo.mp3]"
|
||||||
|
@ -60,7 +60,7 @@ def test_anki2_mediadupes():
|
||||||
open(os.path.join(tmp.media.dir(), "foo.mp3"), "w").write("foo")
|
open(os.path.join(tmp.media.dir(), "foo.mp3"), "w").write("foo")
|
||||||
tmp.close()
|
tmp.close()
|
||||||
# it should be imported correctly into an empty deck
|
# it should be imported correctly into an empty deck
|
||||||
empty = getEmptyDeck()
|
empty = getEmptyCol()
|
||||||
imp = Anki2Importer(empty, tmp.path)
|
imp = Anki2Importer(empty, tmp.path)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
||||||
|
@ -95,7 +95,7 @@ def test_anki2_mediadupes():
|
||||||
assert "_" in n.fields[0]
|
assert "_" in n.fields[0]
|
||||||
|
|
||||||
def test_apkg():
|
def test_apkg():
|
||||||
tmp = getEmptyDeck()
|
tmp = getEmptyCol()
|
||||||
apkg = unicode(os.path.join(testDir, "support/media.apkg"))
|
apkg = unicode(os.path.join(testDir, "support/media.apkg"))
|
||||||
imp = AnkiPackageImporter(tmp, apkg)
|
imp = AnkiPackageImporter(tmp, apkg)
|
||||||
assert os.listdir(tmp.media.dir()) == []
|
assert os.listdir(tmp.media.dir()) == []
|
||||||
|
@ -122,7 +122,7 @@ def test_anki1():
|
||||||
os.mkdir(mdir)
|
os.mkdir(mdir)
|
||||||
open(os.path.join(mdir, "_foo.jpg"), "w").write("foo")
|
open(os.path.join(mdir, "_foo.jpg"), "w").write("foo")
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
dst = getEmptyDeck()
|
dst = getEmptyCol()
|
||||||
# import src into dst
|
# import src into dst
|
||||||
imp = Anki1Importer(dst, tmp)
|
imp = Anki1Importer(dst, tmp)
|
||||||
imp.run()
|
imp.run()
|
||||||
|
@ -138,7 +138,7 @@ def test_anki1():
|
||||||
|
|
||||||
def test_anki1_diffmodels():
|
def test_anki1_diffmodels():
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
dst = getEmptyDeck()
|
dst = getEmptyCol()
|
||||||
# import the 1 card version of the model
|
# import the 1 card version of the model
|
||||||
tmp = getUpgradeDeckPath("diffmodels1.anki")
|
tmp = getUpgradeDeckPath("diffmodels1.anki")
|
||||||
imp = Anki1Importer(dst, tmp)
|
imp = Anki1Importer(dst, tmp)
|
||||||
|
@ -165,7 +165,7 @@ def test_anki1_diffmodels():
|
||||||
|
|
||||||
def test_suspended():
|
def test_suspended():
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
dst = getEmptyDeck()
|
dst = getEmptyCol()
|
||||||
# import the 1 card version of the model
|
# import the 1 card version of the model
|
||||||
tmp = getUpgradeDeckPath("suspended12.anki")
|
tmp = getUpgradeDeckPath("suspended12.anki")
|
||||||
imp = Anki1Importer(dst, tmp)
|
imp = Anki1Importer(dst, tmp)
|
||||||
|
@ -174,7 +174,7 @@ def test_suspended():
|
||||||
|
|
||||||
def test_anki2_diffmodels():
|
def test_anki2_diffmodels():
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
dst = getEmptyDeck()
|
dst = getEmptyCol()
|
||||||
# import the 1 card version of the model
|
# import the 1 card version of the model
|
||||||
tmp = getUpgradeDeckPath("diffmodels2-1.apkg")
|
tmp = getUpgradeDeckPath("diffmodels2-1.apkg")
|
||||||
imp = AnkiPackageImporter(dst, tmp)
|
imp = AnkiPackageImporter(dst, tmp)
|
||||||
|
@ -206,7 +206,7 @@ def test_anki2_diffmodels():
|
||||||
|
|
||||||
def test_anki2_updates():
|
def test_anki2_updates():
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
dst = getEmptyDeck()
|
dst = getEmptyCol()
|
||||||
tmp = getUpgradeDeckPath("update1.apkg")
|
tmp = getUpgradeDeckPath("update1.apkg")
|
||||||
imp = AnkiPackageImporter(dst, tmp)
|
imp = AnkiPackageImporter(dst, tmp)
|
||||||
imp.run()
|
imp.run()
|
||||||
|
@ -232,7 +232,7 @@ def test_anki2_updates():
|
||||||
assert dst.db.scalar("select flds from notes").startswith("goodbye")
|
assert dst.db.scalar("select flds from notes").startswith("goodbye")
|
||||||
|
|
||||||
def test_csv():
|
def test_csv():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
file = unicode(os.path.join(testDir, "support/text-2fields.txt"))
|
file = unicode(os.path.join(testDir, "support/text-2fields.txt"))
|
||||||
i = TextImporter(deck, file)
|
i = TextImporter(deck, file)
|
||||||
i.initMapping()
|
i.initMapping()
|
||||||
|
@ -266,7 +266,7 @@ def test_csv():
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
def test_csv2():
|
def test_csv2():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
mm = deck.models
|
mm = deck.models
|
||||||
m = mm.current()
|
m = mm.current()
|
||||||
f = mm.newField("Three")
|
f = mm.newField("Three")
|
||||||
|
@ -289,7 +289,7 @@ def test_csv2():
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
def test_supermemo_xml_01_unicode():
|
def test_supermemo_xml_01_unicode():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
file = unicode(os.path.join(testDir, "support/supermemo1.xml"))
|
file = unicode(os.path.join(testDir, "support/supermemo1.xml"))
|
||||||
i = SupermemoXmlImporter(deck, file)
|
i = SupermemoXmlImporter(deck, file)
|
||||||
#i.META.logToStdOutput = True
|
#i.META.logToStdOutput = True
|
||||||
|
@ -297,12 +297,13 @@ def test_supermemo_xml_01_unicode():
|
||||||
assert i.total == 1
|
assert i.total == 1
|
||||||
cid = deck.db.scalar("select id from cards")
|
cid = deck.db.scalar("select id from cards")
|
||||||
c = deck.getCard(cid)
|
c = deck.getCard(cid)
|
||||||
assert c.factor == 5701
|
# Applies A Factor-to-E Factor conversion
|
||||||
|
assert c.factor == 2879
|
||||||
assert c.reps == 7
|
assert c.reps == 7
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
def test_mnemo():
|
def test_mnemo():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
file = unicode(os.path.join(testDir, "support/mnemo.db"))
|
file = unicode(os.path.join(testDir, "support/mnemo.db"))
|
||||||
i = MnemosyneImporter(deck, file)
|
i = MnemosyneImporter(deck, file)
|
||||||
i.run()
|
i.run()
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
from anki.utils import stripHTML
|
from anki.utils import stripHTML
|
||||||
|
|
||||||
def test_latex():
|
def test_latex():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# change latex cmd to simulate broken build
|
# change latex cmd to simulate broken build
|
||||||
import anki.latex
|
import anki.latex
|
||||||
anki.latex.latexCmd[0] = "nolatex"
|
anki.latex.latexCmds[0][0] = "nolatex"
|
||||||
# add a note with latex
|
# add a note with latex
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"[latex]hello[/latex]"
|
f['Front'] = u"[latex]hello[/latex]"
|
||||||
|
@ -17,7 +17,7 @@ def test_latex():
|
||||||
assert len(os.listdir(d.media.dir())) == 0
|
assert len(os.listdir(d.media.dir())) == 0
|
||||||
# check the error message
|
# check the error message
|
||||||
msg = f.cards()[0].q()
|
msg = f.cards()[0].q()
|
||||||
assert "executing latex" in msg
|
assert "executing nolatex" in msg
|
||||||
assert "installed" in msg
|
assert "installed" in msg
|
||||||
# check if we have latex installed, and abort test if we don't
|
# check if we have latex installed, and abort test if we don't
|
||||||
for cmd in ("latex", "dvipng"):
|
for cmd in ("latex", "dvipng"):
|
||||||
|
@ -26,7 +26,7 @@ def test_latex():
|
||||||
print "aborting test; %s is not installed" % cmd
|
print "aborting test; %s is not installed" % cmd
|
||||||
return
|
return
|
||||||
# fix path
|
# fix path
|
||||||
anki.latex.latexCmd[0] = "latex"
|
anki.latex.latexCmds[0][0] = "latex"
|
||||||
# check media db should cause latex to be generated
|
# check media db should cause latex to be generated
|
||||||
d.media.check()
|
d.media.check()
|
||||||
assert len(os.listdir(d.media.dir())) == 1
|
assert len(os.listdir(d.media.dir())) == 1
|
||||||
|
|
|
@ -4,12 +4,12 @@ import tempfile
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from shared import getEmptyDeck, testDir
|
from shared import getEmptyCol, testDir
|
||||||
|
|
||||||
|
|
||||||
# copying files to media folder
|
# copying files to media folder
|
||||||
def test_add():
|
def test_add():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
dir = tempfile.mkdtemp(prefix="anki")
|
dir = tempfile.mkdtemp(prefix="anki")
|
||||||
path = os.path.join(dir, u"foo.jpg")
|
path = os.path.join(dir, u"foo.jpg")
|
||||||
open(path, "w").write("hello")
|
open(path, "w").write("hello")
|
||||||
|
@ -22,7 +22,7 @@ def test_add():
|
||||||
assert d.media.addFile(path) == "foo (1).jpg"
|
assert d.media.addFile(path) == "foo (1).jpg"
|
||||||
|
|
||||||
def test_strings():
|
def test_strings():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
mf = d.media.filesInStr
|
mf = d.media.filesInStr
|
||||||
mid = d.models.models.keys()[0]
|
mid = d.models.models.keys()[0]
|
||||||
assert mf(mid, "aoeu") == []
|
assert mf(mid, "aoeu") == []
|
||||||
|
@ -46,7 +46,7 @@ def test_strings():
|
||||||
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
|
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
|
||||||
|
|
||||||
def test_deckIntegration():
|
def test_deckIntegration():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# create a media dir
|
# create a media dir
|
||||||
d.media.dir()
|
d.media.dir()
|
||||||
# put a file into it
|
# put a file into it
|
||||||
|
@ -68,7 +68,7 @@ def test_deckIntegration():
|
||||||
assert ret[1] == ["foo.jpg"]
|
assert ret[1] == ["foo.jpg"]
|
||||||
|
|
||||||
def test_changes():
|
def test_changes():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
assert d.media._changed()
|
assert d.media._changed()
|
||||||
def added():
|
def added():
|
||||||
return d.media.db.execute("select fname from log where type = 0")
|
return d.media.db.execute("select fname from log where type = 0")
|
||||||
|
@ -103,7 +103,7 @@ def test_changes():
|
||||||
assert len(list(d.media.removed())) == 1
|
assert len(list(d.media.removed())) == 1
|
||||||
|
|
||||||
def test_illegal():
|
def test_illegal():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
aString = u"a:b|cd\\e/f\0g*h"
|
aString = u"a:b|cd\\e/f\0g*h"
|
||||||
good = u"abcdefgh"
|
good = u"abcdefgh"
|
||||||
assert d.media.stripIllegal(aString) == good
|
assert d.media.stripIllegal(aString) == good
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
from anki.utils import stripHTML, joinFields
|
from anki.utils import stripHTML, joinFields
|
||||||
|
|
||||||
def test_modelDelete():
|
def test_modelDelete():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = u'1'
|
f['Front'] = u'1'
|
||||||
f['Back'] = u'2'
|
f['Back'] = u'2'
|
||||||
|
@ -14,7 +14,7 @@ def test_modelDelete():
|
||||||
assert deck.cardCount() == 0
|
assert deck.cardCount() == 0
|
||||||
|
|
||||||
def test_modelCopy():
|
def test_modelCopy():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
m = deck.models.current()
|
m = deck.models.current()
|
||||||
m2 = deck.models.copy(m)
|
m2 = deck.models.copy(m)
|
||||||
assert m2['name'] == "Basic copy"
|
assert m2['name'] == "Basic copy"
|
||||||
|
@ -27,7 +27,7 @@ def test_modelCopy():
|
||||||
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
||||||
|
|
||||||
def test_fields():
|
def test_fields():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u'1'
|
f['Front'] = u'1'
|
||||||
f['Back'] = u'2'
|
f['Back'] = u'2'
|
||||||
|
@ -74,7 +74,7 @@ def test_fields():
|
||||||
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
|
||||||
|
|
||||||
def test_templates():
|
def test_templates():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current(); mm = d.models
|
||||||
t = mm.newTemplate("Reverse")
|
t = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t['qfmt'] = "{{Back}}"
|
||||||
|
@ -107,8 +107,31 @@ def test_templates():
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
assert not d.models.remTemplate(m, m['tmpls'][0])
|
assert not d.models.remTemplate(m, m['tmpls'][0])
|
||||||
|
|
||||||
|
def test_cloze_ordinals():
|
||||||
|
d = getEmptyCol()
|
||||||
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
|
m = d.models.current(); mm = d.models
|
||||||
|
|
||||||
|
#We replace the default Cloze template
|
||||||
|
t = mm.newTemplate("ChainedCloze")
|
||||||
|
t['qfmt'] = "{{text:cloze:Text}}"
|
||||||
|
t['afmt'] = "{{text:cloze:Text}}"
|
||||||
|
mm.addTemplate(m, t)
|
||||||
|
mm.save(m)
|
||||||
|
d.models.remTemplate(m, m['tmpls'][0])
|
||||||
|
|
||||||
|
f = d.newNote()
|
||||||
|
f['Text'] = u'{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
|
||||||
|
d.addNote(f)
|
||||||
|
assert d.cardCount() == 2
|
||||||
|
(c, c2) = f.cards()
|
||||||
|
# first card should have first ord
|
||||||
|
assert c.ord == 0
|
||||||
|
assert c2.ord == 1
|
||||||
|
|
||||||
|
|
||||||
def test_text():
|
def test_text():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
m = d.models.current()
|
m = d.models.current()
|
||||||
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
|
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
|
||||||
d.models.save(m)
|
d.models.save(m)
|
||||||
|
@ -118,7 +141,7 @@ def test_text():
|
||||||
assert "helloworld" in f.cards()[0].q()
|
assert "helloworld" in f.cards()[0].q()
|
||||||
|
|
||||||
def test_cloze():
|
def test_cloze():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
d.models.setCurrent(d.models.byName("Cloze"))
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
assert f.model()['name'] == "Cloze"
|
assert f.model()['name'] == "Cloze"
|
||||||
|
@ -163,8 +186,31 @@ def test_cloze():
|
||||||
f.flush()
|
f.flush()
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
|
|
||||||
|
def test_chained_mods():
|
||||||
|
d = getEmptyCol()
|
||||||
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
|
m = d.models.current(); mm = d.models
|
||||||
|
|
||||||
|
#We replace the default Cloze template
|
||||||
|
t = mm.newTemplate("ChainedCloze")
|
||||||
|
t['qfmt'] = "{{cloze:text:Text}}"
|
||||||
|
t['afmt'] = "{{cloze:text:Text}}"
|
||||||
|
mm.addTemplate(m, t)
|
||||||
|
mm.save(m)
|
||||||
|
d.models.remTemplate(m, m['tmpls'][0])
|
||||||
|
|
||||||
|
f = d.newNote()
|
||||||
|
q1 = '<span style=\"color:red\">phrase</span>'
|
||||||
|
a1 = '<b>sentence</b>'
|
||||||
|
q2 = '<span style=\"color:red\">en chaine</span>'
|
||||||
|
a2 = '<i>chained</i>'
|
||||||
|
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
|
||||||
|
assert d.addNote(f) == 1
|
||||||
|
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q()
|
||||||
|
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
|
||||||
|
|
||||||
def test_modelChange():
|
def test_modelChange():
|
||||||
deck = getEmptyDeck()
|
deck = getEmptyCol()
|
||||||
basic = deck.models.byName("Basic")
|
basic = deck.models.byName("Basic")
|
||||||
cloze = deck.models.byName("Cloze")
|
cloze = deck.models.byName("Cloze")
|
||||||
# enable second template and add a note
|
# enable second template and add a note
|
||||||
|
@ -238,7 +284,7 @@ def test_modelChange():
|
||||||
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
||||||
|
|
||||||
def test_availOrds():
|
def test_availOrds():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current(); mm = d.models
|
||||||
t = m['tmpls'][0]
|
t = m['tmpls'][0]
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
|
|
|
@ -1,171 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
import nose, os, time
|
|
||||||
from tests.shared import assertException
|
|
||||||
|
|
||||||
from anki.sync import Syncer, FullSyncer, RemoteServer, \
|
|
||||||
MediaSyncer, RemoteMediaServer, httpCon
|
|
||||||
from anki import Collection as aopen
|
|
||||||
|
|
||||||
deck1=None
|
|
||||||
deck2=None
|
|
||||||
client=None
|
|
||||||
server=None
|
|
||||||
server2=None
|
|
||||||
|
|
||||||
# Remote tests
|
|
||||||
##########################################################################
|
|
||||||
|
|
||||||
import tests.test_sync as ts
|
|
||||||
from tests.test_sync import setup_basic
|
|
||||||
import anki.sync
|
|
||||||
anki.sync.SYNC_URL = "http://localhost:5000/sync/"
|
|
||||||
TEST_USER = "synctest@ichi2.net"
|
|
||||||
TEST_PASS = "abc123"
|
|
||||||
TEST_HKEY = "WqYF0m7fOHCNPI4a"
|
|
||||||
TEST_REMOTE = True
|
|
||||||
|
|
||||||
def setup_remote():
|
|
||||||
setup_basic()
|
|
||||||
# mark deck1 as changed
|
|
||||||
ts.deck1.save()
|
|
||||||
ts.server = RemoteServer(TEST_HKEY)
|
|
||||||
ts.client.server = ts.server
|
|
||||||
|
|
||||||
@nose.with_setup(setup_remote)
|
|
||||||
def test_meta():
|
|
||||||
global TEST_REMOTE
|
|
||||||
try:
|
|
||||||
# if the key is wrong, meta returns nothing
|
|
||||||
ts.server.hkey = "abc"
|
|
||||||
assert not ts.server.meta()
|
|
||||||
except Exception, e:
|
|
||||||
if e.errno == 61:
|
|
||||||
TEST_REMOTE = False
|
|
||||||
print "aborting; server offline"
|
|
||||||
return
|
|
||||||
ts.server.hkey = TEST_HKEY
|
|
||||||
meta = ts.server.meta()
|
|
||||||
assert meta['mod']
|
|
||||||
assert meta['scm']
|
|
||||||
assert meta['mod'] != ts.client.col.mod
|
|
||||||
assert abs(meta['ts'] - time.time()) < 3
|
|
||||||
|
|
||||||
@nose.with_setup(setup_remote)
|
|
||||||
def test_hkey():
|
|
||||||
if not TEST_REMOTE:
|
|
||||||
return
|
|
||||||
assert not ts.server.hostKey(TEST_USER, "wrongpass")
|
|
||||||
ts.server.hkey = "willchange"
|
|
||||||
k = ts.server.hostKey(TEST_USER, TEST_PASS)
|
|
||||||
assert k == ts.server.hkey == TEST_HKEY
|
|
||||||
|
|
||||||
@nose.with_setup(setup_remote)
|
|
||||||
def test_download():
|
|
||||||
if not TEST_REMOTE:
|
|
||||||
return
|
|
||||||
f = FullSyncer(ts.client.col, "abc", ts.server.con)
|
|
||||||
assertException(Exception, f.download)
|
|
||||||
f.hkey = TEST_HKEY
|
|
||||||
f.download()
|
|
||||||
|
|
||||||
@nose.with_setup(setup_remote)
|
|
||||||
def test_remoteSync():
|
|
||||||
if not TEST_REMOTE:
|
|
||||||
return
|
|
||||||
# not yet associated, so will require a full sync
|
|
||||||
assert ts.client.sync() == "fullSync"
|
|
||||||
# upload
|
|
||||||
f = FullSyncer(ts.client.col, TEST_HKEY, ts.server.con)
|
|
||||||
assert f.upload()
|
|
||||||
ts.client.col.reopen()
|
|
||||||
# should report no changes
|
|
||||||
assert ts.client.sync() == "noChanges"
|
|
||||||
# bump local col
|
|
||||||
ts.client.col.setMod()
|
|
||||||
ts.client.col.save()
|
|
||||||
assert ts.client.sync() == "success"
|
|
||||||
# again, no changes
|
|
||||||
assert ts.client.sync() == "noChanges"
|
|
||||||
|
|
||||||
# Remote media tests
|
|
||||||
##########################################################################
|
|
||||||
# We can't run useful tests for local media, because the desktop code assumes
|
|
||||||
# the current directory is the media folder.
|
|
||||||
|
|
||||||
def setup_remoteMedia():
|
|
||||||
setup_basic()
|
|
||||||
con = httpCon()
|
|
||||||
ts.server = RemoteMediaServer(TEST_HKEY, con)
|
|
||||||
ts.server2 = RemoteServer(TEST_HKEY)
|
|
||||||
ts.client = MediaSyncer(ts.deck1, ts.server)
|
|
||||||
|
|
||||||
@nose.with_setup(setup_remoteMedia)
|
|
||||||
def test_media():
|
|
||||||
if not TEST_REMOTE:
|
|
||||||
return
|
|
||||||
print "media test disabled"
|
|
||||||
return
|
|
||||||
ts.server.mediatest("reset")
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 0
|
|
||||||
assert ts.server.mediatest("count") == 0
|
|
||||||
# initially, nothing to do
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
# add a file
|
|
||||||
time.sleep(1)
|
|
||||||
os.chdir(ts.deck1.media.dir())
|
|
||||||
p = os.path.join(ts.deck1.media.dir(), "foo.jpg")
|
|
||||||
open(p, "wb").write("foo")
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
|
||||||
assert ts.server.mediatest("count") == 0
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
time.sleep(1)
|
|
||||||
# should have been synced
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
|
||||||
assert ts.server.mediatest("count") == 1
|
|
||||||
# if we remove the file, should be removed
|
|
||||||
os.unlink(p)
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 0
|
|
||||||
assert ts.server.mediatest("count") == 0
|
|
||||||
# we should be able to add it again
|
|
||||||
time.sleep(1)
|
|
||||||
open(p, "wb").write("foo")
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
|
||||||
assert ts.server.mediatest("count") == 1
|
|
||||||
# if we modify it, it should get sent too. also we set the zip size very
|
|
||||||
# low here, so that we can test splitting into multiple zips
|
|
||||||
import anki.media; anki.media.SYNC_ZIP_SIZE = 1
|
|
||||||
time.sleep(1)
|
|
||||||
open(p, "wb").write("bar")
|
|
||||||
open(p+"2", "wb").write("baz")
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
|
||||||
assert ts.server.mediatest("count") == 2
|
|
||||||
# if we lose our media db, we should be able to bring it back in sync
|
|
||||||
time.sleep(1)
|
|
||||||
ts.deck1.media.close()
|
|
||||||
os.unlink(ts.deck1.media.dir()+".db")
|
|
||||||
ts.deck1.media.connect()
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
|
||||||
assert ts.server.mediatest("count") == 2
|
|
||||||
# if we send an unchanged file, the server should cope
|
|
||||||
time.sleep(1)
|
|
||||||
ts.deck1.media.db.execute("insert into log values ('foo.jpg', 0)")
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
|
||||||
assert ts.server.mediatest("count") == 2
|
|
||||||
# if we remove foo.jpg on the ts.server, the removal should be synced
|
|
||||||
assert ts.server.mediatest("removefoo") == "OK"
|
|
||||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
|
||||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
|
||||||
assert ts.server.mediatest("count") == 1
|
|
|
@ -3,13 +3,13 @@
|
||||||
import time
|
import time
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from anki.hooks import addHook
|
from anki.hooks import addHook
|
||||||
|
|
||||||
|
|
||||||
def test_clock():
|
def test_clock():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
if (d.sched.dayCutoff - intTime()) < 10*60:
|
if (d.sched.dayCutoff - intTime()) < 10*60:
|
||||||
raise Exception("Unit tests will fail around the day rollover.")
|
raise Exception("Unit tests will fail around the day rollover.")
|
||||||
|
|
||||||
|
@ -18,12 +18,12 @@ def checkRevIvl(d, c, targetIvl):
|
||||||
return min <= c.ivl <= max
|
return min <= c.ivl <= max
|
||||||
|
|
||||||
def test_basics():
|
def test_basics():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
def test_new():
|
def test_new():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 0
|
assert d.sched.newCount == 0
|
||||||
# add a note
|
# add a note
|
||||||
|
@ -43,29 +43,31 @@ def test_new():
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
assert c.type == 1
|
assert c.type == 1
|
||||||
assert c.due >= t
|
assert c.due >= t
|
||||||
# the default order should ensure siblings are not seen together, and
|
|
||||||
# should show all cards
|
# disabled for now, as the learn fudging makes this randomly fail
|
||||||
m = d.models.current(); mm = d.models
|
# # the default order should ensure siblings are not seen together, and
|
||||||
t = mm.newTemplate("Reverse")
|
# # should show all cards
|
||||||
t['qfmt'] = "{{Back}}"
|
# m = d.models.current(); mm = d.models
|
||||||
t['afmt'] = "{{Front}}"
|
# t = mm.newTemplate("Reverse")
|
||||||
mm.addTemplate(m, t)
|
# t['qfmt'] = "{{Back}}"
|
||||||
mm.save(m)
|
# t['afmt'] = "{{Front}}"
|
||||||
f = d.newNote()
|
# mm.addTemplate(m, t)
|
||||||
f['Front'] = u"2"; f['Back'] = u"2"
|
# mm.save(m)
|
||||||
d.addNote(f)
|
# f = d.newNote()
|
||||||
f = d.newNote()
|
# f['Front'] = u"2"; f['Back'] = u"2"
|
||||||
f['Front'] = u"3"; f['Back'] = u"3"
|
# d.addNote(f)
|
||||||
d.addNote(f)
|
# f = d.newNote()
|
||||||
d.reset()
|
# f['Front'] = u"3"; f['Back'] = u"3"
|
||||||
qs = ("2", "3", "2", "3")
|
# d.addNote(f)
|
||||||
for n in range(4):
|
# d.reset()
|
||||||
c = d.sched.getCard()
|
# qs = ("2", "3", "2", "3")
|
||||||
assert qs[n] in c.q()
|
# for n in range(4):
|
||||||
d.sched.answerCard(c, 2)
|
# c = d.sched.getCard()
|
||||||
|
# assert qs[n] in c.q()
|
||||||
|
# d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
def test_newLimits():
|
def test_newLimits():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add some notes
|
# add some notes
|
||||||
g2 = d.decks.id("Default::foo")
|
g2 = d.decks.id("Default::foo")
|
||||||
for i in range(30):
|
for i in range(30):
|
||||||
|
@ -95,7 +97,7 @@ def test_newLimits():
|
||||||
assert d.sched.newCount == 9
|
assert d.sched.newCount == 9
|
||||||
|
|
||||||
def test_newBoxes():
|
def test_newBoxes():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -108,7 +110,7 @@ def test_newBoxes():
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
def test_learn():
|
def test_learn():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"; f['Back'] = u"two"
|
f['Front'] = u"one"; f['Back'] = u"two"
|
||||||
|
@ -176,15 +178,13 @@ def test_learn():
|
||||||
c.queue = 1
|
c.queue = 1
|
||||||
c.odue = 321
|
c.odue = 321
|
||||||
c.flush()
|
c.flush()
|
||||||
print "----begin"
|
|
||||||
d.sched.removeLrn()
|
d.sched.removeLrn()
|
||||||
c.load()
|
c.load()
|
||||||
print c.__dict__
|
|
||||||
assert c.queue == 2
|
assert c.queue == 2
|
||||||
assert c.due == 321
|
assert c.due == 321
|
||||||
|
|
||||||
def test_learn_collapsed():
|
def test_learn_collapsed():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add 2 notes
|
# add 2 notes
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"1"
|
f['Front'] = u"1"
|
||||||
|
@ -210,7 +210,7 @@ def test_learn_collapsed():
|
||||||
assert not c.q().endswith("2")
|
assert not c.q().endswith("2")
|
||||||
|
|
||||||
def test_learn_day():
|
def test_learn_day():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -268,7 +268,7 @@ def test_learn_day():
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
|
|
||||||
def test_reviews():
|
def test_reviews():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"; f['Back'] = u"two"
|
f['Front'] = u"one"; f['Back'] = u"two"
|
||||||
|
@ -360,7 +360,7 @@ def test_reviews():
|
||||||
assert c.queue == -1
|
assert c.queue == -1
|
||||||
|
|
||||||
def test_button_spacing():
|
def test_button_spacing():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -382,7 +382,7 @@ def test_button_spacing():
|
||||||
def test_overdue_lapse():
|
def test_overdue_lapse():
|
||||||
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
||||||
return
|
return
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -415,7 +415,7 @@ def test_overdue_lapse():
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
|
|
||||||
def test_finished():
|
def test_finished():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# nothing due
|
# nothing due
|
||||||
assert "Congratulations" in d.sched.finishedMsg()
|
assert "Congratulations" in d.sched.finishedMsg()
|
||||||
assert "limit" not in d.sched.finishedMsg()
|
assert "limit" not in d.sched.finishedMsg()
|
||||||
|
@ -434,7 +434,7 @@ def test_finished():
|
||||||
assert "limit" not in d.sched.finishedMsg()
|
assert "limit" not in d.sched.finishedMsg()
|
||||||
|
|
||||||
def test_nextIvl():
|
def test_nextIvl():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"; f['Back'] = u"two"
|
f['Front'] = u"one"; f['Back'] = u"two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -490,7 +490,7 @@ def test_nextIvl():
|
||||||
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
||||||
|
|
||||||
def test_misc():
|
def test_misc():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -504,7 +504,7 @@ def test_misc():
|
||||||
assert d.sched.getCard()
|
assert d.sched.getCard()
|
||||||
|
|
||||||
def test_suspend():
|
def test_suspend():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -547,7 +547,7 @@ def test_suspend():
|
||||||
assert c.did == 1
|
assert c.did == 1
|
||||||
|
|
||||||
def test_cram():
|
def test_cram():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -655,7 +655,7 @@ def test_cram():
|
||||||
assert c.did == 1
|
assert c.did == 1
|
||||||
|
|
||||||
def test_cram_rem():
|
def test_cram_rem():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -676,7 +676,7 @@ def test_cram_rem():
|
||||||
|
|
||||||
def test_cram_resched():
|
def test_cram_resched():
|
||||||
# add card
|
# add card
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -780,7 +780,7 @@ def test_cram_resched():
|
||||||
# print c.__dict__
|
# print c.__dict__
|
||||||
|
|
||||||
def test_ordcycle():
|
def test_ordcycle():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add two more templates and set second active
|
# add two more templates and set second active
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current(); mm = d.models
|
||||||
t = mm.newTemplate("Reverse")
|
t = mm.newTemplate("Reverse")
|
||||||
|
@ -804,7 +804,7 @@ def test_ordcycle():
|
||||||
assert d.sched.getCard().ord == 2
|
assert d.sched.getCard().ord == 2
|
||||||
|
|
||||||
def test_counts_idx():
|
def test_counts_idx():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"; f['Back'] = u"two"
|
f['Front'] = u"one"; f['Back'] = u"two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -826,7 +826,7 @@ def test_counts_idx():
|
||||||
assert d.sched.counts() == (0, 2, 0)
|
assert d.sched.counts() == (0, 2, 0)
|
||||||
|
|
||||||
def test_repCounts():
|
def test_repCounts():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -878,7 +878,7 @@ def test_repCounts():
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
|
|
||||||
def test_timing():
|
def test_timing():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a few review cards, due today
|
# add a few review cards, due today
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
|
@ -904,7 +904,7 @@ def test_timing():
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
|
|
||||||
def test_collapse():
|
def test_collapse():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -918,7 +918,7 @@ def test_collapse():
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
def test_deckDue():
|
def test_deckDue():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -968,7 +968,7 @@ def test_deckDue():
|
||||||
d.sched.deckDueTree()
|
d.sched.deckDueTree()
|
||||||
|
|
||||||
def test_deckTree():
|
def test_deckTree():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
d.decks.id("new::b::c")
|
d.decks.id("new::b::c")
|
||||||
d.decks.id("new2")
|
d.decks.id("new2")
|
||||||
# new should not appear twice in tree
|
# new should not appear twice in tree
|
||||||
|
@ -977,7 +977,7 @@ def test_deckTree():
|
||||||
assert "new" not in names
|
assert "new" not in names
|
||||||
|
|
||||||
def test_deckFlow():
|
def test_deckFlow():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -1001,7 +1001,7 @@ def test_deckFlow():
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
def test_reorder():
|
def test_reorder():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -1039,7 +1039,7 @@ def test_reorder():
|
||||||
assert f4.cards()[0].due == 2
|
assert f4.cards()[0].due == 2
|
||||||
|
|
||||||
def test_forget():
|
def test_forget():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -1053,7 +1053,7 @@ def test_forget():
|
||||||
assert d.sched.counts() == (1, 0, 0)
|
assert d.sched.counts() == (1, 0, 0)
|
||||||
|
|
||||||
def test_resched():
|
def test_resched():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -1069,7 +1069,7 @@ def test_resched():
|
||||||
assert c.ivl == +1
|
assert c.ivl == +1
|
||||||
|
|
||||||
def test_norelearn():
|
def test_norelearn():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
@ -1090,7 +1090,7 @@ def test_norelearn():
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
|
|
||||||
def test_failmult():
|
def test_failmult():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"; f['Back'] = u"two"
|
f['Front'] = u"one"; f['Back'] = u"two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
def test_stats():
|
def test_stats():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "foo"
|
f['Front'] = "foo"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
@ -18,7 +18,7 @@ def test_stats():
|
||||||
assert d.cardStats(c)
|
assert d.cardStats(c)
|
||||||
|
|
||||||
def test_graphs_empty():
|
def test_graphs_empty():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
assert d.stats().report()
|
assert d.stats().report()
|
||||||
|
|
||||||
def test_graphs():
|
def test_graphs():
|
||||||
|
|
|
@ -5,7 +5,7 @@ import nose, os, shutil, time
|
||||||
from anki import Collection as aopen, Collection
|
from anki import Collection as aopen, Collection
|
||||||
from anki.utils import intTime
|
from anki.utils import intTime
|
||||||
from anki.sync import Syncer, LocalServer
|
from anki.sync import Syncer, LocalServer
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol, getEmptyDeckWith
|
||||||
|
|
||||||
# Local tests
|
# Local tests
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -18,7 +18,7 @@ server2=None
|
||||||
|
|
||||||
def setup_basic():
|
def setup_basic():
|
||||||
global deck1, deck2, client, server
|
global deck1, deck2, client, server
|
||||||
deck1 = getEmptyDeck()
|
deck1 = getEmptyCol()
|
||||||
# add a note to deck 1
|
# add a note to deck 1
|
||||||
f = deck1.newNote()
|
f = deck1.newNote()
|
||||||
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"]
|
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"]
|
||||||
|
@ -26,7 +26,7 @@ def setup_basic():
|
||||||
# answer it
|
# answer it
|
||||||
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
|
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
|
||||||
# repeat for deck2
|
# repeat for deck2
|
||||||
deck2 = getEmptyDeck(server=True)
|
deck2 = getEmptyDeckWith(server=True)
|
||||||
f = deck2.newNote()
|
f = deck2.newNote()
|
||||||
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"]
|
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"]
|
||||||
deck2.addNote(f)
|
deck2.addNote(f)
|
||||||
|
@ -247,7 +247,7 @@ def test_threeway2():
|
||||||
anki.notes.intTime = lambda x=1: intTime(1000)
|
anki.notes.intTime = lambda x=1: intTime(1000)
|
||||||
def setup():
|
def setup():
|
||||||
# create collection 1 with a single note
|
# create collection 1 with a single note
|
||||||
c1 = getEmptyDeck()
|
c1 = getEmptyCol()
|
||||||
f = c1.newNote()
|
f = c1.newNote()
|
||||||
f['Front'] = u"startingpoint"
|
f['Front'] = u"startingpoint"
|
||||||
nid = f.id
|
nid = f.id
|
||||||
|
@ -325,7 +325,7 @@ def _test_speed():
|
||||||
deck1.tags.tags[tx] = -1
|
deck1.tags.tags[tx] = -1
|
||||||
deck1._usn = -1
|
deck1._usn = -1
|
||||||
deck1.save()
|
deck1.save()
|
||||||
deck2 = getEmptyDeck(server=True)
|
deck2 = getEmptyDeckWith(server=True)
|
||||||
deck1.scm = deck2.scm = 0
|
deck1.scm = deck2.scm = 0
|
||||||
server = LocalServer(deck2)
|
server = LocalServer(deck2)
|
||||||
client = Syncer(deck1, server)
|
client = Syncer(deck1, server)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from tests.shared import getEmptyDeck
|
from tests.shared import getEmptyCol
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
|
||||||
def test_op():
|
def test_op():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
# should have no undo by default
|
# should have no undo by default
|
||||||
assert not d.undoName()
|
assert not d.undoName()
|
||||||
# let's adjust a study option
|
# let's adjust a study option
|
||||||
|
@ -36,7 +36,7 @@ def test_op():
|
||||||
assert d.undoName() == "Review"
|
assert d.undoName() == "Review"
|
||||||
|
|
||||||
def test_review():
|
def test_review():
|
||||||
d = getEmptyDeck()
|
d = getEmptyCol()
|
||||||
d.conf['counts'] = COUNT_REMAINING
|
d.conf['counts'] = COUNT_REMAINING
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = u"one"
|
f['Front'] = u"one"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import datetime, shutil
|
import datetime, shutil, tempfile
|
||||||
from anki import Collection
|
from anki import Collection
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from shared import getUpgradeDeckPath, testDir
|
from shared import getUpgradeDeckPath, testDir
|
||||||
|
@ -63,8 +63,9 @@ def test_invalid_ords():
|
||||||
assert deck.db.scalar("select count() from cards where ord = 1") == 1
|
assert deck.db.scalar("select count() from cards where ord = 1") == 1
|
||||||
|
|
||||||
def test_upgrade2():
|
def test_upgrade2():
|
||||||
p = "/tmp/alpha-upgrade.anki2"
|
fd, p = tempfile.mkstemp(suffix=".anki2", prefix="alpha-upgrade")
|
||||||
if os.path.exists(p):
|
if os.path.exists(p):
|
||||||
|
os.close(fd)
|
||||||
os.unlink(p)
|
os.unlink(p)
|
||||||
shutil.copy2(os.path.join(testDir, "support/anki2-alpha.anki2"), p)
|
shutil.copy2(os.path.join(testDir, "support/anki2-alpha.anki2"), p)
|
||||||
col = Collection(p)
|
col = Collection(p)
|
||||||
|
|
11
thirdparty/pyaudio.py
vendored
|
@ -92,16 +92,7 @@ __author__ = "Hubert Pham"
|
||||||
__version__ = "0.2.4"
|
__version__ = "0.2.4"
|
||||||
__docformat__ = "restructuredtext en"
|
__docformat__ = "restructuredtext en"
|
||||||
|
|
||||||
import sys
|
import _portaudio as pa
|
||||||
|
|
||||||
# attempt to import PortAudio
|
|
||||||
try:
|
|
||||||
import _portaudio as pa
|
|
||||||
except ImportError:
|
|
||||||
print "Please build and install the PortAudio Python " +\
|
|
||||||
"bindings first."
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
|
|
||||||
# Try to use Python 2.4's built in `set'
|
# Try to use Python 2.4's built in `set'
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -29,7 +29,7 @@ do
|
||||||
echo " * "$py
|
echo " * "$py
|
||||||
pyuic4 $i -o $py
|
pyuic4 $i -o $py
|
||||||
# munge the output to use gettext
|
# munge the output to use gettext
|
||||||
perl -pi.bak -e 's/QtGui.QApplication.translate\(".*?", /_(/; s/, None, QtGui.*/))/' $py
|
perl -pi.bak -e 's/(QtGui\.QApplication\.)?_?translate\(".*?", /_(/; s/, None.*/))/' $py
|
||||||
rm $py.bak
|
rm $py.bak
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|